How Kotlin's Coroutines help us to deal with Bluetooth

July 16, 2019 – Romain Guefveneu 5-minute read

This article was written before Drivy was acquired by Getaround, and became Getaround EU. Some references to Drivy may therefore remain in the post

At Drivy, we want to enable users to open the car even if it’s on the bottom floor of the deepest, underground parking. Since we can’t rely on a GSM connection when so deep underground, we need to use a Bluetooth connection.
But communicating with a Bluetooth device is easier said than done, due to the fact that it’s low-level and requires many asynchronous calls. Let’s see how we can improve this.

Bluetooth 101

Bluetooth communication is not exactly like HTTP communication. We don’t have URLs or ports. All we have are services and caracteristics. And UUIDs, lots of UUIDs.

According to the official doc, Bluetooth GATT services are collections of characteristics and relationships to other services that encapsulate the behavior of part of a device. So basically a service is a set of characteristics.

According to the same official doc, “Characteristics are defined attribute types that contain a single logical value.”
Characteristics are where the data is, that’s what we want to read or write.

Last thing, services and characteristics are identified by UUIDs.

Bluetooth callbacks

On Android, a Bluetooth device communicates with us via a BluetoothGattCallback:

abstract class BluetoothGattCallback {
    fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {}

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

    fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {}
    
    fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {}
    
    [...]
}

Here is our issue: when we write a characteristic to the Bluetooth device to send a command, we want to wait for the device’s acknowledgement to continue. In other words, we want to communicate synchronously with the device.
To do so, we need to block the execution until onCharacteristicWrite is called back for my characteristic.

Kotlin’s coroutines and channels

Coroutines are a great tool for dealing with asynchronous calls. Combined with channels, we have here the perfect tools to communicate synchronously with a Bluetooth device.

Here is a simple “Hello World!” using a channel:

suspend fun main() = coroutineScope {
    val channel = Channel<String>()

    launch {
        delay(3000L)
        channel.offer("World!")
    }

    println("Hello ${channel.receive()}")
}

channel.receive() will wait for the channel to have something to offer. In this way, “Hello World!” will be displayed 3 seconds later.

Bluetooth callbacks, Coroutines and Channels

What we need is a way to wait for the device acknowledgment before sending another command. We’ll use a coroutine and a channel to achieve this.

The channel setup

We’ll use a channel of BluetoothResults, a data class composed of the characteristic’s UUID and value, and the event status:

data class BluetoothResult(val uuid: UUID, val value: ByteArray?, val status: Int)

Each call to onCharacteristicRead or onCharacteristicWrite will offer to the channel a BluetoothResult:

private val channel = Channel<BluetoothResult>()

private val gattCallback = object : BluetoothGattCallback() {
    
    override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
      channel.offer(BluetoothResult(characteristic.uuid, characteristic.value, status))
    }

    override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
      channel.offer(BluetoothResult(characteristic.uuid, characteristic.value, status))
    }

}

We now need a function that will wait for the channel to have a matching BluetoothResult to offer:

private suspend fun waitForResult(uuid: UUID): BluetoothResult {
    return withTimeoutOrNull(TimeUnit.SECONDS.toMillis(3)) {
        var bluetoothResult: BluetoothResult = channel.receive()
        while (bluetoothResult.uuid != uuid) {
            bluetoothResult = channel.receive()
        }
        bluetoothResult
    } ?: run {
        throw BluetoothTimeoutException()
    }
}

This waitForResult function will wait for the channel for 3 seconds, or throw a custom BluetoothTimeoutException.

Then, we’ll use a new BluetoothGatt.readCharacteristic function that will wait for the response, via the channel:

private suspend fun BluetoothGatt.readCharacteristic(serviceUUID: UUID, characteristicUUID: UUID): BluetoothResult {
    val characteristic = getService(serviceUUID).getCharacteristic(characteristicUUID)
    readCharacteristic(characteristic)
    
    return waitForResult(characteristicUUID)
} 

Et voilà! Now we can communicate synchronously with a Bluetooth device:

val gatt : BluetoothGatt = connectToBluetoothDevice()
try {
	val result = gatt.readCharacteristic(MY_SERVICE_UUID, MY_CHARACTERISTIC_UUID)
} catch(e : BluetoothTimeoutException) {
	Log.e("Bluetooth", "Can't communicate with the device.", e)
}
Did you enjoy this post? Join Getaround's engineering team!
View openings