AndroidとBLE
Android 4.3でBluetooth LE(以下BLE)がサポートされましたが,ぼちぼち対応端末が増えてきた頃合いなのと,今後iBeaconとかも流行るのかもしれないので,今更ながらBLEについて書きます.
iPhoneでは,iPhone 4Sからサポートしています.Androidについて説明していますが,iPhoneのCore Bluetoothを使う場合もほとんど同じです.
BLEモジュールの入手
Android端末はBLEのペリフェラル(センサなどを載せた周辺機器の方)にはなれないので,BLEデバイスを用意する必要があります.(追記:Android 5.0でペリフェラル用のAPIが追加されました)
電子工作が出来る人なら,BLEモジュールを買ってきて自分で作ることも可能ですが,一部の技適マークが付いているものをのぞいて,個人が国内で合法的に使うことは難しいです.また,モジュール単体は数百円程度で買えますが,ファームウェアの開発キットなども購入すると数万円かかるので,やはり割高になります.
技適通っていて,ファームウェア書き込み済みで,お手頃価格なモジュールもスイッチサイエンス等で買えるので,機会があったらちらの方の記事も書くかもしれません.
分失防止BLEタグ
面倒くさそうなことを書きましたが,以下の条件がそろったBLEモジュールがAmazonで買えます.
- 1000円以下(送料込)
- 技適通っていて国内で利用可能
- ファームウェアの開発不要
- 半田付け不要
というわけで,いわゆる分失防止タグと呼ばれているBLEタグをそのまま利用します.
- iBUFFALO BSHSBTPT01BK http://www.amazon.co.jp/dp/B007NPKWJE
- Logitec LBT-MPVRU01WH http://www.amazon.co.jp/dp/B006QZ9QXU
Androidに対応しているとは書いていませんが.電波さえ出してくれれば良いので,気にせず購入しました.どちらも,買った時点ではCSR1000というBLEモジュールが使われていました.他にもいくつかあるのでどれでも良いと思います(責任は持てませんが)
上の2つだと,Logitecのやつは切断して暫く時間がたつと自動的にパワーオフしてしまうようなので,iBUFFALOのやつがお勧めです.
(開封して5秒後にはケースをはがしていたので,初期状態の状態の写真がありませんでしたorz)
後で出てきますが,BLEのRead/Write/Notifyが一通り試せるので良いです.
AndroidでBLEを使う
さっそく,Androidアプリを作ってみます.
足りないところはSDKのリファレンス呼んでください.
Manifestファイル
まず,Manifestに以下のパーミッションを追加します.BLUETOOTHは必須で,デバイスの検索やペアリングをアプリ上から行うならば,BLUETOOTH_ADMINも必要になります.
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
デバイスを見つける
まずは,Context.getSystemService()でBluetoothManagerを取得し,BluetoothAdapterを得ます.
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
final BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
Log.e("BLE_TEST", "No available Bluetooth adapter.");
}
デバイスを取得するためにはいくつかの方法がありますが,大きく分けると以下の3パターンになると思います.
- BluetoothのAdvertisingで検索し,返答したデバイスを取得する場合
- ペアリング済みデバイスをAPIで取得する場合
- ハードウェアアドレスが分かっている場合
検索する場合
デバイスを探すためには,startLeScan()メソッドを呼び出します.名前の通りBLEデバイスのみを検索します.
BluetoothのLEとEDRは完全に別物なので同時には検索できません.
アプリのパーミッションに,BLUETOOTH_ADMINが必要です.
消費電力を削減するために,適当な時間でstopLeScan()を呼ぶか,検索をキャンセルできる手段を用意したほうが良いかもしれません.
注意点ですが,既にペアリング済みのデバイスについてはコールバックが呼び出されません.下のgetBondedDevices()を使う必要があります.
final BluetoothAdapter.LeScanCallback callback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
Log.d("BLE_TEST", "found: " + device.getName() + " addr:" + device.getAddress() + " rssi: " + rssi);
// ここでデバイス名やアドレスなどを見て接続処理を行う
}
};
bluetoothAdapter.startLeScan(callback);
// 5秒後に停止
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.d("BLE_TEST", "stopLeScan");
bluetoothAdapter.stopLeScan(callback);
}
}, 5000);
ペアリング済みデバイスの場合
BLEデバイスはペアリング不要ですが,端末の設定からペアリングしておくことで,ペアリング済みデバイスの一覧を取得できます.アプリ上でのデバイス検索などのUI実装をさぼりたい場合には,AndroidのBluetooth設定画面を開いてペアリングすれば,少し楽ができます.
for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
Log.d("BLE_TEST", "Bonded " + device.getName() + "addr:" + device.getAddress());
}
これでペアリング済みのデバイスがすべて取れます.(BLE以外のデバイスも取得するので,device.getType()でチェックしてください)
ハードウェアアドレスが分かっている場合
128ビットのアドレスが分かっている場合は,いきなり接続できます.
すでに接続したことがあるデバイスであればこの方法も取れます.
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(addr);
注意点として,getRemoteDevice()で取得したデバイスは,場合によってはデバイス名等の情報が取得できていない場合があります.名前などが必要な場合は,実際に接続する必要があります.
接続
デバイスが取得出来たらGATTに接続します.GATTについてはこの下で説明します.
接続に成功したり,何らかのイベントが起きると,BluetoothGattCallbackに通知される仕組みです.
gatt = device.connectGatt(context, false, new BluetoothGattCallback(){~省略~});
サービスとキャラクタリスティック(GATT)
Bluetooth 4.0対応デバイスを扱うための概念として重要なものに,ServiceとCharacteristicがあります.
まず,BLEデバイスがどんな機能を持っているかは,これもBluetooth 4.0から存在するGATT(Generic Attribute Profile)で調べられます.
GATTは個別のUUIDが割り当てられたServiceとCharacteristicからなり,Serviceの下にCharacteristicがぶら下がっています.
UUIDと一般的なサービスの対応は,https://developer.bluetooth.org/gatt/services/Pages/ServicesHome.aspx あたりをみると良いです.あとはLinuxのBluetoothスタックであるBlueZのソースコードも参考になります.
Serviceを列挙するためには接続が完了してからdiscoverServices()メソッドを呼び出します.
結果は,BluetoothGattCallback.onServicesDiscovered()が呼び出されて渡されます.
gatt.getServices()というメソッドがありますが,これはdiscoverServiceに成功するまで結果を返しません.
逆に,一度,discoverServicesに成功した後であれば,いつでもgetServices()で取得できます.
gatt.discoverServices();
たとえば,iBUFFALO BSHSBTPT01BKのServiceとCharacteristicを列挙してみると以下のようになっていました.
s:00001800-0000-1000-8000-00805f9b34fb
c:00002a00-0000-1000-8000-00805f9b34fb
c:00002a01-0000-1000-8000-00805f9b34fb
c:00002a04-0000-1000-8000-00805f9b34fb
s:00001803-0000-1000-8000-00805f9b34fb
c:00002a06-0000-1000-8000-00805f9b34fb
s:00001802-0000-1000-8000-00805f9b34fb
c:00002a06-0000-1000-8000-00805f9b34fb
s:00001804-0000-1000-8000-00805f9b34fb
c:00002a07-0000-1000-8000-00805f9b34fb
s:0000180f-0000-1000-8000-00805f9b34fb
c:00002a19-0000-1000-8000-00805f9b34fb
c:00002a1a-0000-1000-8000-00805f9b34fb
c:00002a1b-0000-1000-8000-00805f9b34fb
c:00002a3a-0000-1000-8000-00805f9b34fb
"s:"の行がServiceで "c:"がCharacteristicです.
定義済みのUUIDの後ろの方は必ず同じなので,先頭だけ見れば良いです.
- Generic Access(1800) デバイス名やその他の情報取得したり
- Link Loss(1803) 接続が切れたときの挙動を設定する
- Immediate Alert(1802) アラーム鳴らしたり
- Tx Power(1804) BLEの送信のパワー
- Battery Service(180f) バッテリーの状態
のようなサービスがあることがわかります.
あとは,Serviceスの下に,データをやり取りするためのCharacteristicsがぶら下がっているので,いくつか確認してみます.
Characteristicsの読み書き
Read/Write/Notifyを試してみます.
使ってるコードをそのままコピペしたので汚いですが,以下のような定数とメソッドがあるとして説明します.
public static final UUID ALERT_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb");
public static final UUID BATTERY_SERVICE_UUID = UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
public static final UUID BATTERY_UUID = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");
public static final UUID BATTERY_POWER_STATE_UUID = UUID.fromString("00002a1b-0000-1000-8000-00805f9b34fb");
public static final UUID ALERT_LEVEL_UUID = UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic characteristic(UUID sid, UUID cid) {
if (!isConnected()) {
return null;
}
BluetoothGattService s = gatt.getService(sid);
if (s == null) {
Log.w(TAG, "Service NOT found :" + sid.toString());
return null;
}
BluetoothGattCharacteristic c = s.getCharacteristic(cid);
if (c == null) {
Log.w(TAG, "Characteristic NOT found :" + cid.toString());
return null;
}
return c;
}
Write
まず書き込みを試します.
Immediate Alertの説明を見ると,0,1,2を書き込むと音が鳴りそうです.
byte level = 2;
BluetoothGattCharacteristic c = characteristic(ALERT_SERVICE_UUID, ALERT_LEVEL_UUID);
c.setValue(new byte[] { level });
gatt.writeCharacteristic(c);
試した感じだと,0 = OFF, 1 = バイブレーション, 2 = アラームでした.
Read
次に読み込み.BLEタグの電池の残量を取得してみます.
BluetoothGattCharacteristic c = characteristic(BATTERY_SERVICE_UUID, BATTERY_UUID);
gatt.readCharacteristic(c);
電池残量が 0 ~ 100 の値で取得できます.
値は,BluetoothGattCallbackのonCharacteristicRead()メソッドに渡されます.
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (BATTERY_UUID.equals(characteristic.getUuid())) {
Log.d(TAG, "onCharacteristicRead battery: " + characteristic.getValue()[0]);
}
}
Notify
購入したタグの商品説明を読むと,ボタンを押すとiPhoneが鳴るそうです.
たぶんBLEのNotifyメッセージをやり取りしているのだと想像できます.
ボタンのようなものに使っていそうなCharacteristicsは見つかりません.
GATTで取得できないサービスである可能性もあるけれど,iPhoneのCore Bluetooth APIでアプリ作ったりしてることを考えると,普通にCharacteristicsを見えるようにしておくだろうという勝手な思い込みで探したところ,Battery Power Stateでした.
Notify受け取れそうなCharacteristicsが Battery Power State(00002a1a)くらいしか無いのでそいつに対して,setCharacteristicNotification()をします.
BluetoothGattCharacteristic c = characteristic(BATTERY_SERVICE_UUID, BATTERY_POWER_STATE_UUID);
gatt.setCharacteristicNotification(c, true);
このあとで,ボタンを押すと通知がアプリに届くと思います.
通知されると,characteristicがBluetoothGattCallbackのonCharacteristicRead()メソッドに渡されます.
1バイト目に 0/1でボタンが押されてるかどうかが入っています.
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
if (BATTERY_POWER_STATE_UUID.equals(characteristic.getUuid())) {
Log.d(TAG, "onCharacteristicChanged button: " + characteristic.getValue()[0]);
}
}
onCharacteristicChangedですが,値が変わっていなくてもデバイスからデータが来ればとりあえず呼ばれるようです.
応用編
1
財布をよく無くすので,財布にBLEタグつけてます.
- http://www.binzume.net/diary/2013-12-14:A1
- https://github.com/binzume/where-is-saifu-android/tree/master/src/net/binzume/android/whereissaifu
家のPC,カバンの中のNexus7,持ち歩いているNexus5のどれからも財布が見えなくなるとGCMでプッシュ通知してくれます.状態はGPSの位置情報とともにサーバに送ってるので,財布を手放した時間と場所がだいたい分かります.出かけた先で財布が無くても,家にあるのが確認できれば安心して行動できます.結果として,財布を忘れて出かける頻度は増えました...
1年くらい前に作って使ってます.
この記事に書いたコードもこのアプリからのコピペ.
2
会社のトイレのドアに取り付けて様子を見ていましたが,
接続が不安定なので頓挫...
扉の開閉を検出できるようにマイクロスイッチをBLEモジュールのボタンと同じ入力につないでいます.
ここで問題なのが,購入したBLEタグのボタンと電源ボタンが共用で,長押しすると電源が切れてしまうことです.
そこで,ATMELのTiny13を間に入れて,マイクロスイッチが押されている間,3秒くらいの周期でON/OFFを繰り返すようにしています.
Tiny13は通常は割り込み待ちでパワーダウンモードにしておけば消費電力は1μA以下なのでボタン電池で十分利用できます.今回は思い切ってマイクロスイッチでマイコンの電源ごと落とすことにしてしまったので電力消費があるのは扉が閉じている間だけです.
3つ同じものを作ってみたはいいものの数日間動かしていると通信できなくなったりします.
再接続できる時もアレば,そうでない時もありまだ原因がわかりません.(Wifiと干渉しているのかもしれません)
最後に
適当に買った紛失防止タグですが,Read/Write/Notifyをひと通り試せて良かったです.
というか,構成がCSR1000のデータシートに載ってるサンプルそっくりなので,試しに使うのに最適でした.
アドベントカレンダーのために,慌てて書いてます.細かいところは徐々に直すかもしれません...