@capacitor-community/bluetooth-le 8.0.0 → 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.
- package/CapacitorCommunityBluetoothLe.podspec +17 -17
- package/LICENSE +21 -21
- package/Package.swift +27 -27
- package/README.md +4 -2
- package/android/build.gradle +73 -73
- package/android/src/main/AndroidManifest.xml +22 -22
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +1094 -1094
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Conversion.kt +51 -51
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +771 -771
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt +28 -28
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceScanner.kt +189 -189
- package/dist/esm/bleClient.js.map +1 -1
- package/dist/esm/conversion.js.map +1 -1
- package/dist/esm/queue.js.map +1 -1
- package/dist/esm/validators.js.map +1 -1
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +41 -41
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +41 -41
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/BluetoothLe/Conversion.swift +83 -83
- package/ios/Sources/BluetoothLe/Device.swift +422 -423
- package/ios/Sources/BluetoothLe/DeviceListView.swift +121 -121
- package/ios/Sources/BluetoothLe/DeviceManager.swift +409 -503
- package/ios/Sources/BluetoothLe/Logging.swift +8 -8
- package/ios/Sources/BluetoothLe/Plugin.swift +768 -775
- package/ios/Sources/BluetoothLe/ScanFilters.swift +114 -0
- package/ios/Sources/BluetoothLe/ThreadSafeDictionary.swift +61 -15
- package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -55
- package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -27
- package/ios/Tests/BluetoothLeTests/ScanFiltersTests.swift +153 -0
- package/package.json +114 -115
|
@@ -1,503 +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
|
|
18
|
-
private var displayStrings: [String: String]!
|
|
19
|
-
private
|
|
20
|
-
private var scanResultCallback: ScanResultCallback?
|
|
21
|
-
private var stateReceiver: StateReceiver?
|
|
22
|
-
private
|
|
23
|
-
private var stopScanWorkItem: DispatchWorkItem?
|
|
24
|
-
private var alertController: UIAlertController?
|
|
25
|
-
private var deviceListView: DeviceListView?
|
|
26
|
-
private var popoverController: UIPopoverPresentationController?
|
|
27
|
-
private
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
118
|
-
|
|
119
|
-
self.stopScan()
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
self.
|
|
141
|
-
self.stopScanWorkItem
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
guard
|
|
179
|
-
|
|
180
|
-
guard
|
|
181
|
-
guard
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
self.scanResultCallback
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
case .
|
|
223
|
-
|
|
224
|
-
case .
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
self?.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
_
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
self.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
peripheral
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
_
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|