@capacitor-community/bluetooth-le 8.0.1 → 8.0.2

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 +27 -27
  4. package/README.md +2 -1
  5. package/android/build.gradle +73 -73
  6. package/android/src/main/AndroidManifest.xml +22 -22
  7. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +1094 -1094
  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 -771
  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 +43 -43
  13. package/dist/esm/bleClient.d.ts +278 -278
  14. package/dist/esm/bleClient.js +361 -361
  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 -56
  19. package/dist/esm/conversion.js +134 -134
  20. package/dist/esm/conversion.js.map +1 -1
  21. package/dist/esm/definitions.d.ts +352 -352
  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/timeout.d.ts +1 -1
  31. package/dist/esm/timeout.js +9 -9
  32. package/dist/esm/validators.d.ts +1 -1
  33. package/dist/esm/validators.js +11 -11
  34. package/dist/esm/web.d.ts +57 -57
  35. package/dist/esm/web.js +403 -403
  36. package/dist/esm/web.js.map +1 -1
  37. package/dist/plugin.cjs.js +964 -964
  38. package/dist/plugin.cjs.js.map +1 -1
  39. package/dist/plugin.js +964 -964
  40. package/dist/plugin.js.map +1 -1
  41. package/ios/Sources/BluetoothLe/Conversion.swift +83 -83
  42. package/ios/Sources/BluetoothLe/Device.swift +422 -423
  43. package/ios/Sources/BluetoothLe/DeviceListView.swift +121 -121
  44. package/ios/Sources/BluetoothLe/DeviceManager.swift +409 -415
  45. package/ios/Sources/BluetoothLe/Logging.swift +8 -8
  46. package/ios/Sources/BluetoothLe/Plugin.swift +768 -763
  47. package/ios/Sources/BluetoothLe/ScanFilters.swift +114 -114
  48. package/ios/Sources/BluetoothLe/ThreadSafeDictionary.swift +61 -15
  49. package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -55
  50. package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -27
  51. package/ios/Tests/BluetoothLeTests/ScanFiltersTests.swift +153 -153
  52. package/package.json +114 -114
