11.7. GATT操作

キャラクタリスティックは、名前付きの値としてGATTデータベースの中に存在しているだけです。それを有用にするのは、クライアントがその上で実行できる、小さくて明確に定義された操作のセットです。各キャラクタリスティックは、自身がサポートする操作を プロパティビットマスク として宣言します。公開するものが何もないサーバーは読み取り専用の値を公開でき、制御レジスタは書き込み専用かもしれず、更新をストリーミングするセンサーはnotifyビットを設定するでしょう。クライアントはディスカバリ中にこのビットマスクを発見し、それを尊重します。

5つの操作は readwritewrite without responsenotifyindicate です。これらはプル(クライアントが要求する)とプッシュ(サーバーが送信する)の2つのグループに分かれます。

11.7.1. プル: readとwrite

この2つは最も単純で、関数呼び出しとまったく同じように見えます。

  • Read。 クライアントが現在の値を要求し、サーバーがそれを送り返します。1往復で、クライアントはサーバーがそのキャラクタリスティックに設定したバイト列を受け取り、サーバーは誰が読んだかについて何も得ません。

  • Write。 クライアントが新しいバイト列を送り、サーバーがそれを保存します(さらに任意で新しい値に対してアプリケーションロジックを実行します)。2つの種類があります。

    • Write with response -- サーバーが確認応答し、非ゼロのステータスではアプリケーションエラーを送出します。信頼性が高く、1往復です。

    • Write without response -- サーバーはバイト列を静かに保存し、クライアントは確認応答をまったく得ません。(確認応答を待つ往復がないため)高速で、ストリーミングに役立ちますが、エラーをサイドチャネルの読み戻しを通じてしか知れないという代償があります。

aioble では、クライアント側のAPIはこの選択を、response キーワード(True / False / None でピアがアドバタイズする内容に基づき自動選択)を持つ単一の aioble.ClientCharacteristic.write() メソッドの背後に隠します。

11.7.2. プッシュ: notifyとindicate

プルモデルはセンサーデータには不適切です。フォンが毎秒ポーリングしなければならない心拍ストラップは、100個もの不要なラジオイベントでバッテリーを消費してしまいます。新しい読み取り値があるときだけ値をプッシュするものこそ、そもそもBLEの要点です。

GATTはこれを サーバー起点 の操作で解決します。クライアントはキャラクタリスティックを 購読 します。その時点以降、サーバーが値を更新するたびに、新しい値がリンクを越えてクライアントへプッシュされます。2つの種類があります。

  • Notify。 Fire-and-forget(送りっぱなし)です。サーバーが通知をキューに入れ、リンク層が次の接続イベント中にそれを送信し、クライアントが受信します。GATTレベルでの確認応答はありません。リンク層の通常の再送がラジオ側の損失を処理しますが、アプリケーションは値が処理されたという確認を受け取りません。

  • Indicate。 サーバーは通知を送り、さらに 次の通知を送る前にクライアントのGATTレベルの確認を待ちます。一度に1つのインディケーションです。サーバーがクライアントが実際に値を見たことを知る必要があるとき(重要なアラームのキャラクタリスティックや構成の確認応答など)に使われます。

サーバーとクライアントを並べた2つの図。左側 では、クライアントが「read」を送り、サーバー が値で応答する。3回連続のreadで、各ペアが 矢印になっている。右側では、クライアントが 単一の「subscribe」を送り、その後サーバーが 自身の選んだタイミングで、間にクライアントの 要求なしに、3つの「notify」パケットをプッシュ する。

プル(read)対プッシュ(notify)。通知では、クライアントは一度購読し、値が変わるたびにサーバーが新しい値をプッシュします。

購読は、キャラクタリスティックに付随するディスクリプタ(Client Characteristic Configuration Descriptor(CCCD、0x2902))に書き込むことで行われます。0x0001 を書き込むと通知が有効になり、0x0002 でインディケーションが有効になり、0x0000 で両方が無効になります。aioble.ClientCharacteristic.subscribe() メソッドは、notify=Trueindicate=True のキーワードフラグを使って、この書き込みをあなたの代わりに実行します。

購読すると、クライアントは notified()indicated() で受信プッシュを待ちます。どちらも、次のプッシュが到着するまで一時停止する非同期コルーチンです。

11.7.3. MTUがペイロードサイズを管理する

すべての操作は、接続がリンクアップ時に落ち着いた、ネゴシエート済みの MTU によって制約されます。デフォルトのMTUは23バイトで、GATTヘッダの後にキャラクタリスティックの値バイトとして20バイトが残ります。それより大きいものは、より大きなMTU(aioble.DeviceConnection.exchange_mtu() で、カメラでは最大512バイトまでネゴシエートされる)に収めるか、複数のキャラクタリスティックや複数の通知に分割する必要があります。

MTUより大きい値のクライアント起点の読み取りと書き込みは、舞台裏でGATTの long 手続き(Read Long / Prepare-Write + Execute-Write)によって処理されます。aioble はこれらを透過的に実行するので、特大の値で read() / write() を呼び出しても、単により多くの往復がかかるだけです。サーバー起点の通知とインディケーションは断片化され ません。1つのプッシュはMTUによって境界づけられ、アプリケーションはそれより大きいものを複数の通知に分割するか、GATTから完全に離れます。

本当に大きな転送(キャプチャしたフレーム、測定値のバッチ、ファームウェアのblob)については、正しい答えは通常、GATTから完全に離れて代わりに L2CAPチャネル を使うことです(L2CAP チャネル を参照)。

11.7.4. ひと目で見る両側

5つの操作は、接続の各側で異なる形で公開されます。