11.6. サービスとキャラクタリスティック¶
GAP が 2 つのデバイスを開いた接続状態にしたら、その上位レイヤである Generic Attribute Profile(GATT)が、その接続を流れるバイト列に意味を与えなければなりません。ここでの BLE の選択は珍しいものです。TCP が生のバイトストリームを公開して、独自のフレーミングを考案する役目をアプリケーションに任せるのに対し、GATT は一方がホストし、もう一方が読み取り、書き込み、または購読する 小さなキー/値データベース を公開します。
アプリケーション設計者は BLE における時間の大半を、このデータベースについて考えることに費やします。カメラが電話機に公開するもの、リモートセンサーで監視するもの、Bluetooth キーボードがどのキーが押されたかをホストに伝える方法 -- これらはすべて、どこかの GATT データベースにあるキャラクタリスティック値です。
11.6.1. 1 つではなく 2 つのロール軸¶
よくある混乱の原因: peripheral / central と server / client は同義語ではなく、2 つの独立した軸 です。
Peripheral と central は GAP のロールで、接続時に決まります。peripheral はアドバタイズして接続される側、central はスキャンして接続を開始する側です。これはリンクが確立した瞬間に確定し、変わることはありません。
Server と client は GATT のロールで、キャラクタリスティック操作ごとに決まります。server はキャラクタリスティックをホストし、client はそれを読み取り、書き込み、または購読します。
この 2 つの軸は仕様によって分離されています。peripheral は 通常 server であり(心拍ストラップは測定値を公開します)、central は 通常 client です(電話機がそれらを読み取ります)が、BLE は任意の組み合わせを許します -- peripheral が接続したばかりの central 上のキャラクタリスティックを探索することもあれば、1 つの接続で両側にサービスを同時にホストすることもできます。
ほとんどのカメラアプリケーションは従来の組み合わせ(peripheral + server、または central + client)に従うため、本節の残りでは、従来のケースを説明している場合はこれらを 1 つの軸として扱います。区別が重要な場合は、両方の用語を明示的に表記します。
11.6.2. データベースの内部¶
GATT データベースはツリーです。葉が実際のバイト列を保持します。枝が関連する葉を、人間にとって意味のある単位にまとめます。
GATT データベース。サービスはキャラクタリスティックをまとめ、キャラクタリスティックはアプリケーションのバイト列を保持し、ディスクリプタはキャラクタリスティックに関するメタデータを保持します。¶
ノードには 3 種類あります:
サービス は、関連する値の論理的なグループです。Bluetooth SIG は一般的なユースケース向けに標準サービス定義を公開しています -- バッテリー残量向けの Battery Service、温度/湿度/気圧向けの Environmental Sensing、心拍計向けの Heart Rate など -- これにより、電話機上の汎用アプリは、これまで見たことのないサービスでも認識できます。アプリケーションは、SIG が標準化していないもののために独自のサービスを自由に定義することもできます。
キャラクタリスティック は、サービス内の 1 つの名前付き値です。Battery サービスには単一のキャラクタリスティック -- Battery Level、1 バイトのパーセンテージ -- があります。Environmental Sensing には温度、湿度、気圧などごとに別々のキャラクタリスティックがあります。キャラクタリスティックは GATT 操作の単位です -- キャラクタリスティックを読み取り、キャラクタリスティックに書き込み、キャラクタリスティックを購読します。
ディスクリプタ は、キャラクタリスティックに付随するメタデータです。一部のディスクリプタは標準化されています -- Client Characteristic Configuration Descriptor(CCCD)が有名なもので、これに書き込むことが、client が server に「このキャラクタリスティックの通知を送ってほしい」と伝える方法だからです。それ以外はユーザー定義で、表示フォーマットや拡張プロパティといったものを保持します。
GATT server(通常は peripheral)は起動時に一度だけデータベースを宣言し、実行中はデータベースは変わりません。GATT client(通常は central)は接続後にデータベースの中身を 探索 します -- ツリーをたどり、見つけたサービスの UUID を読み取り、続いて各サービス内のキャラクタリスティックを読み取ります。
11.6.3. UUID¶
すべてのサービス、キャラクタリスティック、ディスクリプタには、それがどんな種類のものか を識別する UUID(Universally Unique IDentifier)があります。UUID には 3 つの幅があります:
16 ビット。 Bluetooth SIG が定義する標準向けに予約されています。Battery Service は
0x180Fです。Battery Level(キャラクタリスティック)は0x2A19です。完全な一覧は Bluetooth SIG の assigned-numbers サイト https://www.bluetooth.com/specifications/assigned-numbers/ で公開されています。32 ビット。 めったに使われない中間的なものです。
128 ビット。 その他すべての人が使うものです -- ベンダーやアプリケーションがランダムに 1 つ生成し、独自のサービスやキャラクタリスティックに使用します。独自のプロトコルを定義するカメラはここに該当します。
bluetooth.UUID クラスは、これら 3 つの幅のいずれも受け付けます:
import bluetooth
BATTERY_SERVICE = bluetooth.UUID(0x180F)
CUSTOM_SERVICE = bluetooth.UUID("12345678-1234-5678-9abc-def012345678")
16 ビット UUID は小さなアドバタイジングペイロードにエンコードされます。これは、標準サービスが存在する場合にそれが望ましい理由の 1 つです -- 0x180D(Heart Rate)をアドバタイズする心拍ストラップは 2 バイトで済みますが、カスタム UUID は 16 バイトかかります。標準的な相互運用性を必要としないアプリケーションには、生成した 128 ビット UUID が適切な答えです。
11.6.4. SIG 標準化サービスが提供する利点¶
標準サービスを使う根拠は単純です: 既存のアプリがすでにそれと通信する方法を知っている ということです。Heart Rate サービス(0x180D)をアドバタイズし、Heart Rate Measurement キャラクタリスティック(0x2A37)を公開するデバイスは、新しいコードを誰も書かなくても、世界中のあらゆるフィットネスアプリで動作します。同じデータをカスタム UUID で再実装するデバイスには、独自のコンパニオンアプリと独自のプロトコル文書が必要になります。
標準にはコストが伴います。各キャラクタリスティック内のバイトレイアウトが規定されています -- SIG は Heart Rate Measurement が単一バイトのフラグフィールドに続いて 8 ビットまたは 16 ビットの心拍値、オプションで R-R 間隔が続くと決めました -- 準拠デバイスはこれらのレイアウトに従わなければなりません。カスタムサービスはその制約から自由です。
カメラにとっての現実的な答え: 手元のデータの種類に対して標準サービスが存在する場合(Battery Service、Environmental Sensing)はそれを使い、アプリケーション固有のものには 128 ビット UUID を持つカスタムサービスを定義します。
11.6.5. サーバ側とクライアント側のオブジェクト¶
同じ概念上の構成要素(サービス、キャラクタリスティック、ディスクリプタ)に対して、すべての GATT ライブラリは 2 組の並行したオブジェクトを公開します:
サーバ側 オブジェクト -- peripheral がホストすると宣言するもの。
aiobleでは、aioble.Service、aioble.Characteristic、aioble.Descriptorです。これらはアドバタイジング開始前に構築され、aioble.register_services()で登録されます。クライアント側 オブジェクト -- central が接続後にピア上で探索するもの。
aiobleでは、aioble.ClientService、aioble.ClientCharacteristic、aioble.ClientDescriptorです。これらは接続後にaioble.DeviceConnection.service()/services()を通じてたどられます。