'How do I detect data sent from a serial monitor on my BLE app?

I have asked multiple questions to try to get to the bottom of my problem regarding receiving notifications. My app communicates with a BGX board which is a BLE kit from Silicon Labs: https://docs.silabs.com/gecko-os/1/bgx/latest/ble-services

The kit has 2 characteristics used for exchanging data. RX to receive from my app, and TX to send to my app. Writing to the device isn't the issue, the issue is with receiving anything. The queuing mechanism I implemented is that of the Punchthrough BLE guide.

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

@Synchronized
private fun signalEndOfOperation() {
    Timber.d("End of $pendingOperation")
    pendingOperation = null
    if (operationQueue.isNotEmpty()) {
        doNextOperation()
    }
}

/**
 * Perform a given [BleOperationType]. All permission checks are performed before an operation
 * can be enqueued by [enqueueOperation].
 */
@Synchronized
private fun doNextOperation() {
    if (pendingOperation != null) {
        Timber.e("doNextOperation() called when an operation is pending! Aborting.")
        return
    }

    val operation = operationQueue.poll() ?: run {
        Timber.v("Operation queue empty, returning")
        return
    }
    pendingOperation = operation

    // Handle Connect separately from other operations that require device to be connected
    if (operation is Connect) {
        with(operation) {
            Timber.w("Connecting to ${device.name} | ${device.address}")
            device.connectGatt(context, false, gattCallback)
            isConnected.value = true
        }
        return
    }

    // Check BluetoothGatt availability for other operations
    val gatt = deviceGattMap[operation.device]
        ?: [email protected] {
            Timber.e("Not connected to ${operation.device.address}! Aborting $operation operation.")
            signalEndOfOperation()
            return
        }

    // TODO: Make sure each operation ultimately leads to signalEndOfOperation()
    // TODO: Refactor this into an BleOperationType abstract or extension function
    when (operation) {
        is Disconnect -> with(operation) {
            Timber.w("Disconnecting from ${device.address}")
            gatt.close()
            deviceGattMap.remove(device)
            listeners.forEach { it.get()?.onDisconnect?.invoke(device) }
            signalEndOfOperation()
            setConnectionStatus(STATE_DISCONNECTED)
            isConnected.value = false
        }
        is CharacteristicWrite -> with(operation) {
            gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
                characteristic.writeType = writeType
                characteristic.value = payLoad
                gatt.writeCharacteristic(characteristic)
                Timber.i("Writing to $characteristicUUID")
            } ?: [email protected] {
                Timber.e("Cannot find $characteristicUUID to write to")
                signalEndOfOperation()
            }
        }
        is CharacteristicRead -> with(operation) {
            gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
                gatt.readCharacteristic(characteristic)
                Timber.i("Reading from $characteristicUUID")
            } ?: [email protected] {
                Timber.e("Cannot find $characteristicUUID to read from")
                signalEndOfOperation()
            }
        }
        is DescriptorWrite -> with(operation) {
            gatt.findDescriptor(descriptorUUID)?.let { descriptor ->
                descriptor.value = payLoad
                gatt.writeDescriptor(descriptor)
                /**  ***************************************************/
                Timber.i("DescriptorUUID:${descriptorUUID}")
                /**  **************************************************/
            } ?: [email protected] {
                Timber.e("Cannot find $descriptorUUID to write to")
                signalEndOfOperation()
            }
        }
        is DescriptorRead -> with(operation) {
            gatt.findDescriptor(descriptorUUID)?.let { descriptor ->
                gatt.readDescriptor(descriptor)
            } ?: [email protected] {
                Timber.e("Cannot find $descriptorUUID to read from")
                signalEndOfOperation()
            }
        }
        is EnableNotifications -> with(operation) {
            //(1)Get the characteristic from the connected device:
            gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
                val cccdUuid = UUID.fromString(CCC_DESCRIPTOR_UUID)
                Timber.d("cccdUuid: $cccdUuid, characteristic:$characteristicUUID")
            //(2)enable the notifications or indications:
                gatt.setCharacteristicNotification(characteristic,true)

                val payload = when {
                    characteristic.isIndicatable() ->
                        BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
                    characteristic.isNotifiable() ->
                        BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                    else ->
                        error("${characteristic.uuid} doesn't support notifications/indications")
                }
                //(3)write to the CCCD to enable notifications:
                //(3.1)
                characteristic.getDescriptor(cccdUuid)?.let { cccDescriptor ->
                    if (!gatt.setCharacteristicNotification(characteristic, true)) {
                        Timber.e("setCharacteristicNotification failed for ${characteristic.uuid}")
                        signalEndOfOperation()
                        return
                    }
                    /** **/
                    else if(gatt.setCharacteristicNotification(characteristic, true)){
                        Timber.i("setCharacteristicNotification succeeded for ${characteristic.uuid}")
                    }
                    /** **/
                //(3.2)
                    cccDescriptor.value = payload
                    Timber.d("cccDescriptor value: ${payload.toHexString()}")
                //(3.3)
                    gatt.writeDescriptor(cccDescriptor)
                } ?: [email protected] {
                    Timber.e("${characteristic.uuid} doesn't contain the CCC descriptor!")
                    signalEndOfOperation()
                }
            } ?: [email protected] {
                Timber.e("Cannot find $characteristicUUID! Failed to enable notifications.")
                signalEndOfOperation()
            }
        }
        is DisableNotifications -> with(operation) {
            gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
                val cccdUuid = UUID.fromString(CCC_DESCRIPTOR_UUID)
                characteristic.getDescriptor(cccdUuid)?.let { cccDescriptor ->
                    if (!gatt.setCharacteristicNotification(characteristic, false)) {
                        Timber.e("setCharacteristicNotification failed for ${characteristic.uuid}")
                        signalEndOfOperation()
                        return
                    }

                    cccDescriptor.value = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
                    gatt.writeDescriptor(cccDescriptor)
                } ?: [email protected] {
                    Timber.e("${characteristic.uuid} doesn't contain the CCC descriptor!")
                    signalEndOfOperation()
                }
            } ?: [email protected] {
                Timber.e("Cannot find $characteristicUUID! Failed to disable notifications.")
                signalEndOfOperation()
            }
        }
        is MtuRequest -> with(operation) {
            gatt.requestMtu(mtu)
        }
    }
}

