android: BLE 操作
2024/10/29
Central として BLE機器を操作するには、Peripheral の Advertising を見つけて接続する必要がある。
スキャン
サンプルコードが載っていて、だいたいそのまま動かすことができる。
startScan()
して SCAN_PERIOD
経過したら停止するのだが Kotlin のコルーチンではなく handler.postDelayed()
が使われていた。
ViewModel だと Android での Kotlin コルーチン にいろいろ書いてあって viewModelScope.launch{}
などでうまいことできそうだが、Data Layer 以下はどうか。
APIを公開するでは、suspend
関数にしておく(ViewModel で launch
するのだろう)か、時間経過でデータが変更されるなら Flow
を使うよう書かれている。
私は startScan()
をそのまま使って、停止時には外側から stopScan()
を呼ぶようにしているが、
これを「一定時間かかるがその間にスキャンした結果を返すメソッド」と考え直せば suspend
関数にすることができる。
あるいはこちらに従って Flow
を使うタイプにするかだ。
悩ましいが、BLE機器の場合はスキャンに多少時間が必要なのと、長時間スキャンさせたい場合もあるので定期的に結果を戻してほしいと考えると Flow
にするのが良かろう。
今は Repository もコールバックで返すようにしているので ViewModel がコールバックで受けとった後に UiState を更新している。
Flow
での書き換えはそのうちやるかもしれないが、今は止めておく。
スキャンして検出したデバイスはコールバックで通知される。
私の実装だとここである。
BLE 機器は状況がしばしば変わるので、同じデバイスが何度も通知されることになる。
そこをどうするかはやりたいこと次第だろう。
サンプルコードでは 10秒スキャンして終わらせていたが、接続するためのスキャンであればそのくらいじゃないの、という感じだろうか。
スキャンし直すとき、もしスキャン済みのデバイスのリストがあるなら当てにならないのでクリアした方がよいと思う。
検出したデバイスは ScanResult にデータが載っている。
どれがいるかは人それぞれだろうが、今回は RSSI(getRssi()
)、アドレス(getDevice().getAddress()
)、名前(getScanRecord().getDeviceName()
) を持つようにした(コード)。
“名前”は ScanRecord に入っているが、他にも ManufacturerSpecificData など Advertising しているデータが載っているようなのでまるごと保持するようにした。
が、接続するのにBluetoothDeviceの方が必要になったため コードを修正 して保持するようにした。
startScan()
はパラメータを取る呼び出し方もあるので場合によって使うとよいだろう。
接続
- GATT サーバーへの接続 - Connectivity - Android Developers
- デバイスのGATTサーバーへの接続
- バインドされたサービスの設定
- BluetoothAdapterの設定
- デバイスに接続
- GATTコールバックの宣言
- GATTサービスへの接続
デバイスから情報を取り出したりデバイスに制御要求したりするので Peripheral の方がサーバーになる。
BluetoothDevice.connectGatt() で接続するのだが引数の BluetoothGattCallback が面倒そうだ。
ScanCallback は 3つしかないがこちらは数が多い。
数が多い・・・。
Geminiさんに相談したら、クラス名にカーソルを置いて Alt + Insert でできるらしい!
同じメソッドが 2つある場合、API33 から追加されてそれまでの API が deprecated になっている。 追加されたのは ATT の読み込みが関係する API だけかな。
onCharacteristicChanged
onCharacteristicRead
onDescriptorRead
// added in API18
// deprecated in API33
public void onDescriptorRead (BluetoothGatt gatt,
BluetoothGattDescriptor descriptor,
int status)
// added in API33
public void onDescriptorRead (BluetoothGatt gatt,
BluetoothGattDescriptor descriptor,
int status,
byte[] value)
コールバックになる場合、API 33 だと deprecated になった方は呼ばれないのだろうか?
Android Studio だとこう見えていて、両方実装がある分には問題はないようだ。
ということは、API 33 未満と API 33 以上で呼ばれるコールバック関数が違う?
あるいは API 33 未満の関数だけ実装しておけばよいのか?
ATT と通信できるところまでやらないと確認できないので後回しだ。
Android Developer のページを見ているが、バインドされたサービスを設定する 以降は Service(Androidの方)だのBinder
だのなんだの出てくる。
アプリがバックグラウンドに回って終わってしまうことを考えると Service にした方がよいとは思うが、Service でないとできないことというのもないと思うのでスルーしよう。
Characteristic へのアクセス
コールバック onConnectionStateChange()
が BluetoothProfile.STATE_CONNECTED
で呼び出されたら接続完了になる。
以降はconnectGatt()
の戻り値を使う。
サービス検出
BluetoothGatt.discoverServices()
でサービスの検出を開始する。
完了は onServicesDiscovered()
が呼び出される。
どういうときに失敗するのか分からないがstatus
のチェックもした方が無難か?
あるいは BluetoothGatt.getServices()
の戻り値がおかしくなるのか。
コールバックの引数がBluetoothGatt?
なので null チェックはするだろうし、そこでstatus
を一緒に見ておけば良いか。
サービスの検出が終わったので、Service と Characteristic の UUID をログに出すようにしてみた。
LBS(Nordic LED Button service)を載せた Peripheral だとこんな感じだ。
onServicesDiscovered: status=0
service: 00001801-0000-1000-8000-00805f9b34fb
characteristic: 00002a05-0000-1000-8000-00805f9b34fb
characteristic: 00002b29-0000-1000-8000-00805f9b34fb
characteristic: 00002b2a-0000-1000-8000-00805f9b34fb
service: 00001800-0000-1000-8000-00805f9b34fb
characteristic: 00002a00-0000-1000-8000-00805f9b34fb
characteristic: 00002a01-0000-1000-8000-00805f9b34fb
characteristic: 00002a04-0000-1000-8000-00805f9b34fb
service: 00001523-1212-efde-1523-785feabcd123
characteristic: 00001524-1212-efde-1523-785feabcd123
characteristic: 00001525-1212-efde-1523-785feabcd123
今回は LBS にアクセスするアプリと決めているので、BluetoothGatt.getService(UUID)
が !null で、
そのインスタンスに BluetoothGattService.getCharacteristic(UUID)
するというやり方で良い。
UUID はNordicのBlinkyアプリからもらってこよう。
Android 12(API 31) で実行した場合は deprecated の onCharacteristicRead
が呼び出された。
Android 15(API 35) で実行した場合は deprecated もそうでない方も呼び出された。
deprecated な方では BluetoothGattCharacteristic.getValue()
で値を取得するが、API 33 以降はコールバック関数の引数で value
をもらっているのでそのまま使うとよい。
とにかく、API 33 より前に対応するなら deprecated の方だけ実装するのがよいのか。
コールバックについては Read 関係だけだったが、書込みの方も API が追加されていた。
こちらは OS バージョンで切り分けるしかないと思う。
こういう感じか。