@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.
- package/CapacitorCommunityBluetoothLe.podspec +17 -17
- package/LICENSE +21 -21
- package/Package.swift +28 -0
- package/README.md +68 -161
- package/android/build.gradle +71 -68
- package/android/src/main/AndroidManifest.xml +22 -22
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +1094 -1070
- 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 -767
- 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/docs.json +906 -849
- package/dist/esm/bleClient.d.ts +278 -278
- package/dist/esm/bleClient.js +361 -347
- package/dist/esm/bleClient.js.map +1 -1
- package/dist/esm/config.d.ts +53 -53
- package/dist/esm/config.js +2 -2
- package/dist/esm/conversion.d.ts +56 -34
- package/dist/esm/conversion.js +134 -84
- package/dist/esm/conversion.js.map +1 -1
- package/dist/esm/definitions.d.ts +352 -313
- package/dist/esm/definitions.js +42 -42
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +5 -5
- package/dist/esm/index.js +5 -5
- package/dist/esm/plugin.d.ts +2 -2
- package/dist/esm/plugin.js +4 -4
- package/dist/esm/queue.d.ts +3 -3
- package/dist/esm/queue.js +17 -17
- package/dist/esm/queue.js.map +1 -1
- package/dist/esm/timeout.d.ts +1 -1
- package/dist/esm/timeout.js +9 -9
- package/dist/esm/validators.d.ts +1 -1
- package/dist/esm/validators.js +11 -11
- package/dist/esm/validators.js.map +1 -1
- package/dist/esm/web.d.ts +57 -56
- package/dist/esm/web.js +403 -340
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +967 -837
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +967 -837
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin → Sources/BluetoothLe}/Conversion.swift +83 -83
- package/ios/{Plugin → Sources/BluetoothLe}/Device.swift +423 -423
- package/ios/Sources/BluetoothLe/DeviceListView.swift +121 -0
- package/ios/{Plugin → Sources/BluetoothLe}/DeviceManager.swift +503 -401
- package/ios/{Plugin → Sources/BluetoothLe}/Logging.swift +8 -8
- package/ios/{Plugin → Sources/BluetoothLe}/Plugin.swift +775 -682
- package/ios/{Plugin → Sources/BluetoothLe}/ThreadSafeDictionary.swift +15 -13
- package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -0
- package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -0
- package/package.json +115 -101
- package/ios/Plugin/Info.plist +0 -24
- package/ios/Plugin/Plugin.h +0 -10
- package/ios/Plugin/Plugin.m +0 -41
|
@@ -1,401 +1,503 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
private var
|
|
17
|
-
private var
|
|
18
|
-
private var
|
|
19
|
-
private var
|
|
20
|
-
private var
|
|
21
|
-
private var
|
|
22
|
-
private var
|
|
23
|
-
private var
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
self.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self.
|
|
56
|
-
|
|
57
|
-
self.emitState(enabled: false)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
self.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
self.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
self.centralManager.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
_
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
self.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
let
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|