vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Android】Kotlin + Bluetooth Low Energy 2

はじめに

今回はPeripheral側と、データ送受信の1回のデータ容量について試してみたことをまとめます。

JAVAコードからの変更点

今回はKotlinを使って書いているわけですが、大部分は「var」「fun」を使うなどKotlin的な内容に置き換えれば、とりあえずは動くものができます。
1点引っかかったのは以下です。

JAVA

bleCharacteristic = new BluetoothGattCharacteristic(
        UUID.fromString(getResources().getString(R.string.uuid_characteristic))
        ,BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE
        ,BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ);

これが↓こうなります

Kotlin

bleCharacteristic = BluetoothGattCharacteristic(
        UUID.fromString(resources.getString(R.string.uuid_characteristic))
        ,BluetoothGattCharacteristic.PROPERTY_NOTIFY or BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE
        ,BluetoothGattDescriptor.PERMISSION_WRITE or BluetoothGattCharacteristic.PERMISSION_READ)

要は「|」が「or」になります、という話なのですが、「|」でググッて見つけるのは面倒くさそうなので載せておきます。

Bluetoothに接続したかの情報を得る

PeripheralのAdvertiseにしても、Central側のScanにしても、処理の途中で端末のBluetoothをOffにすると、再びOnにしても自動で処理が再開されません。

そこで、BroadcastReceiverを使ってBluetoothがOnになったかどうかを調べることにします。

PeripheralActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_peripheral)
    ~省略~
    registerReceiver(broadcastReceiver, IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED))
    ~省略~
}
override fun onPause(){
    super.onPause()
    // BackgroundではBroadcastReceiverを止める.
    unregisterReceiver(broadcastReceiver)
}
override fun onResume(){
    super.onResume()
    // Foregroundに戻ったらBroadcastReceiver再開.
    registerReceiver(broadcastReceiver, IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED))
}
~省略~
private final val broadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context : Context?, intent : Intent?){
        when(intent!!.action){
            BluetoothDevice.ACTION_ACL_CONNECTED -> {
                startAdvertising()
            }
        }
    }
}
~省略~

Android Nでの変更について

以下によると、時期バージョンではBroadcast Intentの「CONNECTIVITY_ACTION」などの挙動が変更となるようです。

今回はBluetoothがOnになったことを感知する「BluetoothDevice.ACTION_ACL_CONNECTED」を使っているのでおそらく影響はないはずですが、間違っていたらあとで直します。
また、Android ver.5.0からはBroadcastReceiverの代わりにJobSchedulerを使う、ということなのですが、BluetoothのOn/Offを感知する方法が見つからなかったため、現在は使用していません。

こちらもより良い方法があればこのブログでも訂正したいと思います。

約20バイト

BLEにはそのコンセプトである省電力を実現するため、一回の通信で送信できるデータ量に制限があります。
これまでは一文字二文字の半角数字をByte化して送受信していたのですが、今回は制限である20バイトを超えた場合の挙動を確かめてみることにしました。

問題点

結果としては、iOSの場合は制限を超えると自動で分割されていたかと思いますが、AndroidではNullになったり受信した文字列が以下のように変化するなどの現象が発生しました。

  • 元の文字列: 1234567890abcdefghijklmnopqrstuvwxyz
  • 送信されたデータを受信したもの: 1234567890abcdefghij1234567890abcdefghij1234567890abcdefghij1234567890abcdefghij...(省略)

なぜか最初の20文字が繰り返された文字列が送信される、という内容です。

なお再現したコードは以下のような感じです。

CentralActivity.kt

~省略~
private fun writeText(){
    // 1台以上接続されていれば書き込みリクエストを送る.
    if(bleManager!!.getConnectedDevices(BluetoothProfile.GATT).isEmpty()){
       return
    }
    var sendValue: String = "1234567890abcdefghijklmnopqrstuvwxyz"
    bleCharacteristic!!.value = sendValue.toByteArray()
    bleGatt!!.writeCharacteristic(bleCharacteristic)
}
~省略~

PeripheralActivity.kt

~省略~
private final val bleGattServerCallback = object: BluetoothGattServerCallback(){
    ~省略~
    override fun onCharacteristicWriteRequest(device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic
            , preparedWrite: Boolean, responseNeeded: Boolean, offset: Int
            , value: ByteArray) {
        super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value)

        // Central側から受け取った値をCharacteristicにセットしてTextViewに入れる.
        characteristic.value = value

        textReceivedValue?.text = characteristic.getStringValue(offset)
        
        if(responseNeeded){
            bleGattServer!!.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, value)
        }
    }
