Bluetooth Classic検索のはなし
Bluetooth Classicの動作原理はBluetooth LEとは似て非なる別物です。登場時にはPAN(Parsonal Area Network)の本命として期待されたBluetoothですが、結局その役割はBluetooth LEに譲り、今ではヘッドセットとキーボード・マウスのプロトコルとして細々と生き残っている感があります。周波数拡散下でのデバイス検索やSDPプロトコルの解説もネットでは見なくなってきました。今回はこのお話です。
Bluetooth Classicの基礎
まず最初におさらいから。Bluetooth Classicは1台の「マスター」下に最大7台の「スレーブ」が同時接続する「ピコネット」というトポロジーを持ちます。通信方式は周波数ホッピング(Frequency Hopping, FH)の周波数拡散で、2402~2481MHzまで1MHz幅79個の「チャンネル」があり、625μ秒間隔=1600回毎秒でチャンネルを切り替えます。チャンネル切り替えは擬似乱数パターンで、「ピコネットマスターのMACアドレス24bit(アクセスコード, IAC)」と「ピコネットマスターのフリーランニングカウンタ(クロック, CLK)(※註1)」の2つをシード(種)として生成されます。これによって複数のピコネットが隣接して稼働しても、チャンネル衝突は確率的にしか起きない仕組みになっています。
(※註1) Bluetooth Classicのクロックは625μ秒間隔=16KHzではなく、その2倍の312.5μ秒間隔=32KHzを使います。以下に説明するように、1スロット間に2回送信する特殊なケースがあるためです。
(図) 周波数ホッピングの原理図
Bluetooth LEでは40チャネル中3つ(37=2402MHz,38=2426MHz,39=2480MHz)のチャネルが「Advertising Channel」として検索専用に使われますが、Bluetooth Classicでは未初期化デバイスの検索にもホッピングが使われます。しかしホッピングシーケンスは疑似乱数なので、擬似乱数系列のシード値である「MACアドレス」「カウンタ」の2つの情報が無ければ同期できません。まるでニワトリタマゴ問答ですが、Bluetooth Classicはどうやってこの問題を解決しているのでしょうか?
Bluetooth Classicのデバイス検索
Bluetooth Classicのデバイス検索は「Inquiry」「Page」「Service Discovery」という3段階の手順を踏みます。Inquiryは「どのピコネットにも所属していないデバイスの検出と初期通信」、Pageは「デバイスのピコネットへの接続」、Service Discoveryは「ピコネットに接続したデバイスの情報取得」という機能になります。
(図) Bluetoothのデバイス検索手順
色の違いはホップシーケンスの違いを表している
青=Inquiryシーケンス(GIAC)
緑=Pollシーケンス(スレーブ側IAC)
赤=ピコネットシーケンス(マスター側IAC)
Inquiry手順
まずInquiryの場合、「Inuquiry専用」のIACが使われます。これにはGIAC(General Inquiry Access Code=0x9E8B33)とLIAC(Limited Dedicated Inquiry Access Code=0x9E8B00)の2つがあり、用途に合わせて使い分ける...ことになっていますが、現実にはほぼGIACのみが使われています。
Inquiryを受信するデバイス(応答側)は「少なくとも2.56秒毎に1回」の周期で、GIACとクロックによって算出される32チャンネルのうち1つを、「少なくとも10.625ミリ秒間」を受信モニターします。これを「Scan Window」と呼びます。クロックは12ビット右シフトして使うことが規定されており、312.5μ秒x4096=1.28秒毎に「ゆっくりと」モニター周波数が切り替わってゆきます(Core Spec 5.2 Vol.2, Part B 8.4.1)。
(図) Inquiry Scan Windowとホッピング
この例では1024スロット=0.64秒ごとにInquiry Scanを行っており、2スキャンごとに周波数がホップしている。TGAP101などの表記はBluetooth Core Specでの定義。
Inquiryを送信するマスター(検索側)は、GIACで定義される32チャンネルをもっと早い周期で巡回しながらInquiry Message=IDパケットを送信します。Bluetooth通信手順のなかでは例外的に1スロット内でも周波数ホップを行い、1スロット中で2つの順列チャネルに2度の送信を行います。その次のスロットではやはり1スロット中で2つの順列チャネルを受信し、スレーブからのInquiry Response=FHSパケットを待ちます。Scan Window期間中にInquiry Message=IDパケットを受信したデバイスは、その受信時刻から+625μ秒後にInquiry Response=FHSパケットを返送します。
(図) Inquiry Scan-Responseの例
スレーブ側はInquiry Window中でch=10を10.625msec間受信している。マスター側は1250μsec間に2チャンネルずつInquiry Scanを送信し、チャンネルが一致するとスレーブからInquiry Responseが返される。
Inquiry Message送信・Inquiry Response返信もホッピング規則に従うので、異なるチャネルで行われます。GIAC 0x9E8B33から発生するチャネル数列は、偶数スロット(マスター→スレーブ、Inquiry Message発信チャネル)が
[43 59 27 77 45 61 29 00 47 63 31 02 49 65 33 04 51 67 35 06 53 69 37 08 55 71 39 10 57 73 41 75]
奇数スロット(スレーブ→マスター、Inquiry Response返送チャネル)が
[16 44 12 56 24 52 20 50 18 46 14 58 26 54 22 64 32 60 28 72 40 68 36 66 34 62 30 74 42 70 38 48]
になります。これは必ずペアで使われ、例えばチャネル47で発信されたパケットの返答は必ずチャネル18で返送されます。
Inquiryのホッピング手順は少々特殊で、32チャンネルを16チャンネルの「A-Train」「B-Train」に分割し、A⇔Bの切り替えは「少なくとも256回のチャネル巡回」後でなければならないと規定されています(Core Spec 5.2 Vol 2, Part.B 8.4.2)。256回のチャネル巡回には
625usec x 2(T+R) x 16(ch/train) x 256(Repeat) = 5.12sec
かかりますから、32チャンネルを全スキャンするためには10.24secかかる計算になります。BlueZのhcitoolコマンドで「hcitool scan」を実行すると10秒くらいかかるのは、この仕様規定によるものです。
Inquiryでわかる情報
Inquiry Responseに使われるFHSパケットは下記のような構造を持っています。
(図) FHSパケットの構造
FHSは全ビット長144bitで、ACLにおけるDM1パケットと同じ16bit CRC+2/3 FECのエンコードで送信されます。
発信元アドレスのMACアドレス 48bitは最下位24bitのLAP、最上位16bitのNAP、その中間8bitのUAPに分かれて通達されます。この中でLAPはホップシーケンスを決定するアクセスコードに使われます。
CLKはFHS発信時点における送信元のクロック値です。アクセスコード(=LAP)とCLK値からホップシーケンスを生成することができ、つまりFHSの受信以後はそのノードのホップシーケンスに同期して周波数を切替えてゆくことが可能になります。
COD(Class Of Device)はBluetooth 2.0以前で唯一、Inquiry Responseから返送元の素性を知ることのできる情報でした。CODは3階層に分かれており、
Bit 23-13: Major Service Class (bitfield)
Bit 12-8: Major Device Class
Bit 7-0: Minor Service Class
となっています。例えばCOD=0x0200408の場合
Bit 13=1: Service Class "Audio"
BIt 12-8=0x04: Major Device Class="Audio/Video"
Bit 7-0=0x08: Minor Device Class="Hands-Free device"
となります(※註2)。GUIを持つ実装系では、検出されたBluetoothデバイスのアイコン(PC、携帯電話、ヘッドフォン、キーボードなど)をCODから参照することが多いようです。逆に言えば、それ以外にはあまり用途がありません。
(※註2) CODの定義については https://www.bluetooth.com/specifications/assigned-numbers/baseband/ を参照してください。
CODはこのように分かりにくいし、正直言ってあまり役に立たないので、Bluetooth 2.1からは「Extended Inquiry Response(EIR)」が拡張されました。EIRはその機能を示すビットです。EIR=1のFHSパケットが返信された場合、その2スロット後にACLパケット形式(DM1, DM3, DM5, DH1, DH3, DH5のどれか)で「拡張Inquiry Response情報」が追加で送信されます。EIRのフォーマットはいわゆるTLV形式で、Core Spec 5.2 Vol.3, Part C Section 8に定義されています。
EIRタグは60種類くらいが定義されていますが(※註3)、本当に使われているかどうか怪しいものが大半を占めます。そもそもEIRじたいどれだけ実際に活用されているのかよくわかりません。たぶん一番有用な情報は「デバイス名(タグ0x08または0x09)」だと思いますが、全てのBluetoothデバイスにEIRが実装されている保証もなければ、EIRでデバイス名が返される保証もないので、あまり活用されていないと感じます。
(※註3) タグ値の定義はCore Specには記されておらず、Bluetooth SIGのAssgined Numbers https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ から参照する必要があります。
とにかく、こうしてBluetoothマスターはInquiry手順で10秒かけて周波数を巡回し、Inquiry Response=FHSパケットから、周辺にあるデバイスの情報(MACアドレス、CLKオフセット、Class Of Device)を集めることができました。それ以上の情報を得るためには、マスター・スレーブ接続を行わなければなりません。そのための手順がPageになります。
Page手順
デバイスは、自身のIACによって定義される32チャネルのうち1つを受信モニターしています。この受信チャネルもホッピングシーケンスで、1.28秒間隔でゆっくりと切り替わってゆきます。この状態を「Page Scan State」と呼びますが、デバイスがどの間隔で・どれだけの時間Pageチャネルをモニターするかはけっこう複雑で、"Standard scan"と"Generalized interlaced scan"の2モードがあり、更にPage Scan Repetition Mode(FHSのSRビットで示される)にR0, R1, R2のモードがあります。
話を進めるため、最も単純なシナリオを仮定します。まだピコネットに所属していないデバイスは常時Page Scan Stateにあり、Page受信を待ち続けています(ただしInquiry Windowの期間を除く)。また、マスターは直前のInquiry手順を通じてスレーブから受信したFHSからIACとCLKを入手しており、スレーブがモニターしているはずのPage受信チャネルを算出することができます。
(図) Inquiry ScanとPage Scanが並行動作しているスレーブの様子
43-59-27...というホップシーケンスはGIACから、
24-56-53...というホップシーケンスはIAC(0x112233)から生成されている
マスターはPage Request=IDパケットを送信し、スレーブはPage Response=FHSパケットで応答します。このやり取りはInquiryにそっくりですが、違うのはホッピングシーケンスがGIACではなくスレーブのIACに基づいて行われることです。
かくして「スレーブがホッピング主導権を持った」状態で、マスターはFHSパケットを送信します。ここにはマスターのIACとCLKのほかに、スレーブがピコネット内で持つ3bitのアドレス(Logical Transport Address)が含まれます。スレーブはこれに対してIDパケットで応答し、これ以後はホッピングを「マスターのIACとCLKから算出されるシーケンス」に切り替え、マスターのピコネット配下に入る「Connection State」に移行します。
「最も単純」でないシナリオでは、「マスターがスレーブのIACだけ持っていてCLKが判らない状態」があり得ます。これは一度ペアリングしたマスターとスレーブが電源再投入されたような状況です。この場合、マスターはInquiry手順を介さずに、IACから算出される「スレーブがモニターしているであろうチャネル」に片っ端からPage Request=IDパケットを送ってスレーブを検索する手順が定義されています。このときに使われるホッピングパターン(A/B-Train)や繰り返し回数(16チャネルx256回xA/B Train)はInquiryとほぼ同じ手順(※註4)が定義されています。
(※註4) Bluetooth Core SpecではPage Scanの解説が先にあり、Inquiry ScanについてはPage Scanとの違いをもって解説されています。
Page手順をもってデバイスはピコネットマスターのホッピングパターンとクロックに同期し、通信に必要な論理アドレスも割り当てられ、いつでも通信できる状態になります。この次のService Discoveryは通常のマスター・スレーブ間通信(ACL)上のやり取りになります。
BlueZ上でのinquryとpage
Linux BlueZにおいてはhcitool inqを用いてInquiry、hcitool scanを用いてInquiry + Pageの実行ができます。なおBlueZのコマンドの多くは古い仕様のまま止まっているので、inqを用いてEIRを取得することはできません。
hcitool inq
Inquiring ...
A4:FC:77:41:0F:D2 clock offset: 0x0ed6 class: 0x0a010c
C8:58:C0:C1:7F:18 clock offset: 0x0007 class: 0x2a010c
14:C1:4E:2D:96:42 clock offset: 0x0624 class: 0x5a020c
EA:0A:EF:0D:92:F5 clock offset: 0x65a6 class: 0x240404
hcitool scan
Scanning ...
A4:FC:77:41:0F:D2 n/a
C8:58:C0:C1:7F:18 Windows PC
14:C1:4E:2D:96:42 Chromecast
EA:0A:EF:0D:92:F5 LC-B41
inq, scanともデフォルトではGIAC=0x9E8B33が使われます。オプション--iac=0x9E8B00を付けることでLIACでのinq/scan動作も可能ですが、多分応答するデバイスは見つからないと思います。
SDPサービス検索
SDP(Service Discovery Protocol)はデバイスが持つ「サービス」情報を読み出すプロトコルです。サービスはService Recordという単位で格納され、1つのService Recordは32bit整数のService Record Handle(RH)に紐づけられます。Service Recordは複数のService Attributeを含み、Service Attributeは「名前」「型」「値」からなるデータ単位です。Service Attributeの名前は16bit幅のAttribute ID(AID)です。紛らわしいのですがAIDはUUID16ではなく16bitの整数で、数字と機能の対応はBluetooth SIGが管理しています(※註5)
(※註5) https://www.bluetooth.com/specifications/assigned-numbers/の"Service Discovery"に一覧があります。
(図) Service Recordの例
この例ではデバイスが4つのサービスを含み、その中の1つのサービス(RH=0x10000)を取り出すと6つのService Attributeが含まれています。AID=0は自分自身のRHを保持しており、AID=1はそのサービスの名前(UUID16=0x1200, "Plug and Play Information Service")、AID=6は言語情報として"en, UTF-8"を持ち、その言語情報で記された文字列アトリビュートがAID=0x100, AID=0x101に格納されています。
AID=0(ServiceRecordHandle)が32bit UINT、AID=1(ServiceClassIDList)がUUIDの配列(Sequence)、AID=6(LanguageBaseAttributeIDList)が16bit UINTの配列というように型を持っていますが、これは各サービスのプロファイルによって定義されています。全てのサービスに共通するAIDについてはBluetooth Core Spec 5.2 Vol.3 Part B Section 5に定義されています。AIDの型はProtocolDescriptorList(AID=4)のように、異種構造体が入れ子になった構造体という複雑な型を取る場合もあります。
(図) ProtocolDescriptorListの例
この例では((L2CAP, PSM=1), (SDP))という構造をなしており、「L2CAP(UUID16=0x0100)というプロトコルのPSM=1(ポート番号に相当)で、SDPという名前のプロトコル(UUID=0x0001)が動いている」ことを意味しています。
このように、Bluetooth SDPは「サービスレコードハンドル(RH, 32bit整数, 値は実装依存)」「サービスレコードUUID(UUID16, Bluetooth SIGにて管理)」「アトリビュートID(AID, 16bit整数, 値はBluetooth SIGにて管理)」という、「名前」「名前の名前」「名前の名前を指す名前」が混在しているのでわかりにくいです。個人的には、Bluetooth LEが使うGATTよりもめんどくさいと感じます。慣れると読めるようになってきますが、少しBluetoothを離れているとすぐに忘れてしまいます。
SDPプロトコル
SDPプロトコルは「リクエスト」と「レスポンス」がペアになっており、3種類のリクエストに対応する3種類のレスポンス+エラー応答の合計7種類の「PDU ID」が規定されています。
PDU ID | PDU Function | Parameters | 解説 |
---|---|---|---|
0x00 | Reserved | 予約 | |
0x01 | SDP_ErrorResponse | ErrorCode, ErrorInfo |
エラー応答 |
0x02 | SDP_ServiceSearchRequest | ServiceSearchPattern, MaximumServiceRecordCount, ContinuationState |
UUIDに対応するレコードを検索する |
0x03 | SDP_ServiceSearchResponse | TotalServiceRecordCount, CurrentServiceRecordCount, ServiceRecordHandleList, ContinuationState |
検索応答(RHの一覧が返る) |
0x04 | SDP_ServiceAttributeRequest | ServiceRecordHandle, MaximumAttributeByteCount, AttributeIDList, ContinuationState |
RHに対応するレコードの 指定範囲のアトリビュートを読み出す |
0x05 | SDP_ServiceAttributeResponse | AttributeListByteCount, AttributeList, ContinuationState |
サービスレコード読み出し応答 |
0x06 | SDP_ServiceSearchAttributeRequest | ServiceSearchPattern, MaximumAttributeByteCount, AttributeIDList, ContinuationState |
UUIDに対応するレコードを検索し、 指定範囲のアトリビュートを読み出す |
0x07 | SDP_ServiceSearchAttributeResponse | AttributeListsByteCount, AttributeLists, ContinuationState |
サービスレコード読み出し応答 |
(表)SDP PDU一覧
Bluetoothマスターの実装では、主にSDP_ServiceSearchRequestないしSDP_ServiceSearchAttributeRequestを発行して「自分が利用したい機能」の有無を調べます。例えばスマートフォンがヘッドセットに接続する場合は
0x1108 ヘッドセットプロファイル(HSP)
0x111E ハンズフリープロファイル(HFP)
0x110B オーディオ配送プロトコル(A2DP Sink)
0x110C AVリモートコントロール(AVRCP Target)
0x110F AVリモートコントロール(AVRCP Controller)
のサービスUUIDを検索し、
・通話用ヘッドセット
・通話発着信制御
・オーディオヘッドセット
・オーディオ再生制御
の対応の有無を調べることになります。逆に言えば、Inquiryで返ってきたCODからService Class, Major Device Class, Minor Device Classを読み取って「これはHands-Free deviceである」という情報がわかっても、具体的にどんな機能を備えているかはSDPを読んでみなければ判らないわけです。「CODは正直言ってあまり役に立たない」というのはそういうことです。
BlueZ sdptoolについて
Linux BlueZではsdptoolを用いてSDPの動作を見ることができます。例によって例のごとくインターフェースが不愛想で統一性が無かったり、古い仕様のまま実装が凍結されたりしていますが、BlueZの持病なので諦めてください。
sdptool get --bdaddr <アドレス> <サービスレコードハンドル>
SDP_ServiceAttributeRequestを用いて、<サービスレコードハンドル>で指定されたRHを持つサービスレコードを読み出します。プロトコル仕様上SDP_ServiceAttributeRequestではAttributeListを指定できますが、sdptoolからは指定できません(常に全アトリビュートを読み出して返します)。<サービスレコードハンドル>は0xを付けても付けなくても常に16進として解釈されます。
# sdptool get --bdaddr C8:58:C0:C1:7F:18 0x10000
Service Name: Device ID Service Record
Service Description: Device ID Service Record
Service RecHandle: 0x10000
Service Class ID List:
"PnP Information" (0x1200)
Protocol Descriptor List:
"L2CAP" (0x0100)
PSM: 1
"SDP" (0x0001)
Language Base Attr List:
code_ISO639: 0x656e
encoding: 0x6a
base_offset: 0x100
sdptool search --bdaddr <アドレス> <サービス名/サービスID>
SDP_ServiceSearchAttributeRequestを用いて、<サービス名/サービスID>で指定されたサービスUUIDを持つサービスレコードを読み出します。SDP_ServiceAttributeRequestではAttributeListを指定できますが、sdptoolからは指定できません(常に全アトリビュートを読み出して返します)。<サービス名/サービスID>は文字列または数値で指定し、0xで始まる場合はサービスIDとして、それ以外の場合はサービス名として解釈されます。
# sdptool search --bdaddr C8:58:C0:C1:7F:18 0x1200
Class 0x1200
Searching for 0x1200 on C8:58:C0:C1:7F:18 ...
Service Name: Device ID Service Record
Service Description: Device ID Service Record
Service RecHandle: 0x10000
Service Class ID List:
"PnP Information" (0x1200)
Protocol Descriptor List:
"L2CAP" (0x0100)
PSM: 1
"SDP" (0x0001)
Language Base Attr List:
code_ISO639: 0x656e
encoding: 0x6a
base_offset: 0x100
Searching for 0x1200 on C8:58:C0:C1:7F:18 ...
Service Search failed: Invalid argument
searchコマンドで指定する「サービス名」はレスポンスで表示される文字列とは異なります。例えば0x1200は"PnP Information"と表示されますが、sdptool searchで指定する場合は"DID"(大文字小文字区別なし)がUUID16=0x1200と等価になります。
サービスUUIDもまたBluetooth SIGで管理されています。紛らわしいのは、同じ名前空間のなかで「プロファイル」と「サービスセット」が一緒くたに定義されていることです。全てのサービスハンドルはサービスUUIDを持ちますが、それ以外の複数のUUIDにも紐付けられます。そして必ずしも「自身のUUID=プロファイルID」「外部から紐付けられるUUID=サービスセットUUID」とは限りません。
たとえばUUID=0x1108は"Hands Free Profile(HSP)"ですが、これは「プロファイル」でもあり「サービスセット」でもあります。一方UUID=0x1203は"Generic Audio"で、これは「サービスセット」ですが「プロファイル」ではありません。
逆にUUID=0x110Dは再生元・再生先に関わらず"A2DP"プロファイルで、UUID=0x110Dで検索するとA2DP Source再生元)とA2DP Sink(再生先)の両方のサービスレコードが出てきます。A2DP SourceにはUUID=0x110A、A2DP SinkにはUUID=0x110BのサービスセットIDが定義されており、これを用いれば「A2DP再生側」などを名指しで検索することができます。
(図) サービスレコード紐付けの例
ここでもまた「レコードの名前」「名前の名前」「名前の名前の名前」みたいな関係があって、それがデータ構造としての階層化に紐付いておらず、十把一絡げのUUID16空間に並んでいるのがBluetooth SDPのわかりにくい所だと思います。少なくともこの点に関しては、Bluetooth LE GATTの「アトリビュート」「キャラクタリスティック」「サービス/グループ」の方が整理されていてわかりやすいと思います。
sdptool browse <アドレス>
SDP_ServiceSearchAttributeRequestを用いて、デバイスの持つ全てのサービスレコードを列挙します。sdptool search --bdaddr <アドレス> 0x1002と同じ動作です。サービスUUID=0x1002は特殊なUUIDで、全てのサービスレコードが「該当」として列挙される"PublicBrowseRoot"として定義されています。ただし、必ずしも全てのBluetooth機器がPublicBrowseをサポートしている訳ではありません。
sdptool records <アドレス>
SDP_ServiceAttributeRequestを用いて、デバイスの持つ全てのサービスレコード列挙を試みます。PublicBrowseに対応していないSDPサーバでも列挙できるかも知れません。
「試みる」「かも知れない」という微妙な表記になっているのは、このコマンドがレコードハンドルを手当り次第に読み出してみるという強引な実装だからです。読み出しを試みるレコードハンドルの基底値は
0x10000, 0x10300, 0x10500, 0x1002e, 0x110b, 0x90000, 0x2008000, 0x4000000, 0x100000, 0x1000000, 0x4f491100, 0x4f491200
としてハードコーディングされており、各base値から+0~+31までを引数としてSDP_ServiceAttributeRequestを発行し、SDP_ServiceAttributeResponse応答があればそれを表示するという実装です。なので他のsdptoolコマンドより実行が遅く、数十秒かかる場合があります。
BlueZ sdptoolではSDP_ServiceSearchRequestを発行することはできません。
sdptoolのコマンド毎に--bdaddrが付いたり付かなかったりバラバラなのは、古いバージョン(4.0未満)のBlueZではsdptoolを用いて「自分自身」のSDPサーバを検索したり、サービスレコードを追加・削除(add/del/setattr/setseq)する機能があった盲腸です。BlueZ 4.0以降ではBlueZ APIはD-BUSに偏重してゆきレガシーAPIはどんどん廃止されたので、sdptoolのローカルブラウズ機能やレコード編集機能も今では動作しません。動作しないけれど何故か実装だけ残されています。
まとめ
今どき役に立つかどうかはわかりませんが、Bluetooth Classicの検索手順(Inquiry, Page, SDP)を簡単にまとめてみました。Inquiryの同期捕捉手順やSDPの構造は、今から見ると「考えすぎ」な感もあり、比較してみるとBluetooth LEで何がどう簡易化されたのかよくわかります。とはいえ、そのBluetooth LEも5以後で2MbpsモードやSecondary Advertisingや、「これ本当に誰か使うのかよ」という機能拡張がなされていますが...無線規格というのは時間とともに複雑化して盲腸がどんどん増える宿命なのでしょうかね。