I have a connection event listener declared in my Main activity:

    private val connectionEventListener by lazy {
    ConnectionEventListener().apply {
        onDisconnect = {
            runOnUiThread {
                alert {
                    title = "Disconnected"
                    message = "Disconnected from device."
                    positiveButton("OK") { onBackPressed() }
                }.show()

                if(STATE_DISCONNECTED == BleConnectionManager.getConnectionStatus()){
                    Timber.i("Connection Status: ${BleConnectionManager.getConnectionStatus()}, therefore disconnected, ${BleConnectionManager.isConnected.value}")
                    Timber.i("Device Disconnected from: ${BleConnectionManager.getBleDevice().name} | ${BleConnectionManager.getBleDevice().address}")
                    Toast.makeText(this@MainActivity,"Please turn on BLUETOOTH",Toast.LENGTH_SHORT).show()

                }else if(STATE_DISCONNECTED != BleConnectionManager.getConnectionStatus()){
                    Timber.e("Connection Status: ${BleConnectionManager.getConnectionStatus()}, did not disconnect")
                }
            }
        }
        onConnectionSetupComplete = {
            if (STATE_CONNECTED == BleConnectionManager.getConnectionStatus()) {
                Timber.i("Connection Status: ${BleConnectionManager.getConnectionStatus()}, therefore connected, ${BleConnectionManager.isConnected.value}")
                Timber.i("Device Connected to: ${BleConnectionManager.getBleDevice().name} | ${BleConnectionManager.getBleDevice().address}")
            }
        }

        onCharacteristicChanged = { _, characteristic ->
            Timber.i("onCharacteristicChanged - detected in MainActivity")
        }
        onNotificationsEnabled = { _, characteristic ->
            Timber.i("Notify enabled on: ${characteristic.uuid} - detected in MainActivity")
            notifyingCharacteristics.add(characteristic.uuid)
        }
        onNotificationsDisabled = { _, characteristic ->
            Timber.i("Notify disabled on: ${characteristic.uuid} - detected in MainActivity")
            notifyingCharacteristics.remove(characteristic.uuid)
        }
    }
}

