BLEプログラムはじめの第一歩
周囲にたくさんあるBluetoothデバイス。いろんなデバイス持ってると一度ぐらいは「自分のiPhoneでデバイスにつないでデータ集計とかしたい!」という気持ちになったことがあるのではないでしょうか?
とはいえよく分からない世界。接続程度ならAPIリファレンスやサンプルを見れば察しはつくのですが、どういうデータが飛んでくるのかがさっぱりです。
そこで今回のテーマは Bluetooth初心者がBluetooth接続してデータを読み取るまで。コードよりもコードを書く前の前提知識です。今回はBluetoothな中でもセンサー系に多いBluetoothLowEnergy(BLE)、さらにその中でも仕様が簡単な心拍計にフォーカスし、簡単な用語や仕様の調べ方を解説します。
BLE初学者なので多少間違いあるかもしれませんが、参考になれば幸いです。
基本用語解説
大雑把に基本用語の解説です。
- Peripheral
- 一般的なセンサー系デバイスと思ってたぶん大丈夫
- センシングしたデータを通知する
- Central
- Peripheralに接続し、値を読み取る
- アプリ制作の場合、普通はCentralを作る(Peripheralの場合もありはする)
- Service
- Characteristic
- Serviceに含まれるデータのこと
- 一つのサービスは複数のCharacteristicを持つ
- UUID
- 上の全部の識別につかうID
- 128bitの文字列表現
- 例: 00001816-0000-1000-8000-00805F9B34FB
- blutooth.org登録済みのものは 4文字ショートカット で取り扱える(場合がある。128bitフルが要求されることも)
- https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf
- 4文字ショートカットの128bit化は
0000XXXX-0000-1000-8000-00805f9b34fb
センサーに接続するアプリの場合、基本的な流れは以下のようになります。
- 欲しいServiceUUIDを指定し、デバイス(Peripheral)をスキャン
- 見つかったデバイスに接続
- Service一覧を取得する
- Serviceの対応Characteristic一覧を取得、欲しいCharacteristicを見つける
- Characteristic に対してRead。バイト配列がくる
- データ種別に応じてバイト配列をパース、表示
- 必要に応じて監視する
//表記簡略化のため、Swift風架空の言語 & APIです。 //実際はdelegateでのcallbackになります let peripherals: [Peripheral] = await bleManager.scanPeripherals(ServiceUUID) for p in peripherals { //接続し、保持してるサービスを取得 await p.connect() let sercives: [Services] = await p.discoverServices() guard let target = services.first(where: {$0.UUID == ServiceUUID}) else { break } //サービスがサポートしてるChar取得し、読み取り let chars: [Characteristic] = await target.discoverChars() for c in chars { let data: ByteArray = c.read() //UUIDに応じバイト列をパース switch (c.uuid) { case .... } } await p.disconnect() }
書くと簡単ですが、調べている最中はここにたどり着くことすら苦労しました...
BLE仕様を見る
この記事のメインテーマ、BLEのUUIDや各バイト列のフォーマットの調べ方です。最初はどこを見ればいいのかさっぱりでしたが、わかりさえすれば簡単です。
今回は比較的データ仕様がシンプルな、心拍数センサ を例に取り上げます。
といっても答えは簡単。Bluetooth.orgのドキュメントを見るだけです...が、PDFでずらっと並んでで分かりづらい。本当にこれの見方がまだ分かってないです。ありがたいことに、これのXML仕様を全部保持してるリポジトリがあるので全部ダウンロードしておきます。(昔はこのXMLが公式に公開されてたようです。明らかにXMLのほうが読みやすい。)
仕様調査
まずはServiceを調べます。org.bluetooth.service
で始まってて heartrate
/ beat
などそれっぽい単語があるファイルがないか調べます(※ゴリ押し)。もしくはそれっぽい単語で全文検索します。
眺めると org.bluetooth.service.heart_rate.xmlを発見。XMLの中身を見ていきましょう。
Service
まずは先頭、Service
タグです(以後長いので省略/簡略化します)
<Service type="org.bluetooth.service.heart_rate" uuid="180D">
あっさりですね. UUID=180D であることがわかりました。これでデバイス検索できます。
さらに中を見ていくと、Characteristics
タグの中に Characteristic
タグが複数あります。ここが保持しているCharacteristic一覧の情報とみて間違いありません。
中を詳しく観察すると
<Characteristics> <Characteristic name="Heart Rate Measurement" type="org.bluetooth.characteristic.heart_rate_measurement"> <InformativeText> This characteristic is used to send a heartrate measurement. </InformativeText> <Requirement>Mandatory</Requirement> <Descriptors> <Descriptor name="Client Characteristic Configuration" type="org.bluetooth.descriptor.gatt.client_characteristic_configuration"> <Requirement>Mandatory</Requirement> <Properties> <Read>Mandatory</Read> <Write>Mandatory</Write> </Properties> </Descriptor> </Descriptors> </Characteristic> <!-- もう片方は省略 --> </Chacteristics>
説明文を読むとどうもこのCharacteristicsが実際の心拍データのようです。また Requirement=Mandatory
なので必須情報であることが伺えます。しかしまだ実際のバイト配列レベルでどういうデータのなのかはわかりません。このCharacteristic情報は必ずある、と分かっただけです。
ここから先は Characteristicの仕様ファイル に記述されています。 type="org.bluetooth.characteristic.heart_rate_measurement"
というXMLの記述とまったく 同じ名前のファイル が存在するのでこちらの解析に取り掛かります。
Characteristic
まず先頭の Characteristic
タグから
<Characteristic type="org.bluetooth.characteristic.heart_rate_measurement" uuid="2A37" name="Heart Rate Measurement"> <Value> <!-- 略してます --> <Field/> <Field/> </Value> </Characterictic>
UUID=2A37
であることが分かりました。さらに Value
タグがありこの中に実際の値に関する情報が、かつ Field
に各変数の情報があると見て間違いないでしょう。さっそく詳細に見ていきます。
まず一個目の Field
を見ます。
<Field name="Flags"> <Requirement>Mandatory</Requirement> <Format>8bit</Format> <BitField> <Bit index="0" size="1" name="Heart Rate Value Format bit"> <Enumerations> <Enumeration key="0" value="UINT8. Units: beats per minute (bpm)" requires="C1"/> <Enumeration key="1" value="UINT16. Units: beats per minute (bpm)" requires="C2"/> </Enumerations> </Bit> <!--以後省略 --> </BitField> </Field>
以下が分かります。(※ちゃんとXMLを読めば分かるのでちゃんと読んでくださいね(笑))
- 8bitである
- 1bit目がフラグ
- 0だと心拍数がByte
- requires=C1 (※次で使います)
- 1だと心拍数がShort
- requires=C2
次に二個目と三顧目の Field
を見ていきましょう
<Field name="Heart Rate Measurement Value (uint8)"> <Requirement>C1</Requirement> <Format>uint8</Format> </Field> <Field name="Heart Rate Measurement Value (uint16)"> <Requirement>C2</Requirement> <Format>uint16</Format> </Field>
簡単ですね。
- 2個目のフィールドは 8bit で心拍数を表す
- 3個目のフィールドは 16bit で心拍数を表す
とはいえ 両方の値が常にあるわけではありません。(フラグを見ていれば想像できますが...)
ここで一個目のField, Flagsにあった C1/C2
を使います。 Flagsのrequiresと、FieldのRequirementタグに同じC1/C2とある のをよく見てください。ここが肝。つまり
- flagsの1bit目が0
- requires=C1 になる
- Requirment=C1 となってるFieldは必須になる
- flagsの1bit目が1
- requires=C2 になる
- Requirment=C2 となってるFieldは必須になる
要は flag=0ならuint8のフィールドのみあり、flag=1ならuint16のフィールドのみ存在する、ということです。今回はシンプルな例なのでrequiresを見るまでもありませんが、他のBLE仕様だと込み入ってたりもするのでしっかり見ましょう。少なくとも上記のルールは把握しておいたほうがいいと思います。
読み込み部分は以下のようなコードでしょうか?
//Characteristicのバイト配列を読み込み、心拍数を返す func parse(_ bytes: Data) -> Int { let flag = Int(bytes[0]) & 0xFF if flag == 0 { return Int(bytes[1]) & 0xFF } else { let s1 = Int(bytes[1]) & 0xFF let s2 = Int(bytes[2]) & 0xFF return (s1) | (s2 << 8) } }
コード全体像
ではこれまでの情報から再びコード全体像をまとめてみましょう。まず今までの情報の整理です
- ServiceUUID=180D
- CharactericsicUUID=2A37
- 4文字UUIDの正規のフルUUID化は
0000XXXX-0000-1000-8000-00805f9b34fb
- 自分の端末/OSだと、フルUUIDを指定しないとService検索できませんでした
さっそく最初のほうにあったコードに適用します!
let serviceUUID = "0000180D-0000-1000-8000-00805f9b34fb" let characteristicUUID = "2A37" let peripherals: [Peripheral] = await bleManager.scanPeripherals(serviceUUID) guard let p = peripherals.first else { return } await p.connect() let sercives: [Services] = await p.discoverServices() guard let service = services.first(where: {$0.UUID == serviceUUID}) else { break } //Characteristic取得 let chars: [Characteristic] = await target.discoverChars() guard let ch = chars.first(where: {$0.UUID == characteristicUUID}) else { return } //データ読み込み let bytes = await ch.value let heartRate = parse(bytes) //parse関数は上記鑑賞 print(heartRate)
※実際のBleAPIはリファレンス見れば分かると思うので公式リファレンスを参照してください。
通知
上記だけではまだ足りません。というのも普通心拍計だと「ずっと表示していたい」という要件があるはずです。上記コードでは一定間隔で延々ポーリングする必要があります。
それへの対処がBleには存在し「Notification」を使います。仕組みは簡単で、有効化したら値の変更のたびに delegate に通知が飛んでくるだけです。
peripheral.setNotifyValue(true, characteristic) //CBPeripheralDelegate func peripheral( _ peripheral: CBPeripheral, didUpdateValueForCharacteristic c: CBCharacteristic, error: Error? ) { guard error == nil, let data: Data = c.value else { return } let heartRate = parse(data) //表示など }
よくある仕組み。簡単ですね。読み込んだあとの処理はどうデータを活用するかによるので省略します。
まとめ
一通りBleのデータ仕様の調べ方と簡単なコードを紹介してみました。分かるまでは森の中をさまよってるような状態でしたが、分かってしまえば決して難しくありません。特にXML仕様書は「分かりさえすればあとは単なるビット/バイト操作」にすぎないので多少プログラムの経験がある人なら余裕。上位/下位ビット(バイト)に注意するぐらいでしょうか。
むしろ作ってみて難しいと分かったのは 接続状態の管理/切断/エラー処理 です。この辺は上記コードではごっそり抜けてます。というのもまだいいやり方が分かってないからです。ここはアプリ仕様にも依存するので、一概に「こうつくればOK!」というものはないと思います。
特に個人制作の場合だと自分のデバイスにつながればOK!というパターンが多いでしょうから、デバイスUUIDを直接指定して接続するほうが処理が楽になります。
まだまだ課題は多いですが、自分専用アプリでちょっとBleデバイス使って遊んでみるぐらいのことはできるようになりました。仕様の調べ方も分かりさえすれば簡単です。
BLEのXMLファイル一覧を見ると、心拍計以外にも体重計、バッテリー、キーボード/マウスなど様々な仕様があります。ラズベリーパイなどのIOT系デバイスと組み合わせても面白いですね。ぜひ身近なデバイスに接続して遊んでみてください。