From 2c0d36b9d15d45362af4d58a3ae3360ed11e94a3 Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Mon, 17 Feb 2025 06:42:28 +0100 Subject: [PATCH] Improve ManufacturerDataFilter (#139) * Improve-ManufacturerDataFilter-docs * Improve ManufacturerDataFilter namings and code * Perform more renamings Add tests --- CHANGELOG.md | 5 +- README.md | 10 +- .../lib/home/widgets/scan_filter_widget.dart | 3 +- lib/src/models/manufacturer_data_filter.dart | 26 +++ lib/src/models/model_exports.dart | 1 + lib/src/models/scan_filter.dart | 25 +-- lib/src/universal_ble_filter_util.dart | 91 ++++----- .../universal_ble_linux.dart | 8 +- .../universal_ble_pigeon_channel.dart | 4 +- .../universal_ble_web/universal_ble_web.dart | 4 +- test/manufacturer_data_filter_test.dart | 178 ++++++++++++++++++ test/scan_filter_test.dart | 52 ++--- 12 files changed, 302 insertions(+), 105 deletions(-) create mode 100644 lib/src/models/manufacturer_data_filter.dart create mode 100644 test/manufacturer_data_filter_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d91ceb..41c1de9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ ## 0.16.0 +* BREAKING CHANGE: `payload` is now `payloadPrefix` +* BREAKING CHANGE: `mask` is now `payloadMask` * Bump bluez to 0.8.3 -* Improve readme * Do not normalize UUIDs on native side * Reverse _permissionStatus return values in example app +* Improve readme +* Improve ManufacturerDataFilter docs ## 0.15.0 * `getSystemDevices(withServices:)` now sets several generic services by default as filter diff --git a/README.md b/README.md index 04396da..4c51a79 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ UniversalBle.startScan(); UniversalBle.startScan( scanFilter: ScanFilter( withServices: ["SERVICE_UUID"], - withManufacturerData: [ManufacturerDataFilter(companyIdentifier: COMPANY_IDENTIFIER)], + withManufacturerData: [ManufacturerDataFilter(companyIdentifier: 0x004c)], withNamePrefix: ["NAME_PREFIX"], ) ); @@ -131,8 +131,14 @@ Note: On web **you have to** specify services before you are able to use them. S Use the `withManufacturerData` parameter to filter devices by manufacturer data. When you pass a list of `ManufacturerDataFilter` objects to this parameter, the scan results will only include devices that contain any of the specified manufacturer data. +You can filter manufacturer data by company identifier, payload prefix, or payload mask. + ```dart -List withManufacturerData; +List withManufacturerData = [ManufacturerDataFilter( + companyIdentifier: 0x004c, + payloadPrefix: Uint8List.fromList([0x001D,0x001A]), + payloadMask: Uint8List.fromList([1,0,1,1])) + ]; ``` ##### With namePrefix diff --git a/example/lib/home/widgets/scan_filter_widget.dart b/example/lib/home/widgets/scan_filter_widget.dart index 0759411..cd21487 100644 --- a/example/lib/home/widgets/scan_filter_widget.dart +++ b/example/lib/home/widgets/scan_filter_widget.dart @@ -60,8 +60,7 @@ class _ScanFilterWidgetState extends State { throw Exception("Invalid Manufacturer Data $manufacturer"); } manufacturerDataFilters.add( - ManufacturerDataFilter(companyIdentifier: companyIdentifier), - ); + ManufacturerDataFilter(companyIdentifier: companyIdentifier)); } } diff --git a/lib/src/models/manufacturer_data_filter.dart b/lib/src/models/manufacturer_data_filter.dart new file mode 100644 index 0000000..e1d68f0 --- /dev/null +++ b/lib/src/models/manufacturer_data_filter.dart @@ -0,0 +1,26 @@ +import 'dart:typed_data'; + +class ManufacturerDataFilter { + /// Must be of integer type, in hex or decimal form (e.g. 0x004c or 76). + int companyIdentifier; + + /// Matches as prefix the peripheral's advertised data. + Uint8List? payloadPrefix; + + /// For each bit in the mask, set it to 1 if it needs to match + /// the corresponding one in manufacturer data, or otherwise set it to 0. + /// The 'mask' must have the same length as the payload. + Uint8List? payloadMask; + + /// Filter manufacturer data by company identifier, payload prefix, or payload mask. + ManufacturerDataFilter({ + required this.companyIdentifier, + this.payloadPrefix, + this.payloadMask, + }); + + @override + String toString() { + return 'ManufacturerDataFilter(companyIdentifier: $companyIdentifier, payloadPrefix: $payloadPrefix, mask: $payloadMask)'; + } +} diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart index 60db6fb..07e5354 100644 --- a/lib/src/models/model_exports.dart +++ b/lib/src/models/model_exports.dart @@ -4,6 +4,7 @@ export 'package:universal_ble/src/models/platform_config.dart'; export 'package:universal_ble/src/models/queue_type.dart'; export 'package:universal_ble/src/models/ble_uuid_parser.dart'; export 'package:universal_ble/src/models/scan_filter.dart'; +export 'package:universal_ble/src/models/manufacturer_data_filter.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 82885fa..31455ef 100644 --- a/lib/src/models/scan_filter.dart +++ b/lib/src/models/scan_filter.dart @@ -1,4 +1,4 @@ -import 'dart:typed_data'; +import 'package:universal_ble/src/models/manufacturer_data_filter.dart'; class ScanFilter { List withServices; @@ -16,26 +16,3 @@ class ScanFilter { return 'ScanFilter(withServices: $withServices, withManufacturerData: $withManufacturerData, withNamePrefix: $withNamePrefix)'; } } - -class ManufacturerDataFilter { - int companyIdentifier; - - // Mask and data must be of same length - Uint8List? payload; - - /// 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({ - required this.companyIdentifier, - this.payload, - this.mask, - }); - - @override - String toString() { - return 'ManufacturerDataFilter(companyIdentifier: $companyIdentifier, payload: $payload, mask: $mask)'; - } -} diff --git a/lib/src/universal_ble_filter_util.dart b/lib/src/universal_ble_filter_util.dart index 9c19b19..e4d3345 100644 --- a/lib/src/universal_ble_filter_util.dart +++ b/lib/src/universal_ble_filter_util.dart @@ -7,7 +7,7 @@ import 'package:universal_ble/universal_ble.dart'; class UniversalBleFilterUtil { ScanFilter? scanFilter; - bool filterDevice(BleDevice device) { + bool matchesDevice(BleDevice device) { final filter = scanFilter; if (filter == null) return true; @@ -23,13 +23,12 @@ class UniversalBleFilterUtil { } // Else check one of the filter passes - return hasNamePrefixFilter && isNameMatchingFilters(filter, device) || - hasServiceFilter && isServicesMatchingFilters(filter, device) || - hasManufacturerDataFilter && - isManufacturerDataMatchingFilters(filter, device); + return hasNamePrefixFilter && nameMatches(filter, device) || + hasServiceFilter && servicesMatch(filter, device) || + hasManufacturerDataFilter && manufacturerDataMatches(filter, device); } - bool isNameMatchingFilters(ScanFilter scanFilter, BleDevice device) { + bool nameMatches(ScanFilter scanFilter, BleDevice device) { var namePrefixFilter = scanFilter.withNamePrefix; if (namePrefixFilter.isEmpty) return true; @@ -38,7 +37,7 @@ class UniversalBleFilterUtil { return namePrefixFilter.any(name.startsWith); } - bool isServicesMatchingFilters(ScanFilter scanFilter, BleDevice device) { + bool servicesMatch(ScanFilter scanFilter, BleDevice device) { var serviceFilters = scanFilter.withServices; if (serviceFilters.isEmpty) return true; @@ -49,48 +48,56 @@ class UniversalBleFilterUtil { return serviceFilters.any(serviceUuids.contains); } - bool isManufacturerDataMatchingFilters( - ScanFilter scanFilter, - BleDevice device, - ) { + bool manufacturerDataMatches(ScanFilter scanFilter, BleDevice device) { final manufacturerDataFilters = scanFilter.withManufacturerData; if (manufacturerDataFilters.isEmpty) return true; - List manufacturerDataList = device.manufacturerDataList; - if (manufacturerDataList.isEmpty) return false; + List deviceDataList = device.manufacturerDataList; + if (deviceDataList.isEmpty) return false; - return manufacturerDataList.any((deviceMsd) => manufacturerDataFilters.any( - (filterMsd) => _isManufacturerDataMatch(filterMsd, deviceMsd), - )); + return deviceDataList + .any((deviceData) => manufacturerDataFilters.any((filter) { + // Early return if company identifiers don't match + if (filter.companyIdentifier != deviceData.companyId) { + return false; + } + + final payloadPrefix = filter.payloadPrefix; + final payload = deviceData.payload; + + // Handle cases where payload prefix is null or empty + if (payloadPrefix == null || payloadPrefix.isEmpty) { + return true; + } + + // Validate payload lengths + if (payload.isEmpty || payloadPrefix.length > payload.length) { + return false; + } + + final filterMask = filter.payloadMask; + + // Choose comparison strategy based on filter mask + return filterMask != null && + filterMask.length == payloadPrefix.length + ? _compareWithMask(payloadPrefix, payload, filterMask) + : _compareWithoutMask(payloadPrefix, payload); + })); } - bool _isManufacturerDataMatch( - ManufacturerDataFilter filterMsd, - ManufacturerData deviceMsd, - ) { - if (filterMsd.companyIdentifier != deviceMsd.companyId) return false; - - Uint8List? filterPayload = filterMsd.payload; - Uint8List devicePayload = deviceMsd.payload; - - if (filterPayload == null || filterPayload.isEmpty) return true; - if (devicePayload.isEmpty) return false; - if (filterPayload.length > devicePayload.length) return false; - - Uint8List? filterMask = filterMsd.mask; - - if (filterMask != null && filterMask.length == filterPayload.length) { - for (int i = 0; i < filterPayload.length; i++) { - if ((filterPayload[i] & filterMask[i]) != - (devicePayload[i] & filterMask[i])) { - return false; - } + bool _compareWithMask(Uint8List prefix, Uint8List payload, Uint8List mask) { + for (int i = 0; i < prefix.length; i++) { + if ((prefix[i] & mask[i]) != (payload[i] & mask[i])) { + return false; } - } else { - for (int i = 0; i < filterPayload.length; i++) { - if (filterPayload[i] != devicePayload[i]) { - return false; - } + } + return true; + } + + bool _compareWithoutMask(Uint8List prefix, Uint8List payload) { + for (int i = 0; i < prefix.length; i++) { + if (prefix[i] != payload[i]) { + return false; } } return true; diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 8e6c44e..4827c46 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -107,12 +107,12 @@ class UniversalBleLinux extends UniversalBlePlatform { _deviceAdded = null; _deviceRemoved = null; - // Stop Disovery + // Stop Discovery if (_activeAdapter?.discovering == true) { await _activeAdapter?.stopDiscovery(); } - // Clean all advertiseemnt listeners + // Clean all advertisement listeners _deviceAdvertisementSubscriptions.removeWhere((e, value) { value.cancel(); return true; @@ -455,7 +455,7 @@ class UniversalBleLinux extends UniversalBlePlatform { void _onDeviceAdd(BlueZDevice device) { BleDevice bleDevice = device.toBleDevice(); - if (!_bleFilter.filterDevice(bleDevice)) { + if (!_bleFilter.matchesDevice(bleDevice)) { return; } @@ -475,7 +475,7 @@ class UniversalBleLinux extends UniversalBlePlatform { e.contains(BluezProperty.manufacturerData) || e.contains(BluezProperty.uuids)) .listen((_) { - if (_bleFilter.filterDevice(bleDevice)) { + if (_bleFilter.matchesDevice(bleDevice)) { updateScanResult(device.toBleDevice()); } }); 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 6d82573..9071a18 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -245,8 +245,8 @@ extension _ScanFilterExtension on ScanFilter? { ?.withManufacturerData .map((e) => UniversalManufacturerDataFilter( companyIdentifier: e.companyIdentifier, - data: e.payload, - mask: e.mask, + data: e.payloadPrefix, + mask: e.payloadMask, )) .toList(); diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 42a5733..65afb17 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -369,8 +369,8 @@ class UniversalBleWeb extends UniversalBlePlatform { manufacturerData: [ ManufacturerDataFilterBuilder( companyIdentifier: manufacturerData.companyIdentifier, - dataPrefix: manufacturerData.payload, - mask: manufacturerData.mask, + dataPrefix: manufacturerData.payloadPrefix, + mask: manufacturerData.payloadMask, ), ], ), diff --git a/test/manufacturer_data_filter_test.dart b/test/manufacturer_data_filter_test.dart new file mode 100644 index 0000000..a5265dc --- /dev/null +++ b/test/manufacturer_data_filter_test.dart @@ -0,0 +1,178 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:universal_ble/src/universal_ble_filter_util.dart'; +import 'dart:typed_data'; + +import 'package:universal_ble/universal_ble.dart'; + +void main() { + group('ManufacturerData Matching Tests', () { + late UniversalBleFilterUtil filterUtil; + late ScanFilter scanFilter; + late BleDevice device; + + setUp(() { + filterUtil = UniversalBleFilterUtil(); + scanFilter = ScanFilter( + withNamePrefix: [], + withServices: [], + withManufacturerData: [], + ); + }); + + test('should match when no manufacturer filters are present', () { + device = BleDevice( + deviceId: '1', + name: 'Test Device', + manufacturerDataList: [], + services: [], + ); + + expect(filterUtil.manufacturerDataMatches(scanFilter, device), true); + }); + + test('should not match when device has no manufacturer data', () { + scanFilter = ScanFilter( + withNamePrefix: [], + withServices: [], + withManufacturerData: [ + ManufacturerDataFilter(companyIdentifier: 0x004C), + ], + ); + + device = BleDevice( + deviceId: '1', + name: 'Test Device', + manufacturerDataList: [], + services: [], + ); + + expect(filterUtil.manufacturerDataMatches(scanFilter, device), false); + }); + + test( + 'should match when company identifiers are equal and no payload prefix', + () { + scanFilter = ScanFilter( + withNamePrefix: [], + withServices: [], + withManufacturerData: [ + ManufacturerDataFilter(companyIdentifier: 0x004C), + ], + ); + + device = BleDevice( + deviceId: '1', + name: 'Test Device', + manufacturerDataList: [ + ManufacturerData(0x004C, Uint8List.fromList([])), + ], + services: [], + ); + + expect(filterUtil.manufacturerDataMatches(scanFilter, device), true); + }); + + group('Payload Prefix Tests', () { + test('should match when payload prefix matches start of payload', () { + scanFilter = ScanFilter( + withNamePrefix: [], + withServices: [], + withManufacturerData: [ + ManufacturerDataFilter( + companyIdentifier: 0x004C, + payloadPrefix: Uint8List.fromList([0x01, 0x02]), + ), + ], + ); + + device = BleDevice( + deviceId: '1', + name: 'Test Device', + manufacturerDataList: [ + ManufacturerData(0x004C, Uint8List.fromList([0x01, 0x02, 0x03])), + ], + services: [], + ); + + expect(filterUtil.manufacturerDataMatches(scanFilter, device), true); + }); + + test('should match with multiple manufacturer data entries', () { + scanFilter = ScanFilter( + withNamePrefix: [], + withServices: [], + withManufacturerData: [ + ManufacturerDataFilter( + companyIdentifier: 0x004D, + payloadPrefix: Uint8List.fromList([0x01, 0x02]), + ), + ], + ); + + device = BleDevice( + deviceId: '1', + name: 'Test Device', + manufacturerDataList: [ + ManufacturerData(0x004C, Uint8List.fromList([0x03, 0x04])), + ManufacturerData(0x004D, Uint8List.fromList([0x01, 0x02, 0x03])), + ], + services: [], + ); + + expect(filterUtil.manufacturerDataMatches(scanFilter, device), true); + }); + }); + + group('Payload Mask Tests', () { + test('should match when masked values are equal', () { + scanFilter = ScanFilter( + withNamePrefix: [], + withServices: [], + withManufacturerData: [ + ManufacturerDataFilter( + companyIdentifier: 0x004C, + payloadPrefix: Uint8List.fromList([0xFF, 0xFF]), + payloadMask: Uint8List.fromList([0xF0, 0xF0]), + ), + ], + ); + + device = BleDevice( + deviceId: '1', + name: 'Test Device', + manufacturerDataList: [ + ManufacturerData(0x004C, Uint8List.fromList([0xF5, 0xF8])), + ], + services: [], + ); + + expect(filterUtil.manufacturerDataMatches(scanFilter, device), true); + }); + + test('should not match when masked values are different', () { + scanFilter = ScanFilter( + withNamePrefix: [], + withServices: [], + withManufacturerData: [ + ManufacturerDataFilter( + companyIdentifier: 0x004C, + payloadPrefix: Uint8List.fromList([0xFF, 0xFF]), + payloadMask: Uint8List.fromList([0xF0, 0xF0]), + ), + ], + ); + + device = BleDevice( + deviceId: '1', + name: 'Test Device', + manufacturerDataList: [ + ManufacturerData(0x004C, Uint8List.fromList([0xE5, 0xF8])), + ], + services: [], + ); + + expect(filterUtil.manufacturerDataMatches(scanFilter, device), false); + }); + }); + }); +} diff --git a/test/scan_filter_test.dart b/test/scan_filter_test.dart index 2e5ea74..de27b07 100644 --- a/test/scan_filter_test.dart +++ b/test/scan_filter_test.dart @@ -37,15 +37,15 @@ void main() { withNamePrefix: ['1', '2'], ); expect( - universalBleFilter.isNameMatchingFilters(scanFilter, device1), + universalBleFilter.nameMatches(scanFilter, device1), isTrue, ); expect( - universalBleFilter.isNameMatchingFilters(scanFilter, device2), + universalBleFilter.nameMatches(scanFilter, device2), isTrue, ); expect( - universalBleFilter.isNameMatchingFilters(scanFilter, device3), + universalBleFilter.nameMatches(scanFilter, device3), isFalse, ); }); @@ -55,15 +55,15 @@ void main() { withServices: ['1_ser', 'random', '3_ser'], ); expect( - universalBleFilter.isServicesMatchingFilters(scanFilter, device1), + universalBleFilter.servicesMatch(scanFilter, device1), isTrue, ); expect( - universalBleFilter.isServicesMatchingFilters(scanFilter, device2), + universalBleFilter.servicesMatch(scanFilter, device2), isFalse, ); expect( - universalBleFilter.isNameMatchingFilters(scanFilter, device3), + universalBleFilter.nameMatches(scanFilter, device3), isTrue, ); }); @@ -72,32 +72,32 @@ void main() { var scanFilter = ScanFilter(withManufacturerData: [ ManufacturerDataFilter( companyIdentifier: 0x01, - payload: Uint8List.fromList([1, 2]), + payloadPrefix: Uint8List.fromList([1, 2]), ), ManufacturerDataFilter( companyIdentifier: 0x02, ), ManufacturerDataFilter( companyIdentifier: 0x03, - payload: Uint8List.fromList([3, 4]), + payloadPrefix: Uint8List.fromList([3, 4]), ) ]); expect( - universalBleFilter.isManufacturerDataMatchingFilters( + universalBleFilter.manufacturerDataMatches( scanFilter, device1, ), isTrue, ); expect( - universalBleFilter.isManufacturerDataMatchingFilters( + universalBleFilter.manufacturerDataMatches( scanFilter, device2, ), isTrue, ); expect( - universalBleFilter.isManufacturerDataMatchingFilters( + universalBleFilter.manufacturerDataMatches( scanFilter, device3, ), @@ -118,15 +118,15 @@ void main() { ], ); expect( - universalBleFilter.filterDevice(device1), + universalBleFilter.matchesDevice(device1), isTrue, ); expect( - universalBleFilter.filterDevice(device2), + universalBleFilter.matchesDevice(device2), isTrue, ); expect( - universalBleFilter.filterDevice(device3), + universalBleFilter.matchesDevice(device3), isTrue, ); }); @@ -135,15 +135,15 @@ void main() { withNamePrefix: ['1'], ); expect( - universalBleFilter.filterDevice(device1), + universalBleFilter.matchesDevice(device1), isTrue, ); expect( - universalBleFilter.filterDevice(device2), + universalBleFilter.matchesDevice(device2), isFalse, ); expect( - universalBleFilter.filterDevice(device3), + universalBleFilter.matchesDevice(device3), isFalse, ); }); @@ -152,45 +152,45 @@ void main() { withNamePrefix: ['1', '2'], ); expect( - universalBleFilter.filterDevice(device1), + universalBleFilter.matchesDevice(device1), isTrue, ); expect( - universalBleFilter.filterDevice(device2), + universalBleFilter.matchesDevice(device2), isTrue, ); expect( - universalBleFilter.filterDevice(device3), + universalBleFilter.matchesDevice(device3), isFalse, ); }); test('Test filterDevice: Empty Filter', () { universalBleFilter.scanFilter = ScanFilter(); expect( - universalBleFilter.filterDevice(device1), + universalBleFilter.matchesDevice(device1), isTrue, ); expect( - universalBleFilter.filterDevice(device2), + universalBleFilter.matchesDevice(device2), isTrue, ); expect( - universalBleFilter.filterDevice(device3), + universalBleFilter.matchesDevice(device3), isTrue, ); }); test('Test filterDevice: Null Filter', () { universalBleFilter.scanFilter = ScanFilter(); expect( - universalBleFilter.filterDevice(device1), + universalBleFilter.matchesDevice(device1), isTrue, ); expect( - universalBleFilter.filterDevice(device2), + universalBleFilter.matchesDevice(device2), isTrue, ); expect( - universalBleFilter.filterDevice(device3), + universalBleFilter.matchesDevice(device3), isTrue, ); });