Wireless・のおと

GATT のはなし2

ブログ
規格 技術解説 Bluetooth

今回は久しぶりにプロトコル系のおはなしです。Bluetooth LE におけるデータ表現の基本フォーマット GATT(General ATTribute) については以前にも「GATT のはなし」で一度紹介していますが、今回はもう少し踏み込んだ解説をしてみます。

ATT の構造と機能

まず GATT はアトリビュート(ATT) の配列であり、ATT はハンドル、名前(UUID)、値(Value) の3つからなります。これだけ見ると SNMP の MIB と似ているように思えますが、MIB との類似性はここで終わりです。
SNMP MIB のオブジェクトは OID と値の2つからなり、OID と値をまとめて読み出すことができます。一方、ATT はハンドル、UUID、値の3つをまとめて読みだす術を持ちません。基本的には「UUID を指定してハンドルを読み出す」または「ハンドルを指定して UUIDまたは値を読み出す」という操作になります(※註)。

※註:個人的には、SNMP の GetNext に相当する「ハンドル範囲を指定して UUID および値を読み出す」操作が無いことが ATT を直観的に判りにくくしていると思います。これはおそらく BLE のパケット長が短く、16 バイトの UUID128 と値の両方を納めると MTU を超える場合が多いためあえて避けられたのでしょう。MTU と ATT の関係についてはchar-read-hnd コマンドの解説でも触れます。

具体的には、ATT プロトコルには下記のコマンドが定義されています。なお括弧内の番号は Bluetooth 5 Core Spec Part.F におけるセクション番号です。

Find Information (3.4.3.1)
ハンドル範囲を指定して UUID とハンドルを読み出す

Find by Type Value (3.4.3.3)
ハンドル範囲、UUID と値を指定してハンドルを読み出す

Read by Type (3.4.4.1)
ハンドル範囲と UUID を指定してハンドルと値を読み出す

Read by Handle (3.4.4.3)
ハンドルを指定して値を読み出す

Read by Group Type (3.4.4.9)
ハンドル範囲とグループ UUID を指定してハンドルとグループ終端ハンドルと値を読み出す

Write Requet (3.4.5.1)
ハンドルを指定して値を書き込む

※註:これら以外にも Read Blob や Prepare Write などのコマンドもありますが、割愛します。

この中で「Read by Type」と「Read by Group Type」の違いがわかりにくく、「ATT はハンドルと UUID しか持っていない筈なのに、グループ UUID って何だ?!」と悩んでしまいますが、意味としては「ATT の持つ UUID 値」には違いありません。ただし「グループ UUID」が特別な意味を持つのは GATT の上位構造に関係します。これについては次の章で解説します。
 

GATT の構造

SNMP MIB は OID そのものが iso.org.dod.internet.mgmt.mib-2... (1.3.6.1.2.1...) のような階層構造をなしており、OID の上位桁を共有することで MIB グループが定義されたり、あるいはテーブルと呼ばれる配列のようなデータ構造が表記されます。これに対して ATT そのものは単純な一次配列で、構成要素の1つ1つは等しく「アトリビュート」です。しかし GATT においては、アトリビュートの名前(UUID)と並び順によって上階層の構造が表現されます。

GATT データ構造の最小単位は「キャラクタリスティック」です。これは UUID=0x2803(キャラクタリスティック・ディスクリプタ) を持つ ATT と値を表現する ATT(キャラクタリスティック値、UUID は任意)2つのペアで表現されます。この2つはハンドルを連番にすることが規定されている(※註)ほか、ディスクリプタの値フィールドに含まれる Value Handle でもその関連が明示されます。

※註:Vol.3 Part.G 3.3, "The Characteristic Value declaration shall exist immediately following the characteristic declaration."

gatt03_l.jpg

(図) GATTのペア構造

キャラクタリスティックは常に2つの ATT で構成されるとは限らず、用途に応じて UUID=0x2900(拡張プロパティ)、0x2901(解説)、0x2902(クライアント設定)、0x2903(サーバー設定)、0x2904(フォーマット記述)などの拡張 ATT を含む場合もあります。これら拡張 ATT はキャラクタリスティック値 ATT の下にぞろぞろと並ぶだけで、その順番も規定されていません(※註)。それらがどのキャラクタリスティックに所属するのか(あるいは逆に、あるキャラクタリスティックが拡張 ATT を持つのか否か)の判断は、一意にハンドルの並びだけで定義されます。

