@capacitor-community/bluetooth-le 7.2.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 (55) hide show
  1. package/CapacitorCommunityBluetoothLe.podspec +17 -17
  2. package/LICENSE +21 -21
  3. package/Package.swift +28 -0
  4. package/README.md +68 -161
  5. package/android/build.gradle +71 -68
  6. package/android/src/main/AndroidManifest.xml +22 -22
  7. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +1094 -1070
  8. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Conversion.kt +51 -51
  9. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +771 -767
  10. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt +28 -28
  11. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceScanner.kt +189 -189
  12. package/dist/docs.json +906 -849
  13. package/dist/esm/bleClient.d.ts +278 -278
  14. package/dist/esm/bleClient.js +361 -347
  15. package/dist/esm/bleClient.js.map +1 -1
  16. package/dist/esm/config.d.ts +53 -53
  17. package/dist/esm/config.js +2 -2
  18. package/dist/esm/conversion.d.ts +56 -34
  19. package/dist/esm/conversion.js +134 -84
  20. package/dist/esm/conversion.js.map +1 -1
  21. package/dist/esm/definitions.d.ts +352 -313
  22. package/dist/esm/definitions.js +42 -42
  23. package/dist/esm/definitions.js.map +1 -1
  24. package/dist/esm/index.d.ts +5 -5
  25. package/dist/esm/index.js +5 -5
  26. package/dist/esm/plugin.d.ts +2 -2
  27. package/dist/esm/plugin.js +4 -4
  28. package/dist/esm/queue.d.ts +3 -3
  29. package/dist/esm/queue.js +17 -17
  30. package/dist/esm/queue.js.map +1 -1
  31. package/dist/esm/timeout.d.ts +1 -1
  32. package/dist/esm/timeout.js +9 -9
  33. package/dist/esm/validators.d.ts +1 -1
  34. package/dist/esm/validators.js +11 -11
  35. package/dist/esm/validators.js.map +1 -1
  36. package/dist/esm/web.d.ts +57 -56
  37. package/dist/esm/web.js +403 -340
  38. package/dist/esm/web.js.map +1 -1
  39. package/dist/plugin.cjs.js +967 -837
  40. package/dist/plugin.cjs.js.map +1 -1
  41. package/dist/plugin.js +967 -837
  42. package/dist/plugin.js.map +1 -1
  43. package/ios/{Plugin → Sources/BluetoothLe}/Conversion.swift +83 -83
  44. package/ios/{Plugin → Sources/BluetoothLe}/Device.swift +423 -423
  45. package/ios/Sources/BluetoothLe/DeviceListView.swift +121 -0
  46. package/ios/{Plugin → Sources/BluetoothLe}/DeviceManager.swift +503 -401
  47. package/ios/{Plugin → Sources/BluetoothLe}/Logging.swift +8 -8
  48. package/ios/{Plugin → Sources/BluetoothLe}/Plugin.swift +775 -682
  49. package/ios/{Plugin → Sources/BluetoothLe}/ThreadSafeDictionary.swift +15 -13
  50. package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -0
  51. package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -0
  52. package/package.json +115 -101
  53. package/ios/Plugin/Info.plist +0 -24
  54. package/ios/Plugin/Plugin.h +0 -10
  55. package/ios/Plugin/Plugin.m +0 -41
