diff --git a/CHANGELOG.md b/CHANGELOG.md index b7581651..6572d192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased * Windows: Harden BLE connection lifetime, asynchronous callbacks, and notification subscription handling * Android: instantly close the GATT client on disconnect +* Add Descriptor Read and Write apis ## 2.1.1 * Android: Fix BluetoothDevice null-safety compile error under Kotlin 2.x diff --git a/README.md b/README.md index a6e4bbab..9b35e63c 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,16 @@ await characteristic.write([0x01, 0x02, 0x03]); await characteristic.write([0x01, 0x02, 0x03], withResponse: false); ``` +To read or write a descriptor of this characteristic + +```dart +Uint8List value = await characteristic.descriptor('2901').read(); +``` + +```dart +await characteristic.descriptor('2901').write([0x01, 0x02, 0x03]); +``` + ## Subscriptions Get `BleCharacteristic` using `bleDevice.getCharacteristic` diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt index 19fdad4c..84a5c404 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt @@ -1714,8 +1714,10 @@ interface UniversalBlePlatformChannel { fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, callback: (Result) -> Unit) fun discoverServices(deviceId: String, withDescriptors: Boolean, callback: (Result>) -> Unit) fun readValue(deviceId: String, service: String, characteristic: String, callback: (Result) -> Unit) + fun readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, callback: (Result) -> Unit) fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) fun writeValue(deviceId: String, service: String, characteristic: String, value: ByteArray, bleOutputProperty: BleOutputProperty, callback: (Result) -> Unit) + fun writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: ByteArray, callback: (Result) -> Unit) fun isPaired(deviceId: String, callback: (Result) -> Unit) fun pair(deviceId: String, callback: (Result) -> Unit) fun unPair(deviceId: String) @@ -1977,6 +1979,29 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readDescriptorValue$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + val serviceArg = args[1] as String + val characteristicArg = args[2] as String + val descriptorArg = args[3] as String + api.readDescriptorValue(deviceIdArg, serviceArg, characteristicArg, descriptorArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(UniversalBlePigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu$separatedMessageChannelSuffix", codec) if (api != null) { @@ -2021,6 +2046,29 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeDescriptorValue$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + val serviceArg = args[1] as String + val characteristicArg = args[2] as String + val descriptorArg = args[3] as String + val valueArg = args[4] as ByteArray + api.writeDescriptorValue(deviceIdArg, serviceArg, characteristicArg, descriptorArg, valueArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(UniversalBlePigeonUtils.wrapError(error)) + } else { + reply.reply(UniversalBlePigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired$separatedMessageChannelSuffix", codec) if (api != null) { diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt index a6b8d5e9..392e3ee4 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -462,4 +462,20 @@ class SubscriptionResultFuture( class RssiResultFuture( val deviceId: String, val result: (Result) -> Unit, +) + +class ReadDescriptorResultFuture( + val deviceId: String, + val descriptorId: String, + val characteristicId: String, + val serviceId: String, + val result: (Result) -> Unit, +) + +class WriteDescriptorResultFuture( + val deviceId: String, + val descriptorId: String, + val characteristicId: String, + val serviceId: String, + val result: (Result) -> Unit, ) \ No newline at end of file diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index a3027672..86893f4a 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -64,6 +64,8 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), private val mtuResultFutureList = mutableListOf() private val readResultFutureList = mutableListOf() private val writeResultFutureList = mutableListOf() + private val readDescriptorResultFutureList = mutableListOf() + private val writeDescriptorResultFutureList = mutableListOf() private val subscriptionResultFutureList = mutableListOf() private val pairResultFutures = mutableMapOf) -> Unit>() private val rssiResultFutureList = mutableListOf() @@ -827,6 +829,206 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } + override fun readDescriptorValue( + deviceId: String, + service: String, + characteristic: String, + descriptor: String, + callback: (Result) -> Unit, + ) { + try { + UniversalBleLogger.logDebug("READ_DESCRIPTOR -> $deviceId $service $characteristic $descriptor") + val gatt = deviceId.toBluetoothGatt() + val gattCharacteristic = gatt.getCharacteristic(service, characteristic) + if (gattCharacteristic == null) { + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND, + "Characteristic not found" + ) + ) + ) + return + } + val gattDescriptor = gattCharacteristic.getDescriptor(UUID.fromString(descriptor)) + if (gattDescriptor == null) { + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND, + "Descriptor not found" + ) + ) + ) + return + } + if (!gatt.readDescriptor(gattDescriptor)) { + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.READ_FAILED, + "Failed to read descriptor" + ) + ) + ) + return + } + synchronized(readDescriptorResultFutureList) { + readDescriptorResultFutureList.add( + ReadDescriptorResultFuture( + gatt.device.address, + gattDescriptor.uuid.toString(), + gattCharacteristic.uuid.toString(), + gattCharacteristic.service.uuid.toString(), + callback + ) + ) + } + } catch (e: FlutterError) { + callback(Result.failure(e)) + } catch (e: Exception) { + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.READ_FAILED, + "Failed to read descriptor value", + e.toString() + ) + ) + ) + } + } + + override fun onDescriptorRead( + gatt: BluetoothGatt, + descriptor: BluetoothGattDescriptor, + status: Int, + value: ByteArray, + ) { + val future: ReadDescriptorResultFuture? + synchronized(readDescriptorResultFutureList) { + future = readDescriptorResultFutureList.firstOrNull { + it.deviceId == gatt.device.address && + it.descriptorId == descriptor.uuid.toString() && + it.characteristicId == descriptor.characteristic.uuid.toString() && + it.serviceId == descriptor.characteristic.service.uuid.toString() + } + future?.let { readDescriptorResultFutureList.remove(it) } + } + if (future != null) { + postToMainLooper { + if (status == BluetoothGatt.GATT_SUCCESS) { + future.result(Result.success(value)) + } else { + future.result( + Result.failure( + createFlutterError( + gattStatusToUniversalBleErrorCode(status), + "Failed to read descriptor", + status.toString() + ) + ) + ) + } + } + } + } + + @Suppress("DEPRECATION") + override fun onDescriptorRead( + gatt: BluetoothGatt, + descriptor: BluetoothGattDescriptor, + status: Int, + ) { + onDescriptorRead(gatt, descriptor, status, descriptor.value ?: byteArrayOf()) + } + + override fun writeDescriptorValue( + deviceId: String, + service: String, + characteristic: String, + descriptor: String, + value: ByteArray, + callback: (Result) -> Unit, + ) { + try { + UniversalBleLogger.logDebug("WRITE_DESCRIPTOR -> $deviceId $service $characteristic $descriptor len=${value.size}") + val gatt = deviceId.toBluetoothGatt() + val gattCharacteristic = gatt.getCharacteristic(service, characteristic) + if (gattCharacteristic == null) { + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND, + "Characteristic not found" + ) + ) + ) + return + } + val gattDescriptor = gattCharacteristic.getDescriptor(UUID.fromString(descriptor)) + if (gattDescriptor == null) { + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND, + "Descriptor not found" + ) + ) + ) + return + } + + val writeFuture = WriteDescriptorResultFuture( + gatt.device.address, + gattDescriptor.uuid.toString(), + gattCharacteristic.uuid.toString(), + gattCharacteristic.service.uuid.toString(), + callback + ) + + synchronized(writeDescriptorResultFutureList) { + writeDescriptorResultFutureList.add(writeFuture) + } + + val status = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gatt.writeDescriptor(gattDescriptor, value) + } else { + @Suppress("DEPRECATION") + gattDescriptor.value = value + @Suppress("DEPRECATION") + if (gatt.writeDescriptor(gattDescriptor)) BluetoothGatt.GATT_SUCCESS else BluetoothGatt.GATT_FAILURE + } + + if (status != BluetoothGatt.GATT_SUCCESS) { + synchronized(writeDescriptorResultFutureList) { + writeDescriptorResultFutureList.remove(writeFuture) + } + callback( + Result.failure( + createFlutterError( + gattStatusToUniversalBleErrorCode(status), + "Failed to write descriptor", + status.toString() + ) + ) + ) + } + } catch (e: FlutterError) { + callback(Result.failure(e)) + } catch (e: Exception) { + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.FAILED, + "Failed to write descriptor value", + e.toString() + ) + ) + ) + } + } override fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) { UniversalBleLogger.logDebug("REQUEST_MTU -> $deviceId expected=$expectedMtu") @@ -1157,6 +1359,34 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } } + val pendingDescriptorReads: List + synchronized(readDescriptorResultFutureList) { + pendingDescriptorReads = readDescriptorResultFutureList.filter { it.deviceId == deviceId } + readDescriptorResultFutureList.removeAll(pendingDescriptorReads) + } + for (future in pendingDescriptorReads) { + postToMainLooper { + try { + future.result(Result.failure(deviceDisconnectedError)) + } catch (e: Exception) { + UniversalBleLogger.logError("Read descriptor completion delivery failed: $e") + } + } + } + val pendingDescriptorWrites: List + synchronized(writeDescriptorResultFutureList) { + pendingDescriptorWrites = writeDescriptorResultFutureList.filter { it.deviceId == deviceId } + writeDescriptorResultFutureList.removeAll(pendingDescriptorWrites) + } + for (future in pendingDescriptorWrites) { + postToMainLooper { + try { + future.result(Result.failure(deviceDisconnectedError)) + } catch (e: Exception) { + UniversalBleLogger.logError("Write descriptor completion delivery failed: $e") + } + } + } subscriptionResultFutureList.removeAll { if (it.deviceId == deviceId) { it.result(Result.failure(deviceDisconnectedError)) @@ -1374,6 +1604,36 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), status: Int, ) { super.onDescriptorWrite(gatt, descriptor, status) + if (descriptor != null) { + val future: WriteDescriptorResultFuture? + synchronized(writeDescriptorResultFutureList) { + future = writeDescriptorResultFutureList.firstOrNull { + it.deviceId == gatt?.device?.address && + it.descriptorId == descriptor.uuid.toString() && + it.characteristicId == descriptor.characteristic?.uuid?.toString() && + it.serviceId == descriptor.characteristic?.service?.uuid?.toString() + } + future?.let { writeDescriptorResultFutureList.remove(it) } + } + if (future != null) { + postToMainLooper { + if (status == BluetoothGatt.GATT_SUCCESS) { + future.result(Result.success(Unit)) + } else { + future.result( + Result.failure( + createFlutterError( + gattStatusToUniversalBleErrorCode(status), + "Failed to write descriptor", + status.toString() + ) + ) + ) + } + } + } + } + if (descriptor?.uuid.toString() == ccdCharacteristic.toString()) { val char: String? = descriptor?.characteristic?.uuid?.toString() val service: String? = descriptor?.characteristic?.service?.uuid?.toString() diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift index 85a6d671..ea5afc31 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift @@ -1505,8 +1505,10 @@ protocol UniversalBlePlatformChannel { func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, completion: @escaping (Result) -> Void) func discoverServices(deviceId: String, withDescriptors: Bool, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) func readValue(deviceId: String, service: String, characteristic: String, completion: @escaping (Result) -> Void) + func readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, completion: @escaping (Result) -> Void) func requestMtu(deviceId: String, expectedMtu: Int64, completion: @escaping (Result) -> Void) func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: BleOutputProperty, completion: @escaping (Result) -> Void) + func writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData, completion: @escaping (Result) -> Void) func isPaired(deviceId: String, completion: @escaping (Result) -> Void) func pair(deviceId: String, completion: @escaping (Result) -> Void) func unPair(deviceId: String) throws @@ -1731,6 +1733,26 @@ class UniversalBlePlatformChannelSetup { } else { readValueChannel.setMessageHandler(nil) } + let readDescriptorValueChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readDescriptorValue\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + readDescriptorValueChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + let serviceArg = args[1] as! String + let characteristicArg = args[2] as! String + let descriptorArg = args[3] as! String + api.readDescriptorValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, descriptor: descriptorArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + readDescriptorValueChannel.setMessageHandler(nil) + } let requestMtuChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { requestMtuChannel.setMessageHandler { message, reply in @@ -1770,6 +1792,27 @@ class UniversalBlePlatformChannelSetup { } else { writeValueChannel.setMessageHandler(nil) } + let writeDescriptorValueChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeDescriptorValue\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + writeDescriptorValueChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + let serviceArg = args[1] as! String + let characteristicArg = args[2] as! String + let descriptorArg = args[3] as! String + let valueArg = args[4] as! FlutterStandardTypedData + api.writeDescriptorValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, descriptor: descriptorArg, value: valueArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + writeDescriptorValueChannel.setMessageHandler(nil) + } let isPairedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { isPairedChannel.setMessageHandler { message, reply in diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift index 45001b55..1a18e065 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift @@ -154,6 +154,14 @@ public extension CBPeripheral { return c } + func getDescriptor(_ descriptor: String, for characteristic: String, of service: String) -> CBDescriptor? { + let GSS_SUFFIX = "0000-1000-8000-00805f9b34fb" + guard let c = getCharacteristic(characteristic, of: service) else { return nil } + return c.descriptors?.first { + $0.uuid.uuidStr.lowercased() == descriptor.lowercased() || descriptor.lowercased() == "0000\($0.uuid.uuidStr)-\(GSS_SUFFIX)".lowercased() + } + } + func setNotifiable(_ bleInputProperty: String, for characteristic: String, of service: String) { guard let characteristic = getCharacteristic(characteristic, of: service) else { return @@ -230,3 +238,35 @@ class RssiReadFuture { self.result = result } } + +class DescriptorReadFuture { + let deviceId: String + let descriptorId: String + let characteristicId: String + let serviceId: String? + let result: (Result) -> Void + + init(deviceId: String, descriptorId: String, characteristicId: String, serviceId: String?, result: @escaping (Result) -> Void) { + self.deviceId = deviceId + self.descriptorId = descriptorId + self.characteristicId = characteristicId + self.serviceId = serviceId + self.result = result + } +} + +class DescriptorWriteFuture { + let deviceId: String + let descriptorId: String + let characteristicId: String + let serviceId: String? + let result: (Result) -> Void + + init(deviceId: String, descriptorId: String, characteristicId: String, serviceId: String?, result: @escaping (Result) -> Void) { + self.deviceId = deviceId + self.descriptorId = descriptorId + self.characteristicId = characteristicId + self.serviceId = serviceId + self.result = result + } +} diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift index f73f417c..be29438b 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift @@ -97,6 +97,8 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral private var characteristicWriteFutures = [CharacteristicWriteFuture]() private var characteristicWriteWithoutResponseFutures = [CharacteristicWriteFuture]() private var characteristicNotifyFutures = [CharacteristicNotifyFuture]() + private var descriptorReadFutures = [DescriptorReadFuture]() + private var descriptorWriteFutures = [DescriptorWriteFuture]() private var discoverServicesFutures = [DiscoverServicesFuture]() private var rssiReadFutures = [RssiReadFuture]() private var isManageScanning = false @@ -314,6 +316,24 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } return false } + descriptorReadFutures.removeAll { future in + if future.deviceId == deviceId { + future.result( + Result.failure(createFlutterError(code: .deviceDisconnected, message: "Device Disconnected")) + ) + return true + } + return false + } + descriptorWriteFutures.removeAll { future in + if future.deviceId == deviceId { + future.result( + Result.failure(createFlutterError(code: .deviceDisconnected, message: "Device Disconnected")) + ) + return true + } + return false + } discoverServicesFutures.removeAll { future in if future.deviceId == deviceId { future.result( @@ -454,6 +474,67 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } } + func readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, completion: @escaping (Result) -> Void) { + UniversalBleLogger.shared.logDebug("READ_DESCRIPTOR -> \(deviceId) \(service) \(characteristic) \(descriptor)") + guard let peripheral = deviceId.findPeripheral(manager: manager) else { + completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) + return + } + guard let gattDescriptor = peripheral.getDescriptor(descriptor, for: characteristic, of: service) else { + completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Descriptor not found:\(descriptor)"))) + return + } + peripheral.readValue(for: gattDescriptor) + descriptorReadFutures.append(DescriptorReadFuture( + deviceId: deviceId, + descriptorId: gattDescriptor.uuid.uuidStr, + characteristicId: gattDescriptor.characteristic?.uuid.uuidStr ?? characteristic, + serviceId: gattDescriptor.characteristic?.service?.uuid.uuidStr ?? service, + result: completion + )) + } + + func writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData, completion: @escaping (Result) -> Void) { + UniversalBleLogger.shared.logDebug("WRITE_DESCRIPTOR -> \(deviceId) \(service) \(characteristic) \(descriptor) len=\(value.data.count)") + guard let peripheral = deviceId.findPeripheral(manager: manager) else { + completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) + return + } + guard let gattDescriptor = peripheral.getDescriptor(descriptor, for: characteristic, of: service) else { + completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Descriptor not found:\(descriptor)"))) + return + } + + // CoreBluetooth throws NSInternalInconsistencyException if writeValue:forDescriptor: is called on CCCD (0x2902). + // CoreBluetooth requires using setNotifyValue:forCharacteristic: for CCCD. + let cccdUUID = CBUUID(string: CBUUIDClientCharacteristicConfigurationString) + let fullCccdUUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") + if gattDescriptor.uuid == cccdUUID || gattDescriptor.uuid == fullCccdUUID { + guard let gattCharacteristic = gattDescriptor.characteristic else { + completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Characteristic not found for descriptor"))) + return + } + let enable = value.data.contains { $0 != 0 } + peripheral.setNotifyValue(enable, for: gattCharacteristic) + characteristicNotifyFutures.append(CharacteristicNotifyFuture( + deviceId: deviceId, + characteristicId: gattCharacteristic.uuid.uuidStr, + serviceId: gattCharacteristic.service?.uuid.uuidStr, + result: completion + )) + return + } + + peripheral.writeValue(value.data, for: gattDescriptor) + descriptorWriteFutures.append(DescriptorWriteFuture( + deviceId: deviceId, + descriptorId: gattDescriptor.uuid.uuidStr, + characteristicId: gattDescriptor.characteristic?.uuid.uuidStr ?? characteristic, + serviceId: gattDescriptor.characteristic?.service?.uuid.uuidStr ?? service, + result: completion + )) + } + func requestMtu(deviceId: String, expectedMtu _: Int64, completion: @escaping (Result) -> Void) { UniversalBleLogger.shared.logDebug("REQUEST_MTU -> \(deviceId)") guard let peripheral = deviceId.findPeripheral(manager: manager) else { @@ -676,6 +757,55 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } } + public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) { + descriptorReadFutures.removeAll { future in + if future.deviceId == peripheral.uuid.uuidString && + future.descriptorId == descriptor.uuid.uuidStr && + future.characteristicId == descriptor.characteristic?.uuid.uuidStr && + future.serviceId == descriptor.characteristic?.service?.uuid.uuidStr { + if let flutterError = error?.toFlutterError() { + UniversalBleLogger.shared.logError("READ_DESCRIPTOR_FAILED <- \(peripheral.uuid.uuidString) \(descriptor.uuid.uuidStr): \(flutterError.message ?? "")") + future.result(Result.failure(flutterError)) + } else { + if let valueData = descriptor.value as? Data { + future.result(Result.success(FlutterStandardTypedData(bytes: valueData))) + } else if let numberVal = descriptor.value as? NSNumber { + var val = numberVal.uint16Value + let data = Data(bytes: &val, count: MemoryLayout.size) + future.result(Result.success(FlutterStandardTypedData(bytes: data))) + } else if let stringVal = descriptor.value as? String { + let data = Data(stringVal.utf8) + future.result(Result.success(FlutterStandardTypedData(bytes: data))) + } else if let cbuuid = descriptor.value as? CBUUID { + future.result(Result.success(FlutterStandardTypedData(bytes: cbuuid.data))) + } else { + future.result(Result.success(FlutterStandardTypedData(bytes: Data()))) + } + } + return true + } + return false + } + } + + public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) { + descriptorWriteFutures.removeAll { future in + if future.deviceId == peripheral.uuid.uuidString && + future.descriptorId == descriptor.uuid.uuidStr && + future.characteristicId == descriptor.characteristic?.uuid.uuidStr && + future.serviceId == descriptor.characteristic?.service?.uuid.uuidStr { + if let flutterError = error?.toFlutterError() { + UniversalBleLogger.shared.logError("WRITE_DESCRIPTOR_FAILED <- \(peripheral.uuid.uuidString) \(descriptor.uuid.uuidStr): \(flutterError.message ?? "")") + future.result(Result.failure(flutterError)) + } else { + future.result(Result.success({}())) + } + return true + } + return false + } + } + public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { characteristicNotifyFutures.removeAll { future in if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index 79dbb60d..58f86655 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -99,6 +99,30 @@ class MockUniversalBle extends UniversalBlePlatform { _serviceValue = value; } + @override + Future readDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, { + Duration? timeout, + }) async { + await Future.delayed(const Duration(milliseconds: 500)); + return _serviceValue; + } + + @override + Future writeDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value, + ) async { + await Future.delayed(const Duration(milliseconds: 500)); + _serviceValue = value; + } + @override Future requestMtu(String deviceId, int expectedMtu) async { await Future.delayed(const Duration(seconds: 1)); diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index 090e42c2..11e350a6 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -32,6 +32,7 @@ class _PeripheralDetailPageState extends State { StreamSubscription? pairingStateSubscription; BleService? selectedService; BleCharacteristic? selectedCharacteristic; + BleDescriptor? selectedDescriptor; @override void initState() { @@ -114,7 +115,7 @@ class _PeripheralDetailPageState extends State { const webWarning = "Note: Only services added in ScanFilter or WebOptions will be discovered"; try { - var services = await bleDevice.discoverServices(withDescriptors: false); + var services = await bleDevice.discoverServices(withDescriptors: true); debugPrint('${services.length} services discovered'); debugPrint(services.toString()); setState(() { @@ -132,6 +133,54 @@ class _PeripheralDetailPageState extends State { } } + Future _readDescriptorValue() async { + BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic; + BleService? selectedService = this.selectedService; + BleDescriptor? selectedDescriptor = this.selectedDescriptor; + if (selectedCharacteristic == null || + selectedService == null || + selectedDescriptor == null) { + return; + } + try { + Uint8List value = await selectedDescriptor.read(); + String s = String.fromCharCodes(value); + String data = '$s\nraw : ${value.toString()}'; + _addLog('ReadDescriptor', data); + } catch (e) { + _addLog('ReadDescriptorError', e); + } + } + + Future _writeDescriptorValue() async { + BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic; + BleService? selectedService = this.selectedService; + BleDescriptor? selectedDescriptor = this.selectedDescriptor; + if (selectedCharacteristic == null || + selectedService == null || + selectedDescriptor == null || + !valueFormKey.currentState!.validate() || + binaryCode.text.isEmpty) { + return; + } + + Uint8List value; + try { + value = Uint8List.fromList(hex.decode(binaryCode.text)); + } catch (e) { + _addLog('WriteDescriptorError', "Error parsing hex $e"); + return; + } + + try { + await selectedDescriptor.write(value); + _addLog('WriteDescriptor', value); + } catch (e) { + debugPrint(e.toString()); + _addLog('WriteDescriptorError', e); + } + } + Future _readValue() async { BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic; if (selectedCharacteristic == null) return; @@ -250,10 +299,14 @@ class _PeripheralDetailPageState extends State { : ServicesListWidget( discoveredServices: discoveredServices, scrollable: true, - onTap: (service, characteristic) { + onTap: (service, characteristic, descriptor) { setState(() { selectedService = service; selectedCharacteristic = characteristic; + selectedDescriptor = descriptor ?? + (characteristic.descriptors.isNotEmpty + ? characteristic.descriptors.first + : null); }); }, ), @@ -325,6 +378,48 @@ class _PeripheralDetailPageState extends State { Text( "Properties: ${selectedCharacteristic?.properties.map((e) => e.name)}", ), + if (selectedCharacteristic != null && + selectedCharacteristic! + .descriptors.isNotEmpty) ...[ + const SizedBox(height: 4), + Row( + children: [ + const Text( + "Selected Descriptor: ", + style: TextStyle( + fontWeight: FontWeight.bold), + ), + Expanded( + child: + DropdownButton( + isDense: true, + isExpanded: true, + value: selectedDescriptor ?? + selectedCharacteristic! + .descriptors.first, + items: selectedCharacteristic! + .descriptors + .map( + (d) => DropdownMenuItem( + value: d, + child: Text( + d.uuid, + overflow: + TextOverflow + .ellipsis, + ), + )) + .toList(), + onChanged: (val) { + setState(() { + selectedDescriptor = val; + }); + }, + ), + ), + ], + ), + ], ], ), ), @@ -332,9 +427,10 @@ class _PeripheralDetailPageState extends State { ), if (_hasSelectedCharacteristicProperty([ - CharacteristicProperty.write, - CharacteristicProperty.writeWithoutResponse - ])) + CharacteristicProperty.write, + CharacteristicProperty.writeWithoutResponse + ]) || + selectedDescriptor != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Form( @@ -429,6 +525,20 @@ class _PeripheralDetailPageState extends State { onPressed: () => _writeValue(withResponse: false), text: 'WriteWithoutResponse', ), + PlatformButton( + enabled: isConnected && + discoveredServices.isNotEmpty && + selectedDescriptor != null, + onPressed: _readDescriptorValue, + text: 'Read Descriptor', + ), + PlatformButton( + enabled: isConnected && + discoveredServices.isNotEmpty && + selectedDescriptor != null, + onPressed: _writeDescriptorValue, + text: 'Write Descriptor', + ), PlatformButton( enabled: isConnected && discoveredServices.isNotEmpty && @@ -491,10 +601,14 @@ class _PeripheralDetailPageState extends State { if (deviceType != DeviceType.desktop) ServicesListWidget( discoveredServices: discoveredServices, - onTap: (service, characteristic) { + onTap: (service, characteristic, descriptor) { setState(() { selectedService = service; selectedCharacteristic = characteristic; + selectedDescriptor = descriptor ?? + (characteristic.descriptors.isNotEmpty + ? characteristic.descriptors.first + : null); }); }, ), diff --git a/example/lib/peripheral_details/widgets/services_list_widget.dart b/example/lib/peripheral_details/widgets/services_list_widget.dart index 15ee23b8..0704ce6f 100644 --- a/example/lib/peripheral_details/widgets/services_list_widget.dart +++ b/example/lib/peripheral_details/widgets/services_list_widget.dart @@ -8,6 +8,7 @@ class ServicesListWidget extends StatelessWidget { final void Function( BleService service, BleCharacteristic characteristic, + BleDescriptor? descriptor, )? onTap; const ServicesListWidget({ @@ -48,7 +49,13 @@ class ServicesListWidget extends StatelessWidget { children: [ InkWell( onTap: () { - onTap?.call(discoveredServices[index], e); + onTap?.call( + discoveredServices[index], + e, + e.descriptors.isNotEmpty + ? e.descriptors.first + : null, + ); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -62,13 +69,38 @@ class ServicesListWidget extends StatelessWidget { Text( "Properties: ${e.properties.map((e) => e.name)}", ), - if (e.descriptors.isNotEmpty) - Text( - "Descriptors: ${e.descriptors.map((e) => e.uuid).join(', ')}", - ) ], ), ), + if (e.descriptors.isNotEmpty) ...[ + const SizedBox(height: 4), + Wrap( + spacing: 6, + runSpacing: 4, + children: e.descriptors + .map( + (d) => ActionChip( + avatar: const Icon( + Icons.description, + size: 14, + ), + label: Text( + d.uuid, + style: + const TextStyle(fontSize: 11), + ), + onPressed: () { + onTap?.call( + discoveredServices[index], + e, + d, + ); + }, + ), + ) + .toList(), + ), + ], ], ), )) diff --git a/lib/src/extensions/ble_characteristic_extension.dart b/lib/src/extensions/ble_characteristic_extension.dart index 39a22a9a..b1b56868 100644 --- a/lib/src/extensions/ble_characteristic_extension.dart +++ b/lib/src/extensions/ble_characteristic_extension.dart @@ -1,12 +1,13 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart'; import 'package:universal_ble/universal_ble.dart'; /// Extension methods for [BleCharacteristic] to simplify common operations. extension BleCharacteristicExtension on BleCharacteristic { /// A stream of [Uint8List] that emits values received from the characteristic. Stream get onValueReceived => - UniversalBle.characteristicValueStream(_deviceId, uuid); + UniversalBle.characteristicValueStream(_metaData.deviceId, uuid); /// Subscribes to notifications for this characteristic. /// @@ -20,11 +21,17 @@ extension BleCharacteristicExtension on BleCharacteristic { CharacteristicSubscription get indications => CharacteristicSubscription(this, CharacteristicProperty.indicate); + /// Get descriptor by uuid + BleDescriptor descriptor(String descriptorUuid) => descriptors.firstWhere( + (e) => BleUuidParser.compareStrings(e.uuid, descriptorUuid), + orElse: () => throw NotFoundError.forDescriptor(descriptorUuid, uuid), + ); + /// Unsubscribes notifications/indications from this characteristic. Future unsubscribe({Duration? timeout, String? queueId}) => UniversalBle.unsubscribe( - _deviceId, - _serviceId, + _metaData.deviceId, + _metaData.serviceId, uuid, timeout: timeout, queueId: queueId, @@ -33,8 +40,8 @@ extension BleCharacteristicExtension on BleCharacteristic { /// Reads the current value of the characteristic. Future read({Duration? timeout, String? queueId}) => UniversalBle.read( - _deviceId, - _serviceId, + _metaData.deviceId, + _metaData.serviceId, uuid, timeout: timeout, queueId: queueId, @@ -53,8 +60,8 @@ extension BleCharacteristicExtension on BleCharacteristic { String? queueId, }) async { await UniversalBle.write( - _deviceId, - _serviceId, + _metaData.deviceId, + _metaData.serviceId, uuid, Uint8List.fromList(value), withoutResponse: !withResponse, @@ -63,20 +70,49 @@ extension BleCharacteristicExtension on BleCharacteristic { ); } - String get _deviceId { - String? deviceId = metaData?.deviceId; - if (deviceId == null) { - throw "DeviceId is not preset in characteristic metaData"; - } - return deviceId; - } - - String get _serviceId { - String? serviceId = metaData?.serviceId; - if (serviceId == null) { - throw "ServiceId is not preset in characteristic metaData"; - } - return serviceId; + /// Reads the value of a descriptor of this characteristic. + /// + /// [descriptorUuid] is the UUID of the descriptor to read. + /// [timeout] is the timeout for the read operation. + /// [queueId] is the ID of the queue to use for the read operation. + Future readDescriptor( + String descriptorUuid, { + Duration? timeout, + String? queueId, + }) => UniversalBle.readDescriptor( + _metaData.deviceId, + _metaData.serviceId, + uuid, + descriptorUuid, + timeout: timeout, + queueId: queueId, + ); + + /// Writes a value to a descriptor of this characteristic. + /// + /// [descriptorUuid] is the UUID of the descriptor to write. + /// [value] is the value to write. + /// [timeout] is the timeout for the write operation. + /// [queueId] is the ID of the queue to use for the write operation. + Future writeDescriptor( + String descriptorUuid, + Uint8List value, { + Duration? timeout, + String? queueId, + }) => UniversalBle.writeDescriptor( + _metaData.deviceId, + _metaData.serviceId, + uuid, + descriptorUuid, + value, + timeout: timeout, + queueId: queueId, + ); + + BleCharOperationMetadata get _metaData { + BleCharOperationMetadata? metaData = this.metaData; + if (metaData == null) throw "Characteristic metaData is not preset"; + return metaData; } } @@ -122,8 +158,8 @@ class CharacteristicSubscription { if (_property == CharacteristicProperty.indicate) { return UniversalBle.subscribeIndications( - _characteristic._deviceId, - _characteristic._serviceId, + _characteristic._metaData.deviceId, + _characteristic._metaData.serviceId, _characteristic.uuid, timeout: timeout, queueId: queueId, @@ -131,8 +167,8 @@ class CharacteristicSubscription { } return UniversalBle.subscribeNotifications( - _characteristic._deviceId, - _characteristic._serviceId, + _characteristic._metaData.deviceId, + _characteristic._metaData.serviceId, _characteristic.uuid, timeout: timeout, queueId: queueId, @@ -143,8 +179,8 @@ class CharacteristicSubscription { Future unsubscribe({Duration? timeout, String? queueId}) { if (!isSupported) throw Exception('Operation not supported'); return UniversalBle.unsubscribe( - _characteristic._deviceId, - _characteristic._serviceId, + _characteristic._metaData.deviceId, + _characteristic._metaData.serviceId, _characteristic.uuid, timeout: timeout, queueId: queueId, diff --git a/lib/src/extensions/ble_descriptor_extension.dart b/lib/src/extensions/ble_descriptor_extension.dart new file mode 100644 index 00000000..717d66df --- /dev/null +++ b/lib/src/extensions/ble_descriptor_extension.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'package:universal_ble/universal_ble.dart'; + +/// Extension methods for [BleDescriptor] to simplify common operations. +extension BleDescriptorExtension on BleDescriptor { + /// Reads the value of a descriptor of this characteristic. + Future read({Duration? timeout, String? queueId}) => + UniversalBle.readDescriptor( + _metaData.deviceId, + _metaData.serviceId, + _metaData.characteristicId, + uuid, + timeout: timeout, + queueId: queueId, + ); + + /// Writes a value to a descriptor of this characteristic. + Future write(Uint8List value, {Duration? timeout, String? queueId}) => + UniversalBle.writeDescriptor( + _metaData.deviceId, + _metaData.serviceId, + _metaData.characteristicId, + uuid, + value, + timeout: timeout, + queueId: queueId, + ); + + BleCharOperationMetadata get _metaData { + BleCharOperationMetadata? metaData = this.metaData; + if (metaData == null) throw "Characteristic metaData is not preset"; + return metaData; + } +} diff --git a/lib/src/extensions/exports.dart b/lib/src/extensions/exports.dart index 8a9e561d..d873e74e 100644 --- a/lib/src/extensions/exports.dart +++ b/lib/src/extensions/exports.dart @@ -1,3 +1,4 @@ export 'package:universal_ble/src/extensions/ble_characteristic_extension.dart'; export 'package:universal_ble/src/extensions/ble_service_extension.dart'; export 'package:universal_ble/src/extensions/ble_device_extension.dart'; +export 'package:universal_ble/src/extensions/ble_descriptor_extension.dart'; diff --git a/lib/src/interfaces/universal_ble_platform_interface.dart b/lib/src/interfaces/universal_ble_platform_interface.dart index aacd62c4..c7b41d32 100644 --- a/lib/src/interfaces/universal_ble_platform_interface.dart +++ b/lib/src/interfaces/universal_ble_platform_interface.dart @@ -89,6 +89,14 @@ abstract class UniversalBlePlatform { Duration? timeout, }); + Future readDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, { + Duration? timeout, + }); + Future writeValue( String deviceId, String service, @@ -97,6 +105,14 @@ abstract class UniversalBlePlatform { BleOutputProperty bleOutputProperty, ); + Future writeDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value, + ); + Future requestMtu(String deviceId, int expectedMtu); Future readRssi(String deviceId); diff --git a/lib/src/models/ble_service.dart b/lib/src/models/ble_service.dart index 8297d2da..1c7408a3 100644 --- a/lib/src/models/ble_service.dart +++ b/lib/src/models/ble_service.dart @@ -19,9 +19,7 @@ class BleCharacteristic { String uuid; List properties; List descriptors; - - /// Metadata for this characteristic. - ({String deviceId, String serviceId})? metaData; + BleCharOperationMetadata? metaData; BleCharacteristic(String uuid, this.properties, this.descriptors) : uuid = BleUuidParser.string(uuid); @@ -32,11 +30,14 @@ class BleCharacteristic { required String uuid, required this.properties, required this.descriptors, - }) : uuid = BleUuidParser.string(uuid), - metaData = ( - deviceId: deviceId, - serviceId: BleUuidParser.string(serviceId), - ); + }) : uuid = BleUuidParser.string(uuid) { + metaData = BleCharOperationMetadata( + deviceId: deviceId, + serviceId: BleUuidParser.string(serviceId), + characteristicId: uuid, + ); + descriptors = descriptors.map((e) => e.copyWithMetadata(metaData)).toList(); + } factory BleCharacteristic.fromJson(Map json) { final propertiesJson = (json['properties'] as List? ?? []) @@ -78,16 +79,18 @@ class BleCharacteristic { } @override - int get hashCode => Object.hash( - uuid, - Object.hashAll(properties), - metaData, - ); + int get hashCode => Object.hash(uuid, Object.hashAll(properties), metaData); } class BleDescriptor { String uuid; BleDescriptor(String uuid) : uuid = BleUuidParser.string(uuid); + BleCharOperationMetadata? metaData; + + BleDescriptor copyWithMetadata(BleCharOperationMetadata? value) { + metaData = value; + return this; + } factory BleDescriptor.fromJson(Map json) { return BleDescriptor(json['uuid'] as String); @@ -281,3 +284,17 @@ PeripheralAttributePermission? _permissionFromName(String permissionName) { return null; } } + +/// Metadata attached with [BleCharacteristic] or [BleDescriptor] to use with +/// BLE operations +class BleCharOperationMetadata { + String deviceId; + String serviceId; + String characteristicId; + + BleCharOperationMetadata({ + required this.deviceId, + required this.serviceId, + required this.characteristicId, + }); +} diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index f3d9e306..ac419410 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -333,6 +333,29 @@ class UniversalBle { ); } + /// Read a characteristic descriptor value. + static Future readDescriptor( + String deviceId, + String service, + String characteristic, + String descriptor, { + Duration? timeout, + String? queueId, + }) async { + return await _bleCommandQueue.queueCommand( + () => _platform.readDescriptorValue( + deviceId, + BleUuidParser.string(service), + BleUuidParser.string(characteristic), + BleUuidParser.string(descriptor), + timeout: timeout ?? _bleCommandQueue.timeout, + ), + timeout: timeout, + deviceId: deviceId, + queueId: queueId, + ); + } + /// Write a characteristic value. /// To write a characteristic value without response, set [withoutResponse] to `true`. static Future write( @@ -360,6 +383,30 @@ class UniversalBle { ); } + /// Write a characteristic descriptor value. + static Future writeDescriptor( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value, { + Duration? timeout, + String? queueId, + }) async { + await _bleCommandQueue.queueCommand( + () => _platform.writeDescriptorValue( + deviceId, + BleUuidParser.string(service), + BleUuidParser.string(characteristic), + BleUuidParser.string(descriptor), + value, + ), + timeout: timeout, + deviceId: deviceId, + queueId: queueId, + ); + } + /// Requests an MTU (Maximum Transmission Unit) value for the connection. /// /// **⚠️ Note:** Requesting an MTU is a *best-effort* operation. On many platforms diff --git a/lib/src/universal_ble.g.dart b/lib/src/universal_ble.g.dart index 5ef82159..27c402a4 100644 --- a/lib/src/universal_ble.g.dart +++ b/lib/src/universal_ble.g.dart @@ -1792,6 +1792,32 @@ class UniversalBlePlatformChannel { return pigeonVar_replyValue! as Uint8List; } + Future readDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readDescriptorValue$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [deviceId, service, characteristic, descriptor], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as Uint8List; + } + Future requestMtu(String deviceId, int expectedMtu) async { final pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu$pigeonVar_messageChannelSuffix'; @@ -1839,6 +1865,32 @@ class UniversalBlePlatformChannel { ); } + Future writeDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeDescriptorValue$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [deviceId, service, characteristic, descriptor, value], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } + Future isPaired(String deviceId) async { final pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired$pigeonVar_messageChannelSuffix'; diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 7a0ba233..7b1fe004 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -256,6 +256,27 @@ class UniversalBleLinux extends UniversalBlePlatform { return c; } + BlueZGattDescriptor _getDescriptor( + String deviceId, + String service, + String characteristic, + String descriptor, + ) { + final c = _getCharacteristic(deviceId, service, characteristic); + final d = c.descriptors.cast().firstWhere( + (d) => BleUuidParser.compareStrings(d?.uuid.toString() ?? '', descriptor), + orElse: () => null, + ); + + if (d == null) { + throw UniversalBleException( + code: UniversalBleErrorCode.characteristicNotFound, + message: 'Unknown descriptor:$descriptor', + ); + } + return d; + } + @override Future setNotifiable( String deviceId, @@ -377,6 +398,59 @@ class UniversalBleLinux extends UniversalBlePlatform { } } + @override + Future readDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, { + Duration? timeout, + }) async { + UniversalLogger.logDebug( + "READ_DESCRIPTOR -> $deviceId $service $characteristic $descriptor", + withTimestamp: true, + ); + try { + final d = _getDescriptor(deviceId, service, characteristic, descriptor); + final data = await d.readValue(); + return Uint8List.fromList(data); + } on BlueZFailedException catch (e) { + UniversalLogger.logError( + "READ_DESCRIPTOR_FAILED <- $deviceId $service $characteristic $descriptor ${e.message}", + withTimestamp: true, + ); + throw e.toUniversalBleException( + defaultCode: UniversalBleErrorCode.readFailed, + ); + } + } + + @override + Future writeDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value, + ) async { + UniversalLogger.logDebug( + "WRITE_DESCRIPTOR -> $deviceId $service $characteristic $descriptor len=${value.length}", + withTimestamp: true, + ); + try { + final d = _getDescriptor(deviceId, service, characteristic, descriptor); + await d.writeValue(value); + } on BlueZFailedException catch (e) { + UniversalLogger.logError( + "WRITE_DESCRIPTOR_FAILED <- $deviceId $service $characteristic $descriptor ${e.message}", + withTimestamp: true, + ); + throw e.toUniversalBleException( + defaultCode: UniversalBleErrorCode.failed, + ); + } + } + @override Future requestMtu(String deviceId, int expectedMtu) async { final device = _findDeviceById(deviceId); diff --git a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart index 133031c1..b46938f1 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -127,6 +127,24 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform ); } + @override + Future readDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, { + Duration? timeout, + }) { + return _executeWithErrorHandling( + () => _channel.readDescriptorValue( + deviceId, + service, + characteristic, + descriptor, + ), + ); + } + @override Future writeValue( String deviceId, @@ -146,6 +164,25 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform ); } + @override + Future writeDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value, + ) { + return _executeWithErrorHandling( + () => _channel.writeDescriptorValue( + deviceId, + service, + characteristic, + descriptor, + value, + ), + ); + } + @override Future requestMtu(String deviceId, int expectedMtu) => _executeWithErrorHandling( diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index a118adc1..c859014f 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -285,6 +285,63 @@ class UniversalBleWeb extends UniversalBlePlatform { return (await data).buffer.asUint8List(); } + @override + Future readDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, { + Duration? timeout, + }) async { + UniversalLogger.logDebug( + "READ_DESCRIPTOR -> $deviceId $service $characteristic $descriptor", + withTimestamp: true, + ); + var bleDescriptor = await _getBleDescriptor( + deviceId: deviceId, + serviceId: service, + characteristicId: characteristic, + descriptorId: descriptor, + ); + if (bleDescriptor == null) { + throw UniversalBleException( + code: UniversalBleErrorCode.characteristicNotFound, + message: + 'Descriptor $descriptor for characteristic $characteristic not found', + ); + } + var data = await bleDescriptor.readValue(); + return data.buffer.asUint8List(); + } + + @override + Future writeDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value, + ) async { + UniversalLogger.logDebug( + "WRITE_DESCRIPTOR -> $deviceId $service $characteristic $descriptor len=${value.length}", + withTimestamp: true, + ); + var bleDescriptor = await _getBleDescriptor( + deviceId: deviceId, + serviceId: service, + characteristicId: characteristic, + descriptorId: descriptor, + ); + if (bleDescriptor == null) { + throw UniversalBleException( + code: UniversalBleErrorCode.characteristicNotFound, + message: + 'Descriptor $descriptor for characteristic $characteristic not found', + ); + } + await bleDescriptor.writeValue(Uint8List.fromList(value)); + } + /// `Unimplemented` @override Future requestMtu(String deviceId, int expectedMtu) { @@ -390,6 +447,33 @@ class UniversalBleWeb extends UniversalBlePlatform { return null; } + Future _getBleDescriptor({ + required String deviceId, + required String serviceId, + required String characteristicId, + required String descriptorId, + }) async { + var bleCharacteristic = await _getBleCharacteristic( + deviceId: deviceId, + serviceId: serviceId, + characteristicId: characteristicId, + ); + if (bleCharacteristic == null) return null; + try { + return await bleCharacteristic.getDescriptor(descriptorId); + } catch (_) { + try { + var descriptors = await bleCharacteristic.getDescriptors(); + for (var desc in descriptors) { + if (BleUuidParser.compareStrings(desc.uuid, descriptorId)) { + return desc; + } + } + } catch (_) {} + } + return null; + } + BluetoothDevice? _getDeviceById(String id) => _bluetoothDeviceList[id]; /// Get services and their characteristics. diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 38cc909e..227ff2f4 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -414,6 +414,14 @@ abstract class UniversalBlePlatformChannel { @async Uint8List readValue(String deviceId, String service, String characteristic); + @async + Uint8List readDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + ); + @async int requestMtu(String deviceId, int expectedMtu); @@ -426,6 +434,15 @@ abstract class UniversalBlePlatformChannel { BleOutputProperty bleOutputProperty, ); + @async + void writeDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value, + ); + @async bool isPaired(String deviceId); diff --git a/test/ble_characteristic_test.dart b/test/ble_characteristic_test.dart index 409c855a..d181bb1c 100644 --- a/test/ble_characteristic_test.dart +++ b/test/ble_characteristic_test.dart @@ -80,11 +80,34 @@ void main() { debugPrint("Read Succeed"); expect(readResult, charValue); }); + + test("Write/Read Descriptor Value Test", () async { + String descriptorId = "2902"; + Uint8List descValue = Uint8List.fromList([0x01, 0x00]); + await UniversalBle.writeDescriptor( + mockDeviceId, + serviceId, + characteristicId, + descriptorId, + descValue, + ); + debugPrint("Write Descriptor Succeed"); + + var readResult = await UniversalBle.readDescriptor( + mockDeviceId, + serviceId, + characteristicId, + descriptorId, + ); + debugPrint("Read Descriptor Succeed"); + expect(readResult, descValue); + }); } class _UniversalBleMock extends UniversalBlePlatformMock { Timer? notifierTimer; Uint8List? charValue; + Uint8List? descValue; @override Future> discoverServices( @@ -128,6 +151,26 @@ class _UniversalBleMock extends UniversalBlePlatformMock { return charValue ?? Uint8List(0); } + @override + Future writeDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value) async { + descValue = value; + } + + @override + Future readDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + {Duration? timeout}) async { + return descValue ?? Uint8List(0); + } + @override Future requestPermissions({bool withAndroidFineLocation = false}) { throw UnimplementedError(); diff --git a/test/universal_ble_test_mock.dart b/test/universal_ble_test_mock.dart index c1f6fdb4..1e5cb56c 100644 --- a/test/universal_ble_test_mock.dart +++ b/test/universal_ble_test_mock.dart @@ -122,6 +122,28 @@ abstract class UniversalBlePlatformMock extends UniversalBlePlatform { throw UnimplementedError(); } + @override + Future readDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, { + Duration? timeout, + }) { + throw UnimplementedError(); + } + + @override + Future writeDescriptorValue( + String deviceId, + String service, + String characteristic, + String descriptor, + Uint8List value, + ) { + throw UnimplementedError(); + } + @override Future isScanning() { throw UnimplementedError(); diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 149346c4..4feb0864 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -2667,6 +2667,53 @@ void UniversalBlePlatformChannel::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readDescriptorValue" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const ::flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + const auto& encodable_service_arg = args.at(1); + if (encodable_service_arg.IsNull()) { + reply(WrapError("service_arg unexpectedly null.")); + return; + } + const auto& service_arg = std::get(encodable_service_arg); + const auto& encodable_characteristic_arg = args.at(2); + if (encodable_characteristic_arg.IsNull()) { + reply(WrapError("characteristic_arg unexpectedly null.")); + return; + } + const auto& characteristic_arg = std::get(encodable_characteristic_arg); + const auto& encodable_descriptor_arg = args.at(3); + if (encodable_descriptor_arg.IsNull()) { + reply(WrapError("descriptor_arg unexpectedly null.")); + return; + } + const auto& descriptor_arg = std::get(encodable_descriptor_arg); + api->ReadDescriptorValue(device_id_arg, service_arg, characteristic_arg, descriptor_arg, [reply](ErrorOr>&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } { BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu" + prepended_suffix, &GetCodec()); if (api != nullptr) { @@ -2755,6 +2802,59 @@ void UniversalBlePlatformChannel::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeDescriptorValue" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const ::flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + const auto& encodable_service_arg = args.at(1); + if (encodable_service_arg.IsNull()) { + reply(WrapError("service_arg unexpectedly null.")); + return; + } + const auto& service_arg = std::get(encodable_service_arg); + const auto& encodable_characteristic_arg = args.at(2); + if (encodable_characteristic_arg.IsNull()) { + reply(WrapError("characteristic_arg unexpectedly null.")); + return; + } + const auto& characteristic_arg = std::get(encodable_characteristic_arg); + const auto& encodable_descriptor_arg = args.at(3); + if (encodable_descriptor_arg.IsNull()) { + reply(WrapError("descriptor_arg unexpectedly null.")); + return; + } + const auto& descriptor_arg = std::get(encodable_descriptor_arg); + const auto& encodable_value_arg = args.at(4); + if (encodable_value_arg.IsNull()) { + reply(WrapError("value_arg unexpectedly null.")); + return; + } + const auto& value_arg = std::get>(encodable_value_arg); + api->WriteDescriptorValue(device_id_arg, service_arg, characteristic_arg, descriptor_arg, value_arg, [reply](std::optional&& output) { + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } { BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired" + prepended_suffix, &GetCodec()); if (api != nullptr) { diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index c8f8ec43..199048b6 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -1180,6 +1180,12 @@ class UniversalBlePlatformChannel { const std::string& service, const std::string& characteristic, std::function> reply)> result) = 0; + virtual void ReadDescriptorValue( + const std::string& device_id, + const std::string& service, + const std::string& characteristic, + const std::string& descriptor, + std::function> reply)> result) = 0; virtual void RequestMtu( const std::string& device_id, int64_t expected_mtu, @@ -1191,6 +1197,13 @@ class UniversalBlePlatformChannel { const std::vector& value, const BleOutputProperty& ble_output_property, std::function reply)> result) = 0; + virtual void WriteDescriptorValue( + const std::string& device_id, + const std::string& service, + const std::string& characteristic, + const std::string& descriptor, + const std::vector& value, + std::function reply)> result) = 0; virtual void IsPaired( const std::string& device_id, std::function reply)> result) = 0; diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index d6720a19..02a1911c 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -547,6 +547,185 @@ void UniversalBlePlugin::WriteValue( } } +void UniversalBlePlugin::ReadDescriptorValue( + const std::string &device_id, const std::string &service, + const std::string &characteristic, const std::string &descriptor, + std::function> reply)> result) { + UniversalBleLogger::LogDebugWithTimestamp("READ_DESCRIPTOR -> " + device_id + " " + + service + " " + characteristic + " " + descriptor); + try { + const auto bluetooth_agent = + GetConnectedDevice(str_to_mac_address(device_id)); + if (!bluetooth_agent) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown deviceId:" + device_id)); + } + + const GattCharacteristicObject gatt_characteristic_holder = + bluetooth_agent->FetchCharacteristic(service, characteristic); + const GattCharacteristic gatt_characteristic = + gatt_characteristic_holder.obj; + + gatt_characteristic.GetDescriptorsForUuidAsync(uuid_to_guid(descriptor), BluetoothCacheMode::Uncached) + .Completed([device_id, service, characteristic, descriptor, result]( + IAsyncOperation const &desc_sender, + AsyncStatus const desc_args) { + try { + if (desc_args != AsyncStatus::Completed) { + safe_reply( + "ReadDescriptorValue", result, + create_flutter_error( + UniversalBleErrorCode::kFailed, + desc_args == AsyncStatus::Canceled ? "Read descriptor was cancelled" + : "Read descriptor failed")); + return; + } + const auto desc_result = desc_sender.GetResults(); + if (desc_result.Status() != GattCommunicationStatus::Success || desc_result.Descriptors().Size() == 0) { + safe_reply("ReadDescriptorValue", result, + create_flutter_error(UniversalBleErrorCode::kCharacteristicNotFound, + "Descriptor not found:" + descriptor)); + return; + } + const auto gatt_descriptor = desc_result.Descriptors().GetAt(0); + gatt_descriptor.ReadValueAsync(BluetoothCacheMode::Uncached) + .Completed([device_id, service, characteristic, descriptor, result]( + IAsyncOperation const &sender, + AsyncStatus const args) { + try { + if (args != AsyncStatus::Completed) { + safe_reply( + "ReadDescriptorValue", result, + create_flutter_error( + UniversalBleErrorCode::kFailed, + args == AsyncStatus::Canceled ? "Read descriptor was cancelled" + : "Read descriptor failed")); + return; + } + const auto read_value_result = sender.GetResults(); + const auto status = read_value_result.Status(); + if (status != GattCommunicationStatus::Success) { + safe_reply("ReadDescriptorValue", result, + create_flutter_error_from_gatt_communication_status(status)); + } else { + safe_reply("ReadDescriptorValue", result, + to_bytevc(read_value_result.Value())); + } + } catch (const hresult_error &err) { + safe_reply("ReadDescriptorValue", result, + create_flutter_error(UniversalBleErrorCode::kFailed, + to_string(err.message()), + std::to_string(err.code()))); + } catch (...) { + safe_reply("ReadDescriptorValue", result, create_flutter_unknown_error()); + } + }); + } catch (const hresult_error &err) { + safe_reply("ReadDescriptorValue", result, + create_flutter_error(UniversalBleErrorCode::kFailed, + to_string(err.message()), + std::to_string(err.code()))); + } catch (...) { + safe_reply("ReadDescriptorValue", result, create_flutter_unknown_error()); + } + }); + } catch (const FlutterError &err) { + return result(err); + } catch (...) { + return result(create_flutter_unknown_error()); + } +} + +void UniversalBlePlugin::WriteDescriptorValue( + const std::string &device_id, const std::string &service, + const std::string &characteristic, const std::string &descriptor, + const std::vector &value, + std::function reply)> result) { + UniversalBleLogger::LogDebugWithTimestamp( + "WRITE_DESCRIPTOR -> " + device_id + " " + service + " " + characteristic + + " " + descriptor + " len=" + std::to_string(value.size())); + try { + const auto bluetooth_agent = + GetConnectedDevice(str_to_mac_address(device_id)); + if (!bluetooth_agent) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); + return; + } + const GattCharacteristicObject gatt_characteristic_holder = + bluetooth_agent->FetchCharacteristic(service, characteristic); + const GattCharacteristic gatt_characteristic = + gatt_characteristic_holder.obj; + + gatt_characteristic.GetDescriptorsForUuidAsync(uuid_to_guid(descriptor), BluetoothCacheMode::Uncached) + .Completed([device_id, service, characteristic, descriptor, value, result]( + IAsyncOperation const &desc_sender, + AsyncStatus const desc_args) { + try { + if (desc_args != AsyncStatus::Completed) { + safe_reply( + "WriteDescriptorValue", result, + create_flutter_error( + UniversalBleErrorCode::kFailed, + desc_args == AsyncStatus::Canceled ? "Write descriptor was cancelled" + : "Write descriptor failed")); + return; + } + const auto desc_result = desc_sender.GetResults(); + if (desc_result.Status() != GattCommunicationStatus::Success || desc_result.Descriptors().Size() == 0) { + safe_reply("WriteDescriptorValue", result, + create_flutter_error(UniversalBleErrorCode::kCharacteristicNotFound, + "Descriptor not found:" + descriptor)); + return; + } + const auto gatt_descriptor = desc_result.Descriptors().GetAt(0); + gatt_descriptor.WriteValueAsync(from_bytevc(value)) + .Completed([device_id, service, characteristic, descriptor, result]( + IAsyncOperation const &sender, + AsyncStatus const args) { + try { + if (args != AsyncStatus::Completed) { + safe_reply( + "WriteDescriptorValue", result, + create_flutter_error( + UniversalBleErrorCode::kFailed, + args == AsyncStatus::Canceled ? "Write descriptor was cancelled" + : "Write descriptor failed")); + return; + } + + const auto status = sender.GetResults(); + if (status != GattCommunicationStatus::Success) { + safe_reply("WriteDescriptorValue", result, + create_flutter_error_from_gatt_communication_status(status)); + } else { + safe_reply("WriteDescriptorValue", result, std::nullopt); + } + } catch (const hresult_error &err) { + safe_reply("WriteDescriptorValue", result, + create_flutter_error(UniversalBleErrorCode::kFailed, + to_string(err.message()), + std::to_string(err.code()))); + } catch (...) { + safe_reply("WriteDescriptorValue", result, create_flutter_unknown_error()); + } + }); + } catch (const hresult_error &err) { + safe_reply("WriteDescriptorValue", result, + create_flutter_error(UniversalBleErrorCode::kFailed, + to_string(err.message()), + std::to_string(err.code()))); + } catch (...) { + safe_reply("WriteDescriptorValue", result, create_flutter_unknown_error()); + } + }); + } catch (const FlutterError &err) { + return result(err); + } catch (...) { + return result(create_flutter_unknown_error()); + } +} + void UniversalBlePlugin::RequestMtu( const std::string &device_id, int64_t expected_mtu, std::function reply)> result) { diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 67332f21..e0c42f4e 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -380,11 +380,20 @@ class UniversalBlePlugin : public flutter::Plugin, const std::string &device_id, const std::string &service, const std::string &characteristic, std::function> reply)> result) override; + void ReadDescriptorValue( + const std::string &device_id, const std::string &service, + const std::string &characteristic, const std::string &descriptor, + std::function> reply)> result) override; void WriteValue( const std::string &device_id, const std::string &service, const std::string &characteristic, const std::vector &value, const BleOutputProperty &ble_output_property, std::function reply)> result) override; + void WriteDescriptorValue( + const std::string &device_id, const std::string &service, + const std::string &characteristic, const std::string &descriptor, + const std::vector &value, + std::function reply)> result) override; void RequestMtu(const std::string &device_id, int64_t expected_mtu, std::function reply)> result) override; void RequestConnectionPriority(