※註:Vol.3 Part.G 3.3, "Any optional characteristic descriptor declarations are placed after the Characteristic Value declaration. The order of the optional characteristic descriptor declarations is not significant."

さて、この「ATT ペア+αで表現されるデータ単位=GATT キャラクタリスティック」を複数まとめたものが「GATT サービス」です。これまた個々の ATT やキャラクタリスティックに「自分がどのサービスに所属するか」という情報は無く、UUID=0x2800(プライマリサービス)または 0x2801(セカンダリサービス)の ATT を区切りとして、「サービス宣言から次のサービス宣言(または終端)までの間に含まれるキャラクタリスティックは、全てそのサービスに所属する」というハンドル並びに依存した構造をなします。

gatt-table_l.jpg

(図) GATT構造の例

この GATT におけるサービス(連続した1群のキャラクタリスティックのまとめ)を、ATT ではグループと呼びます。Read by Group Type に出てきた「グループ UUID」というのはつまり、UUID=0x2800 あるいは 0x2801 のことだと解釈してほぼ間違いないです。実体としては同じものが階層(ATT/GATT)によって異なる呼び方をされることが Bluetooth GATT をわかりにくくしている理由の一つですが、「そういうものだ」と諦めてください。
グループは必ずしもキャラクタリスティックを含むとは限りません。例えば UUID=0x2800, Value=0x1801 (GATT) の ATT はふつう直属のキャラクタリスティックを持たず、「ここから下に GATT サービス群が入る」ことを示す一種のマーカーとして使われます。こういった GATT サービスの仕様については Core Spec には示されておらず、Bluetooth SIG ウェブサイトから GATT 仕様を参照する必要があります。例えば Number=0x1801:Generic Attribute の Service Characteristics は1つだけ、"Service Changed (UUID=0x2A05)" が "Optional" として定義されています。運用中に GATT 仕様が変わり得るのでなければこれを実装する必要はなく、従って Primary Service 0x1801 は「キャラクタリスティックを持たないサービス=一種のマーカー」になります。

GATT と gatttool

gatttool は BlueZ に含まれるコマンドラインベースの GATT クライアントです。BlueZ プロジェクトの伝統に則り極めて無愛想で使いにくいツールなのですが、とりあえず一番手っとり早く動作を試せるコマンドのはずなので、これを基本として GATT の動作を解説します。対象とする GATT 機器は「GATT構造の例」で示した GATT テーブルを持っていると仮定します。

gatttool はコマンドラインからの動作指定と対話モード(--interactive)の2つのモードを持ち、両者の間で使えるコマンドが違うばかりか、紛らわしいことに同じ機能に割り当てられたコマンド名が違ったりもします。以下の解説では対話モードのコマンド名を基本とし、コマンドラインモードの対応コマンドは括弧内に併記します。

primary コマンド (--primary)
プライマリサービスの一覧を取得します。具体的にはハンドル範囲 0x0001~0xFFFF, UUID=0x2800 を引数に持つ Read by Group Type コマンドが発行され、UUID=0x2800 を持つ ATT のハンドルと値の一覧が返されます。なお、UUID=0x2801 を検索する secondary というコマンドは(なぜか)実装されていません。
サービス ATT の Value UUID は 2 バイト長の UUID16 と 16 バイト長の UUID128 どちらも取り得ますが、gatttool の primary コマンドでは(なぜか)常に UUID128 に展開して表示されます。

[CON][00:80:92:12:34:56][LE]> primary
attr handle = 0x0001, end grp handle = 0x0003 uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle = 0x0004, end grp handle = 0x0004 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle = 0x0005, end grp handle = 0x000b uuid: 0000180a-0000-1000-8000-00805f9b34fb
attr handle = 0x000c, end grp handle = 0xffff uuid: 0000180f-0000-1000-8000-00805f9b34fb

primary コマンドには UUID 値を渡すこともでき、この場合は Read by Group Type ではなく Find by Type Value コマンドが発行され、「UUID=2800、値フィールドが指定 UUID に一致する ATT の一覧」が返されます。表示内容も引数無しの「primary」コマンドとは微妙に変わります。

[CON][00:80:92:12:34:56][LE]> primary 180f
Starting handle: 0x000c Ending handle: 0xffff
 

characteristics コマンド (--characteristics)

指定ハンドル範囲内からキャラクタリスティックの一覧を取得します。具体的には指定ハンドル範囲, UUID=0x2803 を引数に持つ Read by Type コマンドが発行され、UUID=2803 を持つ ATT のハンドルと値の一覧が返されます。値の中身は更にパースされ、Characteristic Property, Value Handle, Value UUID まで表示されます。ここでもまた、Value UUID は常に UUID128 として表示されます。

