RxAndroidBle写入外设后等待响应实现及异常解决(Kotlin)
问题背景
在Android Kotlin中使用RxAndroidBle与BLE外设交互,已成功实现设备连接和硬件信息读取功能。现在需要向外设写入指定JSON配置数据,并等待外设返回响应(类似HTTP POST的交互逻辑),但尝试实现时出现以下异常:
com.polidea.rxandroidble2.exceptions.BleCannotSetCharacteristicNotificationException:
Cannot find client characteristic config descriptor (code 2) with
characteristic UUID 4f4a4554-4520-4341-4c4f-520001000002
现有读取功能代码:
override fun connectDeviceToGetInfoHardwareByBle(mac: String): Observable<Resource<HardwareInfoResponse>> { val device: RxBleDevice = bleClient.getBleDevice(mac) return Observable.defer { device.bluetoothDevice.createBond()// 阻塞函数 device.establishConnection(false) // 返回Observable<RxBleConnection> } .delay(5, TimeUnit.SECONDS) .flatMapSingle { connection -> connection.requestMtu(515) .flatMap { Single.just(connection) } } .flatMapSingle { it.readCharacteristic(UUID.fromString(GET_HARDWARE_INFORMATION_CHARACTERISTIC)) .map { byteArray -> evaluateHardwareInfoResponse(byteArray = byteArray) } } .map { Resource.success(data = it) } .take(1) .onErrorReturn { Timber.i("Rointe Ble* Error getting ble information. {$it}") Resource.error(data = null, message = it.message.toString()) } .doOnError { Timber.i("Rointe Ble*","Error getting ble information."+it) } .subscribeOn(ioScheduler) .observeOn(uiScheduler) }
尝试的写入逻辑代码:
private fun setupNotifications(connection: RxBleConnection): Observable<Observable<ByteArray>> = connection.setupNotification(UUID.fromString(SET_FACTORY_SETTINGS_CHARACTERISTIC)) private fun performWrite(connection: RxBleConnection, notifications: Observable<ByteArray>, data: ByteArray): Observable<ByteArray> { return connection.writeCharacteristic(UUID.fromString(SET_FACTORY_SETTINGS_CHARACTERISTIC), data).toObservable() } override fun connectDeviceToWriteFactorySettingsByBle(mac: String, data: ByteArray): Observable<Resource<HardwareInfoResponse>> { val device: RxBleDevice = bleClient.getBleDevice(mac) return Observable.defer { //device.bluetoothDevice.createBond()// 阻塞函数 device.establishConnection(false) // 返回Observable<RxBleConnection> } .delay(5, TimeUnit.SECONDS) .flatMapSingle { connection -> connection.requestMtu(515) .flatMap { Single.just(connection) } } .flatMap( { connection -> setupNotifications(connection).delay(5, TimeUnit.SECONDS) }, { connection, deviceCallbacks -> performWrite(connection, deviceCallbacks, data) } ) .flatMap { it } //.take(1) // 成功写入后不再关注连接,会释放连接 .map { Timber.i("Rointe Ble: Result write: ok ->{${it.toHex()}}") Resource.success(data = evaluateHardwareInfoResponse(it)) } //.take(1) .onErrorReturn { Timber.i("Rointe Ble: Result write: failed ->{${it.message.toString()}}") Resource.error(data = HardwareInfoResponse.NULL_HARDWARE_INFO_RESPONSE, message = "Error write on device.") } .doOnError { Timber.i("Rointe Ble*","Error getting ble information."+it) } //.subscribeOn(ioScheduler) .observeOn(uiScheduler) }
需要写入的JSON数据:
{ "data": { "id_hardware": "[ID_HARDWARE]", "product_brand": <value>, "product_type": <value>, "product_model": <value>, "nominal_power": <value>, "industrialization_process_date": <value>, "platform_api_path": "[Host_API_REST]", "platform_streaming_path": "[Host_STREAMING]", "updates_main_path": "[Host_UPDATES]", "updates_alternative_path": "[Host_ALTERNATIVE_UPDATES]", "check_updates_time": <value>, "check_updates_day": <value>, "auth_main_path": "[Host_AUTHORIZATION]", "auth_alternative_path": "[Host_BACKUP_AUTHORIZATION]", "analytics_path": "[Host_ANALYTICS]", "idToken": "[ID_TOKEN]", "refreshToken": "[REFRESH_TOKEN]", "expiresIn": "3600", "apiKey": "[API_KEY]", "factory_wifi_ssid": "[FACTORY_WIFI_SSID]", "factory_wifi_security_type": "[FACTORY_WIFI_TYPE]", "factory_wifi_passphrase": "[FACTORY_WIFI_PASS]", "factory_wifi_dhcp": 1, "factory_wifi_device_ip": "[IPv4]", "factory_wifi_subnet_mask": "[SubNetMask_IPv4]", "factory_wifi_gateway": "[IPv4]" }, "factory_version": 1, "crc": "" }
解决方案
1. 解决「Cannot find client characteristic config descriptor」异常
该异常的核心原因是:你尝试设置通知的特征(SET_FACTORY_SETTINGS_CHARACTERISTIC)不支持通知/指示,或者没有对应的Client Characteristic Configuration Descriptor(CCCD,固定UUID:00002902-0000-1000-8000-00805f9b34fb)。
解决步骤:
- 查阅外设的GATT Profile文档,确认:
- 外设返回响应的特征是否是另一个独立的特征,而非写入的特征。如果是,需要监听那个响应特征的通知,而非写入特征。
- 如果写入特征确实需要接收通知,确认外设是否正确配置了该特征的CCCD,并且开启了NOTIFY/INDICATE权限。
2. 正确实现「写入后等待响应」的逻辑
根据外设的响应方式,分两种场景实现:
场景A:外设通过通知/指示返回响应(主流方案)
大多数BLE外设会通过独立的特征发送响应通知,实现代码如下:
// 替换为外设实际的响应特征UUID private const val RESPONSE_CHARACTERISTIC_UUID = "你的响应特征UUID" override fun connectDeviceToWriteFactorySettingsByBle(mac: String, data: ByteArray): Observable<Resource<HardwareInfoResponse>> { val device = bleClient.getBleDevice(mac) val writeCharUuid = UUID.fromString(SET_FACTORY_SETTINGS_CHARACTERISTIC) val responseCharUuid = UUID.fromString(RESPONSE_CHARACTERISTIC_UUID) return Observable.defer { device.establishConnection(false) } // 请求MTU,简化链式调用 .flatMapSingle { connection -> connection.requestMtu(515).andThen(Single.just(connection)) } .flatMap { connection -> // 1. 先设置响应特征的通知 connection.setupNotification(responseCharUuid) .flatMap { notificationObservable -> // 2. 通知设置成功后,执行写入操作 connection.writeCharacteristic(writeCharUuid, data) .toObservable() .ignoreElements() // 忽略写入成功的事件,关注通知的响应 .andThen(notificationObservable) // 切换到响应通知流 } .take(1) // 只取第一个响应结果 .map { responseBytes -> evaluateHardwareInfoResponse(responseBytes) } .map { Resource.success(it) } .doFinally { connection.disconnect() } // 处理完成后主动断开连接 } .onErrorReturn { Timber.i("Rointe Ble: 写入失败 -> ${it.message}") Resource.error(HardwareInfoResponse.NULL_HARDWARE_INFO_RESPONSE, "设备写入错误") } .subscribeOn(ioScheduler) // 必须恢复,避免UI线程阻塞 .observeOn(uiScheduler) }
场景B:外设通过Write Response返回响应(仅适用于小数据)
如果外设通过写入操作的响应包返回数据(这种情况数据长度受限,通常小于MTU-3),可以直接处理写入的返回结果:
override fun connectDeviceToWriteFactorySettingsByBle(mac: String, data: ByteArray): Observable<Resource<HardwareInfoResponse>> { val device = bleClient.getBleDevice(mac) val writeCharUuid = UUID.fromString(SET_FACTORY_SETTINGS_CHARACTERISTIC) return Observable.defer { device.establishConnection(false) } .flatMapSingle { connection -> connection.requestMtu(515) .andThen(connection.writeCharacteristic(writeCharUuid, data)) .map { responseBytes -> evaluateHardwareInfoResponse(responseBytes) } .map { Resource.success(it) } } .onErrorReturn { Timber.i("Rointe Ble: 写入失败 -> ${it.message}") Resource.error(HardwareInfoResponse.NULL_HARDWARE_INFO_RESPONSE, "设备写入错误") } .subscribeOn(ioScheduler) .observeOn(uiScheduler) }
3. 关于长写入的说明
如果你的JSON序列化后的字节数组长度超过MTU-3(这里MTU=515,即最大单次写入512字节),RxAndroidBle会自动处理长写入,将数据分块发送,不需要手动实现分逻辑,直接调用writeCharacteristic即可。
额外优化点
- 移除不必要的
delay(5, TimeUnit.SECONDS):RxAndroidBle的操作都是异步触发的,固定延迟会导致无效等待,甚至错过外设的响应。 - 确保
subscribeOn(ioScheduler)生效:连接、写入等操作必须在IO线程执行,避免阻塞UI线程。 - 确认外设特征权限:写入特征需要具备WRITE权限,响应特征需要具备NOTIFY/INDICATE权限。
内容的提问来源于stack exchange,提问作者FABiO

