AndroidのBLEでCharacteristicsのRead/Writeサンプルを作ってみた - ReDo

2013年11月17日

AndroidのBLEでCharacteristicsのRead/Writeサンプルを作ってみた

○はじめに

Bluetooth Low Energy(BLEと略される、ただしググラビリティが低すぎるという欠点を持つ)がやっとAndroid 4.3で対応したのですが、ちょっと触ってみた結果、どうにも悲しいことに

「1. 安定性がイケてない」
「2. SDKサンプルがイケてない」
「3. API仕様がイケてない」

という三重苦です。

iBeaconとかの盛り上がりもありますし、なるべく低コストで早く正しい方向で盛り上がる様に、AndroidのBLEについてStickNFindという先駆者の製品でピロリーしてみた際の話を以下に記載します。

(注)本エントリではBLEそのものの話は端折ってしまいますので、そのあたりはもっと分かりやすく詳しい他のサイトなどを参照していただければ、と。

コードはgithubにおいてますので、AndroidのBLEについて触ってみたことがある人はそれを見れば話が早いかもしれません。

GitHub - youten / BLERW - Android BLE Scan and Characteristics Read/Write Example
https://github.com/youten/BLERW

○Android SDK付属の本家サンプルについて

Bluetooth Low Energy
http://developer.android.com/guide/topics/connectivity/bluetooth-le.html

にサンプルの説明とともに、Bluetooth Low Energyの説明がありますが、基本的にこれはHEART RATE MONITOR(心拍数モニタ、腕時計あたりのウェアラブルデバイスが想定されていると思われます)でconnectしておいて、『値に変化があったら教えてね』とperipheralである心拍数モニタ側にお願いするサンプルのため、characteristicsの読み書きをする方法が地味に分かりません。

# ベースにしたといわれている他ベンダの実装マニュアルではシーケンスが書いてある様な例もあり、Android本家が一回り不親切といういかAPIの作りがイケてないという評価でいいと思っています。


○iPhone用意してください

いきなり何を言ってるんだという感じですが、iOSの方がBLE対応の歴史が長く、地味にAPIの改善(抽象化の具合の良さがAndroidとかなり違います、どうしてAndroidこうなった...!)もあって正直まずBLEタグ製品とiPhoneを用意するのをオススメします。

BLEタグ側は日本でも対応(技適OK)、Androidにも対応、SDKにも対応してiBeacon化も可能、とStickNFindが一回り先駆者として最強なので現状これが良いのですがちょっとお高い気もします...。

iPhoneアプリではBLExplrが有名な様ですが300円です。LightBlueが無料なのでこれで良いです。スキャン・サービス一覧表示・Read/Writeに対応し、「タグとアプリのどっちが悪いの?」を切り分けるのに大変助かりました。オススメです。


○Phase.1 Scan

こちらはサンプル通りです。BLEはCentral(探す方)とPeripheral(探される方)に分かれており、PeripheralはAdvertiseを適当な感覚で吹いており、それをCentral側がScanします。

SDKサンプルではScanを10秒後に止めていますがこれは必須の実装ではありません。BLEはとにかく省電力が謡われていますが、根本は「通信(≒回路の発振)時間をなるべく短くする」ということによって実現されており、無駄にScanし続けるとBLEの利点である省電力がイマイチになってしまうため、マメにScanを停止するコードが「行儀が良い」ということです。

なお、Androidではお馴染み「行儀の良い」という表現ですが、Central側の電池消費を無視すればScanし続けることは可能です。iBeaconとBLEの普及具合を確認すべく通勤経路でバックグラウンドScanし続けるアプリを書いたりもしているのですが、僕の通勤経路ではまだまだBLEは未来の技術の模様です。

また、このあたりの「まわりに色々バレバレである」という点については偉大な先駆者が居ますのでおさえておくべきでしょう。

コード上特筆すべき点はスキャン結果のコールバックのみです。

BluetoothAdapter.LeScanCallback

onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord)