[CON][00:80:92:12:34:56][LE]> characteristics
handle: 0x0002, char properties: 0x02, char value handle: 0x0003, uuid: 00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0006, char properties: 0x02, char value handle: 0x0007, uuid: 00002a23-0000-1000-8000-00805f9b34fb
handle: 0x0008, char properties: 0x02, char value handle: 0x0009, uuid: 00002a24-0000-1000-8000-00805f9b34fb
handle: 0x000a, char properties: 0x02, char value handle: 0x000b, uuid: 00002a25-0000-1000-8000-00805f9b34fb
handle: 0x000d, char properties: 0x0a, char value handle: 0x000e, uuid: 00002a19-0000-1000-8000-00805f9b34fb

characteristics コマンドはオプションでハンドル範囲に加えて UUID を指定することができます。この場合は検索結果の中から指定した値に一致する Value UUID を持つキャラクタリスティックだけが抽出表示されますが、primary コマンドとは異なり ATT レベルでのプロトコルは同じです。つまり UUID 指定の有無に関わらずハンドル範囲の全検索が行われます。

[CON][00:80:92:12:34:56][LE]> characteristics 0 ffff 2a19
handle: 0x000d, char properties: 0x0a, char value handle: 0x000e, uuid: 00002a19-0000-1000-8000-00805f9b34fb

char-desc コマンド

指定ハンドル範囲内の ATT ハンドルと UUID の一覧を取得します。具体的には指定ハンドル範囲を引数に持つ Find Information コマンドが発行され、範囲内にある ATT のハンドルと UUID の一覧が表示されます。primary や characteristics コマンドとは異なり、UUID16 と UUID128 は区別して表示されます。何でこんな所で一貫性が無いのか気持ち悪いのですが、「そういうものだ」と諦めてください。

[CON][00:80:92:12:34:56][LE]> char-desc
handle: 0x0001, uuid: 2800
handle: 0x0002, uuid: 2803
handle: 0x0003, uuid: 2a00
handle: 0x0004, uuid: 2800
handle: 0x0005, uuid: 2800

名前が「char-desc」なので紛らわしいのですが、char-desc コマンドはキャラクタリスティック構造とは無関係に ATT を取得します。また char-desc はコマンド~レスポンス1往復で動作を終えるので、BLE の1パケット(MTU サイズ)に収まる分の返答しか返ってきません。ATT の全検索を行うためには、"No attribute found within the given range" エラーが返ってくるまで、人力で「直前の返答の最終ハンドル+1」を始点ハンドルとして char-desc を反復発行しなければなりません。

[CON][00:80:92:12:34:56][LE]> char-desc 6
handle: 0x0006, uuid: 2803
handle: 0x0007, uuid: 2a23
handle: 0x0008, uuid: 2803
handle: 0x0009, uuid: 2a24
handle: 0x000a, uuid: 2803
[CON][00:80:92:12:34:56][LE]> char-desc b
handle: 0x000b, uuid: 2a25
handle: 0x000c, uuid: 2800
handle: 0x000d, uuid: 2803
handle: 0x000e, uuid: 2a19
[CON][00:80:92:12:34:56][LE]> char-desc f
handle: 0x000f, uuid: 2904
handle: 0x0010, uuid: 2902
[CON][00:80:92:12:34:56][LE]> char-desc 11
Discover all characteristics descriptors failed: No attribute found within the given range

characteristics コマンドが自動的に Read by Type を反復発行して指定ハンドル範囲内を全検索するのに、何故こんな不便な仕様になっているのか不思議ですが、そういうものだと諦めてください。
 

char-read-uuid コマンド

char-desc とよく似ていますが、指定ハンドル範囲内から指定 UUID に一致する ATT ハンドルと値の一覧を取得します。具体的には指定ハンドル範囲と UUID を引数に持つ Read by Type コマンドが発行され、範囲内にある ATT のハンドルと値の一覧が表示されます。

[CON][00:80:92:12:34:56][LE]> char-read-uuid 2803
handle: 0x2803   value: 02 03 00 00 2A
handle: 0x2803   value: 02 07 00 23 2A
handle: 0x2803   value: 02 09 00 24 2A

このコマンドも char-desc 同様、キャラクタリスティックやサービス構造とは直接無関係に動作します。またコマンド~レスポンスの1往復で動作が完結し、全範囲の検索には人力での反復発行が必要になることも同じです。
 

