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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user