1

I am attempting to subscribe to multiple characteristics of a BLE peripheral within Android API 28.

Due to the asynchronous nature of the BLE API I need to make the function that subscribes to each characteristic (gatt.writeDescriptor()) block; otherwise the BLE API will attempt to subscribe to multiple characteristics at once, despite the fact that only one descriptor can be written at a time: meaning that only one characteristic is ever subscribed to.

The blocking is achieved by overriding the onServicesDiscovered callback and calling an asynchronous function to loop through and subscribe to characteristics. This is blocked with a simple Boolean value (canContinue). Unfortunately, the callback function onDescriptorWrite is never called.

See the code below:

override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
    canContinue = true 
} 

override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { 
    runBlocking {
        loopAsync(gatt)
    }
}

private suspend fun loopAsync(gatt: BluetoothGatt) {
    coroutineScope {
        async {
            gatt.services.forEach { gattService ->                      
                gattService.characteristics.forEach { gattChar ->
                    CHAR_LIST.forEach {
                        if (gattChar.uuid.toString().contains(it)) {
                            canContinue = false
                            gatt.setCharacteristicNotification(gattChar, true)

                            val descriptor = gattChar.getDescriptor(UUID.fromString(BleNamesResolver.CLIENT_CHARACTERISTIC_CONFIG))                                     
                            descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE

                            val write = Runnable {
                                gatt.writeDescriptor(descriptor)
                            }
                            //private val mainHandler = Handler(Looper.getMainLooper())
                            //mainHandler.post(write)
                            //runOnUiThread(write)
                            gatt.writeDescriptor(descriptor)

                        }

                        while (!canContinue)
                    }
                }
            }
        }
    }
}

It was suggested in a related post that I run the gatt.writeDescriptor() function in the main thread. As you can see in the code above I have tried this to no avail using both runOnUiThread() and creating a Handler object following suggestions from this question.

The callback gets called if I call gatt.writeDescriptor() from a synchronous function, I have no idea why it doesn't get called from an asynchronous function.

EDIT: It appears that the while(!canContinue); loop is actually blocking the callback. If I comment this line out, the callback triggers but then I face the same issue as before. How can I block this function?

Any suggestions are most welcome! Forgive my ignorance, but I am very much used to working on embedded systems, Android is very much a new world to me!

Thanks, Adam

Tim
  • 41,901
  • 18
  • 127
  • 145
amitchone
  • 1,630
  • 3
  • 21
  • 45
  • 1
    Shouldn't your while loop be inside of an if block? Meaning that you wait only if you wrote the descriptor. – Ernest Zamelczyk Sep 27 '18 at 12:15
  • @ErnestZamelczyk You're correct, whilst that had no effect on the issue itself, moving it will help to mitigate against future issues - thank you – amitchone Sep 27 '18 at 14:25

2 Answers2

3

I posted some notes in the comments but I figured it would be better to format it as an answer.

Even though you already fixed your issue I'd suggest running the actual coroutine asynchronously and inside of it wait for the write notification using channels

private var channel: Channel<Boolean> = Channel()

override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
    GlobalScope.async {
        channel.send(true)
    }
} 

override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { 
    GlobalScope.async {
        loopAsync(gatt)
    }
}

private suspend fun loopAsync(gatt: BluetoothGatt) {
    gatt.services.forEach { gattService ->                      
        gattService.characteristics.forEach { gattChar ->
            CHAR_LIST.forEach {
                if (gattChar.uuid.toString().contains(it)) {
                    gatt.setCharacteristicNotification(gattChar, true)

                    val descriptor = gattChar.getDescriptor(UUID.fromString(BleNamesResolver.CLIENT_CHARACTERISTIC_CONFIG))                                     
                    descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE

                    gatt.writeDescriptor(descriptor)
                    channel.receive()
                }
            }
        }
    }
}
Ernest Zamelczyk
  • 2,562
  • 2
  • 18
  • 32
  • `channel.send()` needs to be called from either a `coroutine` or another `suspend` function. How would you get around that? Is the best way to use `runBlocking`? – amitchone Oct 02 '18 at 12:51
0

So I actually figured out the answer myself.

The while(!canContinue); loop was actually blocking the callback as it was running in the main thread and took priority over the callback required to set the canContinue variable.

This was solved simply by calling both the gatt.writeDescriptor() function and the while loop from within the main thread:

val subscribe = Runnable {
    gatt.writeDescriptor(descriptor)
    while (!canContinue);
    }

runOnUiThread(subscribe)
amitchone
  • 1,630
  • 3
  • 21
  • 45
  • 1
    By the way, a much better way would be to run the coroutine on the background thread and use [channels](https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md) to receive the notification. So you'd replace `while(!canContinue)` with `channel.receive()` – Ernest Zamelczyk Sep 28 '18 at 10:26
  • And to run the coroutine on the background thread you use `GlobalScope.async` instead of `runBlocking`. And you can remove the coroutineScope and async from the suspend function – Ernest Zamelczyk Sep 28 '18 at 10:29