最近、React NativeでBLEを触る案件があって、まあまあハマったというか、思い出し作業が大変だったので、自分用のメモも兼ねて思考の過程を書き出してみる。正直、BLEって「Bluetoothでしょ?」くらいに思ってると、思ったよりお作法が多くて戸惑うんだよね。
使うのは、まあ定番の `react-native-ble-plx`。これが一番情報多いし、安定してる気がする。でも、これ使ってもハマる時はハマる。特にパーミッション周りとか、AndroidとiOSでの微妙な違いとか。
TL;DR
先に結論から言うと、`react-native-ble-plx`を使ったBLE通信のキモは、「正しい順序で、非同期処理を待ってあげること」と「OSからのお許し(パーミッション)をちゃんと貰うこと」。この2つに尽きる。コードをコピペするだけだと、だいたいどっちかでコケる。
そもそもBLE通信ってどんな流れ?
コードに入る前に、頭の中を整理したい。BLEデバイスとのやり取りって、人間同士のコミュニケーションに似てるなと個人的には思ってる。
- スキャン (辺りを見回す): まず、周りに誰かいるかなーって探す感じ。これが`scan()`。
- 接続 (挨拶する): 「あ、〇〇さんだ」って見つけたら、「こんにちは」って声をかける。これが`connect()`。
- サービス発見 (何の話ができるか聞く): 挨拶したら、「今日はどんなお話ができますか?」って相手の持ってる話題(サービス)と、その詳細(キャラクタリスティック)を聞き出す。これが`discoverAllServicesAndCharacteristics()`。これ、意外と忘れがちだけど超重要。
- 通信 (会話する): 話題がわかったら、実際に「このデータくださいな (`read`)」とか、「これ書いといて (`write`)」とか、「何かあったら教えて (`notify`)」ってやり取りが始まる。
この流れを無視して、いきなり接続しようとしたり、いきなりデータを読み書きしようとしても、まあ無理だよね、っていう。BLEライブラリは、この手続きを簡単にしてくれる魔法、みたいなもの。
じゃ、実際にどうやるの? - セットアップと一番めんどくさい権限の話
ここからが本番。まずはプロジェクトにライブラリを入れるところから。
# yarnでもnpmでもお好きな方で
yarn add react-native-ble-plx
# podも忘れずに
cd ios && pod install
インストールはまあ、いい。問題はここから。パーミッション。これが最初の関門。
正直、ここが一番ややこしい。特にAndroid。バージョンによって要求されるものが変わるから、もう大変。
Androidのパーミッション地獄
Androidは本当に…うん。まず、`AndroidManifest.xml`に色々書かないといけない。
で、`AndroidManifest.xml`に書くだけじゃダメで、アプリ実行時にユーザーに許可を求めるコードも必要になる。`PermissionsAndroid`を使って、`BLUETOOTH_SCAN`と`BLUETOOTH_CONNECT`、そして`ACCESS_FINE_LOCATION`をリクエストする感じ。この辺のコードは長くなるから省略するけど、まあ定型文みたいなもの。
あ、ちなみに、`react-native-ble-plx`の公式ドキュメント(これはグローバルな情報源だよね)だけ見てるとハマることがあって。日本の開発者ブログとかを漁ってると、「国内メーカーの特定のAndroid機種だと、位置情報サービス自体がOFFになってるとスキャンが全く動かない」みたいな知見がゴロゴロ出てくる。だから、パーミッションを許可してもらうだけじゃなく、端末のBluetoothと位置情報がONになってるかもチェックする処理を入れるのが安全。
iOSは比較的シンプル
iOSはAndroidに比べれば、まだ分かりやすい。`Info.plist`に「なんでBluetooth使いたいの?」っていう理由を書いておくだけ。
NSBluetoothAlwaysUsageDescription
近くのBLEデバイスと接続して、データをやり取りするために利用します。
これだけ。あとはライブラリが良しなにやってくれる。平和だ…。
コードで追うBLE通信のライフサイクル
パーミッションという最大の山を越えたら、あとはコードで流れを組み立てていくだけ。ここでもいくつか「お作法」がある。
1. Managerインスタンスは1つだけ!
これが最初の「お作法」。`BleManager`のインスタンスは、アプリ全体で一つだけ作るのが原則。よくある間違いが、Reactコンポーネントが再レンダリングされるたびに`new BleManager()`しちゃうやつ。これをやるとマジで挙動が不安定になる。
import { BleManager } from 'react-native-ble-plx';
// アプリのどこか、グローバルな場所で一度だけ生成する
export const manager = new BleManager();
コンポーネントのstateとかで管理するんじゃなくて、exportしちゃうのが一番手軽で確実かもね。
2. Bluetoothの状態を監視する
アプリを起動しても、すぐにBluetoothが使えるわけじゃない。OSが準備してくれるのを待つ必要がある。そのために`onStateChange`を監視する。
useEffect(() => {
// Bluetoothの状態が変わるたびに呼ばれるリスナー
const subscription = manager.onStateChange((state) => {
if (state === 'PoweredOn') {
// よっしゃ、準備OK!スキャン開始できる
scanAndConnect(); // ←自分で定義したスキャン開始関数
subscription.remove(); // 目的達成したらすぐ解除するのがお作法
}
}, true);
return () => {
subscription.remove(); // クリーンアップ
};
}, [manager]);
`state`が`PoweredOn`になったら、そこがスタート地点。
3. デバイスをスキャンして見つける
準備ができたら、いよいよスキャン。`startDeviceScan`を呼ぶ。
const scanAndConnect = () => {
manager.startDeviceScan(null, null, (error, device) => {
if (error) {
// エラー処理
console.error(error);
manager.stopDeviceScan(); // エラーでもスキャンは止める
return;
}
// 見つかったデバイスの名前とかでフィルタリング
if (device.name === 'MySensor' || device.id === 'XX:XX:XX:XX:XX:XX') {
// 見つけたら、すぐにスキャンを停止するのが鉄則!
// これをしないと、バッテリーを無駄に消費し続ける
manager.stopDeviceScan();
// 接続処理へ進む
connectToDevice(device);
}
});
};
ここでのポイントは、目的のデバイスを見つけたらすぐに`stopDeviceScan()`を呼ぶこと。スキャンは結構バッテリーを食う処理だから、ダラダラ続けないのが大事。
4. 接続して、サービスとキャラクタリスティックを発見する
デバイスが見つかったら、いよいよ接続。そして「何ができるか」を聞き出すフェーズ。
const connectToDevice = async (device) => {
try {
const connectedDevice = await device.connect();
console.log(`Connected to ${connectedDevice.name}`);
// 接続できたら、次はサービスとキャラクタリスティックを発見する
// これをやらないと、データの読み書きができない
await connectedDevice.discoverAllServicesAndCharacteristics();
console.log('Discovery complete');
// これでようやく通信の準備が整った
// readとかwriteとかの処理をここから呼び出す
} catch (error) {
console.error('Connection/Discovery failed', error);
}
};
`discoverAllServicesAndCharacteristics()`、名前が長いけど、これをやらないと次のステップに進めない。超重要。
5. データの読み書き (Read / Write / Notify)
やっと会話の時間。データのやり取りには、主に3つの方法がある。
- Read: こっちから「データちょうだい」って能動的にもらいにいく。
- Write: こっちから「このデータ書き込んどいて」って送る。
- Notify/Indicate (Monitor): 「何か変化があったら勝手に送ってきて」ってお願いしておく。
どれを使うにしても、`serviceUUID`と`characteristicUUID`っていう住所みたいなものが必要になる。これは、通信したいデバイスの仕様書とかを見て確認するしかない。
特に`Write`には2種類あって、使い分けが大事。
| 書き込み方法 | 特徴 | 使いどころ |
|---|---|---|
writeCharacteristicWithResponse |
確実だけど、ちょっと待たされる感じ。「書けたよー」って返事を待つから、通信に一往復半ぶんの時間がかかる。 | ファームウェアのアップデートとか、絶対に失敗したくない重要なデータの書き込み。 |
writeCharacteristicWithoutResponse |
送りっぱなし。速いけど、届いたかは神のみぞ知る。UDPみたいなもんかな。 | LEDの色をリアルタイムに変えるとか、多少取りこぼしても問題ない連続的なデータ送信。 |
あと、送受信するデータは`base64`形式にエンコード/デコードする必要があるのを忘れずに。ライブラリがそういう仕様なので。`base64-js`みたいなライブラリを使うと楽。
import { Base64 } from 'js-base64';
// 書き込むとき
const stringValue = 'hello';
const base64Value = Base64.encode(stringValue);
device.writeCharacteristicWithResponseForDevice(
// ...
base64Value
);
// 読み取ったとき
const characteristic = await device.readCharacteristicForDevice(...);
const receivedBase64 = characteristic.value;
const receivedString = Base64.decode(receivedBase64);
console.log(receivedString); // 'hello'
6. 後片付け (Disconnect)
通信が終わったら、ちゃんと「さようなら」をする。`cancelDeviceConnection`を呼んで接続を切る。これをやらないと、デバイス側が接続中だと思い続けて、他のアプリから接続できなくなったりする。
そして、アプリが完全に終了するときとか、もうBLE機能が不要になったら、`manager.destroy()`を呼んでリソースを解放する。これで一連の流れは終わり。
よくあるハマりどころまとめ
最後に、自分がハマった、あるいはよく見かける失敗例をまとめておく。未来の自分のための備忘録。
- BleManagerを再レンダリングのたびにnewしてる: さっきも言ったけど、マジで挙動が不安定になる。やめよう。
- スキャンを止め忘れる: バッテリーが溶ける。見つけたら即`stopDeviceScan()`。
- `discoverAllServicesAndCharacteristics`を呼び忘れる: 接続はできてるのに、Read/Writeが全部エラーになる。だいたいこいつが原因。
- Androidのパーミッション設定漏れ: `AndroidManifest.xml`の記述、実行時のリクエスト、どっちかが漏れてる。Android 12以上なら`BLUETOOTH_SCAN`と`BLUETOOTH_CONNECT`が必須。
- データのBase64変換を忘れる: なんか文字化けしたり、うまく書き込めなかったりする。送る前、受け取った後のお作法。
- 非同期処理を`await`し忘れる: `connect()`も`discoverAllServicesAndCharacteristics()`も全部Promiseを返す。`await`するか`.then()`で繋がないと、処理の順番がぐちゃぐちゃになって死ぬ。
まあ、こんな感じかな。BLEは一見とっつきにくいけど、この辺の「お作法」さえ守れば、思ったより素直に動いてくれる。…はず。デバイス側の実装にもよるけどね!
みんながBLE開発で一番ハマったのって、やっぱりパーミッション周り?それとも、特定のデバイスの謎挙動?もしよかったらコメントで教えてもらえると、僕も勉強になります。
