54
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

アプリケーション開発視点でのBLE通信

ペアリングや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種類のパケットのみ存在する

デバイスの役割

GAP(汎用アクセスプロファイル)にデバイスの役割としては以下の2つの組み合わせが既定されている。
GAP.JPG

  • 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を解除)
    • 未接続/接続済みはコネクション状態を表している模様
  • アプリケーション観点

Android

  • Android設定のBluetooth画面
    • 「ペアリング済みデバイス」にBonding済のデバイスが表示される
  • アプリケーション観点
    • アプリ側で、Bonding済デバイス一覧の取得が可能
    • Bondingの解除も可能(reflectionを利用した実装になる)

android_bluetooth_setting.pngbonded_list.png

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が含まれる
    • 各characteristicには0個以上のdescriptorが含まれる
    • characteristic propertiesで、当該characteristicに利用できる操作を示す(Read/Write/Notifyなど) gattserver.JPG LightBlue.png

UUID

  • 短縮UUID
  • ベンダー固有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

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
54
Help us understand the problem. What are the problem?