このBluetoothDeviceがParcelableなこと、また、BluetoothAdapter#getRemoteDeviceでMACアドレスのStringのみから取得可能なことから

「Scanして履歴を管理するActvity/Serviceは分けるべき」ってアッピルされてる気がします。

○Phase.2 Connect - Service Discovery - Read/Write

Phase.2とか言いながらいきなり最後まで連なってます。

これから先の処理において、GATTやCharacteristicsのインスタンスが登場してきますが、これらのインスタンスはnewしてはいけません。すべてcallbackで返ってきたインスタンスに対して操作をするのが望ましいです。

サンプルでは中途半端にSTATE_DISCONNECTED・STATE_CONNECTING・STATE_CONNECTEDと3つの状態を持ちますが、ぶっちゃけ以下の7状態を意識してください。

1. disconnected
↓(BluetoothDevice#connectGatt())
2. connecting
↓(BluetoothGattCallback#onConnectionStateChange(), newState == BluetoothProfile.STATE_CONNECTED)
3. connected
↓(BluetoothGatt#discoverServices())
4. service discovering
↓(BluetoothGattCallback#onServicesDiscovered())
5. service discovered ←ここでやっとCharacteristicsのRead/Write Ready
↓↑(BluetoothGattCharacteristic#readCharacteristic or writeCharacteristic)
6. Reading or Writing
↓((BluetoothGattCallback#onConnectionStateChange(), newState == BluetoothProfile.STATE_DISCONNECTED))
7. disconnected

全ての操作は非同期でcallbackベースな癖に、callbackされたインスタンスが期待するインスタンスかどうか自分で判定しないといけません。もしあなたがAndroidらしい、BackgroundなServiceでBLEでなんちゃらするアプリを書きたい場合には、イベントキューイングの仕組みを持つそこそこ大きなBluetoothGattCallbackを実装しなくてはいけないことをあらかじめ覚悟しておいてください。

また、いくつかつまづいた点を備忘録代わりに乗せておきます。

connectGatt()やdiscoverServices()が129とか133とかdocに無い値を返してきてイラッとする件は、

Issue 58381: Android 4.3: Bluetooth LE pretty instable
https://code.google.com/p/android/issues/detail?id=58381

Android BLE API: GATT Notification not received
http://stackoverflow.com/questions/17910322/android-ble-api-gatt-notification-not-received
を参照してください。

・単純な接続失敗なことがあります
・コードミスで「繋げられないGattに接続しにいってる」ことがあります。この場合は変なインスタンスの参照が残っているケースが大半です。
・稀にBluetoothデバイスの状態が悪くなります。「WiFiをオフにして、Bluetoothをoff(ちゃんと待つ)→on」にすると戻ります。Nexus4/4.3やHTC One/4.3ではこれによって復旧するルートがありました。Nexus5/4.4はもう少しだけ安定している気がします。

discoverServices()やreadCharacteristic()・writeCharacteristic()等のbooleanを返すメソッドがしれっとfalseを返す際には「呼び出すのに失敗した」というよりは、「それを呼び出せる状態になっていない」と考えるのが望ましいです。

すでにconnect状態でRead/Writeがfalseを返すのであれば以下の様にService Discoveryまわりを叩いておうかがいを立てるのが妥当そうです。

if (targetGatt.connect()) {
    if (targetGatt.discoverServices()) {
        // connect状態は維持できてる、callbackを期待
    } else {
        // connect状態からもうダメかも、インスタンス全部破棄してBluetoothDevice#connectGatt()からやり直し
    }
} else {
    // 本来はdisconnectedなはず、BluetoothDevice#connectGatt()からやり直し
}

○終わりに

Notificationのみ載せて、Read/Writeのサンプルをサボったばかりか、Serviceにpublicメソッド追加して中途半端にbackgroundにした挙句Broadcast IntentベースというダサいBLEのサンプルコードを書いたGooglerは反省すべきだと思います。

# 素のBluetoothChatもイケてないんですよね...同一犯?

コメントする