Skip to content

Commit c56b670

Browse files
steinerjakobJakob Steiner
andauthored
feat(all): support to pass manufacturerData to the scan filters (#752)
feat(all): support to pass manufacturerData to the scan filters --------- Co-authored-by: Jakob Steiner <jakob.steiner@microtronics.com>
1 parent 06eab26 commit c56b670

5 files changed

Lines changed: 177 additions & 17 deletions

File tree

android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,8 @@ class BluetoothLe : Plugin() {
368368
displayStrings = displayStrings!!,
369369
showDialog = false,
370370
)
371-
deviceScanner?.startScanning(scanFilters,
371+
deviceScanner?.startScanning(
372+
scanFilters,
372373
scanSettings,
373374
allowDuplicates,
374375
namePrefix,
@@ -808,8 +809,11 @@ class BluetoothLe : Plugin() {
808809
val filters: ArrayList<ScanFilter> = ArrayList()
809810

810811
val services = (call.getArray("services", JSArray()) as JSArray).toList<String>()
812+
val manufacturerDataArray = call.getArray("manufacturerData", JSArray())
811813
val name = call.getString("name", null)
814+
812815
try {
816+
// Create filters based on services
813817
for (service in services) {
814818
val filter = ScanFilter.Builder()
815819
filter.setServiceUuid(ParcelUuid.fromString(service))
@@ -818,18 +822,66 @@ class BluetoothLe : Plugin() {
818822
}
819823
filters.add(filter.build())
820824
}
825+
826+
// Manufacturer Data Handling (with optional parameters)
827+
manufacturerDataArray?.let {
828+
for (i in 0 until it.length()) {
829+
val manufacturerDataObject = it.getJSONObject(i)
830+
831+
val companyIdentifier = manufacturerDataObject.getInt("companyIdentifier")
832+
833+
val dataPrefix = if (manufacturerDataObject.has("dataPrefix")) {
834+
val dataPrefixObject = manufacturerDataObject.getJSONObject("dataPrefix")
835+
val byteLength = dataPrefixObject.length()
836+
837+
ByteArray(byteLength).apply {
838+
for (idx in 0 until byteLength) {
839+
val key = idx.toString()
840+
this[idx] = (dataPrefixObject.getInt(key) and 0xFF).toByte()
841+
}
842+
}
843+
} else null
844+
845+
846+
val mask = if (manufacturerDataObject.has("mask")) {
847+
val maskObject = manufacturerDataObject.getJSONObject("mask")
848+
val byteLength = maskObject.length()
849+
850+
ByteArray(byteLength).apply {
851+
for (idx in 0 until byteLength) {
852+
val key = idx.toString()
853+
this[idx] = (maskObject.getInt(key) and 0xFF).toByte()
854+
}
855+
}
856+
} else null
857+
858+
val filterBuilder = ScanFilter.Builder()
859+
860+
if (dataPrefix != null && mask != null) {
861+
filterBuilder.setManufacturerData(companyIdentifier, dataPrefix, mask)
862+
} else if (dataPrefix != null) {
863+
filterBuilder.setManufacturerData(companyIdentifier, dataPrefix)
864+
} else {
865+
// Android requires at least dataPrefix for manufacturer filters.
866+
call.reject("dataPrefix is required when specifying manufacturerData.")
867+
return null
868+
}
869+
870+
if (name != null) {
871+
filterBuilder.setDeviceName(name)
872+
}
873+
874+
filters.add(filterBuilder.build())
875+
}
876+
}
877+
return filters;
821878
} catch (e: IllegalArgumentException) {
822-
call.reject("Invalid service UUID.")
879+
call.reject("Invalid UUID or Manufacturer data provided.")
880+
return null
881+
} catch (e: Exception) {
882+
call.reject("Invalid or malformed filter data provided.")
823883
return null
824884
}
825-
826-
if (name != null && filters.isEmpty()) {
827-
val filter = ScanFilter.Builder()
828-
filter.setDeviceName(name)
829-
filters.add(filter.build())
830-
}
831-
832-
return filters
833885
}
834886

835887
private fun getScanSettings(call: PluginCall): ScanSettings? {

ios/Plugin/DeviceManager.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
2020
private var deviceNamePrefixFilter: String?
2121
private var shouldShowDeviceList = false
2222
private var allowDuplicates = false
23+
private var manufacturerDataFilters: [ManufacturerDataFilter]?
2324

2425
init(_ viewController: UIViewController?, _ displayStrings: [String: String], _ callback: @escaping Callback) {
2526
super.init()
@@ -79,6 +80,7 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
7980
_ serviceUUIDs: [CBUUID],
8081
_ name: String?,
8182
_ namePrefix: String?,
83+
_ manufacturerDataFilters: [ManufacturerDataFilter]?,
8284
_ allowDuplicates: Bool,
8385
_ shouldShowDeviceList: Bool,
8486
_ scanDuration: Double?,
@@ -94,6 +96,7 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
9496
self.allowDuplicates = allowDuplicates
9597
self.deviceNameFilter = name
9698
self.deviceNamePrefixFilter = namePrefix
99+
self.manufacturerDataFilters = manufacturerDataFilters
97100

98101
if shouldShowDeviceList {
99102
self.showDeviceList()
@@ -152,6 +155,7 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
152155

153156
guard self.passesNameFilter(peripheralName: peripheral.name) else { return }
154157
guard self.passesNamePrefixFilter(peripheralName: peripheral.name) else { return }
158+
guard self.passesManufacturerDataFilter(advertisementData) else { return }
155159

156160
let device: Device
157161
if self.allowDuplicates, let knownDevice = discoveredDevices.first(where: { $0.key == peripheral.identifier.uuidString })?.value {
@@ -296,6 +300,54 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
296300
return name.hasPrefix(prefix)
297301
}
298302

303+
private func passesManufacturerDataFilter(_ advertisementData: [String: Any]) -> Bool {
304+
guard let filters = self.manufacturerDataFilters, !filters.isEmpty else {
305+
return true // No filters means everything passes
306+
}
307+
308+
guard let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
309+
manufacturerData.count >= 2 else {
310+
return false // If there's no valid manufacturer data, fail
311+
}
312+
313+
let companyIdentifier = manufacturerData.prefix(2).withUnsafeBytes {
314+
$0.load(as: UInt16.self).littleEndian // Manufacturer ID is little-endian
315+
}
316+
317+
let payload = manufacturerData.dropFirst(2)
318+
319+
for filter in filters {
320+
if filter.companyIdentifier != companyIdentifier {
321+
continue // Skip if company ID does not match
322+
}
323+
324+
if let dataPrefix = filter.dataPrefix {
325+
if payload.count < dataPrefix.count {
326+
continue // Payload too short, does not match
327+
}
328+
329+
if let mask = filter.mask {
330+
var matches = true
331+
for i in 0..<dataPrefix.count {
332+
if (payload[i] & mask[i]) != (dataPrefix[i] & mask[i]) {
333+
matches = false
334+
break
335+
}
336+
}
337+
if matches {
338+
return true
339+
}
340+
} else if payload.starts(with: dataPrefix) {
341+
return true
342+
}
343+
} else {
344+
return true // Company ID matched, and no dataPrefix required
345+
}
346+
}
347+
348+
return false // If none matched, return false
349+
}
350+
299351
private func resolve(_ key: String, _ value: String) {
300352
let callback = self.callbackMap[key]
301353
if callback != nil {

ios/Plugin/Plugin.swift

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import CoreBluetooth
77
let CONNECTION_TIMEOUT: Double = 10
88
let DEFAULT_TIMEOUT: Double = 5
99

10+
struct ManufacturerDataFilter {
11+
let companyIdentifier: UInt16
12+
let dataPrefix: Data?
13+
let mask: Data?
14+
}
15+
1016
@objc(BluetoothLe)
1117
public class BluetoothLe: CAPPlugin {
1218
typealias BleDevice = [String: Any]
@@ -110,15 +116,17 @@ public class BluetoothLe: CAPPlugin {
110116
let serviceUUIDs = self.getServiceUUIDs(call)
111117
let name = call.getString("name")
112118
let namePrefix = call.getString("namePrefix")
119+
let manufacturerDataFilters = self.getManufacturerDataFilters(call)
113120

114121
deviceManager.startScanning(
115122
serviceUUIDs,
116123
name,
117124
namePrefix,
125+
manufacturerDataFilters,
118126
false,
119127
true,
120-
30, {(success, message) in
121-
// selected a device
128+
30,
129+
{(success, message) in
122130
if success {
123131
guard let device = deviceManager.getDevice(message) else {
124132
call.reject("Device not found.")
@@ -130,9 +138,8 @@ public class BluetoothLe: CAPPlugin {
130138
} else {
131139
call.reject(message)
132140
}
133-
}, {(_, _, _) in
134-
135-
}
141+
},
142+
{ (_, _, _) in }
136143
)
137144
}
138145

@@ -143,20 +150,23 @@ public class BluetoothLe: CAPPlugin {
143150
let name = call.getString("name")
144151
let namePrefix = call.getString("namePrefix")
145152
let allowDuplicates = call.getBool("allowDuplicates", false)
153+
let manufacturerDataFilters = self.getManufacturerDataFilters(call)
146154

147155
deviceManager.startScanning(
148156
serviceUUIDs,
149157
name,
150158
namePrefix,
159+
manufacturerDataFilters,
151160
allowDuplicates,
152161
false,
153-
nil, {(success, message) in
162+
nil,
163+
{ (success, message) in
154164
if success {
155165
call.resolve()
156166
} else {
157167
call.reject(message)
158168
}
159-
}, {(device, advertisementData, rssi) in
169+
}, { (device, advertisementData, rssi) in
160170
self.deviceMap[device.getId()] = device
161171
let data = self.getScanResult(device, advertisementData, rssi)
162172
self.notifyListeners("onScanResult", data: data)
@@ -521,6 +531,42 @@ public class BluetoothLe: CAPPlugin {
521531
return serviceUUIDs
522532
}
523533

534+
private func getManufacturerDataFilters(_ call: CAPPluginCall) -> [ManufacturerDataFilter]? {
535+
guard let manufacturerDataArray = call.getArray("manufacturerData") else {
536+
return nil
537+
}
538+
539+
var manufacturerDataFilters: [ManufacturerDataFilter] = []
540+
541+
for index in 0..<manufacturerDataArray.count {
542+
guard let dataObject = manufacturerDataArray[index] as? JSObject,
543+
let companyIdentifier = dataObject["companyIdentifier"] as? UInt16 else {
544+
// Invalid or missing company identifier
545+
return nil
546+
}
547+
548+
let dataPrefix: Data? = {
549+
guard let prefixArray = dataObject["dataPrefix"] as? [Int] else { return nil }
550+
return Data(prefixArray.map { UInt8($0 & 0xFF) })
551+
}()
552+
553+
let mask: Data? = {
554+
guard let maskArray = dataObject["mask"] as? [Int] else { return nil }
555+
return Data(maskArray.map { UInt8($0 & 0xFF) })
556+
}()
557+
558+
let manufacturerFilter = ManufacturerDataFilter(
559+
companyIdentifier: companyIdentifier,
560+
dataPrefix: dataPrefix,
561+
mask: mask
562+
)
563+
564+
manufacturerDataFilters.append(manufacturerFilter)
565+
}
566+
567+
return manufacturerDataFilters
568+
}
569+
524570
private func getDevice(_ call: CAPPluginCall, checkConnection: Bool = true) -> Device? {
525571
guard let deviceId = call.getString("deviceId") else {
526572
call.reject("deviceId required.")

src/definitions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export interface RequestBleDeviceOptions {
4646
* Android scan mode (default: ScanMode.SCAN_MODE_BALANCED)
4747
*/
4848
scanMode?: ScanMode;
49+
/**
50+
* Allow scanning for devices with a specific manufacturer data
51+
* https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#manufacturerdata
52+
*/
53+
manufacturerData?: { companyIdentifier: number; dataPrefix?: Uint8Array; mask?: Uint8Array }[];
4954
}
5055

5156
/**

src/web.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,11 @@ export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin {
376376
namePrefix: options.namePrefix,
377377
});
378378
}
379+
for (const manufacturerData of options?.manufacturerData ?? []) {
380+
filters.push({
381+
manufacturerData: [manufacturerData],
382+
});
383+
}
379384
return filters;
380385
}
381386

0 commit comments

Comments
 (0)