@@ -1,415 +1,409 @@
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 ScanFilterUtils.passesManufacturerDataFilter(advertisementData, filters: self.manufacturerDataFilters) else { return }
183
- guard ScanFilterUtils.passesServiceDataFilter(advertisementData, filters: self.serviceDataFilters) 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
-
365
- private func resolve(_ key: String, _ value: String) {
366
- let callback = self.callbackMap[key]
367
- if callback != nil {
368
- log("Resolve", key, value)
369
- callback!(true, value)
370
- self.callbackMap[key] = nil
371
- self.timeoutMap[key]?.cancel()
372
- self.timeoutMap[key] = nil
373
- }
374
- }
375
-
376
- private func reject(_ key: String, _ value: String) {
377
- let callback = self.callbackMap[key]
378
- if callback != nil {
379
- log("Reject", key, value)
380
- callback!(false, value)
381
- self.callbackMap[key] = nil
382
- self.timeoutMap[key]?.cancel()
383
- self.timeoutMap[key] = nil
384
- }
385
- }
386
-
387
- private func setTimeout(
388
- _ key: String,
389
- _ message: String,
390
- _ timeout: Double
391
- ) {
392
- let workItem = DispatchWorkItem {
393
- self.reject(key, message)
394
- }
395
- self.timeoutMap[key] = workItem
396
- DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: workItem)
397
- }
398
-
399
- private func setConnectionTimeout(
400
- _ key: String,
401
- _ message: String,
402
- _ device: Device,
403
- _ connectionTimeout: Double
404
- ) {
405
- let workItem = DispatchWorkItem {
406
- // do not call onDisconnnected, which is triggered by cancelPeripheralConnection
407
- let key = "onDisconnected|\(device.getId())"
408
- self.callbackMap[key] = nil
409
- self.centralManager.cancelPeripheralConnection(device.getPeripheral())
410
- self.reject(key, message)
411
- }
412
- self.timeoutMap[key] = workItem
413
- DispatchQueue.main.asyncAfter(deadline: .now() + connectionTimeout, execute: workItem)
414
- }
415
- }
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 let viewController: UIViewController?
18
+ private var displayStrings: [String: String]!
19
+ private let callbackMap = ThreadSafeDictionary<String, Callback>()
20
+ private var scanResultCallback: ScanResultCallback?
21
+ private var stateReceiver: StateReceiver?
22
+ private let timeoutMap = ThreadSafeDictionary<String, DispatchWorkItem>()
23
+ private var stopScanWorkItem: DispatchWorkItem?
24
+ private var alertController: UIAlertController?
25
+ private var deviceListView: DeviceListView?
26
+ private var popoverController: UIPopoverPresentationController?
27
+ private let discoveredDevices = ThreadSafeDictionary<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
+ self.viewController = viewController
37
+ super.init()
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.removeAll()
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 let scanDuration = scanDuration {
118
+ let workItem = DispatchWorkItem {
119
+ self.stopScan()
120
+ }
121
+ self.stopScanWorkItem = workItem
122
+ DispatchQueue.main.asyncAfter(deadline: .now() + scanDuration, execute: workItem)
123
+ }
124
+ self.centralManager.scanForPeripherals(
125
+ withServices: serviceUUIDs,
126
+ options: [CBCentralManagerScanOptionAllowDuplicatesKey: allowDuplicates]
127
+ )
128
+
129
+ if deviceListMode == .none {
130
+ self.resolve("startScanning", "Scan started.")
131
+ }
132
+ } else {
133
+ self.stopScan()
134
+ self.reject("startScanning", "Already scanning. Stopping now.")
135
+ }
136
+ }
137
+
138
+ func stopScan() {
139
+ log("Stop scanning.")
140
+ self.centralManager.stopScan()
141
+ self.stopScanWorkItem?.cancel()
142
+ self.stopScanWorkItem = nil
143
+ DispatchQueue.main.async { [weak self] in
144
+ guard let self = self else { return }
145
+ switch self.deviceListMode {
146
+ case .alert:
147
+ if self.discoveredDevices.count == 0 {
148
+ self.alertController?.title = self.displayStrings["noDeviceFound"]
149
+ } else {
150
+ self.alertController?.title = self.displayStrings["availableDevices"]
151
+ }
152
+ case .list:
153
+ if self.discoveredDevices.count == 0 {
154
+ self.deviceListView?.setTitle(self.displayStrings["noDeviceFound"])
155
+ } else {
156
+ self.deviceListView?.setTitle(self.displayStrings["availableDevices"])
157
+ }
158
+ case .none:
159
+ break
160
+ }
161
+ }
162
+ }
163
+
164
+ // didDiscover
165
+ func centralManager(
166
+ _ central: CBCentralManager,
167
+ didDiscover peripheral: CBPeripheral,
168
+ advertisementData: [String: Any],
169
+ rssi RSSI: NSNumber
170
+ ) {
171
+
172
+ guard peripheral.state != CBPeripheralState.connected else {
173
+ log("found connected device", peripheral.name ?? "Unknown")
174
+ // make sure we do not touch connected devices
175
+ return
176
+ }
177
+
178
+ guard self.passesNameFilter(peripheralName: peripheral.name) else { return }
179
+ guard self.passesNamePrefixFilter(peripheralName: peripheral.name) else { return }
180
+ guard ScanFilterUtils.passesManufacturerDataFilter(advertisementData, filters: self.manufacturerDataFilters) else { return }
181
+ guard ScanFilterUtils.passesServiceDataFilter(advertisementData, filters: self.serviceDataFilters) else { return }
182
+
183
+ let deviceId = peripheral.identifier.uuidString
184
+ let result = self.discoveredDevices.getOrInsert(
185
+ key: deviceId,
186
+ create: { Device(peripheral) },
187
+ update: { $0.updatePeripheral(peripheral) }
188
+ )
189
+ let device = result.value
190
+ let isNew = result.wasInserted
191
+
192
+ if isNew || self.allowDuplicates {
193
+ log("New device found: ", device.getName() ?? "Unknown")
194
+
195
+ switch deviceListMode {
196
+ case .none:
197
+ if let callback = self.scanResultCallback {
198
+ callback(device, advertisementData, RSSI)
199
+ }
200
+ case .alert:
201
+ DispatchQueue.main.async { [weak self] in
202
+ self?.alertController?.addAction(UIAlertAction(title: device.getName() ?? "Unknown", style: UIAlertAction.Style.default, handler: { (_) in
203
+ log("Selected device")
204
+ self?.stopScan()
205
+ self?.resolve("startScanning", device.getId())
206
+ }))
207
+ }
208
+ case .list:
209
+ DispatchQueue.main.async { [weak self] in
210
+ self?.deviceListView?.addItem(device.getName() ?? "Unknown", action: {
211
+ log("Selected device")
212
+ self?.stopScan()
213
+ self?.resolve("startScanning", device.getId())
214
+ })
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ func showDeviceList() {
221
+ switch deviceListMode {
222
+ case .none:
223
+ break
224
+ case .alert:
225
+ showDeviceListAlert()
226
+ case .list:
227
+ showDeviceListView()
228
+ }
229
+ }
230
+
231
+ func showDeviceListAlert() {
232
+ DispatchQueue.main.async { [weak self] in
233
+ self?.alertController = UIAlertController(title: self?.displayStrings["scanning"], message: nil, preferredStyle: UIAlertController.Style.alert)
234
+ self?.alertController?.addAction(UIAlertAction(title: self?.displayStrings["cancel"], style: UIAlertAction.Style.cancel, handler: { (_) in
235
+ log("Cancelled request device.")
236
+ self?.stopScan()
237
+ self?.reject("startScanning", "requestDevice cancelled.")
238
+ }))
239
+ self?.viewController?.present((self?.alertController)!, animated: true, completion: nil)
240
+ }
241
+ }
242
+
243
+ func showDeviceListView() {
244
+ DispatchQueue.main.async { [weak self] in
245
+ self?.deviceListView = DeviceListView()
246
+ if #available(macCatalyst 15.0, iOS 15.0, *) {
247
+ self?.deviceListView?.sheetPresentationController?.detents = [.medium()]
248
+ }
249
+ self?.viewController?.present((self?.deviceListView)!, animated: true, completion: nil)
250
+ self?.deviceListView?.setTitle(self?.displayStrings["scanning"])
251
+ self?.deviceListView?.setCancelButton(self?.displayStrings["cancel"], action: {
252
+ log("Cancelled request device.")
253
+ self?.stopScan()
254
+ self?.reject("startScanning", "requestDevice cancelled.")
255
+ })
256
+ }
257
+ }
258
+
259
+ func getDevices(
260
+ _ deviceUUIDs: [UUID]
261
+ ) -> [CBPeripheral] {
262
+ return self.centralManager.retrievePeripherals(withIdentifiers: deviceUUIDs)
263
+ }
264
+
265
+ func getConnectedDevices(
266
+ _ serviceUUIDs: [CBUUID]
267
+ ) -> [CBPeripheral] {
268
+ return self.centralManager.retrieveConnectedPeripherals(withServices: serviceUUIDs)
269
+ }
270
+
271
+ func connect(
272
+ _ device: Device,
273
+ _ connectionTimeout: Double,
274
+ _ callback: @escaping Callback
275
+ ) {
276
+ let key = "connect|\(device.getId())"
277
+ self.callbackMap[key] = callback
278
+ log("Connecting to peripheral", device.getPeripheral())
279
+ self.centralManager.connect(device.getPeripheral(), options: nil)
280
+ self.setConnectionTimeout(key, "Connection timeout.", device, connectionTimeout)
281
+ }
282
+
283
+ // didConnect
284
+ func centralManager(
285
+ _ central: CBCentralManager,
286
+ didConnect peripheral: CBPeripheral
287
+ ) {
288
+ log("Connected to device", peripheral)
289
+ let key = "connect|\(peripheral.identifier.uuidString)"
290
+ peripheral.discoverServices(nil)
291
+ self.resolve(key, "Successfully connected.")
292
+ // will wait for services in plugin call
293
+ }
294
+
295
+ // didFailToConnect
296
+ func centralManager(
297
+ _ central: CBCentralManager,
298
+ didFailToConnect peripheral: CBPeripheral,
299
+ error: Error?
300
+ ) {
301
+ let key = "connect|\(peripheral.identifier.uuidString)"
302
+ if let error = error {
303
+ self.reject(key, error.localizedDescription)
304
+ return
305
+ }
306
+ self.reject(key, "Failed to connect.")
307
+ }
308
+
309
+ func setOnDisconnected(
310
+ _ device: Device,
311
+ _ callback: @escaping Callback
312
+ ) {
313
+ let key = "onDisconnected|\(device.getId())"
314
+ self.callbackMap[key] = callback
315
+ }
316
+
317
+ func disconnect(
318
+ _ device: Device,
319
+ _ timeout: Double,
320
+ _ callback: @escaping Callback
321
+ ) {
322
+ let key = "disconnect|\(device.getId())"
323
+ self.callbackMap[key] = callback
324
+ if device.isConnected() == false {
325
+ self.resolve(key, "Disconnected.")
326
+ return
327
+ }
328
+ log("Disconnecting from peripheral", device.getPeripheral())
329
+ self.centralManager.cancelPeripheralConnection(device.getPeripheral())
330
+ self.setTimeout(key, "Disconnection timeout.", timeout)
331
+ }
332
+
333
+ // didDisconnectPeripheral
334
+ func centralManager(
335
+ _ central: CBCentralManager,
336
+ didDisconnectPeripheral peripheral: CBPeripheral,
337
+ error: Error?
338
+ ) {
339
+ let key = "disconnect|\(peripheral.identifier.uuidString)"
340
+ let keyOnDisconnected = "onDisconnected|\(peripheral.identifier.uuidString)"
341
+ self.resolve(keyOnDisconnected, "Disconnected.")
342
+ if let error = error {
343
+ log(error.localizedDescription)
344
+ self.reject(key, error.localizedDescription)
345
+ return
346
+ }
347
+ self.resolve(key, "Successfully disconnected.")
348
+ }
349
+
350
+ func getDevice(_ deviceId: String) -> Device? {
351
+ return self.discoveredDevices[deviceId]
352
+ }
353
+
354
+ private func passesNameFilter(peripheralName: String?) -> Bool {
355
+ guard let nameFilter = self.deviceNameFilter else { return true }
356
+ guard let name = peripheralName else { return false }
357
+ return name == nameFilter
358
+ }
359
+
360
+ private func passesNamePrefixFilter(peripheralName: String?) -> Bool {
361
+ guard let prefix = self.deviceNamePrefixFilter else { return true }
362
+ guard let name = peripheralName else { return false }
363
+ return name.hasPrefix(prefix)
364
+ }
365
+
366
+
367
+ private func resolve(_ key: String, _ value: String) {
368
+ guard let callback = self.callbackMap.removeValue(forKey: key) else { return }
369
+ self.timeoutMap.removeValue(forKey: key)?.cancel()
370
+ log("Resolve", key, value)
371
+ callback(true, value)
372
+ }
373
+
374
+ private func reject(_ key: String, _ value: String) {
375
+ guard let callback = self.callbackMap.removeValue(forKey: key) else { return }
376
+ self.timeoutMap.removeValue(forKey: key)?.cancel()
377
+ log("Reject", key, value)
378
+ callback(false, value)
379
+ }
380
+
381
+ private func setTimeout(
382
+ _ key: String,
383
+ _ message: String,
384
+ _ timeout: Double
385
+ ) {
386
+ let workItem = DispatchWorkItem {
387
+ self.reject(key, message)
388
+ }
389
+ self.timeoutMap[key] = workItem
390
+ DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: workItem)
391
+ }
392
+
393
+ private func setConnectionTimeout(
394
+ _ connectionKey: String,
395
+ _ message: String,
396
+ _ device: Device,
397
+ _ connectionTimeout: Double
398
+ ) {
399
+ let workItem = DispatchWorkItem {
400
+ // do not call onDisconnnected, which is triggered by cancelPeripheralConnection
401
+ let onDisconnectedKey = "onDisconnected|\(device.getId())"
402
+ self.callbackMap[onDisconnectedKey] = nil
403
+ self.centralManager.cancelPeripheralConnection(device.getPeripheral())
404
+ self.reject(connectionKey, message)
405
+ }
406
+ self.timeoutMap[connectionKey] = workItem
407
+ DispatchQueue.main.asyncAfter(deadline: .now() + connectionTimeout, execute: workItem)
408
+ }
409
+ }