@capacitor-community/bluetooth-le 7.3.0 → 8.0.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CapacitorCommunityBluetoothLe.podspec +17 -17
  2. package/LICENSE +21 -21
  3. package/Package.swift +28 -0
  4. package/android/build.gradle +71 -68
  5. package/android/src/main/AndroidManifest.xml +22 -22
  6. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +1094 -1094
  7. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Conversion.kt +51 -51
  8. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +771 -771
  9. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt +28 -28
  10. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceScanner.kt +189 -189
  11. package/dist/esm/bleClient.d.ts +278 -278
  12. package/dist/esm/bleClient.js +361 -361
  13. package/dist/esm/bleClient.js.map +1 -1
  14. package/dist/esm/config.d.ts +53 -53
  15. package/dist/esm/config.js +2 -2
  16. package/dist/esm/conversion.d.ts +56 -46
  17. package/dist/esm/conversion.js +134 -117
  18. package/dist/esm/conversion.js.map +1 -1
  19. package/dist/esm/definitions.d.ts +352 -352
  20. package/dist/esm/definitions.js +42 -42
  21. package/dist/esm/index.d.ts +5 -5
  22. package/dist/esm/index.js +5 -5
  23. package/dist/esm/plugin.d.ts +2 -2
  24. package/dist/esm/plugin.js +4 -4
  25. package/dist/esm/queue.d.ts +3 -3
  26. package/dist/esm/queue.js +17 -17
  27. package/dist/esm/queue.js.map +1 -1
  28. package/dist/esm/timeout.d.ts +1 -1
  29. package/dist/esm/timeout.js +9 -9
  30. package/dist/esm/validators.d.ts +1 -1
  31. package/dist/esm/validators.js +11 -11
  32. package/dist/esm/validators.js.map +1 -1
  33. package/dist/esm/web.d.ts +57 -57
  34. package/dist/esm/web.js +403 -403
  35. package/dist/esm/web.js.map +1 -1
  36. package/dist/plugin.cjs.js +965 -947
  37. package/dist/plugin.cjs.js.map +1 -1
  38. package/dist/plugin.js +965 -947
  39. package/dist/plugin.js.map +1 -1
  40. package/ios/{Plugin → Sources/BluetoothLe}/Conversion.swift +83 -83
  41. package/ios/{Plugin → Sources/BluetoothLe}/Device.swift +423 -423
  42. package/ios/{Plugin → Sources/BluetoothLe}/DeviceListView.swift +121 -121
  43. package/ios/{Plugin → Sources/BluetoothLe}/DeviceManager.swift +503 -502
  44. package/ios/{Plugin → Sources/BluetoothLe}/Logging.swift +8 -8
  45. package/ios/{Plugin → Sources/BluetoothLe}/Plugin.swift +775 -737
  46. package/ios/{Plugin → Sources/BluetoothLe}/ThreadSafeDictionary.swift +15 -13
  47. package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -0
  48. package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -0
  49. package/package.json +115 -111
  50. package/ios/Plugin/Info.plist +0 -24
  51. package/ios/Plugin/Plugin.h +0 -10
  52. package/ios/Plugin/Plugin.m +0 -41
