Improve ManufacturerDataFilter (#139)

* Improve-ManufacturerDataFilter-docs

* Improve ManufacturerDataFilter namings and code

* Perform more renamings
Add tests
This commit is contained in:
Foti Dim
2025-02-17 06:42:28 +01:00
committed by GitHub
parent 15256283e3
commit 2c0d36b9d1
12 changed files with 302 additions and 105 deletions
+4 -1
View File
@@ -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
+8 -2
View File
@@ -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<ManufacturerDataFilter> withManufacturerData;
List<ManufacturerDataFilter> withManufacturerData = [ManufacturerDataFilter(
companyIdentifier: 0x004c,
payloadPrefix: Uint8List.fromList([0x001D,0x001A]),
payloadMask: Uint8List.fromList([1,0,1,1]))
];
```
##### With namePrefix
@@ -60,8 +60,7 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
throw Exception("Invalid Manufacturer Data $manufacturer");
}
manufacturerDataFilters.add(
ManufacturerDataFilter(companyIdentifier: companyIdentifier),
);
ManufacturerDataFilter(companyIdentifier: companyIdentifier));
}
}
@@ -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)';
}
}
+1
View File
@@ -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';
+1 -24
View File
@@ -1,4 +1,4 @@
import 'dart:typed_data';
import 'package:universal_ble/src/models/manufacturer_data_filter.dart';
class ScanFilter {
List<String> 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)';
}
}
+49 -42
View File
@@ -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<ManufacturerData> manufacturerDataList = device.manufacturerDataList;
if (manufacturerDataList.isEmpty) return false;
List<ManufacturerData> 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;
@@ -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());
}
});
@@ -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();
@@ -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,
),
],
),
+178
View File
@@ -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);
});
});
});
}
+26 -26
View File
@@ -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,
);
});