ペアリングやService/Characteristicなどを整理。
利用したもの
- Central側はGalaxy S8
- Peripheral側はCC2650 SensorTag
- SensorTagアプリ(iOS版)
- LightBlueアプリ(iOS版)
- 検証コード: https://github.com/khwada/android_ble_sample
- AndroidからSensorTagに接続して、battery levelと温度/湿度を読む
- 通信切断や例外処理などは未実装
パケットの種類とデバイスの役割
パケットの種類
BLEには2種類のパケットのみ存在する
- アドバタイズパケット
- コネクションを確立せずにデータをブロードキャストする、デバイスをスキャンしてコネクションを確立するために利用される
- アドバタイズパケット中に、コネクション可否などを示すヘッダー情報が含まれる(PDU Type)
- 参考: http://yegang.hatenablog.com/entry/2014/07/19/224754
- 参考: https://sites.google.com/a/gclue.jp/ble-docs/advertising-1/advertising
- データパケット
デバイスの役割
GAP(汎用アクセスプロファイル)にデバイスの役割としては以下の2つの組み合わせが既定されている。
- Broadcaster ⇔ Observer
- コネクションは確立しない
- Broadcasterがアドバタイズパケットで任意のデバイスへデータ(温度計の測定値など)を送信し、Observerが収集
- 1台のBroadcasterのアドバタイズパケットを、複数台のObserverから収集可能
- いずれかのObserverへデータが届いたかをBroadcasterが知る方法はない
- Beaconはこの仕組み(アドバタイズパケット中のManufacture Specificフィールドでペイロードを送信)、BLE Gatewayもアドバタイズパケットを受信して転送するものが基本
- Central ⇔ Peripheral
- CentralはアドバタイズパケットをScanし、選択したデバイスとのコネクションを開始する(PC、スマホ、タブレットなど)
- Peripheralは定期的にアドバタイズパケットを送信し、Centralからのコネクション要求を受け付ける
- Peripheralは、コネクション確立後はアドバタイズを停止する
PairingとBonding
暗号化通信のための仕組み
- Pairing
- 一時的な共通セキュリティ暗号鍵(STK)を生成する手順(この短期鍵は保存されない)
- STK作成方法は両デバイスでネゴシエーションして決定される
- Just Works:平文でやり取りされるパケットに基づいてSTKが生成され、(ユーザの手間はないが)中間者攻撃に対する保護は提供されない
- PassKey表示方式(数字6桁の入力):中間者攻撃に対する保護が提供される方式の一つ
- Bonding
- Pairingのあとに、永続的なセキュリティ暗号鍵を生成して交換する手順
- pairing with bondingや、単に「ペアリング」と呼ばれることもある
- セキュリティ要求はPeripheral側から発行され、Central側から手順を開始する
- CentralはいつでもBondingを開始できる(既にBondingされている場合、新しい鍵が生成されて古い鍵と置き換えられる)
- ただし、データアクセスによって高いセキュリティモードが要求された際に、Bondingを開始することが多い
- 参考: https://micro.rohm.com/jp/techweb_iot/knowledge/iot02/s-iot02/04-s-iot02/2767
iOS
- iOS設定のBluetooth画面
- 「自分のデバイス」に表示されるデバイスのうち、iマークがついているものがBonding済のデバイスの模様(iマークからBondingを解除)
- 未接続/接続済みはコネクション状態を表している模様
- アプリケーション観点
- アプリ側で、Bonding済デバイス一覧や各デバイスBonding有無を取得したり、Bondingを解除することはできない
- 参考: https://wojciechkulik.pl/ios/swift-bluetooth-low-energy-how-to-get-paired-devices
Android
- Android設定のBluetooth画面
- 「ペアリング済みデバイス」にBonding済のデバイスが表示される
- アプリケーション観点
- アプリ側で、Bonding済デバイス一覧の取得が可能
- Bondingの解除も可能(reflectionを利用した実装になる)
MainActivity.ky
private val scanCallBackForCreateBond = object: ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
if (result?.device?.name != null && result.device.name.contains("SensorTag")) {
Log.i("TAG", result.toString())
Log.i("TAG", result.scanRecord.bytes.joinToString (transform= {String.format("%02X", it)}))
bluetoothScanner.stopScan(this)
Log.i("TAG", "scan stopped")
if (result.device.bondState == BluetoothDevice.BOND_NONE) {
result.device.createBond()
val myToast = Toast.makeText(context, "create bond with SensorTag", Toast.LENGTH_LONG)
myToast.show()
} else {
val myToast = Toast.makeText(context, "create bond skipped: ${result.device.bondState}", Toast.LENGTH_LONG)
myToast.show()
}
}
}
}
fun bondedList(view: View) {
// ペアリング済みのデバイスを表示
val bondedList = bluetoothAdapter.bondedDevices
val myToast = Toast.makeText(context, bondedList.joinToString(transform = {it.name}), Toast.LENGTH_LONG)
myToast.show()
}
fun createBondSensorTag(views: View) {
bluetoothScanner.startScan(scanCallBackForCreateBond)
}
fun removeSensorTag(views: View) {
val bondedList = bluetoothAdapter.bondedDevices
bondedList.filter { it.name == "CC2650 SensorTag" }.forEach{removeBond(it)}
val myToast = Toast.makeText(context, "removed", Toast.LENGTH_LONG)
myToast.show()
}
private fun removeBond(device: BluetoothDevice) {
val clazz = BluetoothDevice::class.java
val method = clazz.getDeclaredMethod("removeBond")
method.isAccessible = true
method.invoke(device)
}
GATT(汎用アトリビュート・プロファイル)
BELコネクション上でのデータ転送手順とフォーマットを既定するもの
参考: https://micro.rohm.com/jp/techweb_iot/knowledge/iot02/s-iot02/04-s-iot02/3469
client ⇔ server
- clientがserverに要求を送信し、serverが応答(およびサーバ主導の通知)を返す。
- clientはまずserverのserviceを検索し、見つかったattributeのread/write/サーバ主導更新の通知を受ける
- GAPのCentral/Peripheralとは独立しており、Central/Peripheralいずれも、client/serverいずれの役割も果たすことができる
serviceとcharacteristic
- 各attributeは、以下の項目で構成される
- Attribute Handle: 16ビットの識別子
- Attribute Type: UUID、attributeの種類を示す
- Attribute Value: 属性値、型には制約がなく最大512バイト
- Attribute Permission: 読み書き可否、暗号化要求、認可要否
- GATT serverのattributeは、serviceにグループ分けされ、各serviceに1個以上のcharacteristicが含まれる
UUID
- 短縮UUID
- 16bitまたは32bitのUUID(現状割り当てられているのは16bit)
- Bluetooth SIGにて規格化されているUUIDに利用される
- https://www.bluetooth.com/specifications/gatt/services
- https://www.bluetooth.com/specifications/gatt/characteristics
- Bluetooth Base UUIDと結合すると、128bit UUIDが構築できる(xxxxxxxx-0000-1000-8000-00805f9b34fb)
- 例) Battery Service: 0x180F ⇒ 0000180f-0000-1000-8000-00805f9b34fb
- ベンダー固有UUID
- Bluetooth SIGにて規格化されていないattributeについては、128bitのUUIDを利用
データ転送
- serviceとcharacteristicの検索
- 暗号化していない通信上で可能
- characteristicとdescriptorの読み出し
- characteristicとdescriptorの書き込み
- サーバ主導更新
- serverからclientに非同期に送信(clientがポーリングせずに、値に変更があったときに受信)
- CCCD(client characteristic configuration descritor)によって有効/無効を切り替える
SensorTagBluetoothGattCallback.kt
companion object {
private val UUID_HUMIDITY_SERVICE = UUID.fromString("f000aa20-0451-4000-b000-000000000000")
private val UUID_HUMIDITY_CHARACTERISTIC_DATA = UUID.fromString("f000aa21-0451-4000-b000-000000000000")
private val UUID_HUMIDITY_CHARACTERISTIC_CONFIGURATION = UUID.fromString("f000aa22-0451-4000-b000-000000000000")
private val UUID_CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
private val UUID_BATTERY_SERVICE = UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb")
private val UUID_BATTERY_CHARACTERISTIC_BATTERY_LEVEL = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")
}
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
Log.i("TAG", "stateChange status=$status state=$newState")
gatt?.discoverServices()
}
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
super.onServicesDiscovered(gatt, status)
Log.i("TAG", "servicesDiscovered [${gatt?.services?.joinToString(transform = {it.uuid.toString()})}]")
val batteryCharacteristic = gatt?.getService(UUID_BATTERY_SERVICE)?.getCharacteristic(UUID_BATTERY_CHARACTERISTIC_BATTERY_LEVEL)
// properties=18 PROPERTY_READ:2 + PROPERTY_NOTIFY:16
Log.i("TAG", "batteryCharacteristic properties=${batteryCharacteristic?.properties} descriptors=${batteryCharacteristic?.descriptors?.joinToString { it.uuid.toString() }}")
gatt?.readCharacteristic(batteryCharacteristic)
// この時点で読んでもnull
Log.i("TAG", "battery level=${batteryCharacteristic?.value?.joinToString(transform = {String.format("%02X", it)})}")
}
override fun onCharacteristicRead(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
when (characteristic?.uuid) {
UUID_BATTERY_CHARACTERISTIC_BATTERY_LEVEL -> {
if (characteristic?.value != null) {
val batteryLevel = characteristic.value[0].toInt()
Log.i("TAG", "onCharacteristicRead battery level=$batteryLevel %")
}
}
}
}
// 以下、湿度測定値をサーバ主導更新で受け取る(先にセンサーをONにしている)
private fun enableHumidityCollection(gatt: BluetoothGatt?) {
val characteristic = gatt?.getService(UUID_HUMIDITY_SERVICE)?.getCharacteristic(UUID_HUMIDITY_CHARACTERISTIC_CONFIGURATION)
// properties=10 -> PROPERTY_READ:2 + PROPERTY_WRITE:8
Log.i("TAG", "humidityConfigufationCharacteristic properties=${characteristic?.properties} descriptors=${characteristic?.descriptors?.joinToString { it.uuid.toString() }}")
characteristic?.value = byteArrayOf(0x01)
gatt?.writeCharacteristic(characteristic)
}
override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
super.onCharacteristicWrite(gatt, characteristic, status)
Log.i("TAG", "onCharacteristicWrite")
enableHumidityNotification(gatt)
}
private fun enableHumidityNotification(gatt: BluetoothGatt?) {
val characteristic = gatt?.getService(UUID_HUMIDITY_SERVICE)?.getCharacteristic(UUID_HUMIDITY_CHARACTERISTIC_DATA)
gatt?.setCharacteristicNotification(characteristic, true)
// Client Characteristic Configuration Description
val descriptor = characteristic?.getDescriptor(UUID_CCCD)
descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt?.writeDescriptor(descriptor)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) {
super.onCharacteristicChanged(gatt, characteristic)
if (characteristic?.value != null) {
// http://processors.wiki.ti.com/index.php/CC2650_SensorTag_User%27s_Guide#Humidity_Sensor
val temperature = ((characteristic.value[1].toInt().shl(8) + characteristic.value[0].toInt()).toDouble() / 65536) * 165 - 40
val humidity = ((characteristic.value[3].toInt().shl(8) + characteristic.value[2].toInt()).toDouble() / 65536) * 100
Log.i("TAG", "onCharacteristicChanged temperature=$temperature °C humidity=$humidity %RH" )
}
}
20:01:51.018 I/TAG: stateChange status=0 state=2
20:01:51.043 I/TAG: servicesDiscovered [00001800-0000-1000-8000-00805f9b34fb, 00001801-0000-1000-8000-00805f9b34fb, 0000180a-0000-1000-8000-00805f9b34fb, 0000180f-0000-1000-8000-00805f9b34fb, f000aa00-0451-4000-b000-000000000000, f000aa20-0451-4000-b000-000000000000, f000aa40-0451-4000-b000-000000000000, f000aa80-0451-4000-b000-000000000000, f000aa70-0451-4000-b000-000000000000, 0000ffe0-0000-1000-8000-00805f9b34fb, f000aa64-0451-4000-b000-000000000000, f000ac00-0451-4000-b000-000000000000, f000ccc0-0451-4000-b000-000000000000, f000ffc0-0451-4000-b000-000000000000]
20:01:51.044 I/TAG: batteryCharacteristic properties=18 descriptors=00002902-0000-1000-8000-00805f9b34fb, 00002908-0000-1000-8000-00805f9b34fb, 00002904-0000-1000-8000-00805f9b34fb
20:01:51.047 I/TAG: battery level=null
20:01:51.134 I/TAG: onCharacteristicRead battery level=86 %
20:01:51.135 I/TAG: humidityConfigufationCharacteristic properties=10 descriptors=
20:01:51.230 I/TAG: onCharacteristicWrite
20:01:51.328 I/TAG: onDescriptorWrite
20:01:51.329 I/TAG: onCharacteristicChanged temperature=24.493408203125 °C humidity=41.2109375 %RH
20:01:52.351 I/TAG: onCharacteristicChanged temperature=24.50347900390625 °C humidity=41.50390625 %RH
20:01:53.328 I/TAG: onCharacteristicChanged temperature=24.52362060546875 °C humidity=41.40625 %RH
20:01:54.350 I/TAG: onCharacteristicChanged temperature=24.52362060546875 °C humidity=40.91796875 %RH
20:01:55.325 I/TAG: onCharacteristicChanged temperature=24.53369140625 °C humidity=40.91796875 %RH
20:01:56.349 I/TAG: onCharacteristicChanged temperature=24.53369140625 °C humidity=40.8203125 %RH
20:01:57.372 I/TAG: onCharacteristicChanged temperature=24.54376220703125 °C humidity=41.11328125 %RH
20:01:58.349 I/TAG: onCharacteristicChanged temperature=24.53369140625 °C humidity=41.021728515625 %RH
参考
- 『Bluetooth Low Energyをはじめよう』
- https://www.amazon.co.jp/dp/4873117135
- BLE通信のプロトコル/プロファイルの概要から、iOS/Androidでの実装サンプル、peripheralの実装サンプルまで、分かりやすく説明されている
- CC2650 SensorTag User's Guide
- Getting started with Bluetooth Low Energy on iOS
- SweetBlueのAndroid BLE Issues 日本語訳
- Android 6.0でBLEデバイスのスキャン結果を受け取るには位置情報モードをオンにする必要がある
- Android BLE API及びAndroid Beacon Libraryの設計の酷さを技術的に説明する
- https://qiita.com/TakahikoKawasaki/items/a2062147b5fa82abc0b3
- iBeanconやEddystoneについて説明されている
- IoTで使用されている Bluetoothを利用したビーコンの基礎と事例