1 year ago
#76429

Mohamed El Kayal
What are the steps to receive notification data using BLE in android from a characteristic that is written to?
I have a characteristic that when written to the BLE device I am attempting to communicate with is supposed to respond with certain data.
When I enabled notification on the characteristic that is used when sending data, the onCharacteristicChanged function was called. But when I enabled notifications on the characteristic that I am supposed to use when receiving data, it never gets called.
My app is supposed to communicate with the BGX ble kit. The documentation here: https://docs.silabs.com/gecko-os/1/bgx/latest/ble-services
According to the link above in order to transmit data to the kit we use the RX characteristic where as to receive we use to TX characteristic.
private val XpressStreamingServiceUUID = UUID.fromString("331a36f5-2459-45ea-9d95-6142f0c4b307")
private val peripheralRX = UUID.fromString("a9da6040-0823-4995-94ec-9ce41ca28833")
private val peripheralTX = UUID.fromString("a73e9a10-628f-4494-a099-12efaf72258f")
If I enable notifications on the RX I do get notified but the same never happens to the TX. To elaborate there is a Processor Board we are using that is connected to the BLE kit. So basically if I send data it is to reach the board. The Write commands have been verified to reach their destination. So transmitting data from my app as a payload works fine.
Now I am supposed to send requests to the BLE device and receive a response in return. When it comes to sending, these functions come into play:
fun write(message:String){
val bytes = BigInteger(message.replace("\\s".toRegex(), ""), 16).toByteArray()
Timber.i("Bytes value ---> ${bytes.toHexString()}")
val device = getBleDevice()
// val characteristicRX = getBleCharacteristic()
val characteristicRX = bluetoothGattRef.getService(XpressStreamingServiceUUID).getCharacteristic(
peripheralRX)
writeCharacteristic(device, characteristicRX, bytes)
}
fun requestReadValues(requestCode:String){
if(isConnected.value!!){
requestData(requestCode)
}else{
Timber.e("Make sure that you connected and paired with the desired device.")
}
}
fun sendMessage(message:String){
Timber.i("Check if isConnected = true --> ${isConnected.value}")
if(isConnected.value == true){
write(message)
}else{
Timber.e("Make sure that you connected and paired with the desired device.")
}
}
fun requestData(data: String) {
val bytes = BigInteger(data.replace("\\s".toRegex(), ""), 16).toByteArray()
Timber.i("Bytes value of request---> ${bytes.toHexString()}")
val device = getBleDevice()
val characteristic = bluetoothGattRef.getService(XpressStreamingServiceUUID).getCharacteristic(peripheralRX)
writeCharacteristic(device, characteristic, bytes)
}
These are all in a BLEConnectionManager Class. Which as the name suggests is used to manage connections with BLE devices.
I have a queuing mechanism in place like this:
@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()
}
}
@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]
?: this@BleConnectionManager.run {
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)
} ?: this@BleConnectionManager.run {
Timber.e("Cannot find $characteristicUUID to write to")
signalEndOfOperation()
}
}
is CharacteristicRead -> with(operation) {
gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
gatt.readCharacteristic(characteristic)
} ?: this@BleConnectionManager.run {
Timber.e("Cannot find $characteristicUUID to read from")
signalEndOfOperation()
}
}
is DescriptorWrite -> with(operation) {
gatt.findDescriptor(descriptorUUID)?.let { descriptor ->
descriptor.value = payLoad
gatt.writeDescriptor(descriptor)
} ?: this@BleConnectionManager.run {
Timber.e("Cannot find $descriptorUUID to write to")
signalEndOfOperation()
}
}
is DescriptorRead -> with(operation) {
gatt.findDescriptor(descriptorUUID)?.let { descriptor ->
gatt.readDescriptor(descriptor)
} ?: this@BleConnectionManager.run {
Timber.e("Cannot find $descriptorUUID to read from")
signalEndOfOperation()
}
}
is EnableNotifications -> with(operation) {
gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
val cccdUuid = UUID.fromString(CCC_DESCRIPTOR_UUID)
val payload = when {
characteristic.isIndicatable() ->
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
characteristic.isNotifiable() ->
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
else ->
error("${characteristic.uuid} doesn't support notifications/indications")
}
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.e("setCharacteristicNotification succeeded for ${characteristic.uuid}")
}
/** **/
cccDescriptor.value = payload
gatt.writeDescriptor(cccDescriptor)
} ?: this@BleConnectionManager.run {
Timber.e("${characteristic.uuid} doesn't contain the CCC descriptor!")
signalEndOfOperation()
}
} ?: this@BleConnectionManager.run {
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)
} ?: this@BleConnectionManager.run {
Timber.e("${characteristic.uuid} doesn't contain the CCC descriptor!")
signalEndOfOperation()
}
} ?: this@BleConnectionManager.run {
Timber.e("Cannot find $characteristicUUID! Failed to disable notifications.")
signalEndOfOperation()
}
}
is MtuRequest -> with(operation) {
gatt.requestMtu(mtu)
}
}
}
This is my onCharacteristicChanged method:
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
with(characteristic) {
Timber.i("Characteristic $uuid changed | value: ${value.toHexString()}")
listeners.forEach { it.get()?.onCharacteristicChanged?.invoke(gatt.device, this) }
}
}
I also have a ConnectionEventListener in the activity or fragment that is used when sending data to detect certain events:
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("Value changed on ${characteristic.uuid}: ${characteristic.value.toHexString()}")
val byteArray = ByteArray(characteristic.value.size)
System.arraycopy(characteristic.value, 0, byteArray, 0, characteristic.value.size)
Timber.i("Message Received: ${byteArray.toHexString()}")
}
onNotificationsEnabled = { _, characteristic ->
Timber.i("Notify enabled on: ${characteristic.uuid}")
}
}
}
I do have to note that a log message confirming that the TX characteristic notifications are enable dis only when the services are discovered. So basically when the onServiceDiscovered function is called the "enableNotifications" is indeed used, but that's it. I don't really receive any notifications and I don't know how to proceed.
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 characteristic: BluetoothGattCharacteristic = this.getService(XpressStreamingServiceUUID).getCharacteristic(peripheralRX)
// this.setCharacteristicNotification(characteristic, true)
// setBleCharacteristic(characteristic)
// setNotification(gatt.getService(XpressStreamingServiceUUID).getCharacteristic(
// peripheralTX), true)
enableNotifications(device, characteristic)
// gatt.setCharacteristicNotification(characteristic, true)
// val desc = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)
// desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
// gatt.writeDescriptor(desc)
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()
}
}
This is a screenshot message of my log when notifications are enabled on the Tx characteristic:
What this shows is that the request was sent using the RX characteristic but nothing is received using the TX. Only that notifications were enabled on the TX.
UPDATE: new screenshots of my log
android
android-studio
bluetooth-lowenergy
0 Answers
Your Answer