diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d7c027..6790c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Add `requestPermissions` api and auto ask permission on `startScan` * `disconnect` now waits for disconnection confirmation before returning * Improve Windows disconnection event handling and cleanup +* Add `withDescriptors` parameter in `discoverServices` API ## 0.21.1 * Fix device name resolution on Windows 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 21108fe..b05f816 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 @@ -229,20 +229,23 @@ data class UniversalBleService ( /** Generated class from Pigeon that represents data sent in messages. */ data class UniversalBleCharacteristic ( val uuid: String, - val properties: List + val properties: List, + val descriptors: List ) { companion object { fun fromList(pigeonVar_list: List): UniversalBleCharacteristic { val uuid = pigeonVar_list[0] as String val properties = pigeonVar_list[1] as List - return UniversalBleCharacteristic(uuid, properties) + val descriptors = pigeonVar_list[2] as List + return UniversalBleCharacteristic(uuid, properties, descriptors) } } fun toList(): List { return listOf( uuid, properties, + descriptors, ) } override fun equals(other: Any?): Boolean { @@ -257,6 +260,34 @@ data class UniversalBleCharacteristic ( override fun hashCode(): Int = toList().hashCode() } +/** Generated class from Pigeon that represents data sent in messages. */ +data class UniversalBleDescriptor ( + val uuid: String +) + { + companion object { + fun fromList(pigeonVar_list: List): UniversalBleDescriptor { + val uuid = pigeonVar_list[0] as String + return UniversalBleDescriptor(uuid) + } + } + fun toList(): List { + return listOf( + uuid, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is UniversalBleDescriptor) { + return false + } + if (this === other) { + return true + } + return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + /** * Scan Filters * @@ -384,15 +415,20 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { } 133.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalScanFilter.fromList(it) + UniversalBleDescriptor.fromList(it) } } 134.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalManufacturerDataFilter.fromList(it) + UniversalScanFilter.fromList(it) } } 135.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalManufacturerDataFilter.fromList(it) + } + } + 136.toByte() -> { return (readValue(buffer) as? List)?.let { UniversalManufacturerData.fromList(it) } @@ -418,18 +454,22 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { stream.write(132) writeValue(stream, value.toList()) } - is UniversalScanFilter -> { + is UniversalBleDescriptor -> { stream.write(133) writeValue(stream, value.toList()) } - is UniversalManufacturerDataFilter -> { + is UniversalScanFilter -> { stream.write(134) writeValue(stream, value.toList()) } - is UniversalManufacturerData -> { + is UniversalManufacturerDataFilter -> { stream.write(135) writeValue(stream, value.toList()) } + is UniversalManufacturerData -> { + stream.write(136) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -452,7 +492,7 @@ interface UniversalBlePlatformChannel { fun connect(deviceId: String) fun disconnect(deviceId: String) fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Long, callback: (Result) -> Unit) - fun discoverServices(deviceId: String, callback: (Result>) -> Unit) + fun discoverServices(deviceId: String, withDescriptors: Boolean, callback: (Result>) -> Unit) fun readValue(deviceId: String, service: String, characteristic: String, callback: (Result) -> Unit) fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) fun writeValue(deviceId: String, service: String, characteristic: String, value: ByteArray, bleOutputProperty: Long, callback: (Result) -> Unit) @@ -657,7 +697,8 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val deviceIdArg = args[0] as String - api.discoverServices(deviceIdArg) { result: Result> -> + val withDescriptorsArg = args[1] as Boolean + api.discoverServices(deviceIdArg, withDescriptorsArg) { result: Result> -> val error = result.exceptionOrNull() if (error != null) { reply.reply(UniversalBlePigeonUtils.wrapError(error)) 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 1e4137e..cce83de 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -315,6 +315,7 @@ fun Int.parseHciErrorCode(): String? { // Future result classes class DiscoverServicesFuture( val deviceId: String, + val withDescriptors: Boolean, val result: (Result>) -> Unit, ) 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 2d61b3f..feef1e1 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -276,12 +276,19 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), override fun discoverServices( deviceId: String, + withDescriptors: Boolean, callback: (Result>) -> Unit, ) { try { val gatt = deviceId.toBluetoothGatt() if (gatt.discoverServices()) { - discoverServicesFutureList.add(DiscoverServicesFuture(deviceId, callback)) + discoverServicesFutureList.add( + DiscoverServicesFuture( + deviceId, + withDescriptors, + callback + ) + ) } else { callback( Result.failure( @@ -313,20 +320,22 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return } setCachedServices(gatt.device.address, gatt.services.map { it.uuid.toString() }) - val universalBleServices = gatt.services.map { service -> - UniversalBleService( - uuid = service.uuid.toString(), - characteristics = service.characteristics.map { - UniversalBleCharacteristic( - uuid = it.uuid.toString(), - properties = it.getPropertiesList() - ) - } - ) - } discoverServicesFutureList.filter { it.deviceId == gatt.device.address }.forEach { discoverServicesFutureList.remove(it) - it.result(Result.success(universalBleServices)) + it.result(Result.success(gatt.services.map { service -> + UniversalBleService( + uuid = service.uuid.toString(), + characteristics = service.characteristics.map { char -> + UniversalBleCharacteristic( + uuid = char.uuid.toString(), + properties = char.getPropertiesList(), + descriptors = if (it.withDescriptors) char.descriptors.map { descriptor -> + UniversalBleDescriptor(descriptor.uuid.toString()) + } else listOf() + ) + } + ) + })) } } @@ -818,7 +827,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (gatt.discoverServices()) { discoverServicesFutureList.add( - DiscoverServicesFuture(device.address) { uuids: Result> -> + DiscoverServicesFuture( + device.address, + false + ) { uuids: Result> -> if (uuids.isSuccess) { updateCallback(uuids.getOrNull()?.map { it.uuid }) } else { diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index 9ebef10..026e2f1 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -275,22 +275,26 @@ struct UniversalBleService: Hashable { struct UniversalBleCharacteristic: Hashable { var uuid: String var properties: [Int64] + var descriptors: [UniversalBleDescriptor] // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBleCharacteristic? { let uuid = pigeonVar_list[0] as! String let properties = pigeonVar_list[1] as! [Int64] + let descriptors = pigeonVar_list[2] as! [UniversalBleDescriptor] return UniversalBleCharacteristic( uuid: uuid, - properties: properties + properties: properties, + descriptors: descriptors ) } func toList() -> [Any?] { return [ uuid, properties, + descriptors, ] } static func == (lhs: UniversalBleCharacteristic, rhs: UniversalBleCharacteristic) -> Bool { @@ -300,6 +304,31 @@ struct UniversalBleCharacteristic: Hashable { } } +/// Generated class from Pigeon that represents data sent in messages. +struct UniversalBleDescriptor: Hashable { + var uuid: String + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBleDescriptor? { + let uuid = pigeonVar_list[0] as! String + + return UniversalBleDescriptor( + uuid: uuid + ) + } + func toList() -> [Any?] { + return [ + uuid + ] + } + static func == (lhs: UniversalBleDescriptor, rhs: UniversalBleDescriptor) -> Bool { + return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashUniversalBle(value: toList(), hasher: &hasher) + } +} + /// Scan Filters /// /// Generated class from Pigeon that represents data sent in messages. @@ -413,10 +442,12 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader { case 132: return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?]) case 133: - return UniversalScanFilter.fromList(self.readValue() as! [Any?]) + return UniversalBleDescriptor.fromList(self.readValue() as! [Any?]) case 134: - return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + return UniversalScanFilter.fromList(self.readValue() as! [Any?]) case 135: + return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + case 136: return UniversalManufacturerData.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) @@ -438,15 +469,18 @@ private class UniversalBlePigeonCodecWriter: FlutterStandardWriter { } else if let value = value as? UniversalBleCharacteristic { super.writeByte(132) super.writeValue(value.toList()) - } else if let value = value as? UniversalScanFilter { + } else if let value = value as? UniversalBleDescriptor { super.writeByte(133) super.writeValue(value.toList()) - } else if let value = value as? UniversalManufacturerDataFilter { + } else if let value = value as? UniversalScanFilter { super.writeByte(134) super.writeValue(value.toList()) - } else if let value = value as? UniversalManufacturerData { + } else if let value = value as? UniversalManufacturerDataFilter { super.writeByte(135) super.writeValue(value.toList()) + } else if let value = value as? UniversalManufacturerData { + super.writeByte(136) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -482,7 +516,7 @@ protocol UniversalBlePlatformChannel { func connect(deviceId: String) throws func disconnect(deviceId: String) throws func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64, completion: @escaping (Result) -> Void) - func discoverServices(deviceId: String, completion: @escaping (Result<[UniversalBleService], Error>) -> 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 requestMtu(deviceId: String, expectedMtu: Int64, completion: @escaping (Result) -> Void) func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: Int64, completion: @escaping (Result) -> Void) @@ -657,7 +691,8 @@ class UniversalBlePlatformChannelSetup { discoverServicesChannel.setMessageHandler { message, reply in let args = message as! [Any?] let deviceIdArg = args[0] as! String - api.discoverServices(deviceId: deviceIdArg) { result in + let withDescriptorsArg = args[1] as! Bool + api.discoverServices(deviceId: deviceIdArg, withDescriptors: withDescriptorsArg) { result in switch result { case .success(let res): reply(wrapResult(res)) diff --git a/darwin/Classes/UniversalBleAsyncServiceDiscovery.swift b/darwin/Classes/UniversalBleAsyncServiceDiscovery.swift new file mode 100644 index 0000000..6f61311 --- /dev/null +++ b/darwin/Classes/UniversalBleAsyncServiceDiscovery.swift @@ -0,0 +1,217 @@ +import CoreBluetooth +import Foundation + +#if os(iOS) + import Flutter +#elseif os(OSX) + import FlutterMacOS +#endif + +/// Handles asynchronous service discovery for a BLE peripheral. +/// Manages the complete discovery flow: services -> characteristics -> descriptors +class UniversalBleAsyncServiceDiscovery: NSObject { + private let peripheral: CBPeripheral + private let deviceId: String + private let completion: (Result<[UniversalBleService], Error>) -> Void + private var discoveredServicesProgressMap: [UniversalBleService] = [] + private var discoveredDescriptorsSet: Set = [] + private var expectedCharacteristicsCountMap: [String: Int] = [:] + private var isDiscoveryInProgress = false + private var withDescriptors: Bool + + init(peripheral: CBPeripheral, deviceId: String, withDescriptors: Bool, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) { + self.peripheral = peripheral + self.deviceId = deviceId + self.completion = completion + self.withDescriptors = withDescriptors + super.init() + } + + /// Starts the service discovery process + func startDiscovery() { + guard !isDiscoveryInProgress else { + print("Service discovery already in progress for device: \(deviceId)") + return + } + isDiscoveryInProgress = true + + // Check if services are already cached + if let cachedServices = peripheral.services, !cachedServices.isEmpty { + handleServicesDiscovered(cachedServices) + } else { + peripheral.discoverServices(nil) + } + } + + /// Cleans up discovery state + func cleanup() { + isDiscoveryInProgress = false + discoveredServicesProgressMap.removeAll() + discoveredDescriptorsSet.removeAll() + expectedCharacteristicsCountMap.removeAll() + } + + private func handleServicesDiscovered(_ services: [CBService]) { + discoveredServicesProgressMap = services.map { UniversalBleService(uuid: $0.uuid.uuidString, characteristics: nil) } + discoveredDescriptorsSet = Set() + expectedCharacteristicsCountMap = [:] + + // If no services, complete discovery immediately + guard !services.isEmpty else { + checkForDiscoveryCompletion() + return + } + + // Discover characteristics for each service + for service in services { + if let cachedChar = service.characteristics, !cachedChar.isEmpty { + // Characteristics already cached, process them + processCharacteristicsForService(service) + } else { + // Need to discover characteristics + peripheral.discoverCharacteristics(nil, for: service) + } + } + } + + private func processCharacteristicsForService(_ service: CBService) { + let serviceUuid = service.uuid.uuidString + guard let characteristics = service.characteristics else { + // Service has no characteristics, mark as complete + expectedCharacteristicsCountMap[serviceUuid] = 0 + if let index = discoveredServicesProgressMap.firstIndex(where: { $0.uuid == serviceUuid }) { + discoveredServicesProgressMap[index] = UniversalBleService(uuid: serviceUuid, characteristics: []) + } + checkForDiscoveryCompletion() + return + } + + // Store expected characteristic count for this service + expectedCharacteristicsCountMap[serviceUuid] = characteristics.count + + // If no characteristics, mark service as complete + if characteristics.isEmpty { + if let index = discoveredServicesProgressMap.firstIndex(where: { $0.uuid == serviceUuid }) { + discoveredServicesProgressMap[index] = UniversalBleService(uuid: serviceUuid, characteristics: []) + } + checkForDiscoveryCompletion() + return + } + + if withDescriptors { + for characteristic in characteristics { + if let cachedDescriptors = characteristic.descriptors, !cachedDescriptors.isEmpty { + handleDescriptorsDiscovered(for: characteristic) + } else { + peripheral.discoverDescriptors(for: characteristic) + } + } + } else { + if let index = discoveredServicesProgressMap.firstIndex(where: { $0.uuid == serviceUuid }) { + discoveredServicesProgressMap[index] = UniversalBleService( + uuid: serviceUuid, + characteristics: characteristics.map { + UniversalBleCharacteristic( + uuid: $0.uuid.uuidString, + properties: $0.properties.toCharacteristicProperty, + descriptors: [] + ) + } + ) + } + checkForDiscoveryCompletion() + } + } + + private func handleDescriptorsDiscovered(for characteristic: CBCharacteristic) { + guard let service = characteristic.service else { + return + } + + let serviceUuid = service.uuid.uuidString + let characteristicUuid = characteristic.uuid.uuidString + let characteristicKey = "\(serviceUuid):\(characteristicUuid)" + + // Mark this characteristic's descriptors as discovered + discoveredDescriptorsSet.insert(characteristicKey) + + // Get expected characteristic count for this service + guard let expectedCount = expectedCharacteristicsCountMap[serviceUuid] else { + return + } + + // Check if all characteristics for this service have had their descriptors discovered + guard let allCharacteristics = service.characteristics else { + return + } + + let discoveredCount = allCharacteristics.filter { char in + let key = "\(serviceUuid):\(char.uuid.uuidString)" + return discoveredDescriptorsSet.contains(key) + }.count + + // Only update the service when all characteristics have descriptors discovered + if discoveredCount == expectedCount { + var universalBleCharacteristicsList: [UniversalBleCharacteristic] = [] + for characteristic in allCharacteristics { + universalBleCharacteristicsList.append( + UniversalBleCharacteristic( + uuid: characteristic.uuid.uuidString, + properties: characteristic.properties.toCharacteristicProperty, + descriptors: (characteristic.descriptors ?? []).map { UniversalBleDescriptor(uuid: $0.uuid.uuidString) } + ) + ) + } + + if let index = discoveredServicesProgressMap.firstIndex(where: { $0.uuid == serviceUuid }) { + discoveredServicesProgressMap[index] = UniversalBleService(uuid: serviceUuid, characteristics: universalBleCharacteristicsList) + } + + checkForDiscoveryCompletion() + } + } + + private func checkForDiscoveryCompletion() { + // Check if all services have been fully discovered (all characteristics with all descriptors) + guard discoveredServicesProgressMap.allSatisfy({ $0.characteristics != nil }) else { + return + } + completion(.success(discoveredServicesProgressMap)) + cleanup() + } +} + +// These methods are called by the main plugin class when it receives delegate callbacks +extension UniversalBleAsyncServiceDiscovery { + func handleDidDiscoverServices(_ peripheral: CBPeripheral, error: Error?) { + guard error == nil else { + completion(.failure(error!)) + cleanup() + return + } + guard let services = peripheral.services else { + completion(.success([])) + cleanup() + return + } + handleServicesDiscovered(services) + } + + func handleDidDiscoverCharacteristicsFor(_: CBPeripheral, service: CBService, error: Error?) { + guard error == nil else { + completion(.failure(error!)) + cleanup() + return + } + processCharacteristicsForService(service) + } + + func handleDidDiscoverDescriptorsFor(_: CBPeripheral, characteristic: CBCharacteristic, error: Error?) { + guard error == nil else { + completion(.failure(error!)) + cleanup() + return + } + handleDescriptorsDiscovered(for: characteristic) + } +} diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index b65c4a5..402c30e 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -34,7 +34,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil) private var availabilityStateUpdateHandlers: [(Result) -> Void] = [] private var requestPermissionStateUpdateHandlers: [(Result) -> Void] = [] - private var discoveredServicesProgressMap: [String: [UniversalBleService]] = [:] + private var activeServiceDiscoveries: [String: UniversalBleAsyncServiceDiscovery] = [:] private var characteristicReadFutures = [CharacteristicReadFuture]() private var characteristicWriteFutures = [CharacteristicWriteFuture]() private var characteristicWriteWithoutResponseFutures = [CharacteristicWriteFuture]() @@ -204,45 +204,46 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } return false } - discoveredServicesProgressMap[deviceId] = nil + activeServiceDiscoveries[deviceId]?.cleanup() + activeServiceDiscoveries[deviceId] = nil } - func discoverServices(deviceId: String, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) { + func discoverServices(deviceId: String, withDescriptors: Bool, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) { guard let peripheral = deviceId.findPeripheral(manager: manager) else { completion( - Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)")) + Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)")) ) return } - if discoveredServicesProgressMap[deviceId] != nil { + // Check if discovery is already in progress + if activeServiceDiscoveries[deviceId] != nil { print("Services discovery already in progress for :\(deviceId), waiting for completion.") discoverServicesFutures.append(DiscoverServicesFuture(deviceId: deviceId, result: completion)) return } - if let cachedServices = peripheral.services { - // If services already discovered no need to discover again - if !cachedServices.isEmpty { - // print("Services already cached for this peripheral") - discoverServicesFutures.append(DiscoverServicesFuture(deviceId: deviceId, result: completion)) - self.peripheral(peripheral, didDiscoverServices: nil) - return + let wrappedCompletion: (Result<[UniversalBleService], Error>) -> Void = { result in + completion(result) + self.discoverServicesFutures.removeAll { future in + if future.deviceId == deviceId { + future.result(result) + return true + } + return false } + self.activeServiceDiscoveries[deviceId] = nil } - peripheral.discoverServices(nil) - discoverServicesFutures.append(DiscoverServicesFuture(deviceId: deviceId, result: completion)) - } + let discovery = UniversalBleAsyncServiceDiscovery( + peripheral: peripheral, + deviceId: deviceId, + withDescriptors: withDescriptors, + completion: wrappedCompletion + ) - private func onServicesDiscovered(deviceId: String, services: [UniversalBleService]) { - discoverServicesFutures.removeAll { future in - if future.deviceId == deviceId { - future.result(Result.success(services)) - return true - } - return false - } + activeServiceDiscoveries[deviceId] = discovery + discovery.startDiscovery() } func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64, completion: @escaping (Result) -> Void) { @@ -432,40 +433,16 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral cleanUpConnection(deviceId: peripheral.uuid.uuidString) } - public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) { - let deviceId = peripheral.identifier.uuidString - guard let services = peripheral.services else { - onServicesDiscovered(deviceId: deviceId, services: []) - return - } - discoveredServicesProgressMap[deviceId] = services.map { UniversalBleService(uuid: $0.uuid.uuidString, characteristics: nil) } - for service in services { - if let cachedChar = service.characteristics { - if !cachedChar.isEmpty { - self.peripheral(peripheral, didDiscoverCharacteristicsFor: service, error: nil) - continue - } - } - peripheral.discoverCharacteristics(nil, for: service) - } + public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { + activeServiceDiscoveries[peripheral.identifier.uuidString]?.handleDidDiscoverServices(peripheral, error: error) } - public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error _: Error?) { - let deviceId = peripheral.identifier.uuidString - var universalBleCharacteristicsList: [UniversalBleCharacteristic] = [] - for characteristic in service.characteristics ?? [] { - universalBleCharacteristicsList.append( - UniversalBleCharacteristic(uuid: characteristic.uuid.uuidString, properties: characteristic.properties.toCharacteristicProperty)) - } - // Update discoveredServicesProgressMap - if let index = discoveredServicesProgressMap[deviceId]?.firstIndex(where: { $0.uuid == service.uuid.uuidString }) { - discoveredServicesProgressMap[deviceId]?[index] = UniversalBleService(uuid: service.uuid.uuidString, characteristics: universalBleCharacteristicsList) - } - // Check if all services and their characteristics have been discovered - if discoveredServicesProgressMap[deviceId]?.allSatisfy({ $0.characteristics != nil }) ?? false { - onServicesDiscovered(deviceId: deviceId, services: discoveredServicesProgressMap[deviceId] ?? []) - discoveredServicesProgressMap[deviceId] = nil - } + public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + activeServiceDiscoveries[peripheral.identifier.uuidString]?.handleDidDiscoverCharacteristicsFor(peripheral, service: service, error: error) + } + + public func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) { + activeServiceDiscoveries[peripheral.identifier.uuidString]?.handleDidDiscoverDescriptorsFor(peripheral, characteristic: characteristic, error: error) } public func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index 1676147..ffbeb7e 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -20,7 +20,7 @@ class MockUniversalBle extends UniversalBlePlatform { CharacteristicProperty.read, CharacteristicProperty.write, CharacteristicProperty.notify, - ]), + ], []), ]); @override @@ -53,7 +53,8 @@ class MockUniversalBle extends UniversalBlePlatform { } @override - Future> discoverServices(String deviceId) async { + Future> discoverServices( + String deviceId, bool withDescriptors) async { return [_mockService]; } diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index 3f80806..893331f 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -98,7 +98,7 @@ class _PeripheralDetailPageState extends State { const webWarning = "Note: Only services added in ScanFilter or WebOptions will be discovered"; try { - var services = await bleDevice.discoverServices(); + var services = await bleDevice.discoverServices(withDescriptors: false); debugPrint('${services.length} services discovered'); debugPrint(services.toString()); setState(() { diff --git a/example/lib/peripheral_details/widgets/services_list_widget.dart b/example/lib/peripheral_details/widgets/services_list_widget.dart index 0839fa8..15ee23b 100644 --- a/example/lib/peripheral_details/widgets/services_list_widget.dart +++ b/example/lib/peripheral_details/widgets/services_list_widget.dart @@ -61,7 +61,11 @@ 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(', ')}", + ) ], ), ), diff --git a/lib/src/extensions/ble_device_extension.dart b/lib/src/extensions/ble_device_extension.dart index 85939d4..863cfbd 100644 --- a/lib/src/extensions/ble_device_extension.dart +++ b/lib/src/extensions/ble_device_extension.dart @@ -78,9 +78,11 @@ extension BleDeviceExtension on BleDevice { /// Returns cached services if already discovered after connection. Future> discoverServices({ Duration? timeout, + bool withDescriptors = false, }) async { List servicesCache = await UniversalBle.discoverServices( deviceId, + withDescriptors: withDescriptors, timeout: timeout, ); CacheHandler.instance.saveServices(deviceId, servicesCache); diff --git a/lib/src/models/ble_service.dart b/lib/src/models/ble_service.dart index fd0fc3a..59919eb 100644 --- a/lib/src/models/ble_service.dart +++ b/lib/src/models/ble_service.dart @@ -18,11 +18,13 @@ class BleService { class BleCharacteristic { String uuid; List properties; + List descriptors; ({String deviceId, String serviceId})? metaData; BleCharacteristic( String uuid, this.properties, + this.descriptors, ) : uuid = BleUuidParser.string(uuid); BleCharacteristic.withMetaData({ @@ -30,6 +32,7 @@ class BleCharacteristic { required String serviceId, required String uuid, required this.properties, + required this.descriptors, }) : uuid = BleUuidParser.string(uuid), metaData = ( deviceId: deviceId, @@ -55,6 +58,23 @@ class BleCharacteristic { int get hashCode => uuid.hashCode ^ properties.hashCode ^ metaData.hashCode; } +class BleDescriptor { + String uuid; + BleDescriptor(String uuid) : uuid = BleUuidParser.string(uuid); + + @override + String toString() => 'BleDescriptor{uuid: $uuid}'; + + @override + bool operator ==(Object other) { + if (other is! BleDescriptor) return false; + return other.uuid == uuid; + } + + @override + int get hashCode => uuid.hashCode; +} + enum CharacteristicProperty { broadcast, read, diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index 0c2a41d..86e3775 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -183,12 +183,14 @@ class UniversalBle { } /// Discover services of a device. + /// Set [withDescriptors] to `true` to discover characteristics with descriptors. static Future> discoverServices( String deviceId, { + bool withDescriptors = false, Duration? timeout, }) async { return await _bleCommandQueue.queueCommand( - () => _platform.discoverServices(deviceId), + () => _platform.discoverServices(deviceId, withDescriptors), timeout: timeout, deviceId: deviceId, ); diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index ffc1dec..474ba6f 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -160,7 +160,10 @@ class UniversalBleLinux extends UniversalBlePlatform { } @override - Future> discoverServices(String deviceId) async { + Future> discoverServices( + String deviceId, + bool withDescriptors, + ) async { final device = _findDeviceById(deviceId); if (device.gattServices.isEmpty && !device.servicesResolved) { await device.propertiesChanged.firstWhere((element) { @@ -205,6 +208,11 @@ class UniversalBleLinux extends UniversalBlePlatform { serviceId: serviceId, uuid: e.uuid.toString(), properties: properties, + descriptors: withDescriptors + ? e.descriptors + .map((e) => BleDescriptor(e.uuid.toString())) + .toList() + : [], ); }).toList(); services.add( diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index 4f36027..e3e9bd3 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -224,16 +224,20 @@ class UniversalBleCharacteristic { UniversalBleCharacteristic({ required this.uuid, required this.properties, + required this.descriptors, }); String uuid; List properties; + List descriptors; + List _toList() { return [ uuid, properties, + descriptors, ]; } @@ -246,6 +250,8 @@ class UniversalBleCharacteristic { return UniversalBleCharacteristic( uuid: result[0]! as String, properties: (result[1] as List?)!.cast(), + descriptors: + (result[2] as List?)!.cast(), ); } @@ -267,6 +273,47 @@ class UniversalBleCharacteristic { int get hashCode => Object.hashAll(_toList()); } +class UniversalBleDescriptor { + UniversalBleDescriptor({ + required this.uuid, + }); + + String uuid; + + List _toList() { + return [ + uuid, + ]; + } + + Object encode() { + return _toList(); + } + + static UniversalBleDescriptor decode(Object result) { + result as List; + return UniversalBleDescriptor( + uuid: result[0]! as String, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! UniversalBleDescriptor || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + /// Scan Filters class UniversalScanFilter { UniversalScanFilter({ @@ -438,15 +485,18 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is UniversalBleCharacteristic) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is UniversalScanFilter) { + } else if (value is UniversalBleDescriptor) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is UniversalManufacturerDataFilter) { + } else if (value is UniversalScanFilter) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is UniversalManufacturerData) { + } else if (value is UniversalManufacturerDataFilter) { buffer.putUint8(135); writeValue(buffer, value.encode()); + } else if (value is UniversalManufacturerData) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -465,10 +515,12 @@ class _PigeonCodec extends StandardMessageCodec { case 132: return UniversalBleCharacteristic.decode(readValue(buffer)!); case 133: - return UniversalScanFilter.decode(readValue(buffer)!); + return UniversalBleDescriptor.decode(readValue(buffer)!); case 134: - return UniversalManufacturerDataFilter.decode(readValue(buffer)!); + return UniversalScanFilter.decode(readValue(buffer)!); case 135: + return UniversalManufacturerDataFilter.decode(readValue(buffer)!); + case 136: return UniversalManufacturerData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -768,7 +820,8 @@ class UniversalBlePlatformChannel { } } - Future> discoverServices(String deviceId) async { + Future> discoverServices( + String deviceId, bool withDescriptors) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -778,7 +831,7 @@ class UniversalBlePlatformChannel { binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = - pigeonVar_channel.send([deviceId]); + pigeonVar_channel.send([deviceId, withDescriptors]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { 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 b513c85..5474d38 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -77,10 +77,13 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { _executeWithErrorHandling(() => _channel.disconnect(deviceId)); @override - Future> discoverServices(String deviceId) async { + Future> discoverServices( + String deviceId, + bool withDescriptors, + ) async { List universalBleServices = await _executeWithErrorHandling( - () => _channel.discoverServices(deviceId)); + () => _channel.discoverServices(deviceId, withDescriptors)); return List.from(universalBleServices .where((e) => e != null) .map((e) => e!.toBleService(deviceId)) @@ -210,6 +213,8 @@ extension _BleServiceExtension on UniversalBleService { deviceId: deviceId, serviceId: uuid, uuid: characteristic.uuid, + descriptors: List.from( + characteristic.descriptors.map((e) => BleDescriptor(e.uuid))), properties: List.from( properties.map((e) => CharacteristicProperty.parse(e ?? 1)), ), diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index b4265da..5a3ae15 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -51,7 +51,8 @@ abstract class UniversalBlePlatform { Future disconnect(String deviceId); - Future> discoverServices(String deviceId); + Future> discoverServices( + String deviceId, bool withDescriptors); Future setNotifiable(String deviceId, String service, String characteristic, BleInputProperty bleInputProperty); diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index a847260..3a4924a 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -62,10 +62,14 @@ class UniversalBleWeb extends UniversalBlePlatform { } @override - Future> discoverServices(String deviceId) async => - (await _getServices(deviceId)) - .map((e) => e._bleService(deviceId)) - .toList(); + Future> discoverServices( + String deviceId, bool withDescriptors) async { + List services = []; + for (var service in await _getServices(deviceId)) { + services.add(await service._toBleService(deviceId, withDescriptors)); + } + return services; + } @override Future getBluetoothAvailabilityState() async { @@ -536,24 +540,40 @@ class _UniversalWebBluetoothService { return null; } - BleService _bleService(String deviceId) => BleService( - service.uuid, - characteristics.map((e) { - return BleCharacteristic.withMetaData( - deviceId: deviceId, - serviceId: service.uuid, - uuid: e.uuid, - properties: [ - if (e.properties.broadcast) CharacteristicProperty.broadcast, - if (e.properties.read) CharacteristicProperty.read, - if (e.properties.write) CharacteristicProperty.write, - if (e.properties.writeWithoutResponse) - CharacteristicProperty.writeWithoutResponse, - if (e.properties.notify) CharacteristicProperty.notify, - if (e.properties.indicate) CharacteristicProperty.indicate, - if (e.properties.authenticatedSignedWrites) - CharacteristicProperty.authenticatedSignedWrites, - ]); - }).toList(), - ); + Future _toBleService( + String deviceId, + bool withDescriptors, + ) async { + List bleCharacteristics = []; + for (var characteristic in characteristics) { + List descriptors = []; + if (withDescriptors) { + try { + var bluetoothDescriptors = await characteristic.getDescriptors(); + descriptors = + bluetoothDescriptors.map((e) => BleDescriptor(e.uuid)).toList(); + } catch (_) {} + } + bleCharacteristics.add(BleCharacteristic.withMetaData( + deviceId: deviceId, + serviceId: service.uuid, + uuid: characteristic.uuid, + properties: [ + if (characteristic.properties.broadcast) + CharacteristicProperty.broadcast, + if (characteristic.properties.read) CharacteristicProperty.read, + if (characteristic.properties.write) CharacteristicProperty.write, + if (characteristic.properties.writeWithoutResponse) + CharacteristicProperty.writeWithoutResponse, + if (characteristic.properties.notify) CharacteristicProperty.notify, + if (characteristic.properties.indicate) + CharacteristicProperty.indicate, + if (characteristic.properties.authenticatedSignedWrites) + CharacteristicProperty.authenticatedSignedWrites, + ], + descriptors: descriptors, + )); + } + return BleService(service.uuid, bleCharacteristics); + } } diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index f55095a..76ac475 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -52,7 +52,10 @@ abstract class UniversalBlePlatformChannel { ); @async - List discoverServices(String deviceId); + List discoverServices( + String deviceId, + bool withDescriptors, + ); @async Uint8List readValue( @@ -138,7 +141,13 @@ class UniversalBleService { class UniversalBleCharacteristic { String uuid; List properties; - UniversalBleCharacteristic(this.uuid, this.properties); + List descriptors; + UniversalBleCharacteristic(this.uuid, this.properties, this.descriptors); +} + +class UniversalBleDescriptor { + String uuid; + UniversalBleDescriptor(this.uuid); } /// Scan Filters diff --git a/test/ble_characteristic_test.dart b/test/ble_characteristic_test.dart index 951f441..d13f31b 100644 --- a/test/ble_characteristic_test.dart +++ b/test/ble_characteristic_test.dart @@ -12,6 +12,7 @@ BleCharacteristic mockBleCharacteristic = BleCharacteristic.withMetaData( serviceId: serviceId, uuid: characteristicId, properties: CharacteristicProperty.values, + descriptors: [], ); BleService mockBleService = BleService(serviceId, [mockBleCharacteristic]); @@ -86,7 +87,8 @@ class _UniversalBleMock extends UniversalBlePlatformMock { Uint8List? charValue; @override - Future> discoverServices(String deviceId) async { + Future> discoverServices( + String deviceId, bool withDescriptors) async { return [mockBleService]; } diff --git a/test/universal_ble_test_mock.dart b/test/universal_ble_test_mock.dart index e1c0824..5eb8bc7 100644 --- a/test/universal_ble_test_mock.dart +++ b/test/universal_ble_test_mock.dart @@ -18,7 +18,8 @@ abstract class UniversalBlePlatformMock extends UniversalBlePlatform { } @override - Future> discoverServices(String deviceId) { + Future> discoverServices( + String deviceId, bool withDescriptors) { throw UnimplementedError(); } diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 7006e13..cd31525 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -214,9 +214,11 @@ UniversalBleService UniversalBleService::FromEncodableList(const EncodableList& UniversalBleCharacteristic::UniversalBleCharacteristic( const std::string& uuid, - const EncodableList& properties) + const EncodableList& properties, + const EncodableList& descriptors) : uuid_(uuid), - properties_(properties) {} + properties_(properties), + descriptors_(descriptors) {} const std::string& UniversalBleCharacteristic::uuid() const { return uuid_; @@ -236,18 +238,56 @@ void UniversalBleCharacteristic::set_properties(const EncodableList& value_arg) } +const EncodableList& UniversalBleCharacteristic::descriptors() const { + return descriptors_; +} + +void UniversalBleCharacteristic::set_descriptors(const EncodableList& value_arg) { + descriptors_ = value_arg; +} + + EncodableList UniversalBleCharacteristic::ToEncodableList() const { EncodableList list; - list.reserve(2); + list.reserve(3); list.push_back(EncodableValue(uuid_)); list.push_back(EncodableValue(properties_)); + list.push_back(EncodableValue(descriptors_)); return list; } UniversalBleCharacteristic UniversalBleCharacteristic::FromEncodableList(const EncodableList& list) { UniversalBleCharacteristic decoded( std::get(list[0]), - std::get(list[1])); + std::get(list[1]), + std::get(list[2])); + return decoded; +} + +// UniversalBleDescriptor + +UniversalBleDescriptor::UniversalBleDescriptor(const std::string& uuid) + : uuid_(uuid) {} + +const std::string& UniversalBleDescriptor::uuid() const { + return uuid_; +} + +void UniversalBleDescriptor::set_uuid(std::string_view value_arg) { + uuid_ = value_arg; +} + + +EncodableList UniversalBleDescriptor::ToEncodableList() const { + EncodableList list; + list.reserve(1); + list.push_back(EncodableValue(uuid_)); + return list; +} + +UniversalBleDescriptor UniversalBleDescriptor::FromEncodableList(const EncodableList& list) { + UniversalBleDescriptor decoded( + std::get(list[0])); return decoded; } @@ -439,12 +479,15 @@ EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( return CustomEncodableValue(UniversalBleCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); } case 133: { - return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleDescriptor::FromEncodableList(std::get(ReadValue(stream)))); } case 134: { - return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); } case 135: { + return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + } + case 136: { return CustomEncodableValue(UniversalManufacturerData::FromEncodableList(std::get(ReadValue(stream)))); } default: @@ -476,18 +519,23 @@ void PigeonInternalCodecSerializer::WriteValue( WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } - if (custom_value->type() == typeid(UniversalScanFilter)) { + if (custom_value->type() == typeid(UniversalBleDescriptor)) { stream->WriteByte(133); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(UniversalScanFilter)) { + stream->WriteByte(134); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalManufacturerDataFilter)) { - stream->WriteByte(134); + stream->WriteByte(135); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalManufacturerData)) { - stream->WriteByte(135); + stream->WriteByte(136); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } @@ -788,7 +836,13 @@ void UniversalBlePlatformChannel::SetUp( return; } const auto& device_id_arg = std::get(encodable_device_id_arg); - api->DiscoverServices(device_id_arg, [reply](ErrorOr&& output) { + const auto& encodable_with_descriptors_arg = args.at(1); + if (encodable_with_descriptors_arg.IsNull()) { + reply(WrapError("with_descriptors_arg unexpectedly null.")); + return; + } + const auto& with_descriptors_arg = std::get(encodable_with_descriptors_arg); + api->DiscoverServices(device_id_arg, with_descriptors_arg, [reply](ErrorOr&& output) { if (output.has_error()) { reply(WrapError(output.error())); return; diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index dc5bece..6216023 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -211,7 +211,8 @@ class UniversalBleCharacteristic { // Constructs an object setting all fields. explicit UniversalBleCharacteristic( const std::string& uuid, - const flutter::EncodableList& properties); + const flutter::EncodableList& properties, + const flutter::EncodableList& descriptors); const std::string& uuid() const; void set_uuid(std::string_view value_arg); @@ -219,6 +220,9 @@ class UniversalBleCharacteristic { const flutter::EncodableList& properties() const; void set_properties(const flutter::EncodableList& value_arg); + const flutter::EncodableList& descriptors() const; + void set_descriptors(const flutter::EncodableList& value_arg); + private: static UniversalBleCharacteristic FromEncodableList(const flutter::EncodableList& list); flutter::EncodableList ToEncodableList() const; @@ -227,6 +231,26 @@ class UniversalBleCharacteristic { friend class PigeonInternalCodecSerializer; std::string uuid_; flutter::EncodableList properties_; + flutter::EncodableList descriptors_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class UniversalBleDescriptor { + public: + // Constructs an object setting all fields. + explicit UniversalBleDescriptor(const std::string& uuid); + + const std::string& uuid() const; + void set_uuid(std::string_view value_arg); + + private: + static UniversalBleDescriptor FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class UniversalBlePlatformChannel; + friend class UniversalBleCallbackChannel; + friend class PigeonInternalCodecSerializer; + std::string uuid_; }; @@ -366,6 +390,7 @@ class UniversalBlePlatformChannel { std::function reply)> result) = 0; virtual void DiscoverServices( const std::string& device_id, + bool with_descriptors, std::function reply)> result) = 0; virtual void ReadValue( const std::string& device_id, diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 0fae6b8..b0f99bc 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -121,9 +121,9 @@ void UniversalBlePlugin::DisableBluetooth( void UniversalBlePlugin::RequestPermissions( bool with_android_fine_location, std::function reply)> result) { - // Windows does not require runtime permissions for Bluetooth - result(std::nullopt); - return; + // Windows does not require runtime permissions for Bluetooth + result(std::nullopt); + return; } std::optional @@ -223,8 +223,10 @@ std::optional UniversalBlePlugin::StopScan() { ErrorOr UniversalBlePlugin::IsScanning() { if (bluetooth_le_watcher_ != nullptr) { try { - return bluetooth_le_watcher_.Status() == BluetoothLEAdvertisementWatcherStatus::Started; - } catch (...) {} + return bluetooth_le_watcher_.Status() == + BluetoothLEAdvertisementWatcherStatus::Started; + } catch (...) { + } } return false; } @@ -269,24 +271,9 @@ UniversalBlePlugin::Disconnect(const std::string &device_id) { } void UniversalBlePlugin::DiscoverServices( - const std::string &device_id, + const std::string &device_id, bool with_descriptors, std::function reply)> result) { - try { - const auto it = connected_devices_.find(str_to_mac_address(device_id)); - if (it == connected_devices_.end()) { - result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, - "Unknown devicesId:" + device_id)); - return; - } - auto device_agent = *it->second; - DiscoverServicesAsync(device_agent, result); - } catch (const FlutterError &err) { - return result(err); - } catch (...) { - std::cout << "DiscoverServicesLog: Unknown error" << std::endl; - return result(create_flutter_error(UniversalBleErrorCode::kUnknownError, - "Unknown error")); - } + DiscoverServicesAsync(device_id, with_descriptors, result); } void UniversalBlePlugin::SetNotifiable( @@ -369,7 +356,8 @@ void UniversalBlePlugin::WriteValue( if ((properties & GattCharacteristicProperties::WriteWithoutResponse) == GattCharacteristicProperties::None) { result(create_flutter_error( - UniversalBleErrorCode::kCharacteristicDoesNotSupportWriteWithoutResponse, + UniversalBleErrorCode:: + kCharacteristicDoesNotSupportWriteWithoutResponse, "Characteristic does not support WriteWithoutResponse")); return; } @@ -384,22 +372,22 @@ void UniversalBlePlugin::WriteValue( } gatt_characteristic.WriteValueAsync(from_bytevc(value), write_option) - .Completed( - [&, result](IAsyncOperation const &sender, - AsyncStatus const args) { - if (args == AsyncStatus::Error) { - result(create_flutter_error(UniversalBleErrorCode::kFailed, - "Encountered an error.")); - return; - } + .Completed([&, result]( + IAsyncOperation const &sender, + AsyncStatus const args) { + if (args == AsyncStatus::Error) { + result(create_flutter_error(UniversalBleErrorCode::kFailed, + "Encountered an error.")); + return; + } - const auto status = sender.GetResults(); - if (status != GattCommunicationStatus::Success) { - result(create_flutter_error_from_gatt_communication_status(status)); - } else { - result(std::nullopt); - } - }); + const auto status = sender.GetResults(); + if (status != GattCommunicationStatus::Success) { + result(create_flutter_error_from_gatt_communication_status(status)); + } else { + result(std::nullopt); + } + }); } catch (const FlutterError &err) { result(err); } catch (...) { @@ -548,7 +536,8 @@ fire_and_forget UniversalBlePlugin::PairAsync( result(is_paired); const std::string *error_msg = nullptr; - const auto error_str = device_pairing_result_to_string(pair_result.Status()); + const auto error_str = + device_pairing_result_to_string(pair_result.Status()); if (error_str.has_value()) { error_msg = &error_str.value(); } @@ -1039,18 +1028,18 @@ void UniversalBlePlugin::CleanConnection(const uint64_t bluetooth_address) { } } -void UniversalBlePlugin::DisposeServices(const std::unique_ptr &device_agent) -{ - for (auto& [service_id, service] : device_agent->gatt_map) { - for (auto& [char_id, characteristic] : service.characteristics) { - if (characteristic.subscription_token.has_value()) { - characteristic.obj.ValueChanged( - characteristic.subscription_token.value()); - characteristic.subscription_token = std::nullopt; - } - } +void UniversalBlePlugin::DisposeServices( + const std::unique_ptr &device_agent) { + for (auto &[service_id, service] : device_agent->gatt_map) { + for (auto &[char_id, characteristic] : service.characteristics) { + if (characteristic.subscription_token.has_value()) { + characteristic.obj.ValueChanged( + characteristic.subscription_token.value()); + characteristic.subscription_token = std::nullopt; + } } - device_agent->gatt_map.clear(); + } + device_agent->gatt_map.clear(); } fire_and_forget UniversalBlePlugin::GetSystemDevicesAsync( @@ -1108,21 +1097,49 @@ fire_and_forget UniversalBlePlugin::GetSystemDevicesAsync( } } -void UniversalBlePlugin::DiscoverServicesAsync( - BluetoothDeviceAgent &bluetooth_device_agent, - const std::function reply)> &result) { +fire_and_forget UniversalBlePlugin::DiscoverServicesAsync( + const std::string &device_id, bool with_descriptors, + std::function reply)> result) { try { + const auto it = connected_devices_.find(str_to_mac_address(device_id)); + if (it == connected_devices_.end()) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); + co_return; + } + auto universal_services = flutter::EncodableList(); - for (auto &[service_id, service] : bluetooth_device_agent.gatt_map) { + for (auto &[service_id, service] : it->second->gatt_map) { flutter::EncodableList universal_characteristics; for (auto [char_id, characteristic] : service.characteristics) { auto &c = characteristic.obj; - const auto properties_value = c.CharacteristicProperties(); auto properties = properties_to_flutter_encodable(properties_value); - - universal_characteristics.push_back(flutter::CustomEncodableValue( - UniversalBleCharacteristic(to_uuidstr(c.Uuid()), properties))); + auto descriptors = flutter::EncodableList(); + if (with_descriptors) { + try { + // move continuation to background and execute in safe thread + // context + co_await winrt::resume_background(); + auto descriptor_result = + co_await c.GetDescriptorsAsync(BluetoothCacheMode::Cached); + if (descriptor_result.Status() == + GattCommunicationStatus::Success) { + auto descriptors_list = descriptor_result.Descriptors(); + for (auto &&descriptor : descriptors_list) { + descriptors.push_back(flutter::CustomEncodableValue( + UniversalBleDescriptor(to_uuidstr(descriptor.Uuid())))); + } + } + } catch (...) { + std::cout << "DiscoverServicesAsync: failed to get descriptors for " + "characteristic: " + << std::endl; + } + } + universal_characteristics.push_back( + flutter::CustomEncodableValue(UniversalBleCharacteristic( + to_uuidstr(c.Uuid()), properties, descriptors))); } auto universal_ble_service = @@ -1132,10 +1149,16 @@ void UniversalBlePlugin::DiscoverServicesAsync( flutter::CustomEncodableValue(universal_ble_service)); } result(universal_services); + } catch (const hresult_error &err) { + const int error_code = err.code(); + result(create_flutter_error(UniversalBleErrorCode::kFailed, + "DiscoverServicesAsync failed", + std::to_string(error_code))); + } catch (const FlutterError &err) { + result(err); } catch (...) { result(create_flutter_error(UniversalBleErrorCode::kUnknownError, "Unknown error")); - std::cout << "DiscoverServiceError: Unknown error" << '\n'; } } diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 3dc688c..4a8386a 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -134,8 +134,10 @@ namespace universal_ble std::vector with_services, std::function reply)> result); static fire_and_forget IsPairedAsync(const std::string& device_id, std::function reply)> result); + fire_and_forget DiscoverServicesAsync(const std::string &device_id, + bool with_descriptors, + std::function reply)> result); - static void DiscoverServicesAsync(BluetoothDeviceAgent& bluetooth_device_agent, const std::function reply)>&); void PairingRequestedHandler(DeviceInformationCustomPairing sender, const DevicePairingRequestedEventArgs& event_args); void RadioStateChanged(const Radio& sender, const IInspectable&); @@ -167,6 +169,7 @@ namespace universal_ble std::function reply)> result) override; void DiscoverServices( const std::string &device_id, + bool with_descriptors, std::function reply)> result) override; void SetNotifiable( const std::string &device_id,