11.14. まとめ

Bluetooth Low Energy について、無線層からそれを駆動するために使うPython APIまでを一通り見てきました:

  • 動機 -- BLEは、カメラが間に何のインフラもなく近くにあるものと通信したいときの答えです。同じ部屋にある電話、手首に着けたウェアラブル、壁のビーコンなど。短距離で、参加すべきネットワークもなく、消費電力もほとんどありません。

  • 無線 -- 2.4 GHz、40チャネル:アドバタイズ用に3つ、接続データ用に37、疑似ランダムなシーケンスでホッピングし、ノイズの多いチャネルを適応的に回避します。短いパケット、ほとんどスリープしている無線。

  • リンク層 -- パケットのフレーミング、アドレッシング、接続スケジューリング、再送、リンク層の暗号化。これらはいずれもPythonからは設定しません。すべては接続パラメータとMTUを通じて表に現れます。

  • Generic Access Profile(GAP) -- ディスカバリと接続管理。4つのロール:ペリフェラルとブロードキャスター(アドバタイズ)、centralとオブザーバー(スキャン)。アドバタイズのペイロードはローカル名、サービスUUID、アピアランス、製造元固有データを運びます -- 31バイトに加えてオプションの31バイトのスキャンレスポンス。接続インターバル、ペリフェラルレイテンシ、スーパービジョンタイムアウトが、オープンな接続の使用感を左右します。

  • Generic Attribute Profile(GATT) -- サービスのツリーで、各サービスはキャラクタリスティックを保持し、各キャラクタリスティックはオプションでディスクリプタを保持し、UUID(Bluetooth-SIG標準では16ビット、カスタムでは128ビット)で識別されます。5つの操作:readwrite(プル、クライアント起点)、notifyindicate(プッシュ、サーバー起点、Client Characteristic Configuration Descriptor を介して購読)。ペイロードサイズはネゴシエートされたMTUによって制限されます。

  • Python API -- aioble はあらゆるBLEパターンをasyncioコルーチンに変換します。ペリフェラルは、Service / Characteristic オブジェクトを一度だけ構築して aioble.register_services() でコミットしたうえで、接続をループする aioble.advertise() です。centralは、ピアを見つける aioble.scan()、リンクを開く connect()、リモートのGATTツリーをたどる service()characteristic()、そして実際のデータのための read() / write() / subscribe() / notified() です。切断は、待機していたコルーチンの内部で aioble.DeviceDisconnectedError として現れます。

  • L2CAPチャネル -- GATTのキー/バリューモデルに収まらないバルクなバイトストリームのための脱出口です。aioble.DeviceConnection.l2cap_accept() / l2cap_connect() は、GAP接続の上にアプリケーションごとのチャネルを開き、クレジットフロー制御された送受信と、GATTが運べるより大きなMTUを備えます。

  • ペアリングと暗号化 -- BLEリンクはデフォルトで公開です。aioble.DeviceConnection.pair() は鍵交換を開始して暗号化されたリンクを生成します。bond=True(デフォルト)は鍵を永続化し、以降の接続でハンドシェイクをスキップできるようにします。mitm=True と利用可能なIO機能がなければ、暗号化は受動的な盗聴者からは保護しますが、元のペアリング中の能動的なリダイレクトからは保護しません。

これだけあれば、ペリフェラルとしてステータスを公開する、centralとしてセンサーデータを読み取る、BLEを介して電話にライブの値をプッシュする、ペアリングとボンディングのステップでリンクをセキュアにする、そして -- まれなバルク転送のケースのために -- GATTから離れてL2CAPチャネルに移る、といったカメラアプリケーションを書くことができます。

11.14.1. トラブルシューティング

BLEの障害のほとんどは、両者が期待しているものの食い違いであり、電話側のインスペクタは誰の期待がずれているのかを最も速く確認する方法です。標準的なツールは nRF Connect for Mobile(Nordic Semiconductor 製、AndroidとiOSで無料)です:スキャンし、接続し、GATTデータベースをたどり、キャラクタリスティックを読み書きし、通知を購読します -- そのため、コンパニオンアプリをまったく書かなくても、カメラ側の動作を単独でテストできます。

よくある障害モード:

  • 「デバイスはスキャナーに表示されるが接続できない。」 最も多いのは、アドバタイズパケットが connectable=False(ブロードキャスターモード)になっているか、以前の接続がまだ開いたままでカメラがすでに aioble.advertise() を通過してしまっている場合です。advertise呼び出しの前後にprint文を追加して確認してください。

  • 「exchange_mtu(512) は実行されたが、通知が依然として20バイトに制限される。」 ネゴシエートされたMTUは min(local, peer) です -- 電話またはcentralライブラリが自身の側でより大きなMTUを要求していない可能性があり、その場合は接続が23のままになります。exchange_mtu() が戻った後に mtu を確認してください。また、exchange_mtu() は接続ごとに一度しか機能しないことにも注意してください。最初の大きな操作の前に呼び出してください。

  • 「ペアリングが一般的なエラーで失敗する。」 よくある2つの原因:IO機能の食い違い(io=3 / 入力なし出力なしを宣言しているカメラに対して mitm=True を要求している -- 数値コードを確認する手段がないため、ペアリングエンジンが中断します)と、ピアがそれを要求するときにカメラの実時刻が大きくずれていることです。最初のペアリング試行の前に ntptime.settime() で時計を設定してください。

  • 「通知がクライアントにまったく届かない。」 順に確認すべき2点:(a) キャラクタリスティックは notify=True で宣言されたか? -- サーバー側でプロパティビットが設定されている必要があります。(b) クライアントは subscribe() を呼び出したか? -- Client Characteristic Configuration Descriptor(CCCD)を書き込まないと、サーバーは通知を望むクライアントがいないと判断され、黙って通知を破棄します。

  • 「アドバタイズされる名前が切り詰められる、または表示されない。」 アドバタイズのペイロードは31バイトで、フラグ + サービスUUID + アピアランスの各フィールドがその先頭から数バイトを消費します。長い name= に加えていくつかのサービスUUIDがあると溢れます。名前を短くするか、アクティブスキャンを使ってスキャンレスポンス(さらに31バイト)に溢れた分を運ばせてください。nRF Connect は両方の半分を別々に表示するので、分割が一目でわかります。

  • 「L2CAP接続がすぐに例外を発生させる。」 通常はPSMの食い違いです -- 両者が帯域外で同じPSM番号に合意している必要があります。L2CAPConnectionError は最初の引数としてBluetoothステータスコードを運びます。ステータス 2(「PSM not supported」)が手がかりです。

  • 「ボンディング済みの接続でも、再接続のたびに完全なペアリングハンドシェイクが発生する。」 起動時に aioble.security.load_secrets() が呼び出されていません。それがないと、保存された鍵はフラッシュ上にあってもメモリには読み込まれず、ピアのアイデンティティが不明なため、毎回最初からペアリングが実行されます。

他のすべてが失敗したときは、より低レベルの bluetooth モジュールが、あらゆる低レベルイベントで発火するIRQコールバックを公開しています。それを一時的に購読してイベントを出力することは、カメラ側にとってWiresharkトレースに相当します。

11.14.2. このリファレンスを後から使う

Bluetoothの各章はリファレンス資料として扱ってください。ペリフェラルのアドバタイズペイロードの正確なレイアウトや、centralのスキャン・購読フローを確認するために戻ってくることが、本来の使い方です。aioble --- 非同期 BLEbluetooth --- 低レベルBluetooth のリファレンスページは、「この呼び出しの正確な名前は何か」という疑問だけのときに、すべてのメソッド、フラグ、定数を1か所に一覧で示します。