@@ -1,502 +1,503 @@
1
- import Foundation
2
- import CoreBluetooth
3
-
4
- enum DeviceListMode {
5
- case none
6
- case alert
7
- case list
8
- }
9
-
10
- class DeviceManager: NSObject, CBCentralManagerDelegate {
11
- typealias Callback = (_ success: Bool, _ message: String) -> Void
12
- typealias StateReceiver = (_ enabled: Bool) -> Void
13
- typealias ScanResultCallback = (_ device: Device, _ advertisementData: [String: Any], _ rssi: NSNumber) -> Void
14
-
15
- private var centralManager: CBCentralManager!
16
- private var viewController: UIViewController?
17
- private var displayStrings: [String: String]!
18
- private var callbackMap = [String: Callback]()
19
- private var scanResultCallback: ScanResultCallback?
20
- private var stateReceiver: StateReceiver?
21
- private var timeoutMap = [String: DispatchWorkItem]()
22
- private var stopScanWorkItem: DispatchWorkItem?
23
- private var alertController: UIAlertController?
24
- private var deviceListView: DeviceListView?
25
- private var popoverController: UIPopoverPresentationController?
26
- private var discoveredDevices = [String: Device]()
27
- private var deviceNameFilter: String?
28
- private var deviceNamePrefixFilter: String?
29
- private var deviceListMode: DeviceListMode = .none
30
- private var allowDuplicates = false
31
- private var manufacturerDataFilters: [ManufacturerDataFilter]?
32
- private var serviceDataFilters: [ServiceDataFilter]?
33
-
34
- init(_ viewController: UIViewController?, _ displayStrings: [String: String], _ callback: @escaping Callback) {
35
- super.init()
36
- self.viewController = viewController
37
- self.displayStrings = displayStrings
38
- self.callbackMap["initialize"] = callback
39
- self.centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main)
40
- }
41
-
42
- func setDisplayStrings(_ displayStrings: [String: String]) {
43
- self.displayStrings = displayStrings
44
- }
45
-
46
- // initialize
47
- func centralManagerDidUpdateState(_ central: CBCentralManager) {
48
- let initializeKey = "initialize"
49
- switch central.state {
50
- case .poweredOn:
51
- self.resolve(initializeKey, "BLE powered on")
52
- self.emitState(enabled: true)
53
- case .poweredOff:
54
- self.stopScan()
55
- self.resolve(initializeKey, "BLE powered off")
56
- self.emitState(enabled: false)
57
- case .resetting:
58
- self.emitState(enabled: false)
59
- case .unauthorized:
60
- self.reject(initializeKey, "BLE permission denied")
61
- self.emitState(enabled: false)
62
- case .unsupported:
63
- self.reject(initializeKey, "BLE unsupported")
64
- self.emitState(enabled: false)
65
- case .unknown:
66
- self.emitState(enabled: false)
67
- default: break
68
- }
69
- }
70
-
71
- func isEnabled() -> Bool {
72
- return self.centralManager.state == CBManagerState.poweredOn
73
- }
74
-
75
- func registerStateReceiver( _ stateReceiver: @escaping StateReceiver) {
76
- self.stateReceiver = stateReceiver
77
- }
78
-
79
- func unregisterStateReceiver() {
80
- self.stateReceiver = nil
81
- }
82
-
83
- func emitState(enabled: Bool) {
84
- guard let stateReceiver = self.stateReceiver else { return }
85
- stateReceiver(enabled)
86
- }
87
-
88
- func startScanning(
89
- _ serviceUUIDs: [CBUUID],
90
- _ name: String?,
91
- _ namePrefix: String?,
92
- _ manufacturerDataFilters: [ManufacturerDataFilter]?,
93
- _ serviceDataFilters: [ServiceDataFilter]?,
94
- _ allowDuplicates: Bool,
95
- _ deviceListMode: DeviceListMode,
96
- _ scanDuration: Double?,
97
- _ callback: @escaping Callback,
98
- _ scanResultCallback: @escaping ScanResultCallback
99
- ) {
100
- self.callbackMap["startScanning"] = callback
101
- self.scanResultCallback = scanResultCallback
102
-
103
- if self.centralManager.isScanning == false {
104
- self.discoveredDevices = [String: Device]()
105
- self.deviceListMode = deviceListMode
106
- self.allowDuplicates = allowDuplicates
107
- self.deviceNameFilter = name
108
- self.deviceNamePrefixFilter = namePrefix
109
- self.manufacturerDataFilters = manufacturerDataFilters
110
- self.serviceDataFilters = serviceDataFilters
111
-
112
- if deviceListMode != .none {
113
- self.showDeviceList()
114
- }
115
-
116
- if scanDuration != nil {
117
- self.stopScanWorkItem = DispatchWorkItem {
118
- self.stopScan()
119
- }
120
- DispatchQueue.main.asyncAfter(deadline: .now() + scanDuration!, execute: self.stopScanWorkItem!)
121
- }
122
- self.centralManager.scanForPeripherals(
123
- withServices: serviceUUIDs,
124
- options: [CBCentralManagerScanOptionAllowDuplicatesKey: allowDuplicates]
125
- )
126
-
127
- if deviceListMode == .none {
128
- self.resolve("startScanning", "Scan started.")
129
- }
130
- } else {
131
- self.stopScan()
132
- self.reject("startScanning", "Already scanning. Stopping now.")
133
- }
134
- }
135
-
136
- func stopScan() {
137
- log("Stop scanning.")
138
- self.centralManager.stopScan()
139
- self.stopScanWorkItem?.cancel()
140
- self.stopScanWorkItem = nil
141
- DispatchQueue.main.async { [weak self] in
142
- guard let self = self else { return }
143
- switch self.deviceListMode {
144
- case .alert:
145
- if self.discoveredDevices.count == 0 {
146
- self.alertController?.title = self.displayStrings["noDeviceFound"]
147
- } else {
148
- self.alertController?.title = self.displayStrings["availableDevices"]
149
- }
150
- case .list:
151
- if self.discoveredDevices.count == 0 {
152
- self.deviceListView?.setTitle(self.displayStrings["noDeviceFound"])
153
- } else {
154
- self.deviceListView?.setTitle(self.displayStrings["availableDevices"])
155
- }
156
- case .none:
157
- break
158
- }
159
- }
160
- }
161
-
162
- // didDiscover
163
- func centralManager(
164
- _ central: CBCentralManager,
165
- didDiscover peripheral: CBPeripheral,
166
- advertisementData: [String: Any],
167
- rssi RSSI: NSNumber
168
- ) {
169
-
170
- guard peripheral.state != CBPeripheralState.connected else {
171
- log("found connected device", peripheral.name ?? "Unknown")
172
- // make sure we do not touch connected devices
173
- return
174
- }
175
-
176
- let isNew = self.discoveredDevices[peripheral.identifier.uuidString] == nil
177
- guard isNew || self.allowDuplicates else { return }
178
-
179
- guard self.passesNameFilter(peripheralName: peripheral.name) else { return }
180
- guard self.passesNamePrefixFilter(peripheralName: peripheral.name) else { return }
181
- guard self.passesManufacturerDataFilter(advertisementData) else { return }
182
- guard self.passesServiceDataFilter(advertisementData) else { return }
183
-
184
- let device: Device
185
- if self.allowDuplicates, let knownDevice = discoveredDevices.first(where: { $0.key == peripheral.identifier.uuidString })?.value {
186
- device = knownDevice
187
- } else {
188
- device = Device(peripheral)
189
- self.discoveredDevices[device.getId()] = device
190
- }
191
- log("New device found: ", device.getName() ?? "Unknown")
192
-
193
- switch deviceListMode {
194
- case .none:
195
- if self.scanResultCallback != nil {
196
- self.scanResultCallback!(device, advertisementData, RSSI)
197
- }
198
- case .alert:
199
- DispatchQueue.main.async { [weak self] in
200
- self?.alertController?.addAction(UIAlertAction(title: device.getName() ?? "Unknown", style: UIAlertAction.Style.default, handler: { (_) in
201
- log("Selected device")
202
- self?.stopScan()
203
- self?.resolve("startScanning", device.getId())
204
- }))
205
- }
206
- case .list:
207
- DispatchQueue.main.async { [weak self] in
208
- self?.deviceListView?.addItem(device.getName() ?? "Unknown", action: {
209
- log("Selected device")
210
- self?.stopScan()
211
- self?.resolve("startScanning", device.getId())
212
- })
213
- }
214
- }
215
- }
216
-
217
- func showDeviceList() {
218
- switch deviceListMode {
219
- case .none:
220
- break
221
- case .alert:
222
- showDeviceListAlert()
223
- case .list:
224
- showDeviceListView()
225
- }
226
- }
227
-
228
- func showDeviceListAlert() {
229
- DispatchQueue.main.async { [weak self] in
230
- self?.alertController = UIAlertController(title: self?.displayStrings["scanning"], message: nil, preferredStyle: UIAlertController.Style.alert)
231
- self?.alertController?.addAction(UIAlertAction(title: self?.displayStrings["cancel"], style: UIAlertAction.Style.cancel, handler: { (_) in
232
- log("Cancelled request device.")
233
- self?.stopScan()
234
- self?.reject("startScanning", "requestDevice cancelled.")
235
- }))
236
- self?.viewController?.present((self?.alertController)!, animated: true, completion: nil)
237
- }
238
- }
239
-
240
- func showDeviceListView() {
241
- DispatchQueue.main.async { [weak self] in
242
- self?.deviceListView = DeviceListView()
243
- if #available(macCatalyst 15.0, iOS 15.0, *) {
244
- self?.deviceListView?.sheetPresentationController?.detents = [.medium()]
245
- }
246
- self?.viewController?.present((self?.deviceListView)!, animated: true, completion: nil)
247
- self?.deviceListView?.setTitle(self?.displayStrings["scanning"])
248
- self?.deviceListView?.setCancelButton(self?.displayStrings["cancel"], action: {
249
- log("Cancelled request device.")
250
- self?.stopScan()
251
- self?.reject("startScanning", "requestDevice cancelled.")
252
- })
253
- }
254
- }
255
-
256
- func getDevices(
257
- _ deviceUUIDs: [UUID]
258
- ) -> [CBPeripheral] {
259
- return self.centralManager.retrievePeripherals(withIdentifiers: deviceUUIDs)
260
- }
261
-
262
- func getConnectedDevices(
263
- _ serviceUUIDs: [CBUUID]
264
- ) -> [CBPeripheral] {
265
- return self.centralManager.retrieveConnectedPeripherals(withServices: serviceUUIDs)
266
- }
267
-
268
- func connect(
269
- _ device: Device,
270
- _ connectionTimeout: Double,
271
- _ callback: @escaping Callback
272
- ) {
273
- let key = "connect|\(device.getId())"
274
- self.callbackMap[key] = callback
275
- log("Connecting to peripheral", device.getPeripheral())
276
- self.centralManager.connect(device.getPeripheral(), options: nil)
277
- self.setConnectionTimeout(key, "Connection timeout.", device, connectionTimeout)
278
- }
279
-
280
- // didConnect
281
- func centralManager(
282
- _ central: CBCentralManager,
283
- didConnect peripheral: CBPeripheral
284
- ) {
285
- log("Connected to device", peripheral)
286
- let key = "connect|\(peripheral.identifier.uuidString)"
287
- peripheral.discoverServices(nil)
288
- self.resolve(key, "Successfully connected.")
289
- // will wait for services in plugin call
290
- }
291
-
292
- // didFailToConnect
293
- func centralManager(
294
- _ central: CBCentralManager,
295
- didFailToConnect peripheral: CBPeripheral,
296
- error: Error?
297
- ) {
298
- let key = "connect|\(peripheral.identifier.uuidString)"
299
- if error != nil {
300
- self.reject(key, error!.localizedDescription)
301
- return
302
- }
303
- self.reject(key, "Failed to connect.")
304
- }
305
-
306
- func setOnDisconnected(
307
- _ device: Device,
308
- _ callback: @escaping Callback
309
- ) {
310
- let key = "onDisconnected|\(device.getId())"
311
- self.callbackMap[key] = callback
312
- }
313
-
314
- func disconnect(
315
- _ device: Device,
316
- _ timeout: Double,
317
- _ callback: @escaping Callback
318
- ) {
319
- let key = "disconnect|\(device.getId())"
320
- self.callbackMap[key] = callback
321
- if device.isConnected() == false {
322
- self.resolve(key, "Disconnected.")
323
- return
324
- }
325
- log("Disconnecting from peripheral", device.getPeripheral())
326
- self.centralManager.cancelPeripheralConnection(device.getPeripheral())
327
- self.setTimeout(key, "Disconnection timeout.", timeout)
328
- }
329
-
330
- // didDisconnectPeripheral
331
- func centralManager(
332
- _ central: CBCentralManager,
333
- didDisconnectPeripheral peripheral: CBPeripheral,
334
- error: Error?
335
- ) {
336
- let key = "disconnect|\(peripheral.identifier.uuidString)"
337
- let keyOnDisconnected = "onDisconnected|\(peripheral.identifier.uuidString)"
338
- self.resolve(keyOnDisconnected, "Disconnected.")
339
- if error != nil {
340
- log(error!.localizedDescription)
341
- self.reject(key, error!.localizedDescription)
342
- return
343
- }
344
- self.resolve(key, "Successfully disconnected.")
345
- }
346
-
347
- func getDevice(_ deviceId: String) -> Device? {
348
- return self.discoveredDevices[deviceId]
349
- }
350
-
351
- private func passesNameFilter(peripheralName: String?) -> Bool {
352
- guard let nameFilter = self.deviceNameFilter else { return true }
353
- guard let name = peripheralName else { return false }
354
- return name == nameFilter
355
- }
356
-
357
- private func passesNamePrefixFilter(peripheralName: String?) -> Bool {
358
- guard let prefix = self.deviceNamePrefixFilter else { return true }
359
- guard let name = peripheralName else { return false }
360
- return name.hasPrefix(prefix)
361
- }
362
-
363
- private func passesManufacturerDataFilter(_ advertisementData: [String: Any]) -> Bool {
364
- guard let filters = self.manufacturerDataFilters, !filters.isEmpty else {
365
- return true // No filters means everything passes
366
- }
367
-
368
- guard let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
369
- manufacturerData.count >= 2 else {
370
- return false // If there's no valid manufacturer data, fail
371
- }
372
-
373
- let companyIdentifier = manufacturerData.prefix(2).withUnsafeBytes {
374
- $0.load(as: UInt16.self).littleEndian // Manufacturer ID is little-endian
375
- }
376
-
377
- let payload = manufacturerData.dropFirst(2)
378
-
379
- for filter in filters {
380
- if filter.companyIdentifier != companyIdentifier {
381
- continue // Skip if company ID does not match
382
- }
383
-
384
- if let dataPrefix = filter.dataPrefix {
385
- if payload.count < dataPrefix.count {
386
- continue // Payload too short, does not match
387
- }
388
-
389
- if let mask = filter.mask {
390
- var matches = true
391
- for i in 0..<dataPrefix.count {
392
- if (payload[i] & mask[i]) != (dataPrefix[i] & mask[i]) {
393
- matches = false
394
- break
395
- }
396
- }
397
- if matches {
398
- return true
399
- }
400
- } else if payload.starts(with: dataPrefix) {
401
- return true
402
- }
403
- } else {
404
- return true // Company ID matched, and no dataPrefix required
405
- }
406
- }
407
-
408
- return false // If none matched, return false
409
- }
410
-
411
- private func passesServiceDataFilter(_ advertisementData: [String: Any]) -> Bool {
412
- guard let filters = self.serviceDataFilters, !filters.isEmpty else {
413
- return true // No filters means everything passes
414
- }
415
-
416
- guard let serviceDataDict = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] else {
417
- return false // If there's no service data, fail
418
- }
419
-
420
- for filter in filters {
421
- guard let serviceData = serviceDataDict[filter.serviceUuid] else {
422
- continue // Skip if service UUID does not match
423
- }
424
-
425
- if let dataPrefix = filter.dataPrefix {
426
- if serviceData.count < dataPrefix.count {
427
- continue // Service data too short, does not match
428
- }
429
-
430
- if let mask = filter.mask {
431
- var matches = true
432
- for i in 0..<dataPrefix.count {
433
- if (serviceData[i] & mask[i]) != (dataPrefix[i] & mask[i]) {
434
- matches = false
435
- break
436
- }
437
- }
438
- if matches {
439
- return true
440
- }
441
- } else if serviceData.starts(with: dataPrefix) {
442
- return true
443
- }
444
- } else {
445
- return true // Service UUID matched, and no dataPrefix required
446
- }
447
- }
448
-
449
- return false // If none matched, return false
450
- }
451
-
452
- private func resolve(_ key: String, _ value: String) {
453
- let callback = self.callbackMap[key]
454
- if callback != nil {
455
- log("Resolve", key, value)
456
- callback!(true, value)
457
- self.callbackMap[key] = nil
458
- self.timeoutMap[key]?.cancel()
459
- self.timeoutMap[key] = nil
460
- }
461
- }
462
-
463
- private func reject(_ key: String, _ value: String) {
464
- let callback = self.callbackMap[key]
465
- if callback != nil {
466
- log("Reject", key, value)
467
- callback!(false, value)
468
- self.callbackMap[key] = nil
469
- self.timeoutMap[key]?.cancel()
470
- self.timeoutMap[key] = nil
471
- }
472
- }
473
-
474
- private func setTimeout(
475
- _ key: String,
476
- _ message: String,
477
- _ timeout: Double
478
- ) {
479
- let workItem = DispatchWorkItem {
480
- self.reject(key, message)
481
- }
482
- self.timeoutMap[key] = workItem
483
- DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: workItem)
484
- }
485
-
486
- private func setConnectionTimeout(
487
- _ key: String,
488
- _ message: String,
489
- _ device: Device,
490
- _ connectionTimeout: Double
491
- ) {
492
- let workItem = DispatchWorkItem {
493
- // do not call onDisconnnected, which is triggered by cancelPeripheralConnection
494
- let key = "onDisconnected|\(device.getId())"
495
- self.callbackMap[key] = nil
496
- self.centralManager.cancelPeripheralConnection(device.getPeripheral())
497
- self.reject(key, message)
498
- }
499
- self.timeoutMap[key] = workItem
500
- DispatchQueue.main.asyncAfter(deadline: .now() + connectionTimeout, execute: workItem)
501
- }
502
- }
1
+ import Foundation
2
+ import UIKit
3
+ import CoreBluetooth
4
+
5
+ enum DeviceListMode {
6
+ case none
7
+ case alert
8
+ case list
9
+ }
10
+
11
+ class DeviceManager: NSObject, CBCentralManagerDelegate {
12
+ typealias Callback = (_ success: Bool, _ message: String) -> Void
13
+ typealias StateReceiver = (_ enabled: Bool) -> Void
14
+ typealias ScanResultCallback = (_ device: Device, _ advertisementData: [String: Any], _ rssi: NSNumber) -> Void
15
+
16
+ private var centralManager: CBCentralManager!
17
+ private var viewController: UIViewController?
18
+ private var displayStrings: [String: String]!
19
+ private var callbackMap = [String: Callback]()
20
+ private var scanResultCallback: ScanResultCallback?
21
+ private var stateReceiver: StateReceiver?
22
+ private var timeoutMap = [String: DispatchWorkItem]()
23
+ private var stopScanWorkItem: DispatchWorkItem?
24
+ private var alertController: UIAlertController?
25
+ private var deviceListView: DeviceListView?
26
+ private var popoverController: UIPopoverPresentationController?
27
+ private var discoveredDevices = [String: Device]()
28
+ private var deviceNameFilter: String?
29
+ private var deviceNamePrefixFilter: String?
30
+ private var deviceListMode: DeviceListMode = .none
31
+ private var allowDuplicates = false
32
+ private var manufacturerDataFilters: [ManufacturerDataFilter]?
33
+ private var serviceDataFilters: [ServiceDataFilter]?
34
+
35
+ init(_ viewController: UIViewController?, _ displayStrings: [String: String], _ callback: @escaping Callback) {
36
+ super.init()
37
+ self.viewController = viewController
38
+ self.displayStrings = displayStrings
39
+ self.callbackMap["initialize"] = callback
40
+ self.centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main)
41
+ }
42
+
43
+ func setDisplayStrings(_ displayStrings: [String: String]) {
44
+ self.displayStrings = displayStrings
45
+ }
46
+
47
+ // initialize
48
+ func centralManagerDidUpdateState(_ central: CBCentralManager) {
49
+ let initializeKey = "initialize"
50
+ switch central.state {
51
+ case .poweredOn:
52
+ self.resolve(initializeKey, "BLE powered on")
53
+ self.emitState(enabled: true)
54
+ case .poweredOff:
55
+ self.stopScan()
56
+ self.resolve(initializeKey, "BLE powered off")
57
+ self.emitState(enabled: false)
58
+ case .resetting:
59
+ self.emitState(enabled: false)
60
+ case .unauthorized:
61
+ self.reject(initializeKey, "BLE permission denied")
62
+ self.emitState(enabled: false)
63
+ case .unsupported:
64
+ self.reject(initializeKey, "BLE unsupported")
65
+ self.emitState(enabled: false)
66
+ case .unknown:
67
+ self.emitState(enabled: false)
68
+ default: break
69
+ }
70
+ }
71
+
72
+ func isEnabled() -> Bool {
73
+ return self.centralManager.state == CBManagerState.poweredOn
74
+ }
75
+
76
+ func registerStateReceiver( _ stateReceiver: @escaping StateReceiver) {
77
+ self.stateReceiver = stateReceiver
78
+ }
79
+
80
+ func unregisterStateReceiver() {
81
+ self.stateReceiver = nil
82
+ }
83
+
84
+ func emitState(enabled: Bool) {
85
+ guard let stateReceiver = self.stateReceiver else { return }
86
+ stateReceiver(enabled)
87
+ }
88
+
89
+ func startScanning(
90
+ _ serviceUUIDs: [CBUUID],
91
+ _ name: String?,
92
+ _ namePrefix: String?,
93
+ _ manufacturerDataFilters: [ManufacturerDataFilter]?,
94
+ _ serviceDataFilters: [ServiceDataFilter]?,
95
+ _ allowDuplicates: Bool,
96
+ _ deviceListMode: DeviceListMode,
97
+ _ scanDuration: Double?,
98
+ _ callback: @escaping Callback,
99
+ _ scanResultCallback: @escaping ScanResultCallback
100
+ ) {
101
+ self.callbackMap["startScanning"] = callback
102
+ self.scanResultCallback = scanResultCallback
103
+
104
+ if self.centralManager.isScanning == false {
105
+ self.discoveredDevices = [String: Device]()
106
+ self.deviceListMode = deviceListMode
107
+ self.allowDuplicates = allowDuplicates
108
+ self.deviceNameFilter = name
109
+ self.deviceNamePrefixFilter = namePrefix
110
+ self.manufacturerDataFilters = manufacturerDataFilters
111
+ self.serviceDataFilters = serviceDataFilters
112
+
113
+ if deviceListMode != .none {
114
+ self.showDeviceList()
115
+ }
116
+
117
+ if scanDuration != nil {
118
+ self.stopScanWorkItem = DispatchWorkItem {
119
+ self.stopScan()
120
+ }
121
+ DispatchQueue.main.asyncAfter(deadline: .now() + scanDuration!, execute: self.stopScanWorkItem!)
122
+ }
123
+ self.centralManager.scanForPeripherals(
124
+ withServices: serviceUUIDs,
125
+ options: [CBCentralManagerScanOptionAllowDuplicatesKey: allowDuplicates]
126
+ )
127
+
128
+ if deviceListMode == .none {
129
+ self.resolve("startScanning", "Scan started.")
130
+ }
131
+ } else {
132
+ self.stopScan()
133
+ self.reject("startScanning", "Already scanning. Stopping now.")
134
+ }
135
+ }
136
+
137
+ func stopScan() {
138
+ log("Stop scanning.")
139
+ self.centralManager.stopScan()
140
+ self.stopScanWorkItem?.cancel()
141
+ self.stopScanWorkItem = nil
142
+ DispatchQueue.main.async { [weak self] in
143
+ guard let self = self else { return }
144
+ switch self.deviceListMode {
145
+ case .alert:
146
+ if self.discoveredDevices.count == 0 {
147
+ self.alertController?.title = self.displayStrings["noDeviceFound"]
148
+ } else {
149
+ self.alertController?.title = self.displayStrings["availableDevices"]
150
+ }
151
+ case .list:
152
+ if self.discoveredDevices.count == 0 {
153
+ self.deviceListView?.setTitle(self.displayStrings["noDeviceFound"])
154
+ } else {
155
+ self.deviceListView?.setTitle(self.displayStrings["availableDevices"])
156
+ }
157
+ case .none:
158
+ break
159
+ }
160
+ }
161
+ }
162
+
163
+ // didDiscover
164
+ func centralManager(
165
+ _ central: CBCentralManager,
166
+ didDiscover peripheral: CBPeripheral,
167
+ advertisementData: [String: Any],
168
+ rssi RSSI: NSNumber
169
+ ) {
170
+
171
+ guard peripheral.state != CBPeripheralState.connected else {
172
+ log("found connected device", peripheral.name ?? "Unknown")
173
+ // make sure we do not touch connected devices
174
+ return
175
+ }
176
+
177
+ let isNew = self.discoveredDevices[peripheral.identifier.uuidString] == nil
178
+ guard isNew || self.allowDuplicates else { return }
179
+
180
+ guard self.passesNameFilter(peripheralName: peripheral.name) else { return }
181
+ guard self.passesNamePrefixFilter(peripheralName: peripheral.name) else { return }
182
+ guard self.passesManufacturerDataFilter(advertisementData) else { return }
183
+ guard self.passesServiceDataFilter(advertisementData) else { return }
184
+
185
+ let device: Device
186
+ if self.allowDuplicates, let knownDevice = discoveredDevices.first(where: { $0.key == peripheral.identifier.uuidString })?.value {
187
+ device = knownDevice
188
+ } else {
189
+ device = Device(peripheral)
190
+ self.discoveredDevices[device.getId()] = device
191
+ }
192
+ log("New device found: ", device.getName() ?? "Unknown")
193
+
194
+ switch deviceListMode {
195
+ case .none:
196
+ if self.scanResultCallback != nil {
197
+ self.scanResultCallback!(device, advertisementData, RSSI)
198
+ }
199
+ case .alert:
200
+ DispatchQueue.main.async { [weak self] in
201
+ self?.alertController?.addAction(UIAlertAction(title: device.getName() ?? "Unknown", style: UIAlertAction.Style.default, handler: { (_) in
202
+ log("Selected device")
203
+ self?.stopScan()
204
+ self?.resolve("startScanning", device.getId())
205
+ }))
206
+ }
207
+ case .list:
208
+ DispatchQueue.main.async { [weak self] in
209
+ self?.deviceListView?.addItem(device.getName() ?? "Unknown", action: {
210
+ log("Selected device")
211
+ self?.stopScan()
212
+ self?.resolve("startScanning", device.getId())
213
+ })
214
+ }
215
+ }
216
+ }
217
+
218
+ func showDeviceList() {
219
+ switch deviceListMode {
220
+ case .none:
221
+ break
222
+ case .alert:
223
+ showDeviceListAlert()
224
+ case .list:
225
+ showDeviceListView()
226
+ }
227
+ }
228
+
229
+ func showDeviceListAlert() {
230
+ DispatchQueue.main.async { [weak self] in
231
+ self?.alertController = UIAlertController(title: self?.displayStrings["scanning"], message: nil, preferredStyle: UIAlertController.Style.alert)
232
+ self?.alertController?.addAction(UIAlertAction(title: self?.displayStrings["cancel"], style: UIAlertAction.Style.cancel, handler: { (_) in
233
+ log("Cancelled request device.")
234
+ self?.stopScan()
235
+ self?.reject("startScanning", "requestDevice cancelled.")
236
+ }))
237
+ self?.viewController?.present((self?.alertController)!, animated: true, completion: nil)
238
+ }
239
+ }
240
+
241
+ func showDeviceListView() {
242
+ DispatchQueue.main.async { [weak self] in
243
+ self?.deviceListView = DeviceListView()
244
+ if #available(macCatalyst 15.0, iOS 15.0, *) {
245
+ self?.deviceListView?.sheetPresentationController?.detents = [.medium()]
246
+ }
247
+ self?.viewController?.present((self?.deviceListView)!, animated: true, completion: nil)
248
+ self?.deviceListView?.setTitle(self?.displayStrings["scanning"])
249
+ self?.deviceListView?.setCancelButton(self?.displayStrings["cancel"], action: {
250
+ log("Cancelled request device.")
251
+ self?.stopScan()
252
+ self?.reject("startScanning", "requestDevice cancelled.")
253
+ })
254
+ }
255
+ }
256
+
257
+ func getDevices(
258
+ _ deviceUUIDs: [UUID]
259
+ ) -> [CBPeripheral] {
260
+ return self.centralManager.retrievePeripherals(withIdentifiers: deviceUUIDs)
261
+ }
262
+
263
+ func getConnectedDevices(
264
+ _ serviceUUIDs: [CBUUID]
265
+ ) -> [CBPeripheral] {
266
+ return self.centralManager.retrieveConnectedPeripherals(withServices: serviceUUIDs)
267
+ }
268
+
269
+ func connect(
270
+ _ device: Device,
271
+ _ connectionTimeout: Double,
272
+ _ callback: @escaping Callback
273
+ ) {
274
+ let key = "connect|\(device.getId())"
275
+ self.callbackMap[key] = callback
276
+ log("Connecting to peripheral", device.getPeripheral())
277
+ self.centralManager.connect(device.getPeripheral(), options: nil)
278
+ self.setConnectionTimeout(key, "Connection timeout.", device, connectionTimeout)
279
+ }
280
+
281
+ // didConnect
282
+ func centralManager(
283
+ _ central: CBCentralManager,
284
+ didConnect peripheral: CBPeripheral
285
+ ) {
286
+ log("Connected to device", peripheral)
287
+ let key = "connect|\(peripheral.identifier.uuidString)"
288
+ peripheral.discoverServices(nil)
289
+ self.resolve(key, "Successfully connected.")
290
+ // will wait for services in plugin call
291
+ }
292
+
293
+ // didFailToConnect
294
+ func centralManager(
295
+ _ central: CBCentralManager,
296
+ didFailToConnect peripheral: CBPeripheral,
297
+ error: Error?
298
+ ) {
299
+ let key = "connect|\(peripheral.identifier.uuidString)"
300
+ if error != nil {
301
+ self.reject(key, error!.localizedDescription)
302
+ return
303
+ }
304
+ self.reject(key, "Failed to connect.")
305
+ }
306
+
307
+ func setOnDisconnected(
308
+ _ device: Device,
309
+ _ callback: @escaping Callback
310
+ ) {
311
+ let key = "onDisconnected|\(device.getId())"
312
+ self.callbackMap[key] = callback
313
+ }
314
+
315
+ func disconnect(
316
+ _ device: Device,
317
+ _ timeout: Double,
318
+ _ callback: @escaping Callback
319
+ ) {
320
+ let key = "disconnect|\(device.getId())"
321
+ self.callbackMap[key] = callback
322
+ if device.isConnected() == false {
323
+ self.resolve(key, "Disconnected.")
324
+ return
325
+ }
326
+ log("Disconnecting from peripheral", device.getPeripheral())
327
+ self.centralManager.cancelPeripheralConnection(device.getPeripheral())
328
+ self.setTimeout(key, "Disconnection timeout.", timeout)
329
+ }
330
+
331
+ // didDisconnectPeripheral
332
+ func centralManager(
333
+ _ central: CBCentralManager,
334
+ didDisconnectPeripheral peripheral: CBPeripheral,
335
+ error: Error?
336
+ ) {
337
+ let key = "disconnect|\(peripheral.identifier.uuidString)"
338
+ let keyOnDisconnected = "onDisconnected|\(peripheral.identifier.uuidString)"
339
+ self.resolve(keyOnDisconnected, "Disconnected.")
340
+ if error != nil {
341
+ log(error!.localizedDescription)
342
+ self.reject(key, error!.localizedDescription)
343
+ return
344
+ }
345
+ self.resolve(key, "Successfully disconnected.")
346
+ }
347
+
348
+ func getDevice(_ deviceId: String) -> Device? {
349
+ return self.discoveredDevices[deviceId]
350
+ }
351
+
352
+ private func passesNameFilter(peripheralName: String?) -> Bool {
353
+ guard let nameFilter = self.deviceNameFilter else { return true }
354
+ guard let name = peripheralName else { return false }
355
+ return name == nameFilter
356
+ }
357
+
358
+ private func passesNamePrefixFilter(peripheralName: String?) -> Bool {
359
+ guard let prefix = self.deviceNamePrefixFilter else { return true }
360
+ guard let name = peripheralName else { return false }
361
+ return name.hasPrefix(prefix)
362
+ }
363
+
364
+ private func passesManufacturerDataFilter(_ advertisementData: [String: Any]) -> Bool {
365
+ guard let filters = self.manufacturerDataFilters, !filters.isEmpty else {
366
+ return true // No filters means everything passes
367
+ }
368
+
369
+ guard let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
370
+ manufacturerData.count >= 2 else {
371
+ return false // If there's no valid manufacturer data, fail
372
+ }
373
+
374
+ let companyIdentifier = manufacturerData.prefix(2).withUnsafeBytes {
375
+ $0.load(as: UInt16.self).littleEndian // Manufacturer ID is little-endian
376
+ }
377
+
378
+ let payload = manufacturerData.dropFirst(2)
379
+
380
+ for filter in filters {
381
+ if filter.companyIdentifier != companyIdentifier {
382
+ continue // Skip if company ID does not match
383
+ }
384
+
385
+ if let dataPrefix = filter.dataPrefix {
386
+ if payload.count < dataPrefix.count {
387
+ continue // Payload too short, does not match
388
+ }
389
+
390
+ if let mask = filter.mask {
391
+ var matches = true
392
+ for i in 0..<dataPrefix.count {
393
+ if (payload[i] & mask[i]) != (dataPrefix[i] & mask[i]) {
394
+ matches = false
395
+ break
396
+ }
397
+ }
398
+ if matches {
399
+ return true
400
+ }
401
+ } else if payload.starts(with: dataPrefix) {
402
+ return true
403
+ }
404
+ } else {
405
+ return true // Company ID matched, and no dataPrefix required
406
+ }
407
+ }
408
+
409
+ return false // If none matched, return false
410
+ }
411
+
412
+ private func passesServiceDataFilter(_ advertisementData: [String: Any]) -> Bool {
413
+ guard let filters = self.serviceDataFilters, !filters.isEmpty else {
414
+ return true // No filters means everything passes
415
+ }
416
+
417
+ guard let serviceDataDict = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] else {
418
+ return false // If there's no service data, fail
419
+ }
420
+
421
+ for filter in filters {
422
+ guard let serviceData = serviceDataDict[filter.serviceUuid] else {
423
+ continue // Skip if service UUID does not match
424
+ }
425
+
426
+ if let dataPrefix = filter.dataPrefix {
427
+ if serviceData.count < dataPrefix.count {
428
+ continue // Service data too short, does not match
429
+ }
430
+
431
+ if let mask = filter.mask {
432
+ var matches = true
433
+ for i in 0..<dataPrefix.count {
434
+ if (serviceData[i] & mask[i]) != (dataPrefix[i] & mask[i]) {
435
+ matches = false
436
+ break
437
+ }
438
+ }
439
+ if matches {
440
+ return true
441
+ }
442
+ } else if serviceData.starts(with: dataPrefix) {
443
+ return true
444
+ }
445
+ } else {
446
+ return true // Service UUID matched, and no dataPrefix required
447
+ }
448
+ }
449
+
450
+ return false // If none matched, return false
451
+ }
452
+
453
+ private func resolve(_ key: String, _ value: String) {
454
+ let callback = self.callbackMap[key]
455
+ if callback != nil {
456
+ log("Resolve", key, value)
457
+ callback!(true, value)
458
+ self.callbackMap[key] = nil
459
+ self.timeoutMap[key]?.cancel()
460
+ self.timeoutMap[key] = nil
461
+ }
462
+ }
463
+
464
+ private func reject(_ key: String, _ value: String) {
465
+ let callback = self.callbackMap[key]
466
+ if callback != nil {
467
+ log("Reject", key, value)
468
+ callback!(false, value)
469
+ self.callbackMap[key] = nil
470
+ self.timeoutMap[key]?.cancel()
471
+ self.timeoutMap[key] = nil
472
+ }
473
+ }
474
+
475
+ private func setTimeout(
476
+ _ key: String,
477
+ _ message: String,
478
+ _ timeout: Double
479
+ ) {
480
+ let workItem = DispatchWorkItem {
481
+ self.reject(key, message)
482
+ }
483
+ self.timeoutMap[key] = workItem
484
+ DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: workItem)
485
+ }
486
+
487
+ private func setConnectionTimeout(
488
+ _ key: String,
489
+ _ message: String,
490
+ _ device: Device,
491
+ _ connectionTimeout: Double
492
+ ) {
493
+ let workItem = DispatchWorkItem {
494
+ // do not call onDisconnnected, which is triggered by cancelPeripheralConnection
495
+ let key = "onDisconnected|\(device.getId())"
496
+ self.callbackMap[key] = nil
497
+ self.centralManager.cancelPeripheralConnection(device.getPeripheral())
498
+ self.reject(key, message)
499
+ }
500
+ self.timeoutMap[key] = workItem
501
+ DispatchQueue.main.asyncAfter(deadline: .now() + connectionTimeout, execute: workItem)
502
+ }
503
+ }