@@ -1,401 +1,503 @@
1
- import Foundation
2
- import CoreBluetooth
3
-
4
- class DeviceManager: NSObject, CBCentralManagerDelegate {
5
- typealias Callback = (_ success: Bool, _ message: String) -> Void
6
- typealias StateReceiver = (_ enabled: Bool) -> Void
7
- typealias ScanResultCallback = (_ device: Device, _ advertisementData: [String: Any], _ rssi: NSNumber) -> Void
8
-
9
- private var centralManager: CBCentralManager!
10
- private var viewController: UIViewController?
11
- private var displayStrings: [String: String]!
12
- private var callbackMap = [String: Callback]()
13
- private var scanResultCallback: ScanResultCallback?
14
- private var stateReceiver: StateReceiver?
15
- private var timeoutMap = [String: DispatchWorkItem]()
16
- private var stopScanWorkItem: DispatchWorkItem?
17
- private var alertController: UIAlertController?
18
- private var discoveredDevices = [String: Device]()
19
- private var deviceNameFilter: String?
20
- private var deviceNamePrefixFilter: String?
21
- private var shouldShowDeviceList = false
22
- private var allowDuplicates = false
23
- private var manufacturerDataFilters: [ManufacturerDataFilter]?
24
-
25
- init(_ viewController: UIViewController?, _ displayStrings: [String: String], _ callback: @escaping Callback) {
26
- super.init()
27
- self.viewController = viewController
28
- self.displayStrings = displayStrings
29
- self.callbackMap["initialize"] = callback
30
- self.centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main)
31
- }
32
-
33
- func setDisplayStrings(_ displayStrings: [String: String]) {
34
- self.displayStrings = displayStrings
35
- }
36
-
37
- // initialize
38
- func centralManagerDidUpdateState(_ central: CBCentralManager) {
39
- let initializeKey = "initialize"
40
- switch central.state {
41
- case .poweredOn:
42
- self.resolve(initializeKey, "BLE powered on")
43
- self.emitState(enabled: true)
44
- case .poweredOff:
45
- self.stopScan()
46
- self.resolve(initializeKey, "BLE powered off")
47
- self.emitState(enabled: false)
48
- case .resetting:
49
- self.emitState(enabled: false)
50
- case .unauthorized:
51
- self.reject(initializeKey, "BLE permission denied")
52
- self.emitState(enabled: false)
53
- case .unsupported:
54
- self.reject(initializeKey, "BLE unsupported")
55
- self.emitState(enabled: false)
56
- case .unknown:
57
- self.emitState(enabled: false)
58
- default: break
59
- }
60
- }
61
-
62
- func isEnabled() -> Bool {
63
- return self.centralManager.state == CBManagerState.poweredOn
64
- }
65
-
66
- func registerStateReceiver( _ stateReceiver: @escaping StateReceiver) {
67
- self.stateReceiver = stateReceiver
68
- }
69
-
70
- func unregisterStateReceiver() {
71
- self.stateReceiver = nil
72
- }
73
-
74
- func emitState(enabled: Bool) {
75
- guard let stateReceiver = self.stateReceiver else { return }
76
- stateReceiver(enabled)
77
- }
78
-
79
- func startScanning(
80
- _ serviceUUIDs: [CBUUID],
81
- _ name: String?,
82
- _ namePrefix: String?,
83
- _ manufacturerDataFilters: [ManufacturerDataFilter]?,
84
- _ allowDuplicates: Bool,
85
- _ shouldShowDeviceList: Bool,
86
- _ scanDuration: Double?,
87
- _ callback: @escaping Callback,
88
- _ scanResultCallback: @escaping ScanResultCallback
89
- ) {
90
- self.callbackMap["startScanning"] = callback
91
- self.scanResultCallback = scanResultCallback
92
-
93
- if self.centralManager.isScanning == false {
94
- self.discoveredDevices = [String: Device]()
95
- self.shouldShowDeviceList = shouldShowDeviceList
96
- self.allowDuplicates = allowDuplicates
97
- self.deviceNameFilter = name
98
- self.deviceNamePrefixFilter = namePrefix
99
- self.manufacturerDataFilters = manufacturerDataFilters
100
-
101
- if shouldShowDeviceList {
102
- self.showDeviceList()
103
- }
104
-
105
- if scanDuration != nil {
106
- self.stopScanWorkItem = DispatchWorkItem {
107
- self.stopScan()
108
- }
109
- DispatchQueue.main.asyncAfter(deadline: .now() + scanDuration!, execute: self.stopScanWorkItem!)
110
- }
111
- self.centralManager.scanForPeripherals(
112
- withServices: serviceUUIDs,
113
- options: [CBCentralManagerScanOptionAllowDuplicatesKey: allowDuplicates]
114
- )
115
-
116
- if shouldShowDeviceList == false {
117
- self.resolve("startScanning", "Scan started.")
118
- }
119
- } else {
120
- self.stopScan()
121
- self.reject("startScanning", "Already scanning. Stopping now.")
122
- }
123
- }
124
-
125
- func stopScan() {
126
- log("Stop scanning.")
127
- self.centralManager.stopScan()
128
- self.stopScanWorkItem?.cancel()
129
- self.stopScanWorkItem = nil
130
- DispatchQueue.main.async { [weak self] in
131
- if self?.discoveredDevices.count == 0 {
132
- self?.alertController?.title = self?.displayStrings["noDeviceFound"]
133
- } else {
134
- self?.alertController?.title = self?.displayStrings["availableDevices"]
135
- }
136
- }
137
- }
138
-
139
- // didDiscover
140
- func centralManager(
141
- _ central: CBCentralManager,
142
- didDiscover peripheral: CBPeripheral,
143
- advertisementData: [String: Any],
144
- rssi RSSI: NSNumber
145
- ) {
146
-
147
- guard peripheral.state != CBPeripheralState.connected else {
148
- log("found connected device", peripheral.name ?? "Unknown")
149
- // make sure we do not touch connected devices
150
- return
151
- }
152
-
153
- let isNew = self.discoveredDevices[peripheral.identifier.uuidString] == nil
154
- guard isNew || self.allowDuplicates else { return }
155
-
156
- guard self.passesNameFilter(peripheralName: peripheral.name) else { return }
157
- guard self.passesNamePrefixFilter(peripheralName: peripheral.name) else { return }
158
- guard self.passesManufacturerDataFilter(advertisementData) else { return }
159
-
160
- let device: Device
161
- if self.allowDuplicates, let knownDevice = discoveredDevices.first(where: { $0.key == peripheral.identifier.uuidString })?.value {
162
- device = knownDevice
163
- } else {
164
- device = Device(peripheral)
165
- self.discoveredDevices[device.getId()] = device
166
- }
167
- log("New device found: ", device.getName() ?? "Unknown")
168
-
169
- if shouldShowDeviceList {
170
- DispatchQueue.main.async { [weak self] in
171
- self?.alertController?.addAction(UIAlertAction(title: device.getName() ?? "Unknown", style: UIAlertAction.Style.default, handler: { (_) in
172
- log("Selected device")
173
- self?.stopScan()
174
- self?.resolve("startScanning", device.getId())
175
- }))
176
- }
177
- } else {
178
- if self.scanResultCallback != nil {
179
- self.scanResultCallback!(device, advertisementData, RSSI)
180
- }
181
- }
182
- }
183
-
184
- func showDeviceList() {
185
- DispatchQueue.main.async { [weak self] in
186
- self?.alertController = UIAlertController(title: self?.displayStrings["scanning"], message: nil, preferredStyle: UIAlertController.Style.alert)
187
- self?.alertController?.addAction(UIAlertAction(title: self?.displayStrings["cancel"], style: UIAlertAction.Style.cancel, handler: { (_) in
188
- log("Cancelled request device.")
189
- self?.stopScan()
190
- self?.reject("startScanning", "requestDevice cancelled.")
191
- }))
192
- self?.viewController?.present((self?.alertController)!, animated: true, completion: nil)
193
- }
194
- }
195
-
196
- func getDevices(
197
- _ deviceUUIDs: [UUID]
198
- ) -> [CBPeripheral] {
199
- return self.centralManager.retrievePeripherals(withIdentifiers: deviceUUIDs)
200
- }
201
-
202
- func getConnectedDevices(
203
- _ serviceUUIDs: [CBUUID]
204
- ) -> [CBPeripheral] {
205
- return self.centralManager.retrieveConnectedPeripherals(withServices: serviceUUIDs)
206
- }
207
-
208
- func connect(
209
- _ device: Device,
210
- _ connectionTimeout: Double,
211
- _ callback: @escaping Callback
212
- ) {
213
- let key = "connect|\(device.getId())"
214
- self.callbackMap[key] = callback
215
- log("Connecting to peripheral", device.getPeripheral())
216
- self.centralManager.connect(device.getPeripheral(), options: nil)
217
- self.setConnectionTimeout(key, "Connection timeout.", device, connectionTimeout)
218
- }
219
-
220
- // didConnect
221
- func centralManager(
222
- _ central: CBCentralManager,
223
- didConnect peripheral: CBPeripheral
224
- ) {
225
- log("Connected to device", peripheral)
226
- let key = "connect|\(peripheral.identifier.uuidString)"
227
- peripheral.discoverServices(nil)
228
- self.resolve(key, "Successfully connected.")
229
- // will wait for services in plugin call
230
- }
231
-
232
- // didFailToConnect
233
- func centralManager(
234
- _ central: CBCentralManager,
235
- didFailToConnect peripheral: CBPeripheral,
236
- error: Error?
237
- ) {
238
- let key = "connect|\(peripheral.identifier.uuidString)"
239
- if error != nil {
240
- self.reject(key, error!.localizedDescription)
241
- return
242
- }
243
- self.reject(key, "Failed to connect.")
244
- }
245
-
246
- func setOnDisconnected(
247
- _ device: Device,
248
- _ callback: @escaping Callback
249
- ) {
250
- let key = "onDisconnected|\(device.getId())"
251
- self.callbackMap[key] = callback
252
- }
253
-
254
- func disconnect(
255
- _ device: Device,
256
- _ timeout: Double,
257
- _ callback: @escaping Callback
258
- ) {
259
- let key = "disconnect|\(device.getId())"
260
- self.callbackMap[key] = callback
261
- if device.isConnected() == false {
262
- self.resolve(key, "Disconnected.")
263
- return
264
- }
265
- log("Disconnecting from peripheral", device.getPeripheral())
266
- self.centralManager.cancelPeripheralConnection(device.getPeripheral())
267
- self.setTimeout(key, "Disconnection timeout.", timeout)
268
- }
269
-
270
- // didDisconnectPeripheral
271
- func centralManager(
272
- _ central: CBCentralManager,
273
- didDisconnectPeripheral peripheral: CBPeripheral,
274
- error: Error?
275
- ) {
276
- let key = "disconnect|\(peripheral.identifier.uuidString)"
277
- let keyOnDisconnected = "onDisconnected|\(peripheral.identifier.uuidString)"
278
- self.resolve(keyOnDisconnected, "Disconnected.")
279
- if error != nil {
280
- log(error!.localizedDescription)
281
- self.reject(key, error!.localizedDescription)
282
- return
283
- }
284
- self.resolve(key, "Successfully disconnected.")
285
- }
286
-
287
- func getDevice(_ deviceId: String) -> Device? {
288
- return self.discoveredDevices[deviceId]
289
- }
290
-
291
- private func passesNameFilter(peripheralName: String?) -> Bool {
292
- guard let nameFilter = self.deviceNameFilter else { return true }
293
- guard let name = peripheralName else { return false }
294
- return name == nameFilter
295
- }
296
-
297
- private func passesNamePrefixFilter(peripheralName: String?) -> Bool {
298
- guard let prefix = self.deviceNamePrefixFilter else { return true }
299
- guard let name = peripheralName else { return false }
300
- return name.hasPrefix(prefix)
301
- }
302
-
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
-
351
- private func resolve(_ key: String, _ value: String) {
352
- let callback = self.callbackMap[key]
353
- if callback != nil {
354
- log("Resolve", key, value)
355
- callback!(true, value)
356
- self.callbackMap[key] = nil
357
- self.timeoutMap[key]?.cancel()
358
- self.timeoutMap[key] = nil
359
- }
360
- }
361
-
362
- private func reject(_ key: String, _ value: String) {
363
- let callback = self.callbackMap[key]
364
- if callback != nil {
365
- log("Reject", key, value)
366
- callback!(false, value)
367
- self.callbackMap[key] = nil
368
- self.timeoutMap[key]?.cancel()
369
- self.timeoutMap[key] = nil
370
- }
371
- }
372
-
373
- private func setTimeout(
374
- _ key: String,
375
- _ message: String,
376
- _ timeout: Double
377
- ) {
378
- let workItem = DispatchWorkItem {
379
- self.reject(key, message)
380
- }
381
- self.timeoutMap[key] = workItem
382
- DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: workItem)
383
- }
384
-
385
- private func setConnectionTimeout(
386
- _ key: String,
387
- _ message: String,
388
- _ device: Device,
389
- _ connectionTimeout: Double
390
- ) {
391
- let workItem = DispatchWorkItem {
392
- // do not call onDisconnnected, which is triggered by cancelPeripheralConnection
393
- let key = "onDisconnected|\(device.getId())"
394
- self.callbackMap[key] = nil
395
- self.centralManager.cancelPeripheralConnection(device.getPeripheral())
396
- self.reject(key, message)
397
- }
398
- self.timeoutMap[key] = workItem
399
- DispatchQueue.main.asyncAfter(deadline: .now() + connectionTimeout, execute: workItem)
400
- }
401
- }
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
+ }