Implement exclusion filter (#175)
* Implement exclusion filter * Fix web filters * Apply Ai Suggestions * Fix scanFilterTest * Resolve Ai comments * Update readme and changelog * Update CHANGELOG.md Co-authored-by: Foti Dim <foti@navideck.com> * Update exclusion filter doc * Fix wording --------- Co-authored-by: Foti Dim <foti@navideck.com> Co-authored-by: Foti Dim <fotios.dimanidis@a2zebra.de>
This commit is contained in:
@@ -4,15 +4,34 @@ class ScanFilter {
|
||||
List<String> withServices;
|
||||
List<ManufacturerDataFilter> withManufacturerData;
|
||||
List<String> withNamePrefix;
|
||||
List<ExclusionFilter> exclusionFilters;
|
||||
|
||||
ScanFilter({
|
||||
this.withServices = const [],
|
||||
this.withManufacturerData = const [],
|
||||
this.withNamePrefix = const [],
|
||||
this.exclusionFilters = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ScanFilter(withServices: $withServices, withManufacturerData: $withManufacturerData, withNamePrefix: $withNamePrefix)';
|
||||
return 'ScanFilter(withServices: $withServices, withManufacturerData: $withManufacturerData, withNamePrefix: $withNamePrefix, exclusionFilters: $exclusionFilters)';
|
||||
}
|
||||
}
|
||||
|
||||
class ExclusionFilter {
|
||||
List<String> services;
|
||||
List<ManufacturerDataFilter> manufacturerDataFilter;
|
||||
String? namePrefix;
|
||||
|
||||
ExclusionFilter({
|
||||
this.services = const [],
|
||||
this.manufacturerDataFilter = const [],
|
||||
this.namePrefix,
|
||||
});
|
||||
|
||||
bool get hasValidFilters =>
|
||||
services.isNotEmpty ||
|
||||
manufacturerDataFilter.isNotEmpty ||
|
||||
(namePrefix?.isNotEmpty ?? false);
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
|
||||
|
||||
void _onDeviceAdd(BlueZDevice device) {
|
||||
BleDevice bleDevice = device.toBleDevice();
|
||||
if (!_bleFilter.matchesDevice(bleDevice)) {
|
||||
if (!_bleFilter.shouldAcceptDevice(bleDevice)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -485,7 +485,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
|
||||
e.contains(BluezProperty.manufacturerData) ||
|
||||
e.contains(BluezProperty.uuids))
|
||||
.listen((_) {
|
||||
if (_bleFilter.matchesDevice(bleDevice)) {
|
||||
if (_bleFilter.shouldAcceptDevice(bleDevice)) {
|
||||
updateScanResult(device.toBleDevice());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart';
|
||||
import 'package:universal_ble/src/utils/universal_ble_filter_util.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
class UniversalBlePigeonChannel extends UniversalBlePlatform {
|
||||
static UniversalBlePigeonChannel? _instance;
|
||||
static UniversalBlePigeonChannel get instance =>
|
||||
_instance ??= UniversalBlePigeonChannel._();
|
||||
late final UniversalBleFilterUtil _bleFilter = UniversalBleFilterUtil();
|
||||
|
||||
UniversalBlePigeonChannel._() {
|
||||
_setupListeners();
|
||||
@@ -42,6 +44,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
|
||||
PlatformConfig? platformConfig,
|
||||
}) async {
|
||||
await _ensureInitialized();
|
||||
_bleFilter.scanFilter = scanFilter;
|
||||
await _channel.startScan(
|
||||
scanFilter.toUniversalScanFilter(),
|
||||
);
|
||||
@@ -136,7 +139,12 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
|
||||
/// To set listeners
|
||||
void _setupListeners() {
|
||||
UniversalBleCallbackChannel.setUp(_UniversalBleCallbackHandler(
|
||||
scanResult: updateScanResult,
|
||||
scanResult: (bleDevice) {
|
||||
// Only check for exclusion filter here,
|
||||
// scan filter handled natively on platform side
|
||||
if (_bleFilter.matchesExclusionFilter(bleDevice)) return;
|
||||
updateScanResult(bleDevice);
|
||||
},
|
||||
availabilityChange: updateAvailability,
|
||||
connectionChanged: updateConnection,
|
||||
valueChanged: updateCharacteristicValue,
|
||||
|
||||
@@ -348,6 +348,7 @@ class UniversalBleWeb extends UniversalBlePlatform {
|
||||
WebOptions? webOptions,
|
||||
) {
|
||||
List<RequestFilterBuilder> filters = [];
|
||||
List<RequestFilterBuilder> exclusionFilters = [];
|
||||
List<int> optionalManufacturerData = [];
|
||||
List<String> optionalServices = [];
|
||||
|
||||
@@ -389,6 +390,25 @@ class UniversalBleWeb extends UniversalBlePlatform {
|
||||
for (var name in scanFilter.withNamePrefix) {
|
||||
filters.add(RequestFilterBuilder(namePrefix: name));
|
||||
}
|
||||
|
||||
// Add exclusion filters
|
||||
for (var exclusionFilter in scanFilter.exclusionFilters) {
|
||||
exclusionFilters.add(RequestFilterBuilder(
|
||||
services: exclusionFilter.services.isEmpty
|
||||
? null
|
||||
: exclusionFilter.services.toValidUUIDList(),
|
||||
namePrefix: exclusionFilter.namePrefix,
|
||||
manufacturerData: exclusionFilter.manufacturerDataFilter.isEmpty
|
||||
? null
|
||||
: exclusionFilter.manufacturerDataFilter.map((e) {
|
||||
return ManufacturerDataFilterBuilder(
|
||||
companyIdentifier: e.companyIdentifier,
|
||||
dataPrefix: e.payloadPrefix,
|
||||
mask: e.payloadMask,
|
||||
);
|
||||
}).toList(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (optionalServices.isEmpty) {
|
||||
@@ -396,8 +416,13 @@ class UniversalBleWeb extends UniversalBlePlatform {
|
||||
"OptionalServices list is empty on web, you have to specify services in the ScanFilter in order to be able to access those after connecting",
|
||||
);
|
||||
}
|
||||
if (filters.isEmpty && exclusionFilters.isNotEmpty) {
|
||||
UniversalLogger.logError(
|
||||
"Web platform requires inclusion filters when using exclusion filters. Please add withServices, withNamePrefix, or withManufacturerData filters.",
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.isEmpty) {
|
||||
if (filters.isEmpty && exclusionFilters.isEmpty) {
|
||||
return RequestOptionsBuilder.acceptAllDevices(
|
||||
optionalServices: optionalServices,
|
||||
optionalManufacturerData: optionalManufacturerData,
|
||||
@@ -407,6 +432,7 @@ class UniversalBleWeb extends UniversalBlePlatform {
|
||||
filters,
|
||||
optionalServices: optionalServices,
|
||||
optionalManufacturerData: optionalManufacturerData,
|
||||
exclusionFilters: exclusionFilters.isEmpty ? null : exclusionFilters,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,42 @@ import 'package:universal_ble/universal_ble.dart';
|
||||
class UniversalBleFilterUtil {
|
||||
ScanFilter? scanFilter;
|
||||
|
||||
bool matchesDevice(BleDevice device) {
|
||||
bool shouldAcceptDevice(BleDevice device) =>
|
||||
!matchesExclusionFilter(device) && matchedScanFilter(device);
|
||||
|
||||
bool matchesExclusionFilter(BleDevice device) {
|
||||
final exclusionFilters = scanFilter?.exclusionFilters;
|
||||
if (exclusionFilters == null || exclusionFilters.isEmpty) return false;
|
||||
|
||||
final deviceName = device.name?.toLowerCase() ?? '';
|
||||
final deviceServices = device.services.toValidUUIDList();
|
||||
|
||||
for (var filter in exclusionFilters) {
|
||||
if (!filter.hasValidFilters) continue;
|
||||
|
||||
// Check name prefix
|
||||
final filterNamePrefix = filter.namePrefix?.toLowerCase();
|
||||
if (filterNamePrefix != null && filterNamePrefix.isNotEmpty) {
|
||||
if (!deviceName.startsWith(filterNamePrefix)) continue;
|
||||
}
|
||||
// Check services
|
||||
final filterServices = filter.services.toValidUUIDList();
|
||||
if (filterServices.isNotEmpty &&
|
||||
!filterServices.any(deviceServices.contains)) {
|
||||
continue;
|
||||
}
|
||||
// Check manufacturer data
|
||||
if (!manufacturerDataMatches(filter.manufacturerDataFilter, device)) {
|
||||
continue;
|
||||
}
|
||||
// Device matches an exclusion filter
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns true if the device matches the filter
|
||||
bool matchedScanFilter(BleDevice device) {
|
||||
final filter = scanFilter;
|
||||
if (filter == null) return true;
|
||||
|
||||
@@ -23,66 +58,67 @@ class UniversalBleFilterUtil {
|
||||
}
|
||||
|
||||
// Else check one of the filter passes
|
||||
return hasNamePrefixFilter && nameMatches(filter, device) ||
|
||||
hasServiceFilter && servicesMatch(filter, device) ||
|
||||
hasManufacturerDataFilter && manufacturerDataMatches(filter, device);
|
||||
return (hasNamePrefixFilter && nameMatches(filter, device)) ||
|
||||
(hasServiceFilter && servicesMatch(filter, device)) ||
|
||||
(hasManufacturerDataFilter &&
|
||||
manufacturerDataMatches(filter.withManufacturerData, device));
|
||||
}
|
||||
|
||||
bool nameMatches(ScanFilter scanFilter, BleDevice device) {
|
||||
var namePrefixFilter = scanFilter.withNamePrefix;
|
||||
final namePrefixFilter = scanFilter.withNamePrefix;
|
||||
if (namePrefixFilter.isEmpty) return true;
|
||||
|
||||
String? name = device.name;
|
||||
final name = device.name;
|
||||
if (name == null || name.isEmpty) return false;
|
||||
return namePrefixFilter.any(name.startsWith);
|
||||
}
|
||||
|
||||
bool servicesMatch(ScanFilter scanFilter, BleDevice device) {
|
||||
var serviceFilters = scanFilter.withServices;
|
||||
final serviceFilters = scanFilter.withServices.toValidUUIDList();
|
||||
if (serviceFilters.isEmpty) return true;
|
||||
|
||||
List<String> serviceUuids = device.services;
|
||||
final serviceUuids = device.services.toValidUUIDList();
|
||||
if (serviceUuids.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return serviceFilters.any(serviceUuids.contains);
|
||||
}
|
||||
|
||||
bool manufacturerDataMatches(ScanFilter scanFilter, BleDevice device) {
|
||||
final manufacturerDataFilters = scanFilter.withManufacturerData;
|
||||
if (manufacturerDataFilters.isEmpty) return true;
|
||||
bool manufacturerDataMatches(
|
||||
List<ManufacturerDataFilter> filters,
|
||||
BleDevice device,
|
||||
) {
|
||||
if (filters.isEmpty) return true;
|
||||
|
||||
List<ManufacturerData> deviceDataList = device.manufacturerDataList;
|
||||
final deviceDataList = device.manufacturerDataList;
|
||||
if (deviceDataList.isEmpty) return false;
|
||||
|
||||
return deviceDataList
|
||||
.any((deviceData) => manufacturerDataFilters.any((filter) {
|
||||
// Early return if company identifiers don't match
|
||||
if (filter.companyIdentifier != deviceData.companyId) {
|
||||
return false;
|
||||
}
|
||||
return deviceDataList.any((deviceData) {
|
||||
return filters.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;
|
||||
final payloadPrefix = filter.payloadPrefix;
|
||||
final payload = deviceData.payload;
|
||||
|
||||
// Handle cases where payload prefix is null or empty
|
||||
if (payloadPrefix == null || payloadPrefix.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
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);
|
||||
}));
|
||||
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 _compareWithMask(Uint8List prefix, Uint8List payload, Uint8List mask) {
|
||||
|
||||
Reference in New Issue
Block a user