~省略~

対策

おそらくもう少しスマートな方法はあるかと思いますが、調べてみても今ひとつよくわからなかったため、単純に20バイトずつ区切って送信することにしました。

CentralActivity.kt

~省略~
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_central)
~省略~
    var editTextWrite = findViewById(R.id.edit_central_write) as EditText
    var buttonWrite = findViewById(R.id.button_central_write) as Button
    buttonWrite.setOnClickListener {
        if(isConntected) {
            // EditTextに入力された文字列をByte化.
            writeOriginalByteArray = editTextWrite.text.toString().toByteArray(Charsets.UTF_8)
            writeValueLengthTo = 0
            isWritingData = true
            // 前の送信処理が終わっていない場合は先に終了させる.
            writeText(resources.getString(R.string.ble_stop_sending_data).toByteArray(Charsets.UTF_8))
        }
    }
~省略~
private final val bleGattCallback = object : BluetoothGattCallback() {
~省略~
    override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int){
        super.onCharacteristicWrite(gatt, characteristic, status)
        // 20byteごとに分割して送信.
        writeValueLengthFrom = writeValueLengthTo

        if(! isWritingData){
            return
        }
        if(writeOriginalByteArray!!.size <= writeValueLengthFrom){
            // すべて送信したら完了通知用の文字列を送信する.
            writeText(resources.getString(R.string.ble_stop_sending_data).toByteArray(Charsets.UTF_8))
            isWritingData = false
            return
        }
        // 送信するデータの範囲を指定(20バイト以上残っている場合は残データの頭から20バイトまで、それ以下なら最後まで).
        if(writeOriginalByteArray!!.size <= (writeValueLengthTo + 20)){
            writeValueLengthTo = writeOriginalByteArray!!.size
        }
        else{
            writeValueLengthTo += 20
        }
        var i = writeValueLengthFrom
        var t = 0
        writeByteArray = ByteArray(writeValueLengthTo - writeValueLengthFrom)

        while(i < writeValueLengthTo){
            writeByteArray!![t] = writeOriginalByteArray!![i]
            i++
            t++
        }
        writeText(writeByteArray!!)
    }
~省略~
    private fun writeText(sendValue: ByteArray){
        // 1台以上接続されていれば書き込みリクエストを送る.
        if(bleManager!!.getConnectedDevices(BluetoothProfile.GATT).isEmpty()){
           return
        }
        bleCharacteristic!!.value = sendValue
        bleGatt!!.writeCharacteristic(bleCharacteristic)
    }
~省略~

PeripheralActivity.kt

private final val bleGattServerCallback = object: BluetoothGattServerCallback(){
~省略~
    override fun onCharacteristicWriteRequest(device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic
            , preparedWrite: Boolean, responseNeeded: Boolean, offset: Int
            , value: ByteArray) {
        super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value)

        characteristic.value = value

        // 送信完了の文字列を受け取ったら、ByteArrayをStringに変換してTextViewにセット.
        if(characteristic.getStringValue(offset)!!.equals(resources.getString(R.string.ble_stop_sending_data))){
            runOnUiThread {
                textReceivedValue?.text = writeValue?.toString(Charsets.UTF_8)
                writeValue = emptyArray().toByteArray()
            }
        }
        else{
            // 送信完了の文字列を受信するまではByteArrayに追加していく.
            writeValue = writeValue!!.plus(characteristic.value)
        }
        if(responseNeeded){
            bleGattServer!!.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, value)
        }
    }
~省略~

処理は以下のような流れで行っています。

Central側から20バイトごとに区切ったByteArray型のデータを送信して、それをPeripheral側で連結

Peripheral側からResponseを送る ↓
Central側でResponseを受け取ったら、次の20バイト分を送信する(データをすべて送り終わるまで繰り返し) ↓
送信完了時に、完了報告用の文字列をCentral側から送信

Peripheral側で、それまでのByteArrayデータをStringへと変換してTextViewにセット

同じように読み込みリクエストや値のアップデート通知でも使用できます。
ただ、どうしてもタイムラグは発生してしまうので、今回のようなケースでなければデータを20バイト以下に抑えるか、Bluetoothではない他の方法での送受信を考えたほうが良いかもしれません(元も子もない気もしますが)。

参考

BroadcastReceiver

Android N