どうもこんばんわ。
容量足りんくてパソコンのスクショ整理してたら見覚えある風景あったから見てきた。
時期が時期なだけあってキラキラしている。
作中は反転してるっぽいので反転してください。
本題
Android
端末同士をBluetooth Low Energy(以降 BLE)
を使って小さなデータをやり取りできるようにしたい。
別件で近くの端末と値の交換をしたくなった。
今回はBLE
のペリフェラル、セントラル側をAndroid
で作ってみて実際にデータのやり取りをしてみる。
お試しにテキストを送ってみます。バイト配列にシリアライズできればなんでも良いはず?Java
のSerializable
とか。Protocol Buffers
は使ったことなくわからないです。。
ところで雲行きが怪しい。
BLE 公式
BLE
だけでサンプル書いてほしかった、な
https://developer.android.com/develop/connectivity/bluetooth/ble/ble-overview
BLE 登場人物
API
を触るのにこの辺知っておかないとなので・・!ざっと
- ペリフェラル(Peripheral)
BLE
の接続を待ち受ける側です。
IoT
だとセンサー側です。スマホ側じゃないです。
- こいつには
GATT サーバー
機能とアドバタイズ
機能を乗せます(後述)
- セントラル
GATT
BLE
で繋いだあとデータを送受信するための仕組み?
- ペリフェラル側には
GATT サーバー
を搭載させます
- キャラクタリスティック
- スペルをようやく覚えました。
Characteristic
- これは
GATT サーバー
の中にあるもので、実際にデータを送り返したり、あるいは書き込んだりする窓口みたいなやつ
- バイト配列をやり取りします。文字列なら
toByteArray()
でバイト配列にします。
- サービス
- キャラクタリスティックをまとめて入れておく箱です
- キャラクタリスティックはサービスに属する必要があります。多分
- アドバタイズ
- 「自分 GATT サーバーありますよ」と宣伝するやつです
- セントラル側が探すのに使います
- ペリフェラル側に
GATT サーバー
を用意するだけじゃ動かない
UUID
- 被らないあれ。
BLE
の世界ではサービス
とキャラクタリスティック
の識別にUUID
を使っています。
UUID
は基本被らないはずなので、自分でUUID
を作ってBLE
で使って良いはず?(よくわからず)
BLE 流れ
- ペリフェラルを探す
- ペリフェラルと接続できたら
GATT サーバー
へ接続を試みる
GATT サーバー
と接続するとサービス一覧が取れる、ので狙ったサービスを探して、キャラクタリスティックを読み出したり書き込んだりする
端末
なまえ | あたい |
---|
Android Studio | Android Studio Ladybug 2024.2.1 Patch 2 |
端末 (ペリフェラル / セントラル 確認のため2台以上必要) | Pixel 8 Pro(15) / Xperia 1 V(14) / Pixel 6 Pro(14) / Pixel 3 XL(12) / Xiaomi Mi 11 Lite 5G(11) / OnePlus 7T Pro(11) / Xperia XZ1 Compact(9) / Xperia Z3 Compact(5) |
minSdkVersion | 21 ? |
そのほか | Jetpack Compose + Navigation Compose 、Kotlin Coroutines |
UI
にはJetpack Compose
を使おうと思います。ペリフェラル、セントラル画面を作るためのナビゲーションも!
あとコールバックが相変わらずしんどいのでCoroutines
も
手持ちの端末の中でまともに動くやつ(重たすぎるやつを除いて)を総動員させた本記事。
つくる
作成
Jetpack Compose
のテンプレで
AndroidManifest
ペリフェラル、セントラル両方を1つのアプリでやるので権限が多い
多分Android 11
以下で動かしたい場合はandroid.permission.BLUETOOTH_ADMIN
も必要です。
app/build.gradle
画面遷移させたいのでnavigation compose
を入れます。
画面を作る
とりあえずペリフェラル、セントラルの各画面と、切り替え画面を作ります。
HomeScreen.kt
PeripheralScreen.kt
CentralScreen.kt
MainActivity
rememberNavController()
がエラーになる場合はちゃんとライブラリ(navigation compose
)が入ってない可能性があります。
権限を求める
最初に表示される画面で必要な権限をリクエストすることにします、付与されていない場合は遷移すらさせない作戦。
バージョンによって必要な権限が違うのがあれ。
あとコードではやってないのですが(じゃあやれ)、Bluetooth
が有効になっているか、BLE
が利用できるかもこのタイミングでやる必要があると思います。
UUID を決める
さて、次はBLE
のサービス
とキャラクタリスティック
に割り当てるUUID
を決めます。
多分自分で作ったものを使えば良いはずです。被んないはずだし。
適当に2つ作ります。多分なにで作っても良いんですが、今回はKotlin Playground
とかいうブラウザから試せるKotlin
環境で作ります。
特に理由はないですが、最近のKotlin
にUUID
生成機能が入ったそうなので。いままではJVM 環境
ならJava
のがあったけどそれ以外(Kotlin/JS
とか)で動かしたい場合はまた考えないといけなかったので。マルチプラットフォームだ!
Kotlin Playground
でこれを貼り付ければ良いはず。
https://play.kotlinlang.org/
2つの値が出てくれば良いはず。
上をサービスの UUID
、下をキャラクタリスティックの UUID
にします。
というわけでUUID
を定義しておきましょう。
ペリフェラル側(ホスト側)を作る
GATT サーバー
とアドバタイズ
機能を持つあれです。
ペリフェラル側を担当するクラスを作ります。BlePeripheralManager
みたいな。
まだ埋まってないところはこれから書きます
ペリフェラル側 GATT サーバーを作る
startGattServer()
を実装します。
キャラクタリスティックへ読み込み、書き込みが要求されたら呼ばれるコールバックを作り、サービス、キャラクタリスティックを登録すれば良いはず。
また、onConnectionStateChange()
なんかのコールバックを使えばいま接続中のデバイスの情報が取れたりします。接続中の端末数を表示させたい場合はこれ。
onCharacteristicReadRequest
に関しては、多分一度には送り切れないのか、前回受信した位置までがoffset
に入っているので何らかの方法で指定バイト数をスキップする必要があります。
今回はByteArrayInputStream
を使いました、指定バイト数をスキップするためだけに使いました。多分オーバースペックな気がします。
キャラクタリスティックのread / write
する値はこのクラスのコンストラクタ引数にあるonCharacteristicReadRequest / onCharacteristicWriteRequest
関数を取って、外から好きな用に渡せるようにしました。
権限チェックは無視しました、多分この画面に来る前に権限を付与してくれると思うので、、
ペリフェラル側 アドバタイジングを作る
次はペリフェラル側にGATT サーバー
があるということを報知するやつです。
今回も今回とて権限チェックは無視しました、多分この画面に来る前に権限を付与してくれると思うので、、
ペリフェラル側 終了処理
GATT サーバー
、アドバタイジング
を終了させる処理です。
ペリフェラル側 ここまで
ペリフェラル側の画面を完成させる
さっき作ったBlePeripheralManager
をインスタンス化し、Jetpack Compose
で適当にUI
を作ります。
キャラクタリスティックのread
で送り返す値を入力するテキストフィールドと、write
で送られてきた値を表示するText()
。
セントラル側(ゲスト側)を作る
多分こっちのがコールバック地獄でしんどい気がする。コルーチンで幸せになろう。
どうしてクライアント側のがつらいんですか?(ここに電話猫の画像を貼る)
まずはクラスを作ります。空の関数はこのあとすぐ実装していきます。
セントラル側 BLE デバイスを見つける
まずはデバイスを探す処理です。
GATT サーバー
のUUID
を指定してすることでアドバタイジングしてるやつが引っかかり、コールバックが呼ばれます。
コールバックしんどいのでコルーチンでいい感じに同期っぽく書きます。ちなみにIoT
端末相手の場合はUUID
指定よりもMACアドレス
指定を使ってそう?
セントラル側 GATT サーバーへ接続する
このあたりから辛くなってくるらしい。
うまくいくとこれで動くらしい。まずはconnectGatt()
を呼び出してGATT サーバー
へ接続したあと、onConnectionStateChange()
コールバックを待ちます。
このコールバックで接続に成功したことが分かれば、discoverServices()
を呼び出しサービスを探します。
サービスが見つかると、onServicesDiscovered()
コールバックが呼ばれるので、ようやくキャラクタリスティックへ操作ができるようになります。
で、で、で、_bluetoothGatt
、なんでMutableStateFlow
にBluetoothGatt
を入れているのか?という話はこの後します。
_characteristicReadChannel
もそうです。
セントラル側 キャラクタリスティックへ read する
ペリフェラル側から値を読み出す処理を書きます。
で、なんで一部の値をFlow
で扱っているかというと、接続し終わった後にread
するとかいう制御が面倒そう。
かわりに、MutableStateFlow
に値が入ってくるまで待つような処理にすれば、まだ接続が成功していなくても、この関数の呼び出しが出来るようになります。
また、read
の結果をconnectGatt()
のコールバックから受け取る必要があるんだけど、できればこの関数の返り値としてread
の結果がほしい。
そこで、Coroutines
のChannel()
を使い、サスペンド関数を超えて値の送受信が出来るようにして、コールバックの値をこの関数のreturn
で返せるようにしました。
(コールバック側はサスペンド関数じゃないのでサスペンド関数間ではないんですがまあ)
Channel
ってこうやって使うんかなあ、、ってのと多分説明下手で伝わってない。
セントラル側 キャラクタリスティックへ write する
こっちも同様にFlow
で接続待ちをするようにします。
Android 12
以前でも使いたい場合は分岐して古い方を使う必要があります。
セントラル側 終了処理
使い終わったときに呼び出す処理です。
セントラル側 ここまで
セントラル側の画面も完成させる
ペリフェラル側同様、インスタンス化し、キャラクタリスティック
へread
するボタンとwrite
するテキストフィールドと送信ボタンを置きます。
ペリフェラル側と違って明示的にread / write
ボタンを置く必要があります。
使ってみる
1台をペリフェラル、もう1台をセントラルにして、接続できるまで待ちます。(デバイス数が増えていること、くるくるが消えていること)
接続すると、セントラル側からペリフェラルの値をread
したり、ペリフェラル側にwrite
出来るようになるはずです。
動いたかな。
手持ちのまともに動く端末を総動員させた画像がこれです。
1台のペリフェラルのキャラクタリスティック対してread / write
出来てそうです。
冒頭で話した通り、今回は文字列をread / write
しているわけですが別に文字列に限ったことはなく、バイト配列にエンコードデコードできる場合はやり取りできるはずです、
BLE
なので小さいデータしか送れないとは思いますが。
そーすこーど
どうぞ
https://github.com/takusan23/AndroidBlePeripheralCentralSample
なぞ
なぜか誰も接続していないのに謎に 1 台接続済みの表示になってしまった。コードがミスってる可能性もある。
Bluetooth
のオンオフを試すと直るけどかなり最終手段。
おわりに
ペリフェラル、セントラル両方ともAndroid
だったのでつらみはあんまりなかった?(API
がコールバックの連続だってのはしんどい)
それよりも古いAndroid
バージョンに対応させたい場合は多分非推奨の方を使う必要がある。
以上でつ。8888888