char-read-hnd コマンド (--char-read)

指定されたハンドルの値を読み出します。具体的には Read by Handle コマンドが発行されます。読み出した値はバイト単位にスペースで区切られた、"0x" を伴わない 16 進表示になります。

[CON][00:80:92:12:34:56][LE]> char-read-hnd 10
Characteristic value/descriptor: 00 00

Ready by Handle ではパケットの分割結合を行わないので、読み出せる値の最大長は MTU に依存します。Bluetooth 4.2 拡張以前の Bluetooth LE の Data PDU Payload size は最大 27 バイト(※註)、L2CAP MTU は最大 23 バイト、Read by Handle Response の最大データ長は ATT_MTU-1 と定義されているので 22 バイトとなります。ただし Notification/Indication 機能を併用する場合は最大データ長が ATT_MTU-3 で 20 バイトに制限されるので、GATT データ長を一律 20 バイトと定義した実装系もあります

※註:Core Spec 4.1 Vol.6 Part.B 2.4 "The Payload field shall be less than or equal to 27 octets in length." なお Core Spec 4.2 以降ではしれっと「251 octets」に書き換えられており、4.1 以前の最大ペイロード長については Vol.6 Part.B 4.5.10 "Data PDU Length Management" Table 4.3 を参照しなければ判らなくなっています。こんな仕様書の書き方やめて欲しいなぁ...。

MTU 上限を超えるサイズのアトリビュート読み書きについて、ATT レベルでは Read Blob や Prepare Write / Execute Write というコマンドで部分読み出し・部分書き込みがサポートされています。ただし BlueZ gatttool はこれらをサポートしていません。
 

char-write-req コマンド (--char-write-req)

指定されたハンドルに値を書き込みます。具体的には Write Request コマンドが発行されます。
値のフォーマットは "0x" を伴わない 16 進のバイト列で、例えば「0x12 0x23」を書き込むためには 1234 と指定します。また Bluetooth 世界では Little Endian が標準なので、16bit 幅や 32bit 幅のアトリビュート値は下位バイトが先に来ます。例えば Notification 機能を有効化するために Client Configuration の bit0 を立てるためには、該当ハンドル(char-read-uuid で UUID=2902 を検索) に 0100 を書き込む必要があります。

[CON][00:80:92:12:34:56][LE]> char-write-req 10 0100
Characteristic value was written sucessfully
[CON][00:80:92:12:34:56][LE]> char-read-hnd 10
Characteristic value/descriptor: 01 00

ハンドルには 0x を付けても付けなくても常に 16 進数としてパースされるのに値には 0x を付けると Length エラーになるとか、char-read での value 表示はバイト単位に区切られるのに char-write 時には連続で指定しなければならないとか、そもそも読み出しが char-read-hnd なのに書き込みがchar-write-req だとかあいちこち対称性が欠けていて気持ち悪いのですが、そういうものだと諦めてください。とにかくユーザーインターフェースが不親切で無愛想でたまに不合理なのは BlueZ project の持病なのです。

まとめ

今回は前回よりも多く具体例を入れて GATT を解説してみました。それだけに「何で同じものが階層によってグループと呼ばれたりサービスと呼ばれたりするんだよ」「ATT コマンドと GATT コマンドも定義が微妙に違ってわかりにくい」「gatttool のパースや表示フォーマット仕様が一貫していないので尚更わかりにくい」といった愚痴成分も多くなりました。まぁコンピュータの世界はどんな分野にも「おやくそく」があって、その世界の中では「常識」だけども外から来た人にはいちいち引っ掛かり「なんじゃこれ」「わからん」となるものです。
Bluetooth LE の GATT は多少癖があるものの、これでも Bluetooth Classic の SDP に比べればずっとシンプルになって洗練されています。SDP は構造体の入れ子構造などが表記可能になっており、メンバもオプション指定が可能だったりして、むしろ SNMP の ASN.1 BER に近いフォーマットになっています。
なお GATT は必ずしも Bluetooth LE の専売特許ではなく、Bluetooth Classic 上でも実装運用可能なことになっていますが、たぶん「GATT over Bluetooth Classic」なんて実際に使っている人は殆どいないと思います。こういう「名前だけの標準」が山ほどあるのも Bluetooth の悪い癖で、余所から来た人には「なんじゃこれ」「わからん」という印象を与えますが、そういうものだと諦めてください(今回はこの台詞が何度も出てきましたね)。

関連リンク

関連記事

製品のご購入・サービスカスタマイズ・資料請求など
お気軽にお問い合わせください