Why do we get ERROR_GATT_WRITE_REQUEST_BUSY error while writing to characteristic if i enable notifications like in the below code?

30 views Asked by At
object CHatServer{
 private fun setupGattService(): BluetoothGattService {
       //......
        val rrepCharacteristic = BluetoothGattCharacteristic(
            RREP_UUID,
            BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_NOTIFY ,
            BluetoothGattCharacteristic.PERMISSION_READ
        )
        val cccd = BluetoothGattDescriptor(
            CCCD_UUID,
            BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
        )
        rrepCharacteristic.addDescriptor(cccd)

        service.addCharacteristic(rrepCharacteristic)
        val uuid = BluetoothGattCharacteristic(
            app?.let { UUIDManager.getStoredUUID(it.applicationContext) },
            BluetoothGattCharacteristic.PROPERTY_READ,
            BluetoothGattCharacteristic.PERMISSION_READ
        )
        service.addCharacteristic(uuid)

        return service
    }

    private class GattClientCallback : BluetoothGattCallback() {
        @SuppressLint("MissingPermission")
       //.......


        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
        @SuppressLint("MissingPermission")
        override fun onServicesDiscovered(discoveredGatt: BluetoothGatt, status: Int) {
            super.onServicesDiscovered(discoveredGatt, status)
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.d(TAG, "onServicesDiscovered: Have gatt $discoveredGatt")
                gatt = discoveredGatt
                val service = discoveredGatt.getService(SERVICE_UUID)
                if (service!=null){
                    if(service.getCharacteristic(MESSAGE_UUID)!=null){
                        messageCharacteristic = service.getCharacteristic(MESSAGE_UUID)
                    }
                    if(service.getCharacteristic(RREQ_UUID)!=null){
                        rreqCharacteristic = service.getCharacteristic(RREQ_UUID)
                    }
                    if(service.getCharacteristic(RREP_UUID)!=null){
                        rrepCharacteristic = service.getCharacteristic(RREP_UUID)
                        enableNotificationsForRrepCharacteristic(gatt, rrepCharacteristic)
                    }
                    val uuid = getUUID(service)
                    notifyConnectionSuccess(gatt!!.device,uuid)
                    Log.d(TAG, "UUID Discovered: $uuid")
                    gatt!!.requestMtu(GATT_MAX_MTU_SIZE)
                }
            }
        }


        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            value: ByteArray
        ) {
            super.onCharacteristicChanged(gatt, characteristic, value)
            Log.d(TAG, "Received RREP: ${gatt.device}")
            val packet = deserializePacket(value)
            if (packet != null) {
                if(packet.src == appUUID.toString()){
                    Log.d(TAG, "Path Found from A to C")
                }
            }
            _rrepRequests.postValue(packet)
        }

     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     @SuppressLint("MissingPermission")
     fun enableNotificationsForRrepCharacteristic(gatt: BluetoothGatt?, rrepCharacteristic: BluetoothGattCharacteristic?) {
        // Step 1: Enable notifications locally
        val notificationEnabled = gatt?.setCharacteristicNotification(rrepCharacteristic, true) ?: false
        if (!notificationEnabled) {
            Log.w(TAG, "Failed to enable local notifications for RREP_UUID")
            return
        }

        // Step 2: Write to the CCCD to enable notifications on the peripheral
        val cccd = rrepCharacteristic?.getDescriptor(CCCD_UUID)
        if (cccd == null) {
            Log.w(TAG, "RREP_UUID characteristic does not have a CCCD")
            return
        }
        val success = gatt?.writeDescriptor(cccd,BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
        if (success != BluetoothStatusCodes.SUCCESS) {
            Log.w(TAG, "Failed to write CCCD value for enabling notifications")
        } else {
            Log.d(TAG, "Successfully requested to enable notifications for RREP_UUID")
        }
    }
}


I think the issue is the writeDescriptor() function, because we are writing when we are still in onDiscoveredServices() callback, in between GATT operation, This is causing the 201 error (BUSY ERROR). But if we can not do that then how can we enable notification.?

When I use only setCharacteristicNotification() function in onServiceDiscovered() callback, then i am able to write to any characteristic but i wont be able to receive the notification. Because i heard you need descriptor to do that.`

2

There are 2 answers

0
Martijn van Welie On

In general, you need to queue all BLE operations so that one can fully complete before you start the next one. All BLE libraries out there do this.

In your case it might actually be the requestMTU() method is the issue. Because the write to the descriptor has not finished yet. Just debug it to see which gatt operation is giving you the error....

0
Emil On

As Martin explained, you are only allowed to have one outstanding GATT operation at a time (per BluetoothGatt object). Always wait for the corresponding callback to be called before issuing another GATT operation for the same BluetoothGatt object.

You must implement the onDescriptorWrite callback, which will be called when the writeDescriptor operation has finished.

You must also implement the onMtuChanged callback which will be called when the requestMtu operation finishes. However, this callback might also be called immediately after onConnectionStateChange in case the device is already connected (e.g. by another app) and MTU negotiation has already taken place, but there is unfortunately no way to know which of these two cases triggered onMtuChanged.

If you want to manually write the flow using the callbacks rather than relying on some generic queue management system, my suggestion is to use the following high level structure:

  • Two state variables are required (per BluetoothGatt object): the first is a boolean flag indicating if services have been discovered and the second one is the resulting MTU (either a nullable integer which is initially null or just a bool that negotiation have taken place).

  • In your onConnectionStateChange callback, whenever the device connected, call discoverServices.

  • In onServicesDiscovered, set some flag that you have discovered the services. If the MTU state variable indicates that onMtuChanged has not yet been called, call requestMtu. Otherwise, enable your notifications and write the descriptor.

  • In the onMtuChanged callback, first store the resulting MTU (if desired) or simply store that MTU negotiation has now taken place. Then, only if the service discovery complete flag set is set, enable your notifications and write the descriptor.

  • After the onDescriptorWrite callback arrives, you may now continue with other GATT operations.

The above structure makes sure the flow works correctly for both of the two cases.