Search code examples
androidkotlinconcurrencybluetooth-lowenergy

Is there a way to convert the BLE GATT callback from asynchronous to synchronous?


I am new to Android development and concurrency and I am trying to build a Kotlin app, where it connects to a BLE device and reads characteristics using readCharacteristic function and use the value read. The issue is, I want the main thread (or whatever thread the readCharacteristic is called in) to block and wait for a the callback. This is because I need this value to be displayed and used for calculation.

Currently all BLE operations are put in a queue so that only one operation will do work at a time. The queue is implemented as the following.

object ConnectionManager {
    private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>()
    private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
    private var pendingOperation: BleOperationType? = null
    private var connecting = false

    @Synchronized
    private fun enqueueOperation(operation: BleOperationType) {
        operationQueue.add(operation)
        if (pendingOperation == null) {
            doNextOperation()
        }
    }

    @Synchronized
    private fun signalEndOfOperation() {
        pendingOperation = null
        if (operationQueue.isNotEmpty()) {
            doNextOperation()
        }
    }

    @SuppressLint("MissingPermission")
    @Synchronized
    private fun doNextOperation() {
        if (pendingOperation != null) {
            Log.e(
                "ConnectionManager",
                "doNextOperation() called when an operation is pending! Aborting."
            )
            return
        }

        val operation = operationQueue.poll() ?: run {
            Log.i("ConnectionManager", "Operation queue empty, returning")
            return
        }

        pendingOperation = operation

        if (operation is Connect) {
            connecting = true
            operation.device.connectGatt(operation.context, false, gattCallback)

            while (connecting) {
            }
            return
        }

        val gatt = deviceGattMap[operation.device]
            ?: [email protected] {

                Log.e(
                    "ConnectionManager",
                    "Not connected to ${operation.device.address}! Aborting $operation operation."
                )
                signalEndOfOperation()
                return
            }

        when (operation) {

            is Disconnect -> {
                gatt.close()
                deviceGattMap.remove(operation.device)
                signalEndOfOperation()
            }

            is CharacteristicWrite -> {
                gatt.findCharacteristic(operation.characteristicUuid)?.let { characteristic ->
                    characteristic.value = operation.value
                    characteristic.writeType = operation.writeType
                    gatt.writeCharacteristic(characteristic)
                } ?: run {
                    Log.e(
                        "ConnectionManager",
                        "Characteristic ${operation.characteristicUuid} not found!"
                    )
                    signalEndOfOperation()
                }
            }

            is CharacteristicRead -> {
                gatt.findCharacteristic(operation.characteristicUuid)?.let { characteristic ->
                    gatt.readCharacteristic(characteristic)

                } ?: [email protected] {
                    Log.e(
                        "ConnectionManager",
                        "Characteristic ${operation.characteristicUuid} not found!"
                    )
                    signalEndOfOperation()
                }
            }

            is WriteDescriptor -> {
                .......
            }
        } is ......
    }

}

This is the GATT callback:

@SuppressLint("MissingPermission")
    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            Log.w("BluetoothGattCallback", "Connection state changed: $status")
            if (status == BluetoothGatt.GATT_SUCCESS) {

                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    deviceGattMap[gatt.device] = gatt
                    Handler(Looper.getMainLooper()).post {
                        deviceGattMap[gatt.device]?.discoverServices()
                    }
                    connecting = false
                } else if (newState == BluetoothProfile.STATE_DISCONNECTING) {
                    disconnect(gatt.device)
                }
            } else {
                connecting = false

                Log.w("BluetoothGattCallback", "Error $status")
                if (pendingOperation is Connect) {
                    signalEndOfOperation()
                }
                //disconnect(gatt.device)
            } // Todo handle errors
        }

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

            with(gatt) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    Log.w("BluetoothGattCallback", "Discovered ${services.size} services")
                    if (services.isEmpty()) {
                        Log.i(
                            "PrintGattTable",
                            "No services characteristics available, Call DiscoverServices() first?"
                        )
                        return
                    }
                    else {
                        services.forEach { service ->
                            val characteristicsTable = service.characteristics.joinToString(
                                separator = "\n | --",
                                prefix = "| --"
                            ) {
                                it.uuid.toString()
                            }
                            Log.i(
                                "PrintGattTable",
                                "\n Services ${service.uuid} \n Characteristics ${characteristicsTable}"
                            )
                        }
                    }
                    ConnectionManager.requestMtu(gatt.device, 517)
                } else {
                    Log.e("BluetoothGattCallback", "Service discovery failed with status $status")
                    disconnect(device)
                }
            }
            if (pendingOperation is Connect) {
                signalEndOfOperation()
            }
        }

        override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
            Log.i("MTU", "ATT MTU changed to $mtu, success ${status == BluetoothGatt.GATT_SUCCESS}")
            if (pendingOperation is MtuRequest) {
                signalEndOfOperation()
            }
        }

        override fun onCharacteristicRead(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            status: Int
        ) {

            with(characteristic) {

                when (status) {
                    BluetoothGatt.GATT_SUCCESS -> {
                        if (characteristic.uuid.toString() == BatteryCharacteristicUUID) {
                            Log.i("BluetoothGattCallback", "Read characteristic $uuid:\n${
                                value.joinToString(
                                    separator = " ",
                                    prefix = "0x"
                                ) { String.format("%02X", it) }
                            } and \n ${value[0].toUInt()}"
                            )
                        }
                        else {
                            var light : MutableList<UInt> = mutableListOf()
                            for (i in 0..value.size-1 step 4) {
                                light.add(
                                ((value[i].toUInt() and 0xFFu) shl 24) or
                                ((value[i + 1].toUInt() and 0xFFu) shl 16) or
                                ((value[i + 2].toUInt() and 0xFFu) shl 8) or
                                (value[i+ 3].toUInt() and 0xFFu))
                            }

                            Log.i("BluetoothGattCallback", "Read characteristic $uuid:\n${
                                value.joinToString(
                                    separator = " ",
                                    prefix = "0x"
                                ) { String.format("%02X", it) }
                            }" +
                                    " and \n ${light}"
                            )
                        }
                    }

                    BluetoothGatt.GATT_READ_NOT_PERMITTED -> {
                    }
                    else -> {
                        Log.e("BluetoothGattCallback", "Characteristic read failed for $uuid, error: $status")
                    }
                }
            }
            if (pendingOperation is CharacteristicRead) {
                signalEndOfOperation()
            }
        }

    }

When to read the characteristics I call ConnectionManager.readCharacteristic() which in turn checks if characteristic is readable and if device is connected then adds the operation to the queue.

When calling

ConnectionManager.readCharacteristic(device, char)

Log.i("Tag", $char.value)

The output is null since the Log statement is executed before the callback for the read is executed.

Is there a way to block the thread without time delay or maybe it is not a good idea to block and should look into a solution involving future or promises? If so, any suggestion is welcome!


Solution

  • In my opinion, if possible, it is better to follow the guidelines suggested by the Google site, i.e., to use a service and notify the activity with Broadcast updates.

    See Connect to a GATT server.

    For details, see my answer How do I connect two android devices instantaneously based on proximity?.