プログラマ英語学習日記

プログラミングと英語学習のまとめなど

BLEプログラムはじめの第一歩

周囲にたくさんあるBluetoothバイス。いろんなデバイス持ってると一度ぐらいは「自分のiPhoneでデバイスにつないでデータ集計とかしたい!」という気持ちになったことがあるのではないでしょうか?

とはいえよく分からない世界。接続程度ならAPIリファレンスやサンプルを見れば察しはつくのですが、どういうデータが飛んでくるのかがさっぱりです。

そこで今回のテーマは Bluetooth初心者がBluetooth接続してデータを読み取るまで。コードよりもコードを書く前の前提知識です。今回はBluetoothな中でもセンサー系に多いBluetoothLowEnergy(BLE)、さらにその中でも仕様が簡単な心拍計にフォーカスし、簡単な用語や仕様の調べ方を解説します。

BLE初学者なので多少間違いあるかもしれませんが、参考になれば幸いです。

基本用語解説

大雑把に基本用語の解説です。

  • Peripheral
    • 一般的なセンサー系デバイスと思ってたぶん大丈夫
    • センシングしたデータを通知する
  • Central
    • Peripheralに接続し、値を読み取る
    • アプリ制作の場合、普通はCentralを作る(Peripheralの場合もありはする)
  • Service
    • バイスがサポートしてるデータのカテゴリ
    • 心拍数サービス、体組成サービス、キーボードサービスなど
    • 1デバイスが複数個のサービスを保持できる
  • Characteristic
    • Serviceに含まれるデータのこと
    • 一つのサービスは複数のCharacteristicを持つ
  • UUID

センサーに接続するアプリの場合、基本的な流れは以下のようになります。

  1. 欲しいServiceUUIDを指定し、デバイス(Peripheral)をスキャン
  2. 見つかったデバイスに接続
  3. Service一覧を取得する
  4. Serviceの対応Characteristic一覧を取得、欲しいCharacteristicを見つける
  5. Characteristic に対してRead。バイト配列がくる
  6. データ種別に応じてバイト配列をパース、表示
  7. 必要に応じて監視する
//表記簡略化のため、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系デバイスと組み合わせても面白いですね。ぜひ身近なデバイスに接続して遊んでみてください。