I register the listener in the onCreate function and unregister it in the onDestroy function. I use a function called enableNotifications in the onServiceDiscovered function to enable notifications on the TX characteristic.

fun enableNotifications(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic) {
    if (device.isConnected() &&
        (characteristic.isIndicatable() || characteristic.isNotifiable())
    ) {
        enqueueOperation(EnableNotifications(device, characteristic.uuid))
    } else if (!device.isConnected()) {
        Timber.e("Not connected to ${device.address}, cannot enable notifications")
    } else if (!characteristic.isIndicatable() && !characteristic.isNotifiable()) {
        Timber.e("Characteristic ${characteristic.uuid} doesn't support notifications/indications")
    }
}



    override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            with(gatt) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    val services = gatt.services
                    Timber.w("Discovered ${services.size} services for ${device.address}.")
                    printGattTable()
                    requestMtu(device, GATT_MAX_MTU_SIZE)

                    val characteristicTx: BluetoothGattCharacteristic = this.getService(XpressStreamingServiceUUID).getCharacteristic(peripheralTX)
                    enableNotifications(device,characteristicTx)

                    listeners.forEach { it.get()?.onConnectionSetupComplete?.invoke(this) }
                } else {
                    Timber.e("Service discovery failed due to status $status")
                    teardownConnection(gatt.device)
                    disconnect()
                }
            }
            if (pendingOperation is Connect) {
                signalEndOfOperation()
            }
            /**    **/
//            val characteristic: BluetoothGattCharacteristic = gatt.getService(
//                XpressStreamingServiceUUID).getCharacteristic(peripheralRX)
//            gatt.setCharacteristicNotification(characteristic, true)
//            val descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)
//            descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
//            gatt.writeDescriptor(descriptor)
//            setBleCharacteristic(characteristic)
            /**   **/
        }

my onCharacteriscChanged function never gets called when notifications are enabled on the TX but it does with the RX. I have no idea why this happens. I tried using the nRF connect App to test communicating with the BLE device and the same thing happened. Only the the BGX commander app made by Silicon Labs is the one that worked but honestly I feel lost in their source code a bit. One of the most visible differences is their use of broadcast receivers but I honestly don't get why receiving data only works with their app but not LightBlue or nRF Connect.

I am using the serial monitor Simplicity Studio to test sending data to my app, like I said only data sent from the app appears but data sent from the serial monitor to the app never makes it except with their app BGX commander.

Any ideas?



Solution 1:[1]

I finally made it work!!!

Using a BGX(Express) device you have to follow this order:

  • Successfully bond with device
  • DiscoverServices from device
  • Start notify for "indications" ONLY to your TxCharacteristics, which was found in service discovery - Attention: TxCharacteristics offers both ("notification" & "indications")
  • all other characteristics stay untouched!
  • Start tears of happiness while watching how data flows in onCharacteristicChanged :-)

Referencing to your code snipper above...

val payload = when {
characteristic.isIndicatable() ->
                        BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
characteristic.isNotifiable() ->
                        BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
else ->
error("${characteristic.uuid} doesn't support notifications/indications")
}

... payload with the value ENABLE_NOTIFICATION_VALUE is written to the descriptor. That's why you never receive any data.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1