はじめに
BLEの予備知識ゼロの状態からスタートして、BLE機器と通信するReact Nativeアプリを作るまでの道のり(経験談)を紹介します。
まずはBLEについて知る
身も蓋もないですが、まずはBLE自体をある程度理解しておく必要があります。
教科書的な本として「Bluetooth Low Energyをはじめよう」がおすすめです。BLE関連の記事ではよく紹介される定番の本です。React Nativeアプリ開発者なら、前半の4章まで読めば大体十分です。
また、以下のWebサイトもあわせて読み進んでいくとBLEの概要を理解しやすいです。初心者にもやさしめに解説されていて、「はじめよう」本を一度読んだだけでは理解が難しかった人の補助として役立ちます。
https://micro.rohm.com/jp/techweb_iot/knowledge
半導体メーカーのロームさんによる技術情報サイトです。
https://fielddesign.jp/technology/ble/
無線技術や組み込み製品などを扱うフィールドデザインさんによる技術コラムです。
ペリフェラルが備えているプロファイルなどを調べる
BLEの概要が理解できたら、次にアプリと通信させたいペリフェラルが備えているプロファイルやサービス、キャラクタリスティックの具体的な内容を調べる必要があります(もうこれらの用語は通じますよね?)。
Bluetooth SIGの公式サイトにあるGATT Specificationsのページで、プロファイルとサービスの仕様書PDFがダウンロードできます。これを参照すると、「このキャラクタリスティックの値は、何ビット目から何ビット目が○○を表していて・・・」といったことが分かります。これを知らないと、アプリが何かしら通信できたとしても、読み書きした値が一体何なのかが分かりません。
サービスやキャラクタリスティックのUUIDは、GATT ServicesのページやGATT Characteristicsのページの表に載っています。
React Native用のBLEライブラリを使ってアプリを実装する
ここまでの下調べができたら、ようやくアプリの実装を始めます。React NativeにおけるBLEのライブラリとしては、以下の2種類が比較的メジャーのようです。
- https://github.com/innoveit/react-native-ble-manager
- https://github.com/Polidea/react-native-ble-plx
私はreact-native-ble-managerしか使ったことがないのですが、ざっと見た感じでは大差はなさそうです。ただ、ble-managerにだけ明示的にボンディングをするAndroid専用メソッドがあり、これが役に立つ場面がありました。詳細は後述します。
ble-managerのインストール方法やメソッドの使い方は、基本的に公式ドキュメントを読めばいいだけです。TypeScript用の型定義も含まれているので、TypeScriptでアプリを実装する場合は助かります。
まとめと補足
というわけで、BLE初心者がReact Nativeアプリで通信をするまでの道のりは以上の通りです。
結局、ライブラリの使い方はドキュメントを読めばいいだけで、それ以前のBLEそのものに対する理解の方が大切です。BLEの基本的な事項を理解していれば、ライブラリを具体的にどう呼べば良いかが自然と判断できるようになります。
以下、補足としてreact-native-ble-manager固有の使い方のコツを書いておきます。
AndroidにおけるボンディングはcreateBondを使った方がスムーズ
ペアリング(およびボンディング)をしていないペリフェラルに対して暗号化通信が必要なキャラクタリスティックを読み書きしようとすると、ペアリングするためのモーダルや通知をiOSやAndroidが自動的に出してくれます。そのため、明示的にボンディングする処理は必須ではないのですが、Androidの場合は注意が必要です(このあたりについてはble-plxのドキュメントでも触れています)。
iOSだと、読み書きしようとする → OSがモーダルを出す → アプリユーザーがボタンを押す → 読み書きが成功する、という風に何事もなく流れます。
ところがAndroidだと、アプリユーザーがボタンを押しても、直後に読み書きの成功はしませんでした。それどころか、write呼び出しで返されたPromiseが**rejectすらされないまま(失敗の確定すらしない)**になりました。そうなると、適当なタイムアウト時間によってdisconnectし、改めてscanから順番に呼び出して、writeし直す必要がありました。先ほどボンディング済みなので、2回目のwriteはさすがに成功します。
これは結構しんどいですが、回避策がありました。Androidに限り、createBondというメソッドを使うと良いです。試したところ、このメソッドは以下の挙動をしました。
- ボンディング未了の状態で呼ぶと、Androidがモーダルを出しつつ、メソッドが返したPromiseが直ちにrejectされる。
- ボンディング済みの状態で呼ぶと、Promiseが直ちにresolveされるだけ。
これを踏まえると、ほどほどにwaitを挟みながら、resolveされるまでcreateBondを繰り返し呼び、resolveされたらwriteやreadを呼ぶ、というリトライ処理を作ると良い感じになります。UXがiOSに似たような感じになり、アプリユーザーがモーダルのボタンを押したあと、1回で読み書きを成功させられます。
startNotification、stopNotificationはIndicationにも対応している
これらのメソッドは、名前からは読み取りにくいですがIndicationにも対応しているようです。なので、「Indicatioはどうやるの?」と戸惑う必要はありません。
キャラクタリスティックの値はnumber型の配列で扱うらしい
read、write、BleManagerDidUpdateValueForCharacteristicイベントで扱うキャラクタリスティックの値については、ドキュメントを見てもざっくりByte array
としか書いていなかったり、TypeScriptの型もanyだったりして、どういう型なのか一見分かりません。試したところnumber[]のようで、それによってバイト列を表現しているようです。配列の要素が普通のnumberなので、0〜255以外の値を型エラーにはできないのですが、そこはプログラマが気をつけるしかなさそうです。
writeの引数となるnumber[]の値を作ったり、readやBleManagerDidUpdateValueForCharacteristicで受け取ったnumber[]を解釈するには、JavaScriptにおけるバイナリデータの扱い方を知っておく必要があります。具体的には、ArrayBufferやDataViewあたりを使うと便利です。使用例として以下の記事が参考になりました(感謝)。
DataViewを使う場合、例えば以下のようにすると最終的に必要なnumber[]を得られます。
const buffer = new ArrayBuffer(8) // 例として8バイトの領域を確保する。
const view = new DataView(buffer)
// 中略(ここでDataViewのメソッドを呼んでデータをセットする)
// いったんUint8Arrayに変換する。
const unsignedInt8Array = new Uint8Array(view.buffer)
// Uint8Arrayは配列風オブジェクトなので、Array.fromを使って普通の配列に変換できる。
const numberArray = Array.from(unsignedInt8Array)
また、キャラクタリスティックによっては複数のバイト領域に1個の値を格納しますので、その場合はバイトオーダー(リトルエンディアンかビッグエンディアンか)を意識しながらDataViewのメソッドを呼ぶ必要があります。とはいえBluetoothにおいてはリトルエンディアンが標準っぽいです(多分)。Bluetooth SIGのサイトでサービスやキャラクタリスティックの仕様を確認すれば、より確実でしょう。