From 1a0e48ba1adba8960ca6f5fbe1e2658078a764a5 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Mon, 18 Mar 2024 23:14:44 +0530 Subject: [PATCH] Add manufacturer data filtering (#23) * Add manufacturer data filtering * Implement manufacturer data filter in windows * Fix manufacturerData filtering on windows * Fix linux * Clean comments --------- Co-authored-by: Foti Dim --- .vscode/settings.json | 1 + .../navideck/universal_ble/UniversalBle.g.kt | 49 +++- .../universal_ble/UniversalBleHelper.kt | 36 ++- darwin/Classes/UniversalBle.g.swift | 43 ++- darwin/Classes/UniversalBleHelper.swift | 47 ++- darwin/Classes/UniversalBlePlugin.swift | 17 +- example/lib/data/mock_universal_ble.dart | 1 - example/lib/home/home.dart | 36 ++- lib/src/models/model_exports.dart | 1 - lib/src/models/scan_filter.dart | 22 ++ .../models/web_request_options_builder.dart | 124 -------- lib/src/universal_ble.dart | 6 +- .../universal_ble_linux.dart | 104 ++++++- .../universal_ble_pigeon/universal_ble.g.dart | 274 ++++++++---------- .../universal_ble_pigeon_channel.dart | 14 +- lib/src/universal_ble_platform_interface.dart | 1 - .../universal_ble_web/universal_ble_web.dart | 46 ++- pigeon/universal_ble.dart | 18 +- windows/src/generated/universal_ble.g.cpp | 108 ++++++- windows/src/generated/universal_ble.g.h | 49 +++- windows/src/universal_ble_plugin.cpp | 127 ++++++-- windows/src/universal_ble_plugin.h | 2 +- 22 files changed, 770 insertions(+), 356 deletions(-) delete mode 100644 lib/src/models/web_request_options_builder.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index 450198f..43e6ba0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,6 +21,7 @@ "pairable", "rssi", "simpleble", + "subdata", "sublist", "unawaited", "UNIVERSALBLE", 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 0695d97..24e9128 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 @@ -127,21 +127,53 @@ data class UniversalBleCharacteristic ( } } -/** Generated class from Pigeon that represents data sent in messages. */ +/** + * Scan Filters + * + * Generated class from Pigeon that represents data sent in messages. + */ data class UniversalScanFilter ( - val withServices: List + val withServices: List, + val withManufacturerData: List ) { companion object { @Suppress("UNCHECKED_CAST") fun fromList(list: List): UniversalScanFilter { val withServices = list[0] as List - return UniversalScanFilter(withServices) + val withManufacturerData = list[1] as List + return UniversalScanFilter(withServices, withManufacturerData) } } fun toList(): List { return listOf( withServices, + withManufacturerData, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class UniversalManufacturerDataFilter ( + val companyIdentifier: Long? = null, + val data: ByteArray? = null, + val mask: ByteArray? = null + +) { + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): UniversalManufacturerDataFilter { + val companyIdentifier = list[0].let { if (it is Int) it.toLong() else it as Long? } + val data = list[1] as ByteArray? + val mask = list[2] as ByteArray? + return UniversalManufacturerDataFilter(companyIdentifier, data, mask) + } + } + fun toList(): List { + return listOf( + companyIdentifier, + data, + mask, ) } } @@ -166,6 +198,11 @@ private object UniversalBlePlatformChannelCodec : StandardMessageCodec() { } } 131.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalManufacturerDataFilter.fromList(it) + } + } + 132.toByte() -> { return (readValue(buffer) as? List)?.let { UniversalScanFilter.fromList(it) } @@ -187,10 +224,14 @@ private object UniversalBlePlatformChannelCodec : StandardMessageCodec() { stream.write(130) writeValue(stream, value.toList()) } - is UniversalScanFilter -> { + is UniversalManufacturerDataFilter -> { stream.write(131) writeValue(stream, value.toList()) } + is UniversalScanFilter -> { + stream.write(132) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } 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 4f1d704..8458962 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -144,7 +144,7 @@ fun Int.parseGattErrorCode(): String? { } fun UniversalScanFilter.toScanFilters(): List { - var scanFilters: List = emptyList() + var scanFilters: ArrayList = arrayListOf() // Add withServices Filter for (service in this.withServices) { @@ -152,7 +152,7 @@ fun UniversalScanFilter.toScanFilters(): List { val serviceUUID = service?.validFullUUID() serviceUUID?.let { val parcelUUId = ParcelUuid.fromString(it) - scanFilters = scanFilters.plus( + scanFilters.add( ScanFilter.Builder().setServiceUuid(parcelUUId).build() ) Log.e(TAG, "scanFilters: $parcelUUId") @@ -167,7 +167,37 @@ fun UniversalScanFilter.toScanFilters(): List { } } - return scanFilters + // Add ManufacturerData Filter + for (manufacturerData in this.withManufacturerData) { + try { + manufacturerData?.companyIdentifier?.let { + val data: ByteArray = manufacturerData.data ?: ByteArray(0) + val mask: ByteArray? = manufacturerData.mask + if (mask == null) { + scanFilters.add( + ScanFilter.Builder().setManufacturerData( + it.toInt(), data + ).build() + ) + } else { + scanFilters.add( + ScanFilter.Builder().setManufacturerData( + it.toInt(), data, mask + ).build() + ) + } + } + } catch (e: Exception) { + Log.e(TAG, e.toString()) + throw FlutterError( + "illegalIllegalArgument", + "Invalid manufacturerData: ${manufacturerData?.companyIdentifier} ${manufacturerData?.data} ${manufacturerData?.mask}", + e.toString() + ) + } + } + + return scanFilters.toList() } val ScanResult.manufacturerDataHead: ByteArray? diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index 59f1a2a..ca44193 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -129,20 +129,52 @@ struct UniversalBleCharacteristic { } } +/// Scan Filters +/// /// Generated class from Pigeon that represents data sent in messages. struct UniversalScanFilter { var withServices: [String?] + var withManufacturerData: [UniversalManufacturerDataFilter?] static func fromList(_ list: [Any?]) -> UniversalScanFilter? { let withServices = list[0] as! [String?] + let withManufacturerData = list[1] as! [UniversalManufacturerDataFilter?] return UniversalScanFilter( - withServices: withServices + withServices: withServices, + withManufacturerData: withManufacturerData ) } func toList() -> [Any?] { return [ - withServices + withServices, + withManufacturerData, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct UniversalManufacturerDataFilter { + var companyIdentifier: Int64? = nil + var data: FlutterStandardTypedData? = nil + var mask: FlutterStandardTypedData? = nil + + static func fromList(_ list: [Any?]) -> UniversalManufacturerDataFilter? { + let companyIdentifier: Int64? = isNullish(list[0]) ? nil : (list[0] is Int64? ? list[0] as! Int64? : Int64(list[0] as! Int32)) + let data: FlutterStandardTypedData? = nilOrValue(list[1]) + let mask: FlutterStandardTypedData? = nilOrValue(list[2]) + + return UniversalManufacturerDataFilter( + companyIdentifier: companyIdentifier, + data: data, + mask: mask + ) + } + func toList() -> [Any?] { + return [ + companyIdentifier, + data, + mask, ] } } @@ -157,6 +189,8 @@ private class UniversalBlePlatformChannelCodecReader: FlutterStandardReader { case 130: return UniversalBleService.fromList(self.readValue() as! [Any?]) case 131: + return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + case 132: return UniversalScanFilter.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) @@ -175,9 +209,12 @@ private class UniversalBlePlatformChannelCodecWriter: FlutterStandardWriter { } else if let value = value as? UniversalBleService { super.writeByte(130) super.writeValue(value.toList()) - } else if let value = value as? UniversalScanFilter { + } else if let value = value as? UniversalManufacturerDataFilter { super.writeByte(131) super.writeValue(value.toList()) + } else if let value = value as? UniversalScanFilter { + super.writeByte(132) + super.writeValue(value.toList()) } else { super.writeValue(value) } diff --git a/darwin/Classes/UniversalBleHelper.swift b/darwin/Classes/UniversalBleHelper.swift index c724fcb..6a35497 100644 --- a/darwin/Classes/UniversalBleHelper.swift +++ b/darwin/Classes/UniversalBleHelper.swift @@ -143,11 +143,11 @@ public extension CBPeripheral { extension String { var validFullUUID: String { - let uuidLength = self.count + let uuidLength = count if uuidLength == 4 || uuidLength == 8 { let baseUuid = "00000000-0000-1000-8000-00805F9B34FB" let start = baseUuid.startIndex - let range = baseUuid.index(start, offsetBy: 4-uuidLength).. Data { + return Data(data) + } +} + +func isManufacturerDataMatchingFilters(filters: [UniversalManufacturerDataFilter], msd: Data?) -> Bool { + guard let msd = msd, !msd.isEmpty else { + return false + } + for filter in filters { + guard let companyIdentifier: Int64 = filter.companyIdentifier else { + continue + } + let manufacturerId = msd.subdata(in: 0 ..< 2).withUnsafeBytes { $0.load(as: UInt16.self) } + let manufacturerData = msd.subdata(in: 2 ..< msd.count) + if manufacturerId == companyIdentifier && findData(find: filter.data?.toData(), inData: manufacturerData, usingMask: filter.mask?.toData()) { + return true + } + } + return false +} + +func findData(find: Data?, inData data: Data, usingMask mask: Data?) -> Bool { + if let find = find { + // If mask is null, use a default mask of all 1s + let mask = mask ?? Data(repeating: 0xFF, count: find.count) + + // Ensure find & mask are same length + guard find.count == mask.count else { + return false + } + + for i in 0 ..< find.count { + // Perform bitwise AND with mask and then compare + if (find[i] & mask[i]) != (data[i] & mask[i]) { + return false + } + } + } + return true +} + // Future classes class CharacteristicReadFuture { let deviceId: String diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index cb872c5..958dd23 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -27,6 +27,7 @@ private var discoveredPeripherals = [String: CBPeripheral]() private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentralManagerDelegate, CBPeripheralDelegate { var callbackChannel: UniversalBleCallbackChannel private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil) + private var scanFilter: UniversalScanFilter? = nil private var discoveredServicesProgressMap: [String: [UniversalBleService]] = [:] private var characteristicReadFutures = [CharacteristicReadFuture]() private var characteristicWriteFutures = [CharacteristicWriteFuture]() @@ -57,18 +58,20 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } func startScan(filter: UniversalScanFilter?) throws { - // Apply services filter var withServices: [CBUUID] = [] for service in filter?.withServices ?? [] { if let service = service { - if UUID(uuidString: service.validFullUUID) == nil { + if UUID(uuidString: service.validFullUUID) == nil { throw FlutterError(code: "IllegalArgument", message: "Invalid service UUID:\(service)", details: nil) } withServices.append(CBUUID(string: service)) } } + // Save scanFilter for later user + scanFilter = filter + manager.scanForPeripherals(withServices: withServices) } @@ -241,6 +244,16 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral discoveredPeripherals[peripheral.uuid.uuidString] = peripheral let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data let services = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] + + // Handle ScanFilters + if let filter = scanFilter { + let manufacturerFilter = filter.withManufacturerData.compactMap { $0 } + // If scan filters are not empty, check if manufacturer data matches filters + if !manufacturerFilter.isEmpty, !isManufacturerDataMatchingFilters(filters: manufacturerFilter, msd: manufacturerData) { + return + } + } + callbackChannel.onScanResult(result: UniversalBleScanResult( deviceId: peripheral.uuid.uuidString, name: peripheral.name, diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index 24e2c13..29cfac3 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -24,7 +24,6 @@ class MockUniversalBle extends UniversalBlePlatform { @override Future startScan({ - WebRequestOptionsBuilder? webRequestOptions, ScanFilter? scanFilter, }) async { onScanResult?.call(_mockBleScanResult); diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart index 3e79e34..ee3d8ed 100644 --- a/example/lib/home/home.dart +++ b/example/lib/home/home.dart @@ -25,9 +25,9 @@ class _MyAppState extends State { bool _isQueueEnabled = true; AvailabilityState? bleAvailabilityState; - late WebRequestOptionsBuilder _requestOptions; final List _services = [ "00001800-0000-1000-8000-00805f9b34fb", + "0000180f-0000-1000-8000-00805f9b34fb", "00002a00-0000-1000-8000-00805f9b34fb", "00002a01-0000-1000-8000-00805f9b34fb", "00002a19-0000-1000-8000-00805f9b34fb", @@ -48,13 +48,6 @@ class _MyAppState extends State { UniversalBle.queuesCommands = _isQueueEnabled; UniversalBle.timeout = const Duration(seconds: 10); - /// Add common services for web - if (kIsWeb) { - _services.addAll(WebRequestOptionsBuilder.defaultServices); - } - _requestOptions = - WebRequestOptionsBuilder.acceptAllDevices(optionalServices: _services); - UniversalBle.onAvailabilityChange = (state) { setState(() { bleAvailabilityState = state; @@ -77,6 +70,28 @@ class _MyAppState extends State { }; } + Future startScan() async { + await UniversalBle.startScan( + scanFilter: ScanFilter( + withServices: kIsWeb ? _services : [], + withManufacturerData: [ + ManufacturerDataFilter( + companyIdentifier: 0x012D, + data: Uint8List.fromList( + [0x03, 0x00, 0x64, 0x00], + ), + ), + ManufacturerDataFilter( + companyIdentifier: 0x012D, + data: Uint8List.fromList( + [0x03, 0x00, 0x65, 0x00], + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -110,10 +125,7 @@ class _MyAppState extends State { _isScanning = true; }); try { - await UniversalBle.startScan( - webRequestOptions: _requestOptions, - // scanFilter: ScanFilter(withServices: ['180f']), - ); + await startScan(); } catch (e) { setState(() { _isScanning = false; diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart index a1143ba..c2830d6 100644 --- a/lib/src/models/model_exports.dart +++ b/lib/src/models/model_exports.dart @@ -1,6 +1,5 @@ export 'package:universal_ble/src/models/uuid.dart'; export 'package:universal_ble/src/models/scan_filter.dart'; -export 'package:universal_ble/src/models/web_request_options_builder.dart'; export 'package:universal_ble/src/models/ble_property.dart'; export 'package:universal_ble/src/models/ble_service.dart'; export 'package:universal_ble/src/models/availability_state.dart'; diff --git a/lib/src/models/scan_filter.dart b/lib/src/models/scan_filter.dart index a0eb028..1c68069 100644 --- a/lib/src/models/scan_filter.dart +++ b/lib/src/models/scan_filter.dart @@ -1,7 +1,29 @@ +import 'dart:typed_data'; + class ScanFilter { final List withServices; + final List withManufacturerData; ScanFilter({ this.withServices = const [], + this.withManufacturerData = const [], + }); +} + +class ManufacturerDataFilter { + int? companyIdentifier; + + // Mask and data must be of same length + Uint8List? data; + + /// For any bit in the mask, set it the 1 if it needs to match + /// the one in manufacturer data, otherwise set it to 0. + /// The 'mask' must have the same length as 'data'. + Uint8List? mask; + + ManufacturerDataFilter({ + this.companyIdentifier, + this.data, + this.mask, }); } diff --git a/lib/src/models/web_request_options_builder.dart b/lib/src/models/web_request_options_builder.dart deleted file mode 100644 index 27d77ef..0000000 --- a/lib/src/models/web_request_options_builder.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'dart:typed_data'; -import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart'; -import 'package:universal_ble/universal_ble.dart'; - -class WebRequestOptionsBuilder { - final bool _acceptAllDevices; - final List _requestFilters; - final List? _exclusionFilters; - List? _optionalServices; - - WebRequestOptionsBuilder( - List requestFilters, { - List? exclusionFilters, - List? optionalServices, - }) : _optionalServices = optionalServices, - _requestFilters = requestFilters, - _exclusionFilters = exclusionFilters, - _acceptAllDevices = false { - if (_requestFilters.isEmpty) { - throw StateError( - 'No filters have been set, consider using ' - 'RequestOptionsBuilder.acceptAllDevices() instead.', - ); - } - } - - /// To accept all devices - WebRequestOptionsBuilder.acceptAllDevices({ - List? optionalServices, - }) : _optionalServices = optionalServices, - _acceptAllDevices = true, - _requestFilters = [], - _exclusionFilters = null; - - /// For internal use - /// To convert UniversalBleRequestOptions to FlutterWebBluetoothRequestOptions - RequestOptionsBuilder toRequestOptionsBuilder({ScanFilter? scanFilter}) { - if (scanFilter != null) _applyScanFilter(scanFilter); - - if (_acceptAllDevices) { - return RequestOptionsBuilder.acceptAllDevices( - optionalServices: _optionalServices, - ); - } - - return RequestOptionsBuilder( - _requestFilters.map((e) => e.getRequestFilterBuilder()).toList(), - exclusionFilters: - _exclusionFilters?.map((e) => e.getRequestFilterBuilder()).toList(), - optionalServices: _optionalServices, - ); - } - - /// To apply scan filter - void _applyScanFilter(ScanFilter scanFilter) { - List filterServices = scanFilter.withServices; - _optionalServices ??= filterServices.toValidUUIDList(); - - for (var requestFilter in _requestFilters) { - // Apply services filter - requestFilter.services ??= filterServices.toValidUUIDList(); - } - } - - /// List of common services - static List get defaultServices { - return BluetoothDefaultServiceUUIDS.values - .map((e) => e.uuid.toString()) - .toList(); - } - - /// List of common services in 16 bit - static List get defaultServices16Bit { - return BluetoothDefaultServiceUUIDS.values - .map((e) => e.uuid16.toString()) - .toList(); - } -} - -/// To filter by name, namePrefix, services, manufacturerData -class WebRequestFilterBuilder { - String? name; - String? namePrefix; - List? services; - List? manufacturerData; - - WebRequestFilterBuilder({ - this.name, - this.namePrefix, - this.services, - this.manufacturerData, - }); - - RequestFilterBuilder getRequestFilterBuilder() { - return RequestFilterBuilder( - name: name, - namePrefix: namePrefix, - services: services, - manufacturerData: manufacturerData - ?.map((e) => e.getManufacturerDataFilterBuilder()) - .toList(), - ); - } -} - -/// To filter by manufacturer data -class WebManufacturerDataFilterBuilder { - int? companyIdentifier; - Uint8List? dataPrefix; - Uint8List? mask; - WebManufacturerDataFilterBuilder({ - this.companyIdentifier, - this.dataPrefix, - this.mask, - }); - - ManufacturerDataFilterBuilder getManufacturerDataFilterBuilder() { - return ManufacturerDataFilterBuilder( - companyIdentifier: companyIdentifier, - dataPrefix: dataPrefix, - mask: mask, - ); - } -} diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index 0992dbf..b07c0df 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -44,14 +44,10 @@ class UniversalBle { /// might throw errors if Bluetooth is not available /// `webRequestOptions` supported on Web only static Future startScan({ - WebRequestOptionsBuilder? webRequestOptions, ScanFilter? scanFilter, }) async { return await _executeCommand( - () => _platform.startScan( - webRequestOptions: webRequestOptions, - scanFilter: scanFilter, - ), + () => _platform.startScan(scanFilter: scanFilter), timeout: null, ); } diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 5a017b5..dc4820b 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -17,6 +17,7 @@ class UniversalBleLinux extends UniversalBlePlatform { final BlueZClient _client = BlueZClient(); BlueZAdapter? _activeAdapter; + ScanFilter? _scanFilter; Completer? _initializationCompleter; final Map _devices = {}; final Map _deviceStreamSubscriptions = {}; @@ -54,7 +55,6 @@ class UniversalBleLinux extends UniversalBlePlatform { @override Future startScan({ - WebRequestOptionsBuilder? webRequestOptions, ScanFilter? scanFilter, }) async { await _ensureInitialized(); @@ -63,6 +63,7 @@ class UniversalBleLinux extends UniversalBlePlatform { _activeAdapter?.setDiscoveryFilter( uuids: scanFilter?.withServices.toValidUUIDList(), ); + _scanFilter = scanFilter; await _activeAdapter?.startDiscovery(); _client.devices.forEach(_onDeviceAdd); } @@ -371,6 +372,16 @@ class UniversalBleLinux extends UniversalBlePlatform { } void _onDeviceAdd(BlueZDevice device) { + if (!_isValidDevice(device)) return; + + // Apply Filters + if (_scanFilter != null) { + List? manufacturerDataFilter = + _scanFilter?.withManufacturerData; + if (manufacturerDataFilter != null && + manufacturerDataFilter.isNotEmpty) {} + } + // Update scan results only if rssi is available if (device.rssi != 0) onScanResult?.call(device.toBleScanResult()); @@ -421,6 +432,79 @@ class UniversalBleLinux extends UniversalBlePlatform { }); } + bool _isValidDevice(BlueZDevice device) { + ScanFilter? scanFilter = _scanFilter; + if (scanFilter == null) return true; + + // Check manufacturerData filter + if (!_isValidManufacturerData( + scanFilter.withManufacturerData, + device.manufacturerDataFilter, + )) { + return false; + } + + return true; + } + + bool _isValidManufacturerData( + List filterMfdList, + List deviceMfdList, + ) { + if (filterMfdList.isEmpty) return true; + if (deviceMfdList.isEmpty) return false; + + // Check all filters + for (var filterMfd in filterMfdList) { + // Check if device have manufacturerData for this filter + for (var deviceMfd in deviceMfdList) { + // Check companyIdentifier + if (filterMfd.companyIdentifier != deviceMfd.companyIdentifier) { + continue; + } + + // Check data + Uint8List? filterData = filterMfd.data; + Uint8List? deviceData = deviceMfd.data; + + // If filter data is null and device data is not, continue to next deviceMfd + if (filterData != null && deviceData == null) continue; + + if (filterData == null || deviceData == null) { + return true; + } + + if (filterData.length > deviceData.length) continue; + + // Apply mask + Uint8List? filterMask = filterMfd.mask; + bool dataMatched = true; + if (filterMask != null && (filterMask.length == filterData.length)) { + for (int i = 0; i < filterData.length; i++) { + if ((filterData[i] & filterMask[i]) != + (deviceData[i] & filterMask[i])) { + dataMatched = false; + break; + } + } + } + // Compare data directly + else { + for (int i = 0; i < filterData.length; i++) { + if (filterData[i] != deviceData[i]) { + dataMatched = false; + break; + } + } + } + + if (dataMatched) return true; + } + } + + return false; + } + void _onDeviceRemoved(BlueZDevice device) { _devices.remove(device.address); // Stop listener @@ -451,6 +535,24 @@ class BluezProperty { } extension BlueZDeviceExtension on BlueZDevice { + List get manufacturerDataFilter { + try { + if (manufacturerData.isEmpty) return []; + return manufacturerData.entries + .map((e) => ManufacturerDataFilter( + companyIdentifier: e.key.id, + data: Uint8List.fromList(e.value), + )) + .toList(); + } catch (e) { + UniversalBlePlatform.logInfo( + 'Error parsing manufacturerData: $e', + isError: true, + ); + return []; + } + } + Uint8List get manufacturerDataHead { try { if (manufacturerData.isEmpty) return Uint8List(0); diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index aed1788..b32d2b4 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -15,8 +15,7 @@ PlatformException _createConnectionError(String channelName) { ); } -List wrapResponse( - {Object? result, PlatformException? error, bool empty = false}) { +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { if (empty) { return []; } @@ -98,8 +97,7 @@ class UniversalBleService { result as List; return UniversalBleService( uuid: result[0]! as String, - characteristics: - (result[1] as List?)?.cast(), + characteristics: (result[1] as List?)?.cast(), ); } } @@ -130,16 +128,21 @@ class UniversalBleCharacteristic { } } +/// Scan Filters class UniversalScanFilter { UniversalScanFilter({ required this.withServices, + required this.withManufacturerData, }); List withServices; + List withManufacturerData; + Object encode() { return [ withServices, + withManufacturerData, ]; } @@ -147,6 +150,38 @@ class UniversalScanFilter { result as List; return UniversalScanFilter( withServices: (result[0] as List?)!.cast(), + withManufacturerData: (result[1] as List?)!.cast(), + ); + } +} + +class UniversalManufacturerDataFilter { + UniversalManufacturerDataFilter({ + this.companyIdentifier, + this.data, + this.mask, + }); + + int? companyIdentifier; + + Uint8List? data; + + Uint8List? mask; + + Object encode() { + return [ + companyIdentifier, + data, + mask, + ]; + } + + static UniversalManufacturerDataFilter decode(Object result) { + result as List; + return UniversalManufacturerDataFilter( + companyIdentifier: result[0] as int?, + data: result[1] as Uint8List?, + mask: result[2] as Uint8List?, ); } } @@ -164,9 +199,12 @@ class _UniversalBlePlatformChannelCodec extends StandardMessageCodec { } else if (value is UniversalBleService) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is UniversalScanFilter) { + } else if (value is UniversalManufacturerDataFilter) { buffer.putUint8(131); writeValue(buffer, value.encode()); + } else if (value is UniversalScanFilter) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -175,13 +213,15 @@ class _UniversalBlePlatformChannelCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return UniversalBleCharacteristic.decode(readValue(buffer)!); - case 129: + case 129: return UniversalBleScanResult.decode(readValue(buffer)!); - case 130: + case 130: return UniversalBleService.decode(readValue(buffer)!); - case 131: + case 131: + return UniversalManufacturerDataFilter.decode(readValue(buffer)!); + case 132: return UniversalScanFilter.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -198,14 +238,11 @@ class UniversalBlePlatformChannel { : __pigeon_binaryMessenger = binaryMessenger; final BinaryMessenger? __pigeon_binaryMessenger; - static const MessageCodec pigeonChannelCodec = - _UniversalBlePlatformChannelCodec(); + static const MessageCodec pigeonChannelCodec = _UniversalBlePlatformChannelCodec(); Future getBluetoothAvailabilityState() async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -231,10 +268,8 @@ class UniversalBlePlatformChannel { } Future enableBluetooth() async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -260,10 +295,8 @@ class UniversalBlePlatformChannel { } Future startScan(UniversalScanFilter? filter) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -284,10 +317,8 @@ class UniversalBlePlatformChannel { } Future stopScan() async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -308,10 +339,8 @@ class UniversalBlePlatformChannel { } Future connect(String deviceId) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -332,10 +361,8 @@ class UniversalBlePlatformChannel { } Future disconnect(String deviceId) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -355,19 +382,15 @@ class UniversalBlePlatformChannel { } } - Future setNotifiable(String deviceId, String service, - String characteristic, int bleInputProperty) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + Future setNotifiable(String deviceId, String service, String characteristic, int bleInputProperty) async { + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel.send( - [deviceId, service, characteristic, bleInputProperty]) - as List?; + final List? __pigeon_replyList = + await __pigeon_channel.send([deviceId, service, characteristic, bleInputProperty]) as List?; if (__pigeon_replyList == null) { throw _createConnectionError(__pigeon_channelName); } else if (__pigeon_replyList.length > 1) { @@ -382,10 +405,8 @@ class UniversalBlePlatformChannel { } Future> discoverServices(String deviceId) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -406,23 +427,19 @@ class UniversalBlePlatformChannel { message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as List?)! - .cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } - Future readValue( - String deviceId, String service, String characteristic) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + Future readValue(String deviceId, String service, String characteristic) async { + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel - .send([deviceId, service, characteristic]) as List?; + final List? __pigeon_replyList = + await __pigeon_channel.send([deviceId, service, characteristic]) as List?; if (__pigeon_replyList == null) { throw _createConnectionError(__pigeon_channelName); } else if (__pigeon_replyList.length > 1) { @@ -442,16 +459,14 @@ class UniversalBlePlatformChannel { } Future requestMtu(String deviceId, int expectedMtu) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel - .send([deviceId, expectedMtu]) as List?; + final List? __pigeon_replyList = + await __pigeon_channel.send([deviceId, expectedMtu]) as List?; if (__pigeon_replyList == null) { throw _createConnectionError(__pigeon_channelName); } else if (__pigeon_replyList.length > 1) { @@ -470,24 +485,15 @@ class UniversalBlePlatformChannel { } } - Future writeValue(String deviceId, String service, - String characteristic, Uint8List value, int bleOutputProperty) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + Future writeValue(String deviceId, String service, String characteristic, Uint8List value, int bleOutputProperty) async { + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel - .send([ - deviceId, - service, - characteristic, - value, - bleOutputProperty - ]) as List?; + final List? __pigeon_replyList = + await __pigeon_channel.send([deviceId, service, characteristic, value, bleOutputProperty]) as List?; if (__pigeon_replyList == null) { throw _createConnectionError(__pigeon_channelName); } else if (__pigeon_replyList.length > 1) { @@ -502,10 +508,8 @@ class UniversalBlePlatformChannel { } Future isPaired(String deviceId) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -531,10 +535,8 @@ class UniversalBlePlatformChannel { } Future pair(String deviceId) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -555,10 +557,8 @@ class UniversalBlePlatformChannel { } Future unPair(String deviceId) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -578,12 +578,9 @@ class UniversalBlePlatformChannel { } } - Future> getConnectedDevices( - List withServices) async { - const String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + Future> getConnectedDevices(List withServices) async { + const String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -604,8 +601,7 @@ class UniversalBlePlatformChannel { message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as List?)! - .cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } } @@ -625,7 +621,7 @@ class _UniversalBleCallbackChannelCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return UniversalBleScanResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -635,8 +631,7 @@ class _UniversalBleCallbackChannelCodec extends StandardMessageCodec { /// Native -> Flutter abstract class UniversalBleCallbackChannel { - static const MessageCodec pigeonChannelCodec = - _UniversalBleCallbackChannelCodec(); + static const MessageCodec pigeonChannelCodec = _UniversalBleCallbackChannelCodec(); void onAvailabilityChanged(int state); @@ -644,25 +639,21 @@ abstract class UniversalBleCallbackChannel { void onScanResult(UniversalBleScanResult result); - void onValueChanged( - String deviceId, String characteristicId, Uint8List value); + void onValueChanged(String deviceId, String characteristicId, Uint8List value); void onConnectionChanged(String deviceId, int state); - static void setup(UniversalBleCallbackChannel? api, - {BinaryMessenger? binaryMessenger}) { + static void setup(UniversalBleCallbackChannel? api, {BinaryMessenger? binaryMessenger}) { { - final BasicMessageChannel __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged was null.'); final List args = (message as List?)!; final int? arg_state = (args[0] as int?); assert(arg_state != null, @@ -672,25 +663,22 @@ abstract class UniversalBleCallbackChannel { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange was null.'); final List args = (message as List?)!; final String? arg_deviceId = (args[0] as String?); assert(arg_deviceId != null, @@ -704,28 +692,24 @@ abstract class UniversalBleCallbackChannel { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult was null.'); final List args = (message as List?)!; - final UniversalBleScanResult? arg_result = - (args[0] as UniversalBleScanResult?); + final UniversalBleScanResult? arg_result = (args[0] as UniversalBleScanResult?); assert(arg_result != null, 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult was null, expected non-null UniversalBleScanResult.'); try { @@ -733,25 +717,22 @@ abstract class UniversalBleCallbackChannel { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null.'); final List args = (message as List?)!; final String? arg_deviceId = (args[0] as String?); assert(arg_deviceId != null, @@ -763,30 +744,26 @@ abstract class UniversalBleCallbackChannel { assert(arg_value != null, 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null, expected non-null Uint8List.'); try { - api.onValueChanged( - arg_deviceId!, arg_characteristicId!, arg_value!); + api.onValueChanged(arg_deviceId!, arg_characteristicId!, arg_value!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged was null.'); final List args = (message as List?)!; final String? arg_deviceId = (args[0] as String?); assert(arg_deviceId != null, @@ -799,9 +776,8 @@ abstract class UniversalBleCallbackChannel { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); } }); } 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 758a23e..845472b 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -29,7 +29,6 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { @override Future startScan({ - WebRequestOptionsBuilder? webRequestOptions, ScanFilter? scanFilter, }) async { await _channel.startScan( @@ -207,12 +206,19 @@ extension _UniversalBleScanResultExtension on UniversalBleScanResult { extension _ScanFilterExtension on ScanFilter? { UniversalScanFilter? toUniversalScanFilter() { + List? manufacturerDataFilters = this + ?.withManufacturerData + .map((e) => UniversalManufacturerDataFilter( + companyIdentifier: e.companyIdentifier, + data: e.data, + mask: e.mask, + )) + .toList(); + // Windows crashes if it's null, so we need to pass empty scan filter in this case - if (this == null && Platform.isWindows) { - return UniversalScanFilter(withServices: []); - } return UniversalScanFilter( withServices: this?.withServices.toValidUUIDList() ?? [], + withManufacturerData: manufacturerDataFilters ?? [], ); } } diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index aaea4eb..0865d49 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -8,7 +8,6 @@ abstract class UniversalBlePlatform { Future enableBluetooth(); Future startScan({ - WebRequestOptionsBuilder? webRequestOptions, ScanFilter? scanFilter, }); diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 5f58304..8b28d3e 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -109,22 +109,11 @@ class UniversalBleWeb extends UniversalBlePlatform { @override Future startScan({ - WebRequestOptionsBuilder? webRequestOptions, ScanFilter? scanFilter, }) async { - RequestOptionsBuilder requestFilterBuilder; - if (webRequestOptions != null) { - requestFilterBuilder = webRequestOptions.toRequestOptionsBuilder( - scanFilter: scanFilter, - ); - } else if (scanFilter != null) { - requestFilterBuilder = scanFilter.toRequestOptionsBuilder(); - } else { - requestFilterBuilder = RequestOptionsBuilder.acceptAllDevices(); - } - BluetoothDevice device = await FlutterWebBluetooth.instance.requestDevice( - requestFilterBuilder, + scanFilter?.toRequestOptionsBuilder() ?? + RequestOptionsBuilder.acceptAllDevices(), ); // Update local device list @@ -389,8 +378,37 @@ extension _UnmodifiableMapViewExtension on UnmodifiableMapView { extension ScanFilterExtension on ScanFilter { RequestOptionsBuilder toRequestOptionsBuilder() { + List filters = []; + // Add services filter + for (var service in withServices.toValidUUIDList()) { + filters.add( + RequestFilterBuilder( + services: [service], + ), + ); + } + + // Add manufacturer data filter + for (var manufacturerData in withManufacturerData) { + filters.add( + RequestFilterBuilder( + manufacturerData: [ + ManufacturerDataFilterBuilder( + companyIdentifier: manufacturerData.companyIdentifier, + dataPrefix: manufacturerData.data, + mask: manufacturerData.mask, + ), + ], + ), + ); + } + + if (filters.isEmpty) { + return RequestOptionsBuilder.acceptAllDevices(); + } + return RequestOptionsBuilder( - [RequestFilterBuilder(services: withServices.toValidUUIDList())], + filters, optionalServices: withServices.toValidUUIDList(), ); } diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 0fa228a..7de5361 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -130,8 +130,24 @@ class UniversalBleCharacteristic { UniversalBleCharacteristic(this.uuid, this.properties); } +/// Scan Filters class UniversalScanFilter { final List withServices; + final List withManufacturerData; - UniversalScanFilter(this.withServices); + UniversalScanFilter( + this.withServices, + this.withManufacturerData, + ); +} + +class UniversalManufacturerDataFilter { + int? companyIdentifier; + Uint8List? data; + Uint8List? mask; + UniversalManufacturerDataFilter({ + this.companyIdentifier, + this.data, + this.mask, + }); } diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index e078180..cc924f2 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -273,8 +273,11 @@ UniversalBleCharacteristic UniversalBleCharacteristic::FromEncodableList(const E // UniversalScanFilter -UniversalScanFilter::UniversalScanFilter(const EncodableList& with_services) - : with_services_(with_services) {} +UniversalScanFilter::UniversalScanFilter( + const EncodableList& with_services, + const EncodableList& with_manufacturer_data) + : with_services_(with_services), + with_manufacturer_data_(with_manufacturer_data) {} const EncodableList& UniversalScanFilter::with_services() const { return with_services_; @@ -285,16 +288,104 @@ void UniversalScanFilter::set_with_services(const EncodableList& value_arg) { } +const EncodableList& UniversalScanFilter::with_manufacturer_data() const { + return with_manufacturer_data_; +} + +void UniversalScanFilter::set_with_manufacturer_data(const EncodableList& value_arg) { + with_manufacturer_data_ = value_arg; +} + + EncodableList UniversalScanFilter::ToEncodableList() const { EncodableList list; - list.reserve(1); + list.reserve(2); list.push_back(EncodableValue(with_services_)); + list.push_back(EncodableValue(with_manufacturer_data_)); return list; } UniversalScanFilter UniversalScanFilter::FromEncodableList(const EncodableList& list) { UniversalScanFilter decoded( - std::get(list[0])); + std::get(list[0]), + std::get(list[1])); + return decoded; +} + +// UniversalManufacturerDataFilter + +UniversalManufacturerDataFilter::UniversalManufacturerDataFilter() {} + +UniversalManufacturerDataFilter::UniversalManufacturerDataFilter( + const int64_t* company_identifier, + const std::vector* data, + const std::vector* mask) + : company_identifier_(company_identifier ? std::optional(*company_identifier) : std::nullopt), + data_(data ? std::optional>(*data) : std::nullopt), + mask_(mask ? std::optional>(*mask) : std::nullopt) {} + +const int64_t* UniversalManufacturerDataFilter::company_identifier() const { + return company_identifier_ ? &(*company_identifier_) : nullptr; +} + +void UniversalManufacturerDataFilter::set_company_identifier(const int64_t* value_arg) { + company_identifier_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void UniversalManufacturerDataFilter::set_company_identifier(int64_t value_arg) { + company_identifier_ = value_arg; +} + + +const std::vector* UniversalManufacturerDataFilter::data() const { + return data_ ? &(*data_) : nullptr; +} + +void UniversalManufacturerDataFilter::set_data(const std::vector* value_arg) { + data_ = value_arg ? std::optional>(*value_arg) : std::nullopt; +} + +void UniversalManufacturerDataFilter::set_data(const std::vector& value_arg) { + data_ = value_arg; +} + + +const std::vector* UniversalManufacturerDataFilter::mask() const { + return mask_ ? &(*mask_) : nullptr; +} + +void UniversalManufacturerDataFilter::set_mask(const std::vector* value_arg) { + mask_ = value_arg ? std::optional>(*value_arg) : std::nullopt; +} + +void UniversalManufacturerDataFilter::set_mask(const std::vector& value_arg) { + mask_ = value_arg; +} + + +EncodableList UniversalManufacturerDataFilter::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(company_identifier_ ? EncodableValue(*company_identifier_) : EncodableValue()); + list.push_back(data_ ? EncodableValue(*data_) : EncodableValue()); + list.push_back(mask_ ? EncodableValue(*mask_) : EncodableValue()); + return list; +} + +UniversalManufacturerDataFilter UniversalManufacturerDataFilter::FromEncodableList(const EncodableList& list) { + UniversalManufacturerDataFilter decoded; + auto& encodable_company_identifier = list[0]; + if (!encodable_company_identifier.IsNull()) { + decoded.set_company_identifier(encodable_company_identifier.LongValue()); + } + auto& encodable_data = list[1]; + if (!encodable_data.IsNull()) { + decoded.set_data(std::get>(encodable_data)); + } + auto& encodable_mask = list[2]; + if (!encodable_mask.IsNull()) { + decoded.set_mask(std::get>(encodable_mask)); + } return decoded; } @@ -312,6 +403,8 @@ EncodableValue UniversalBlePlatformChannelCodecSerializer::ReadValueOfType( case 130: return CustomEncodableValue(UniversalBleService::FromEncodableList(std::get(ReadValue(stream)))); case 131: + return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + case 132: return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); default: return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); @@ -337,8 +430,13 @@ void UniversalBlePlatformChannelCodecSerializer::WriteValue( WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } - if (custom_value->type() == typeid(UniversalScanFilter)) { + if (custom_value->type() == typeid(UniversalManufacturerDataFilter)) { stream->WriteByte(131); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(UniversalScanFilter)) { + stream->WriteByte(132); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index 2b0662b..35375bb 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -179,15 +179,22 @@ class UniversalBleCharacteristic { }; +// Scan Filters +// // Generated class from Pigeon that represents data sent in messages. class UniversalScanFilter { public: // Constructs an object setting all fields. - explicit UniversalScanFilter(const flutter::EncodableList& with_services); + explicit UniversalScanFilter( + const flutter::EncodableList& with_services, + const flutter::EncodableList& with_manufacturer_data); const flutter::EncodableList& with_services() const; void set_with_services(const flutter::EncodableList& value_arg); + const flutter::EncodableList& with_manufacturer_data() const; + void set_with_manufacturer_data(const flutter::EncodableList& value_arg); + private: static UniversalScanFilter FromEncodableList(const flutter::EncodableList& list); @@ -197,6 +204,46 @@ class UniversalScanFilter { friend class UniversalBleCallbackChannel; friend class UniversalBleCallbackChannelCodecSerializer; flutter::EncodableList with_services_; + flutter::EncodableList with_manufacturer_data_; + +}; + + +// Generated class from Pigeon that represents data sent in messages. +class UniversalManufacturerDataFilter { + public: + // Constructs an object setting all non-nullable fields. + UniversalManufacturerDataFilter(); + + // Constructs an object setting all fields. + explicit UniversalManufacturerDataFilter( + const int64_t* company_identifier, + const std::vector* data, + const std::vector* mask); + + const int64_t* company_identifier() const; + void set_company_identifier(const int64_t* value_arg); + void set_company_identifier(int64_t value_arg); + + const std::vector* data() const; + void set_data(const std::vector* value_arg); + void set_data(const std::vector& value_arg); + + const std::vector* mask() const; + void set_mask(const std::vector* value_arg); + void set_mask(const std::vector& value_arg); + + + private: + static UniversalManufacturerDataFilter FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class UniversalBlePlatformChannel; + friend class UniversalBlePlatformChannelCodecSerializer; + friend class UniversalBleCallbackChannel; + friend class UniversalBleCallbackChannelCodecSerializer; + std::optional company_identifier_; + std::optional> data_; + std::optional> mask_; }; diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 0a81d04..7f568a2 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -30,7 +30,8 @@ namespace universal_ble const auto signalStrengthKey = L"System.Devices.Aep.SignalStrength"; std::unique_ptr callbackChannel; - std::unordered_map characteristicsTokens{}; // TODO: Remove the map and store the token inside the characteristic object object + std::unordered_map characteristicsTokens{}; // TODO: Remove the map and store the token inside the characteristic object + std::vector manufacturerScanFilter = std::vector(); void UniversalBlePlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar) { @@ -89,21 +90,49 @@ namespace universal_ble { if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) { + setupDeviceWatcher(); + DeviceWatcherStatus status = deviceWatcher.Status(); + if (status != DeviceWatcherStatus::Started) + { + deviceWatcher.Start(); + } + else + { + return FlutterError("Already scanning"); + } + if (!bluetoothLEWatcher) { bluetoothLEWatcher = BluetoothLEAdvertisementWatcher(); bluetoothLEWatcher.ScanningMode(BluetoothLEScanningMode::Active); + + // reset scan filters + manufacturerScanFilter.clear(); if (filter != nullptr) - ApplyScanFilter(filter, bluetoothLEWatcher); + { + // Apply Services filter + const auto &services = filter->with_services(); + if (!services.empty()) + { + for (const auto &uuid : services) + { + std::string uuid_str = std::get(uuid); + bluetoothLEWatcher.AdvertisementFilter().Advertisement().ServiceUuids().Append(uuid_to_guid(uuid_str)); + } + } + + // Set ManufacturerData filter + const auto &manufacturerData = filter->with_manufacturer_data(); + for (const flutter::EncodableValue &data : manufacturerData) + { + UniversalManufacturerDataFilter manufacturerDataFilter = std::any_cast(std::get(data)); + manufacturerScanFilter.push_back(manufacturerDataFilter); + } + } bluetoothLEWatcherReceivedToken = bluetoothLEWatcher.Received({this, &UniversalBlePlugin::BluetoothLEWatcher_Received}); } bluetoothLEWatcher.Start(); - setupDeviceWatcher(); - DeviceWatcherStatus status = deviceWatcher.Status(); - if (status != DeviceWatcherStatus::Started) - deviceWatcher.Start(); - else - return FlutterError("Already scanning"); + return std::nullopt; } else @@ -112,20 +141,6 @@ namespace universal_ble } }; - void UniversalBlePlugin::ApplyScanFilter(const UniversalScanFilter *filter, BluetoothLEAdvertisementWatcher &bluetoothWatcher) - { - // Apply Services filter - const auto &services = filter->with_services(); - if (!services.empty()) - { - for (const auto &uuid : services) - { - std::string uuid_str = std::get(uuid); - bluetoothWatcher.AdvertisementFilter().Advertisement().ServiceUuids().Append(uuid_to_guid(uuid_str)); - } - } - } - std::optional UniversalBlePlugin::StopScan() { @@ -688,6 +703,17 @@ namespace universal_ble if (!args.IsConnectable()) return; + // Apply ManufacturerData filter + if (!manufacturerScanFilter.empty()) + { + // Avoid devices if ManufacturerData is not present + if (args.Advertisement().ManufacturerData().Size() == 0) + return; + // Avoid devices if ManufacturerData does not match the filter + if (!filterByManufacturerData(args.Advertisement().ManufacturerData())) + return; + } + auto deviceId = _mac_address_to_str(args.BluetoothAddress()); auto universalScanResult = UniversalBleScanResult(deviceId); std::string name = winrt::to_string(args.Advertisement().LocalName()); @@ -747,6 +773,63 @@ namespace universal_ble } } + bool UniversalBlePlugin::filterByManufacturerData(IVector deviceManufactureData) + { + if (manufacturerScanFilter.empty()) + return true; + + for (auto &&filter : manufacturerScanFilter) + { + const int64_t *company_identifier = filter.company_identifier(); + const std::vector *data_filter = filter.data(); + const std::vector *mask = filter.mask(); + + if (company_identifier == nullptr) + continue; + + uint16_t companyId = static_cast(*company_identifier); + + for (auto &&deviceMfData : deviceManufactureData) + { + if (deviceMfData.CompanyId() == companyId) + { + // If data filter is not present then return true + if (data_filter == nullptr) + return true; + + auto deviceData = to_bytevc(deviceMfData.Data()); + if (deviceData.size() < data_filter->size()) + continue; + + bool isMatch = true; + for (size_t i = 0; i < data_filter->size(); i++) + { + if (mask != nullptr && mask->size() > i) + { + if (((*mask)[i] & (*data_filter)[i]) != ((*mask)[i] & deviceData[i])) + { + isMatch = false; + break; + } + } + else + { + if ((*data_filter)[i] != deviceData[i]) + { + isMatch = false; + break; + } + } + } + if (isMatch) + return true; + } + } + } + + return false; + } + AvailabilityState UniversalBlePlugin::getAvailabilityStateFromRadio(RadioState radioState) { auto state = [=]() -> AvailabilityState diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index a3fa666..3070c75 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -123,7 +123,7 @@ namespace universal_ble winrt::fire_and_forget WriteAsync(GattCharacteristic characteristic, GattWriteOption writeOption, const std::vector &value, std::function reply)> result); - void ApplyScanFilter(const UniversalScanFilter *filter, BluetoothLEAdvertisementWatcher &bluetoothWatcher); + bool filterByManufacturerData(IVector deviceManufactureData); // UniversalBlePlatformChannel implementation. void GetBluetoothAvailabilityState(std::function reply)> result) override;