Improve scan filter api (#78)

* Improve scan filter api

* Implement Windows filters

* Add manufacturerDataList and deprecate manufacturerData

* Fix Windows

* Update Readme and changelog

* Remove import

* Fix Linux

* Update readme and changelog

* Cleanup Todo

* Improve ManufacturerData

* Rename webConfig and Manufacturer.data

* Return false for receivesAdvertisements on Linux/Web

* Add delay in discoverServices on Linux

* Remove import

* Update changelog

---------

Co-authored-by: fotidim <foti@navideck.com>
This commit is contained in:
Rohit Sangwan
2024-09-17 03:13:51 +05:30
committed by GitHub
parent 639384af30
commit 7dc22c58a5
38 changed files with 1516 additions and 850 deletions
+40 -12
View File
@@ -74,8 +74,7 @@ struct UniversalBleScanResult {
var name: String? = nil
var isPaired: Bool? = nil
var rssi: Int64? = nil
var manufacturerData: FlutterStandardTypedData? = nil
var manufacturerDataHead: FlutterStandardTypedData? = nil
var manufacturerDataList: [UniversalManufacturerData?]? = nil
var services: [String?]? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
@@ -84,17 +83,15 @@ struct UniversalBleScanResult {
let name: String? = nilOrValue(__pigeon_list[1])
let isPaired: Bool? = nilOrValue(__pigeon_list[2])
let rssi: Int64? = isNullish(__pigeon_list[3]) ? nil : (__pigeon_list[3] is Int64? ? __pigeon_list[3] as! Int64? : Int64(__pigeon_list[3] as! Int32))
let manufacturerData: FlutterStandardTypedData? = nilOrValue(__pigeon_list[4])
let manufacturerDataHead: FlutterStandardTypedData? = nilOrValue(__pigeon_list[5])
let services: [String?]? = nilOrValue(__pigeon_list[6])
let manufacturerDataList: [UniversalManufacturerData?]? = nilOrValue(__pigeon_list[4])
let services: [String?]? = nilOrValue(__pigeon_list[5])
return UniversalBleScanResult(
deviceId: deviceId,
name: name,
isPaired: isPaired,
rssi: rssi,
manufacturerData: manufacturerData,
manufacturerDataHead: manufacturerDataHead,
manufacturerDataList: manufacturerDataList,
services: services
)
}
@@ -104,8 +101,7 @@ struct UniversalBleScanResult {
name,
isPaired,
rssi,
manufacturerData,
manufacturerDataHead,
manufacturerDataList,
services,
]
}
@@ -162,21 +158,25 @@ struct UniversalBleCharacteristic {
/// Generated class from Pigeon that represents data sent in messages.
struct UniversalScanFilter {
var withServices: [String?]
var withNamePrefix: [String?]
var withManufacturerData: [UniversalManufacturerDataFilter?]
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ __pigeon_list: [Any?]) -> UniversalScanFilter? {
let withServices = __pigeon_list[0] as! [String?]
let withManufacturerData = __pigeon_list[1] as! [UniversalManufacturerDataFilter?]
let withNamePrefix = __pigeon_list[1] as! [String?]
let withManufacturerData = __pigeon_list[2] as! [UniversalManufacturerDataFilter?]
return UniversalScanFilter(
withServices: withServices,
withNamePrefix: withNamePrefix,
withManufacturerData: withManufacturerData
)
}
func toList() -> [Any?] {
return [
withServices,
withNamePrefix,
withManufacturerData,
]
}
@@ -184,13 +184,13 @@ struct UniversalScanFilter {
/// Generated class from Pigeon that represents data sent in messages.
struct UniversalManufacturerDataFilter {
var companyIdentifier: Int64? = nil
var companyIdentifier: Int64
var data: FlutterStandardTypedData? = nil
var mask: FlutterStandardTypedData? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ __pigeon_list: [Any?]) -> UniversalManufacturerDataFilter? {
let companyIdentifier: Int64? = isNullish(__pigeon_list[0]) ? nil : (__pigeon_list[0] is Int64? ? __pigeon_list[0] as! Int64? : Int64(__pigeon_list[0] as! Int32))
let companyIdentifier = __pigeon_list[0] is Int64 ? __pigeon_list[0] as! Int64 : Int64(__pigeon_list[0] as! Int32)
let data: FlutterStandardTypedData? = nilOrValue(__pigeon_list[1])
let mask: FlutterStandardTypedData? = nilOrValue(__pigeon_list[2])
@@ -208,6 +208,29 @@ struct UniversalManufacturerDataFilter {
]
}
}
/// Generated class from Pigeon that represents data sent in messages.
struct UniversalManufacturerData {
var companyIdentifier: Int64
var data: FlutterStandardTypedData
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ __pigeon_list: [Any?]) -> UniversalManufacturerData? {
let companyIdentifier = __pigeon_list[0] is Int64 ? __pigeon_list[0] as! Int64 : Int64(__pigeon_list[0] as! Int32)
let data = __pigeon_list[1] as! FlutterStandardTypedData
return UniversalManufacturerData(
companyIdentifier: companyIdentifier,
data: data
)
}
func toList() -> [Any?] {
return [
companyIdentifier,
data,
]
}
}
private class UniversalBlePigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
@@ -221,6 +244,8 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader {
return UniversalScanFilter.fromList(self.readValue() as! [Any?])
case 133:
return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?])
case 134:
return UniversalManufacturerData.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
@@ -244,6 +269,9 @@ private class UniversalBlePigeonCodecWriter: FlutterStandardWriter {
} else if let value = value as? UniversalManufacturerDataFilter {
super.writeByte(133)
super.writeValue(value.toList())
} else if let value = value as? UniversalManufacturerData {
super.writeByte(134)
super.writeValue(value.toList())
} else {
super.writeValue(value)
}
+107
View File
@@ -0,0 +1,107 @@
//
// UniversalBleFilterUtil.swift
// universal_ble
//
// Created by Rohit Sangwan on 23/08/24.
//
import CoreBluetooth
import Foundation
public class UniversalBleFilterUtil {
var scanFilter: UniversalScanFilter?
var scanFilterServicesUUID: [CBUUID] = []
func filterDevice(name: String?, manufacturerData: UniversalManufacturerData?, services: [CBUUID]?) -> Bool {
guard let filter = scanFilter else {
return true
}
let hasNamePrefixFilter = !filter.withNamePrefix.isEmpty
let hasServiceFilter = !filter.withServices.isEmpty
let hasManufacturerDataFilter = !filter.withManufacturerData.isEmpty
// If there is no filter at all, then allow device
if !hasNamePrefixFilter && !hasServiceFilter && !hasManufacturerDataFilter {
return true
}
// Else check one of the filter passes
return hasNamePrefixFilter && isNameMatchingFilters(filter: filter, name: name) ||
hasServiceFilter && isServicesMatchingFilters(services: services) ||
hasManufacturerDataFilter && isManufacturerDataMatchingFilters(scanFilter: filter, msd: manufacturerData)
}
func isNameMatchingFilters(filter: UniversalScanFilter, name: String?) -> Bool {
let prefixFilters = filter.withNamePrefix.compactMap { $0 }.filter { !$0.isEmpty }
guard !prefixFilters.isEmpty else {
return true
}
guard let name = name, !name.isEmpty else {
return false
}
return prefixFilters.contains { name.hasPrefix($0) }
}
func isServicesMatchingFilters(services: [CBUUID]?) -> Bool {
let serviceFilters = Set(scanFilterServicesUUID.compactMap { $0 })
guard !serviceFilters.isEmpty else {
return true
}
guard let services = services, !services.isEmpty else {
return false
}
return !Set(services).isDisjoint(with: serviceFilters)
}
func isManufacturerDataMatchingFilters(scanFilter: UniversalScanFilter, msd: UniversalManufacturerData?) -> Bool {
let filters = scanFilter.withManufacturerData.compactMap { $0 }
if filters.isEmpty {
return true
}
guard let msd = msd else {
return false
}
for filter in filters {
let companyIdentifier: Int64 = filter.companyIdentifier
if msd.companyIdentifier == companyIdentifier && findData(find: filter.data?.toData(), inData: msd.data.toData(), 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
}
}
extension UniversalScanFilter {
var hasCustomFilters: Bool {
return !withManufacturerData.isEmpty || !withNamePrefix.isEmpty
}
}
+1 -38
View File
@@ -150,7 +150,7 @@ extension String {
let baseUuid = "00000000-0000-1000-8000-00805F9B34FB"
let start = baseUuid.startIndex
let range = baseUuid.index(start, offsetBy: 4 - uuidLength) ..< baseUuid.index(start, offsetBy: 4)
return baseUuid.replacingCharacters(in: range, with: self)
return baseUuid.replacingCharacters(in: range, with: self).lowercased()
} else {
return self
}
@@ -163,43 +163,6 @@ extension FlutterStandardTypedData {
}
}
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
+47 -25
View File
@@ -26,8 +26,8 @@ private var discoveredPeripherals = [String: CBPeripheral]()
private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentralManagerDelegate, CBPeripheralDelegate {
var callbackChannel: UniversalBleCallbackChannel
private var universalBleFilterUtil = UniversalBleFilterUtil()
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]()
@@ -59,19 +59,21 @@ 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 {
throw FlutterError(code: "IllegalArgument", message: "Invalid service UUID:\(service)", details: nil)
}
withServices.append(CBUUID(string: service))
}
}
// If filter have any other filter other then official one
let hasCustomFilter = filter?.hasCustomFilters ?? false
// Save scanFilter for later user
scanFilter = filter
// Apply services filter
var withServices: [CBUUID] = try filter?.withServices.compactMap { $0 }.toCBUUID() ?? []
if hasCustomFilter {
print("Using Custom Filters")
universalBleFilterUtil.scanFilter = filter
universalBleFilterUtil.scanFilterServicesUUID = withServices
withServices = []
} else {
universalBleFilterUtil.scanFilter = nil
universalBleFilterUtil.scanFilterServicesUUID = []
}
let options = [CBCentralManagerScanOptionAllowDuplicatesKey: true]
manager.scanForPeripherals(withServices: withServices, options: options)
@@ -280,7 +282,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
completion(Result.failure(FlutterError(code: "NotSupported", message: nil, details: nil)))
}
func pair(deviceId _: String, completion: @escaping (Result<Bool, Error>) -> Void){
func pair(deviceId _: String, completion: @escaping (Result<Bool, Error>) -> Void) {
completion(Result.failure(FlutterError(code: "Implemented in Dart", message: nil, details: nil)))
}
@@ -310,17 +312,26 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
}
public func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
// Store the discovered peripheral using its UUID as the key
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
}
// Extract manufacturer data and service UUIDs from the advertisement data
let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
let services = (advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID])
var manufacturerDataList: [UniversalManufacturerData] = []
var universalManufacturerData: UniversalManufacturerData? = nil
if let msd = manufacturerData, msd.count > 2 {
let companyIdentifier = msd.prefix(2).withUnsafeBytes { $0.load(as: UInt16.self) }
let data = FlutterStandardTypedData(bytes: msd.suffix(from: 2))
universalManufacturerData = UniversalManufacturerData(companyIdentifier: Int64(companyIdentifier), data: data)
manufacturerDataList.append(universalManufacturerData!)
}
// Apply custom filters and return early if the peripheral doesn't match
if !universalBleFilterUtil.filterDevice(name: peripheral.name, manufacturerData: universalManufacturerData, services: services) {
return
}
callbackChannel.onScanResult(result: UniversalBleScanResult(
@@ -328,8 +339,8 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
name: peripheral.name,
isPaired: nil,
rssi: RSSI as? Int64,
manufacturerData: FlutterStandardTypedData(bytes: manufacturerData ?? Data()),
services: services?.map { $0.uuidStr }
manufacturerDataList: manufacturerDataList,
services: services?.map { $0.uuidStr.validFullUUID }
)) { _ in }
}
@@ -451,4 +462,15 @@ extension String {
}
}
extension [String] {
func toCBUUID() throws -> [CBUUID] {
return try compactMap { serviceUUID in
guard UUID(uuidString: serviceUUID.validFullUUID) != nil else {
throw FlutterError(code: "IllegalArgument", message: "Invalid service UUID:\(serviceUUID)", details: nil)
}
return CBUUID(string: serviceUUID)
}
}
}
extension FlutterError: Error {}