@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.
Files changed (32) hide show
  1. package/CapacitorCommunityBluetoothLe.podspec +17 -17
  2. package/LICENSE +21 -21
  3. package/Package.swift +27 -27
  4. package/README.md +4 -2
  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/esm/bleClient.js.map +1 -1
  13. package/dist/esm/conversion.js.map +1 -1
  14. package/dist/esm/queue.js.map +1 -1
  15. package/dist/esm/validators.js.map +1 -1
  16. package/dist/esm/web.js.map +1 -1
  17. package/dist/plugin.cjs.js +41 -41
  18. package/dist/plugin.cjs.js.map +1 -1
  19. package/dist/plugin.js +41 -41
  20. package/dist/plugin.js.map +1 -1
  21. package/ios/Sources/BluetoothLe/Conversion.swift +83 -83
  22. package/ios/Sources/BluetoothLe/Device.swift +422 -423
  23. package/ios/Sources/BluetoothLe/DeviceListView.swift +121 -121
  24. package/ios/Sources/BluetoothLe/DeviceManager.swift +409 -503
  25. package/ios/Sources/BluetoothLe/Logging.swift +8 -8
  26. package/ios/Sources/BluetoothLe/Plugin.swift +768 -775
  27. package/ios/Sources/BluetoothLe/ScanFilters.swift +114 -0
  28. package/ios/Sources/BluetoothLe/ThreadSafeDictionary.swift +61 -15
  29. package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -55
  30. package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -27
  31. package/ios/Tests/BluetoothLeTests/ScanFiltersTests.swift +153 -0
  32. package/package.json +114 -115
@@ -1,775 +1,768 @@
1
- // swiftlint:disable identifier_name
2
- // swiftlint:disable type_body_length
3
- import Foundation
4
- import Capacitor
5
- import CoreBluetooth
6
-
7
- let CONNECTION_TIMEOUT: Double = 10
8
- let DEFAULT_TIMEOUT: Double = 5
9
-
10
- struct ManufacturerDataFilter {
11
- let companyIdentifier: UInt16
12
- let dataPrefix: Data?
13
- let mask: Data?
14
- }
15
-
16
- struct ServiceDataFilter {
17
- let serviceUuid: CBUUID
18
- let dataPrefix: Data?
19
- let mask: Data?
20
- }
21
-
22
- @objc(BluetoothLe)
23
- public class BluetoothLe: CAPPlugin, CAPBridgedPlugin {
24
- public let identifier = "BluetoothLe"
25
- public let jsName = "BluetoothLe"
26
- public let pluginMethods: [CAPPluginMethod] = [
27
- CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
28
- CAPPluginMethod(name: "isEnabled", returnType: CAPPluginReturnPromise),
29
- CAPPluginMethod(name: "requestEnable", returnType: CAPPluginReturnPromise),
30
- CAPPluginMethod(name: "enable", returnType: CAPPluginReturnPromise),
31
- CAPPluginMethod(name: "disable", returnType: CAPPluginReturnPromise),
32
- CAPPluginMethod(name: "startEnabledNotifications", returnType: CAPPluginReturnPromise),
33
- CAPPluginMethod(name: "stopEnabledNotifications", returnType: CAPPluginReturnPromise),
34
- CAPPluginMethod(name: "isLocationEnabled", returnType: CAPPluginReturnPromise),
35
- CAPPluginMethod(name: "openLocationSettings", returnType: CAPPluginReturnPromise),
36
- CAPPluginMethod(name: "openBluetoothSettings", returnType: CAPPluginReturnPromise),
37
- CAPPluginMethod(name: "openAppSettings", returnType: CAPPluginReturnPromise),
38
- CAPPluginMethod(name: "setDisplayStrings", returnType: CAPPluginReturnPromise),
39
- CAPPluginMethod(name: "requestDevice", returnType: CAPPluginReturnPromise),
40
- CAPPluginMethod(name: "requestLEScan", returnType: CAPPluginReturnPromise),
41
- CAPPluginMethod(name: "stopLEScan", returnType: CAPPluginReturnPromise),
42
- CAPPluginMethod(name: "getDevices", returnType: CAPPluginReturnPromise),
43
- CAPPluginMethod(name: "discoverServices", returnType: CAPPluginReturnPromise),
44
- CAPPluginMethod(name: "getConnectedDevices", returnType: CAPPluginReturnPromise),
45
- CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise),
46
- CAPPluginMethod(name: "createBond", returnType: CAPPluginReturnPromise),
47
- CAPPluginMethod(name: "isBonded", returnType: CAPPluginReturnPromise),
48
- CAPPluginMethod(name: "getBondedDevices", returnType: CAPPluginReturnPromise),
49
- CAPPluginMethod(name: "disconnect", returnType: CAPPluginReturnPromise),
50
- CAPPluginMethod(name: "getServices", returnType: CAPPluginReturnPromise),
51
- CAPPluginMethod(name: "getMtu", returnType: CAPPluginReturnPromise),
52
- CAPPluginMethod(name: "requestConnectionPriority", returnType: CAPPluginReturnPromise),
53
- CAPPluginMethod(name: "readRssi", returnType: CAPPluginReturnPromise),
54
- CAPPluginMethod(name: "read", returnType: CAPPluginReturnPromise),
55
- CAPPluginMethod(name: "write", returnType: CAPPluginReturnPromise),
56
- CAPPluginMethod(name: "writeWithoutResponse", returnType: CAPPluginReturnPromise),
57
- CAPPluginMethod(name: "readDescriptor", returnType: CAPPluginReturnPromise),
58
- CAPPluginMethod(name: "writeDescriptor", returnType: CAPPluginReturnPromise),
59
- CAPPluginMethod(name: "startNotifications", returnType: CAPPluginReturnPromise),
60
- CAPPluginMethod(name: "stopNotifications", returnType: CAPPluginReturnPromise)
61
- ]
62
- typealias BleDevice = [String: Any]
63
- typealias BleService = [String: Any]
64
- typealias BleCharacteristic = [String: Any]
65
- typealias BleDescriptor = [String: Any]
66
- private var deviceManager: DeviceManager?
67
- private var deviceMap = [String: Device]()
68
- private var displayStrings = [String: String]()
69
-
70
- override public func load() {
71
- self.displayStrings = self.getDisplayStrings()
72
- }
73
-
74
- @objc func initialize(_ call: CAPPluginCall) {
75
- self.deviceManager = DeviceManager(self.bridge?.viewController, self.displayStrings, {(success, message) in
76
- if success {
77
- call.resolve()
78
- } else {
79
- call.reject(message)
80
- }
81
- })
82
- }
83
-
84
- @objc func isEnabled(_ call: CAPPluginCall) {
85
- guard let deviceManager = self.getDeviceManager(call) else { return }
86
- let enabled: Bool = deviceManager.isEnabled()
87
- call.resolve(["value": enabled])
88
- }
89
-
90
- @objc func requestEnable(_ call: CAPPluginCall) {
91
- call.unavailable("requestEnable is not available on iOS.")
92
- }
93
-
94
- @objc func enable(_ call: CAPPluginCall) {
95
- call.unavailable("enable is not available on iOS.")
96
- }
97
-
98
- @objc func disable(_ call: CAPPluginCall) {
99
- call.unavailable("disable is not available on iOS.")
100
- }
101
-
102
- @objc func startEnabledNotifications(_ call: CAPPluginCall) {
103
- guard let deviceManager = self.getDeviceManager(call) else { return }
104
- deviceManager.registerStateReceiver({(enabled) in
105
- self.notifyListeners("onEnabledChanged", data: ["value": enabled])
106
- })
107
- call.resolve()
108
- }
109
-
110
- @objc func stopEnabledNotifications(_ call: CAPPluginCall) {
111
- guard let deviceManager = self.getDeviceManager(call) else { return }
112
- deviceManager.unregisterStateReceiver()
113
- call.resolve()
114
- }
115
-
116
- @objc func isLocationEnabled(_ call: CAPPluginCall) {
117
- call.unavailable("isLocationEnabled is not available on iOS.")
118
- }
119
-
120
- @objc func openLocationSettings(_ call: CAPPluginCall) {
121
- call.unavailable("openLocationSettings is not available on iOS.")
122
- }
123
-
124
- @objc func openBluetoothSettings(_ call: CAPPluginCall) {
125
- call.unavailable("openBluetoothSettings is not available on iOS.")
126
- }
127
-
128
- @objc func openAppSettings(_ call: CAPPluginCall) {
129
- guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
130
- call.reject("Cannot open app settings.")
131
- return
132
- }
133
-
134
- DispatchQueue.main.async {
135
- if UIApplication.shared.canOpenURL(settingsUrl) {
136
- UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
137
- call.resolve([
138
- "value": success
139
- ])
140
- })
141
- } else {
142
- call.reject("Cannot open app settings.")
143
- }
144
- }
145
- }
146
-
147
- @objc func setDisplayStrings(_ call: CAPPluginCall) {
148
- for key in ["noDeviceFound", "availableDevices", "scanning", "cancel"] {
149
- if call.getString(key) != nil {
150
- self.displayStrings[key] = call.getString(key)
151
- }
152
- }
153
- call.resolve()
154
- }
155
-
156
- @objc func requestDevice(_ call: CAPPluginCall) {
157
- guard let deviceManager = self.getDeviceManager(call) else { return }
158
- deviceManager.setDisplayStrings(self.displayStrings)
159
-
160
- let serviceUUIDs = self.getServiceUUIDs(call)
161
- let name = call.getString("name")
162
- let namePrefix = call.getString("namePrefix")
163
- let manufacturerDataFilters = self.getManufacturerDataFilters(call)
164
- let serviceDataFilters = self.getServiceDataFilters(call)
165
-
166
- let displayModeString = (call.getString("displayMode") ?? "alert").lowercased()
167
- guard ["alert", "list"].contains(displayModeString) else {
168
- call.reject("Invalid displayMode '\(call.getString("displayMode") ?? "")'. Use 'alert' or 'list'.")
169
- return
170
- }
171
- let deviceListMode: DeviceListMode = displayModeString == "list" ? .list : .alert
172
-
173
- deviceManager.startScanning(
174
- serviceUUIDs,
175
- name,
176
- namePrefix,
177
- manufacturerDataFilters,
178
- serviceDataFilters,
179
- false,
180
- deviceListMode,
181
- 30,
182
- {(success, message) in
183
- if success {
184
- guard let device = deviceManager.getDevice(message) else {
185
- call.reject("Device not found.")
186
- return
187
- }
188
- self.deviceMap[device.getId()] = device
189
- let bleDevice: BleDevice = self.getBleDevice(device)
190
- call.resolve(bleDevice)
191
- } else {
192
- call.reject(message)
193
- }
194
- },
195
- { (_, _, _) in }
196
- )
197
- }
198
-
199
- @objc func requestLEScan(_ call: CAPPluginCall) {
200
- guard let deviceManager = self.getDeviceManager(call) else { return }
201
-
202
- let serviceUUIDs = self.getServiceUUIDs(call)
203
- let name = call.getString("name")
204
- let namePrefix = call.getString("namePrefix")
205
- let allowDuplicates = call.getBool("allowDuplicates", false)
206
- let manufacturerDataFilters = self.getManufacturerDataFilters(call)
207
- let serviceDataFilters = self.getServiceDataFilters(call)
208
-
209
- deviceManager.startScanning(
210
- serviceUUIDs,
211
- name,
212
- namePrefix,
213
- manufacturerDataFilters,
214
- serviceDataFilters,
215
- allowDuplicates,
216
- .none,
217
- nil,
218
- { (success, message) in
219
- if success {
220
- call.resolve()
221
- } else {
222
- call.reject(message)
223
- }
224
- }, { (device, advertisementData, rssi) in
225
- self.deviceMap[device.getId()] = device
226
- let data = self.getScanResult(device, advertisementData, rssi)
227
- self.notifyListeners("onScanResult", data: data)
228
- }
229
- )
230
- }
231
-
232
- @objc func stopLEScan(_ call: CAPPluginCall) {
233
- guard let deviceManager = self.getDeviceManager(call) else { return }
234
- deviceManager.stopScan()
235
- call.resolve()
236
- }
237
-
238
- @objc func getDevices(_ call: CAPPluginCall) {
239
- guard let deviceManager = self.getDeviceManager(call) else { return }
240
- guard let deviceIds = call.getArray("deviceIds", String.self) else {
241
- call.reject("deviceIds must be provided")
242
- return
243
- }
244
- let deviceUUIDs: [UUID] = deviceIds.compactMap({ deviceId in
245
- return UUID(uuidString: deviceId)
246
- })
247
- let peripherals = deviceManager.getDevices(deviceUUIDs)
248
- let bleDevices: [BleDevice] = peripherals.map({peripheral in
249
- let deviceId = peripheral.identifier.uuidString
250
- guard let device = self.deviceMap[deviceId] else {
251
- let newDevice = Device(peripheral)
252
- self.deviceMap[newDevice.getId()] = newDevice
253
- return self.getBleDevice(newDevice)
254
- }
255
- return self.getBleDevice(device)
256
- })
257
- call.resolve(["devices": bleDevices])
258
- }
259
-
260
- @objc func getConnectedDevices(_ call: CAPPluginCall) {
261
- guard let deviceManager = self.getDeviceManager(call) else { return }
262
- guard let services = call.getArray("services", String.self) else {
263
- call.reject("services must be provided")
264
- return
265
- }
266
- let serviceUUIDs: [CBUUID] = services.compactMap({ service in
267
- return CBUUID(string: service)
268
- })
269
- let peripherals = deviceManager.getConnectedDevices(serviceUUIDs)
270
- let bleDevices: [BleDevice] = peripherals.map({peripheral in
271
- let deviceId = peripheral.identifier.uuidString
272
- guard let device = self.deviceMap[deviceId] else {
273
- let newDevice = Device(peripheral)
274
- self.deviceMap[newDevice.getId()] = newDevice
275
- return self.getBleDevice(newDevice)
276
- }
277
- return self.getBleDevice(device)
278
- })
279
- call.resolve(["devices": bleDevices])
280
- }
281
-
282
- @objc func connect(_ call: CAPPluginCall) {
283
- guard self.getDeviceManager(call) != nil else { return }
284
- guard let device = self.getDevice(call, checkConnection: false) else { return }
285
- let timeout = self.getTimeout(call, defaultTimeout: CONNECTION_TIMEOUT)
286
- device.setOnConnected(timeout, {(success, message) in
287
- if success {
288
- // only resolve after service discovery
289
- call.resolve()
290
- } else {
291
- call.reject(message)
292
- }
293
- })
294
- self.deviceManager?.setOnDisconnected(device, {(_, _) in
295
- let key = "disconnected|\(device.getId())"
296
- self.notifyListeners(key, data: nil)
297
- })
298
- self.deviceManager?.connect(device, timeout, {(success, message) in
299
- if success {
300
- log("Connected to peripheral. Waiting for service discovery.")
301
- } else {
302
- call.reject(message)
303
- }
304
- })
305
-
306
- }
307
-
308
- @objc func createBond(_ call: CAPPluginCall) {
309
- call.unavailable("createBond is not available on iOS.")
310
- }
311
-
312
- @objc func isBonded(_ call: CAPPluginCall) {
313
- call.unavailable("isBonded is not available on iOS.")
314
- }
315
-
316
- @objc func getBondedDevices(_ call: CAPPluginCall) {
317
- call.unavailable("getBondedDevices is not available on iOS.")
318
- }
319
-
320
- @objc func disconnect(_ call: CAPPluginCall) {
321
- guard self.getDeviceManager(call) != nil else { return }
322
- guard let device = self.getDevice(call, checkConnection: false) else { return }
323
- let timeout = self.getTimeout(call)
324
- self.deviceManager?.disconnect(device, timeout, {(success, message) in
325
- if success {
326
- call.resolve()
327
- } else {
328
- call.reject(message)
329
- }
330
- })
331
- }
332
-
333
- @objc func getServices(_ call: CAPPluginCall) {
334
- guard self.getDeviceManager(call) != nil else { return }
335
- guard let device = self.getDevice(call) else { return }
336
- let services = device.getServices()
337
- var bleServices = [BleService]()
338
- for service in services {
339
- var bleCharacteristics = [BleCharacteristic]()
340
- for characteristic in service.characteristics ?? [] {
341
- var bleDescriptors = [BleDescriptor]()
342
- for descriptor in characteristic.descriptors ?? [] {
343
- bleDescriptors.append([
344
- "uuid": cbuuidToString(descriptor.uuid)
345
- ])
346
- }
347
- bleCharacteristics.append([
348
- "uuid": cbuuidToString(characteristic.uuid),
349
- "properties": getProperties(characteristic),
350
- "descriptors": bleDescriptors
351
- ])
352
- }
353
- bleServices.append([
354
- "uuid": cbuuidToString(service.uuid),
355
- "characteristics": bleCharacteristics
356
- ])
357
- }
358
- call.resolve(["services": bleServices])
359
- }
360
-
361
- private func getProperties(_ characteristic: CBCharacteristic) -> [String: Bool] {
362
- return [
363
- "broadcast": characteristic.properties.contains(CBCharacteristicProperties.broadcast),
364
- "read": characteristic.properties.contains(CBCharacteristicProperties.read),
365
- "writeWithoutResponse": characteristic.properties.contains(CBCharacteristicProperties.writeWithoutResponse),
366
- "write": characteristic.properties.contains(CBCharacteristicProperties.write),
367
- "notify": characteristic.properties.contains(CBCharacteristicProperties.notify),
368
- "indicate": characteristic.properties.contains(CBCharacteristicProperties.indicate),
369
- "authenticatedSignedWrites": characteristic.properties.contains(CBCharacteristicProperties.authenticatedSignedWrites),
370
- "extendedProperties": characteristic.properties.contains(CBCharacteristicProperties.extendedProperties),
371
- "notifyEncryptionRequired": characteristic.properties.contains(CBCharacteristicProperties.notifyEncryptionRequired),
372
- "indicateEncryptionRequired": characteristic.properties.contains(CBCharacteristicProperties.indicateEncryptionRequired)
373
- ]
374
- }
375
-
376
- @objc func discoverServices(_ call: CAPPluginCall) {
377
- guard self.getDeviceManager(call) != nil else { return }
378
- guard let device = self.getDevice(call) else { return }
379
- let timeout = self.getTimeout(call)
380
- device.discoverServices(timeout, {(success, value) in
381
- if success {
382
- call.resolve()
383
- } else {
384
- call.reject(value)
385
- }
386
- })
387
- }
388
-
389
- @objc func getMtu(_ call: CAPPluginCall) {
390
- guard self.getDeviceManager(call) != nil else { return }
391
- guard let device = self.getDevice(call) else { return }
392
- call.resolve([
393
- "value": device.getMtu()
394
- ])
395
- }
396
-
397
- @objc func requestConnectionPriority(_ call: CAPPluginCall) {
398
- call.unavailable("requestConnectionPriority is not available on iOS.")
399
- }
400
-
401
- @objc func readRssi(_ call: CAPPluginCall) {
402
- guard self.getDeviceManager(call) != nil else { return }
403
- guard let device = self.getDevice(call) else { return }
404
- let timeout = self.getTimeout(call)
405
- device.readRssi(timeout, {(success, value) in
406
- if success {
407
- call.resolve([
408
- "value": value
409
- ])
410
- } else {
411
- call.reject(value)
412
- }
413
- })
414
- }
415
-
416
- @objc func read(_ call: CAPPluginCall) {
417
- guard self.getDeviceManager(call) != nil else { return }
418
- guard let device = self.getDevice(call) else { return }
419
- guard let characteristic = self.getCharacteristic(call) else { return }
420
- let timeout = self.getTimeout(call)
421
- device.read(characteristic.0, characteristic.1, timeout, {(success, value) in
422
- if success {
423
- call.resolve([
424
- "value": value
425
- ])
426
- } else {
427
- call.reject(value)
428
- }
429
- })
430
- }
431
-
432
- @objc func write(_ call: CAPPluginCall) {
433
- guard self.getDeviceManager(call) != nil else { return }
434
- guard let device = self.getDevice(call) else { return }
435
- guard let characteristic = self.getCharacteristic(call) else { return }
436
- guard let value = call.getString("value") else {
437
- call.reject("value must be provided")
438
- return
439
- }
440
- let writeType = CBCharacteristicWriteType.withResponse
441
- let timeout = self.getTimeout(call)
442
- device.write(
443
- characteristic.0,
444
- characteristic.1,
445
- value,
446
- writeType,
447
- timeout, {(success, value) in
448
- if success {
449
- call.resolve()
450
- } else {
451
- call.reject(value)
452
- }
453
- })
454
- }
455
-
456
- @objc func writeWithoutResponse(_ call: CAPPluginCall) {
457
- guard self.getDeviceManager(call) != nil else { return }
458
- guard let device = self.getDevice(call) else { return }
459
- guard let characteristic = self.getCharacteristic(call) else { return }
460
- guard let value = call.getString("value") else {
461
- call.reject("value must be provided")
462
- return
463
- }
464
- let writeType = CBCharacteristicWriteType.withoutResponse
465
- let timeout = self.getTimeout(call)
466
- device.write(
467
- characteristic.0,
468
- characteristic.1,
469
- value,
470
- writeType,
471
- timeout, {(success, value) in
472
- if success {
473
- call.resolve()
474
- } else {
475
- call.reject(value)
476
- }
477
- })
478
- }
479
-
480
- @objc func readDescriptor(_ call: CAPPluginCall) {
481
- guard self.getDeviceManager(call) != nil else { return }
482
- guard let device = self.getDevice(call) else { return }
483
- guard let descriptor = self.getDescriptor(call) else { return }
484
- let timeout = self.getTimeout(call)
485
- device.readDescriptor(
486
- descriptor.0,
487
- descriptor.1,
488
- descriptor.2,
489
- timeout, {(success, value) in
490
- if success {
491
- call.resolve([
492
- "value": value
493
- ])
494
- } else {
495
- call.reject(value)
496
- }
497
- })
498
- }
499
-
500
- @objc func writeDescriptor(_ call: CAPPluginCall) {
501
- guard self.getDeviceManager(call) != nil else { return }
502
- guard let device = self.getDevice(call) else { return }
503
- guard let descriptor = self.getDescriptor(call) else { return }
504
- guard let value = call.getString("value") else {
505
- call.reject("value must be provided")
506
- return
507
- }
508
- let timeout = self.getTimeout(call)
509
- device.writeDescriptor(
510
- descriptor.0,
511
- descriptor.1,
512
- descriptor.2,
513
- value,
514
- timeout, {(success, value) in
515
- if success {
516
- call.resolve()
517
- } else {
518
- call.reject(value)
519
- }
520
- })
521
- }
522
-
523
- @objc func startNotifications(_ call: CAPPluginCall) {
524
- guard self.getDeviceManager(call) != nil else { return }
525
- guard let device = self.getDevice(call) else { return }
526
- guard let characteristic = self.getCharacteristic(call) else { return }
527
- let timeout = self.getTimeout(call)
528
- device.setNotifications(
529
- characteristic.0,
530
- characteristic.1,
531
- true, {(_, value) in
532
- let key = "notification|\(device.getId())|\(characteristic.0.uuidString.lowercased())|\(characteristic.1.uuidString.lowercased())"
533
- self.notifyListeners(key, data: ["value": value])
534
- },
535
- timeout, {(success, value) in
536
- if success {
537
- call.resolve()
538
- } else {
539
- call.reject(value)
540
- }
541
- })
542
- }
543
-
544
- @objc func stopNotifications(_ call: CAPPluginCall) {
545
- guard self.getDeviceManager(call) != nil else { return }
546
- guard let device = self.getDevice(call) else { return }
547
- guard let characteristic = self.getCharacteristic(call) else { return }
548
- let timeout = self.getTimeout(call)
549
- device.setNotifications(
550
- characteristic.0,
551
- characteristic.1,
552
- false,
553
- nil,
554
- timeout, {(success, value) in
555
- if success {
556
- call.resolve()
557
- } else {
558
- call.reject(value)
559
- }
560
- })
561
- }
562
-
563
- private func getDisplayStrings() -> [String: String] {
564
- let configDisplayStrings = getConfig().getObject("displayStrings") as? [String: String] ?? [String: String]()
565
- var displayStrings = [String: String]()
566
- displayStrings["noDeviceFound"] = configDisplayStrings["noDeviceFound"] ?? "No device found"
567
- displayStrings["availableDevices"] = configDisplayStrings["availableDevices"] ?? "Available devices"
568
- displayStrings["scanning"] = configDisplayStrings["scanning"] ?? "Scanning..."
569
- displayStrings["cancel"] = configDisplayStrings["cancel"] ?? "Cancel"
570
- return displayStrings
571
- }
572
-
573
- private func getDeviceManager(_ call: CAPPluginCall) -> DeviceManager? {
574
- guard let deviceManager = self.deviceManager else {
575
- call.reject("Bluetooth LE not initialized.")
576
- return nil
577
- }
578
- return deviceManager
579
- }
580
-
581
- private func getServiceUUIDs(_ call: CAPPluginCall) -> [CBUUID] {
582
- let services = call.getArray("services", String.self) ?? []
583
- let serviceUUIDs = services.map({(service) -> CBUUID in
584
- return CBUUID(string: service)
585
- })
586
- return serviceUUIDs
587
- }
588
-
589
- private func getManufacturerDataFilters(_ call: CAPPluginCall) -> [ManufacturerDataFilter]? {
590
- guard let manufacturerDataArray = call.getArray("manufacturerData") else {
591
- return nil
592
- }
593
-
594
- var manufacturerDataFilters: [ManufacturerDataFilter] = []
595
-
596
- for index in 0..<manufacturerDataArray.count {
597
- guard let dataObject = manufacturerDataArray[index] as? JSObject,
598
- let companyIdentifier = dataObject["companyIdentifier"] as? UInt16 else {
599
- // Invalid or missing company identifier
600
- return nil
601
- }
602
-
603
- let dataPrefix: Data? = {
604
- guard let prefixString = dataObject["dataPrefix"] as? String else { return nil }
605
- return stringToData(prefixString)
606
- }()
607
-
608
- let mask: Data? = {
609
- guard let maskString = dataObject["mask"] as? String else { return nil }
610
- return stringToData(maskString)
611
- }()
612
-
613
- let manufacturerFilter = ManufacturerDataFilter(
614
- companyIdentifier: companyIdentifier,
615
- dataPrefix: dataPrefix,
616
- mask: mask
617
- )
618
-
619
- manufacturerDataFilters.append(manufacturerFilter)
620
- }
621
-
622
- return manufacturerDataFilters
623
- }
624
-
625
- private func getServiceDataFilters(_ call: CAPPluginCall) -> [ServiceDataFilter]? {
626
- guard let serviceDataArray = call.getArray("serviceData") else {
627
- return nil
628
- }
629
-
630
- var serviceDataFilters: [ServiceDataFilter] = []
631
-
632
- for index in 0..<serviceDataArray.count {
633
- guard let dataObject = serviceDataArray[index] as? JSObject,
634
- let serviceUuidString = dataObject["serviceUuid"] as? String else {
635
- // Invalid or missing service UUID
636
- return nil
637
- }
638
-
639
- let serviceUuid = CBUUID(string: serviceUuidString)
640
-
641
- let dataPrefix: Data? = {
642
- guard let prefixString = dataObject["dataPrefix"] as? String else { return nil }
643
- return stringToData(prefixString)
644
- }()
645
-
646
- let mask: Data? = {
647
- guard let maskString = dataObject["mask"] as? String else { return nil }
648
- return stringToData(maskString)
649
- }()
650
-
651
- let serviceDataFilter = ServiceDataFilter(
652
- serviceUuid: serviceUuid,
653
- dataPrefix: dataPrefix,
654
- mask: mask
655
- )
656
-
657
- serviceDataFilters.append(serviceDataFilter)
658
- }
659
-
660
- return serviceDataFilters
661
- }
662
-
663
- private func getDevice(_ call: CAPPluginCall, checkConnection: Bool = true) -> Device? {
664
- guard let deviceId = call.getString("deviceId") else {
665
- call.reject("deviceId required.")
666
- return nil
667
- }
668
- guard let device = self.deviceMap[deviceId] else {
669
- call.reject("Device not found. Call 'requestDevice', 'requestLEScan' or 'getDevices' first.")
670
- return nil
671
- }
672
- if checkConnection {
673
- guard device.isConnected() else {
674
- call.reject("Not connected to device.")
675
- return nil
676
- }
677
- }
678
- return device
679
- }
680
-
681
- private func getTimeout(_ call: CAPPluginCall, defaultTimeout: Double = DEFAULT_TIMEOUT) -> Double {
682
- guard let timeout = call.getDouble("timeout") else {
683
- return defaultTimeout
684
- }
685
- return timeout / 1000
686
- }
687
-
688
- private func getCharacteristic(_ call: CAPPluginCall) -> (CBUUID, CBUUID)? {
689
- guard let service = call.getString("service") else {
690
- call.reject("Service UUID required.")
691
- return nil
692
- }
693
- let serviceUUID = CBUUID(string: service)
694
-
695
- guard let characteristic = call.getString("characteristic") else {
696
- call.reject("Characteristic UUID required.")
697
- return nil
698
- }
699
- let characteristicUUID = CBUUID(string: characteristic)
700
- return (serviceUUID, characteristicUUID)
701
- }
702
-
703
- private func getDescriptor(_ call: CAPPluginCall) -> (CBUUID, CBUUID, CBUUID)? {
704
- guard let characteristic = getCharacteristic(call) else {
705
- return nil
706
- }
707
- guard let descriptor = call.getString("descriptor") else {
708
- call.reject("Descriptor UUID required.")
709
- return nil
710
- }
711
- let descriptorUUID = CBUUID(string: descriptor)
712
-
713
- return (characteristic.0, characteristic.1, descriptorUUID)
714
- }
715
-
716
- private func getBleDevice(_ device: Device) -> BleDevice {
717
- var bleDevice = [
718
- "deviceId": device.getId()
719
- ]
720
- if device.getName() != nil {
721
- bleDevice["name"] = device.getName()
722
- }
723
- return bleDevice
724
- }
725
-
726
- private func getScanResult(_ device: Device, _ advertisementData: [String: Any], _ rssi: NSNumber) -> [String: Any] {
727
- var data = [
728
- "device": self.getBleDevice(device),
729
- "rssi": rssi,
730
- "txPower": advertisementData[CBAdvertisementDataTxPowerLevelKey] ?? 127,
731
- "uuids": (advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []).map({(uuid) -> String in
732
- return cbuuidToString(uuid)
733
- })
734
- ]
735
-
736
- let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String
737
- if localName != nil {
738
- data["localName"] = localName
739
- }
740
-
741
- let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
742
- if manufacturerData != nil {
743
- data["manufacturerData"] = self.getManufacturerData(data: manufacturerData!)
744
- }
745
-
746
- let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data]
747
- if serviceData != nil {
748
- data["serviceData"] = self.getServiceData(data: serviceData!)
749
- }
750
- return data
751
- }
752
-
753
- private func getManufacturerData(data: Data) -> [String: String] {
754
- var company = 0
755
- var rest = ""
756
- for (index, byte) in data.enumerated() {
757
- if index == 0 {
758
- company += Int(byte)
759
- } else if index == 1 {
760
- company += Int(byte) * 256
761
- } else {
762
- rest += String(format: "%02hhx ", byte)
763
- }
764
- }
765
- return [String(company): rest]
766
- }
767
-
768
- private func getServiceData(data: [CBUUID: Data]) -> [String: String] {
769
- var result: [String: String] = [:]
770
- for (key, value) in data {
771
- result[cbuuidToString(key)] = dataToString(value)
772
- }
773
- return result
774
- }
775
- }
1
+ // swiftlint:disable identifier_name
2
+ // swiftlint:disable type_body_length
3
+ import Foundation
4
+ import Capacitor
5
+ import CoreBluetooth
6
+
7
+ let CONNECTION_TIMEOUT: Double = 10
8
+ let DEFAULT_TIMEOUT: Double = 5
9
+
10
+ @objc(BluetoothLe)
11
+ public class BluetoothLe: CAPPlugin, CAPBridgedPlugin {
12
+ public let identifier = "BluetoothLe"
13
+ public let jsName = "BluetoothLe"
14
+ public let pluginMethods: [CAPPluginMethod] = [
15
+ CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "isEnabled", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "requestEnable", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "enable", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "disable", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "startEnabledNotifications", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "stopEnabledNotifications", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "isLocationEnabled", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "openLocationSettings", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "openBluetoothSettings", returnType: CAPPluginReturnPromise),
25
+ CAPPluginMethod(name: "openAppSettings", returnType: CAPPluginReturnPromise),
26
+ CAPPluginMethod(name: "setDisplayStrings", returnType: CAPPluginReturnPromise),
27
+ CAPPluginMethod(name: "requestDevice", returnType: CAPPluginReturnPromise),
28
+ CAPPluginMethod(name: "requestLEScan", returnType: CAPPluginReturnPromise),
29
+ CAPPluginMethod(name: "stopLEScan", returnType: CAPPluginReturnPromise),
30
+ CAPPluginMethod(name: "getDevices", returnType: CAPPluginReturnPromise),
31
+ CAPPluginMethod(name: "discoverServices", returnType: CAPPluginReturnPromise),
32
+ CAPPluginMethod(name: "getConnectedDevices", returnType: CAPPluginReturnPromise),
33
+ CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise),
34
+ CAPPluginMethod(name: "createBond", returnType: CAPPluginReturnPromise),
35
+ CAPPluginMethod(name: "isBonded", returnType: CAPPluginReturnPromise),
36
+ CAPPluginMethod(name: "getBondedDevices", returnType: CAPPluginReturnPromise),
37
+ CAPPluginMethod(name: "disconnect", returnType: CAPPluginReturnPromise),
38
+ CAPPluginMethod(name: "getServices", returnType: CAPPluginReturnPromise),
39
+ CAPPluginMethod(name: "getMtu", returnType: CAPPluginReturnPromise),
40
+ CAPPluginMethod(name: "requestConnectionPriority", returnType: CAPPluginReturnPromise),
41
+ CAPPluginMethod(name: "readRssi", returnType: CAPPluginReturnPromise),
42
+ CAPPluginMethod(name: "read", returnType: CAPPluginReturnPromise),
43
+ CAPPluginMethod(name: "write", returnType: CAPPluginReturnPromise),
44
+ CAPPluginMethod(name: "writeWithoutResponse", returnType: CAPPluginReturnPromise),
45
+ CAPPluginMethod(name: "readDescriptor", returnType: CAPPluginReturnPromise),
46
+ CAPPluginMethod(name: "writeDescriptor", returnType: CAPPluginReturnPromise),
47
+ CAPPluginMethod(name: "startNotifications", returnType: CAPPluginReturnPromise),
48
+ CAPPluginMethod(name: "stopNotifications", returnType: CAPPluginReturnPromise)
49
+ ]
50
+ typealias BleDevice = [String: Any]
51
+ typealias BleService = [String: Any]
52
+ typealias BleCharacteristic = [String: Any]
53
+ typealias BleDescriptor = [String: Any]
54
+ private var deviceManager: DeviceManager?
55
+ private let deviceMap = ThreadSafeDictionary<String, Device>()
56
+ private var displayStrings = [String: String]()
57
+
58
+ override public func load() {
59
+ self.displayStrings = self.getDisplayStrings()
60
+ }
61
+
62
+ @objc func initialize(_ call: CAPPluginCall) {
63
+ self.deviceManager = DeviceManager(self.bridge?.viewController, self.displayStrings, {(success, message) in
64
+ if success {
65
+ call.resolve()
66
+ } else {
67
+ call.reject(message)
68
+ }
69
+ })
70
+ }
71
+
72
+ @objc func isEnabled(_ call: CAPPluginCall) {
73
+ guard let deviceManager = self.getDeviceManager(call) else { return }
74
+ let enabled: Bool = deviceManager.isEnabled()
75
+ call.resolve(["value": enabled])
76
+ }
77
+
78
+ @objc func requestEnable(_ call: CAPPluginCall) {
79
+ call.unavailable("requestEnable is not available on iOS.")
80
+ }
81
+
82
+ @objc func enable(_ call: CAPPluginCall) {
83
+ call.unavailable("enable is not available on iOS.")
84
+ }
85
+
86
+ @objc func disable(_ call: CAPPluginCall) {
87
+ call.unavailable("disable is not available on iOS.")
88
+ }
89
+
90
+ @objc func startEnabledNotifications(_ call: CAPPluginCall) {
91
+ guard let deviceManager = self.getDeviceManager(call) else { return }
92
+ deviceManager.registerStateReceiver({(enabled) in
93
+ self.notifyListeners("onEnabledChanged", data: ["value": enabled])
94
+ })
95
+ call.resolve()
96
+ }
97
+
98
+ @objc func stopEnabledNotifications(_ call: CAPPluginCall) {
99
+ guard let deviceManager = self.getDeviceManager(call) else { return }
100
+ deviceManager.unregisterStateReceiver()
101
+ call.resolve()
102
+ }
103
+
104
+ @objc func isLocationEnabled(_ call: CAPPluginCall) {
105
+ call.unavailable("isLocationEnabled is not available on iOS.")
106
+ }
107
+
108
+ @objc func openLocationSettings(_ call: CAPPluginCall) {
109
+ call.unavailable("openLocationSettings is not available on iOS.")
110
+ }
111
+
112
+ @objc func openBluetoothSettings(_ call: CAPPluginCall) {
113
+ call.unavailable("openBluetoothSettings is not available on iOS.")
114
+ }
115
+
116
+ @objc func openAppSettings(_ call: CAPPluginCall) {
117
+ guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
118
+ call.reject("Cannot open app settings.")
119
+ return
120
+ }
121
+
122
+ DispatchQueue.main.async {
123
+ if UIApplication.shared.canOpenURL(settingsUrl) {
124
+ UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
125
+ call.resolve([
126
+ "value": success
127
+ ])
128
+ })
129
+ } else {
130
+ call.reject("Cannot open app settings.")
131
+ }
132
+ }
133
+ }
134
+
135
+ @objc func setDisplayStrings(_ call: CAPPluginCall) {
136
+ for key in ["noDeviceFound", "availableDevices", "scanning", "cancel"] {
137
+ if let value = call.getString(key) {
138
+ self.displayStrings[key] = value
139
+ }
140
+ }
141
+ call.resolve()
142
+ }
143
+
144
+ @objc func requestDevice(_ call: CAPPluginCall) {
145
+ guard let deviceManager = self.getDeviceManager(call) else { return }
146
+ deviceManager.setDisplayStrings(self.displayStrings)
147
+
148
+ let serviceUUIDs = self.getServiceUUIDs(call)
149
+ let name = call.getString("name")
150
+ let namePrefix = call.getString("namePrefix")
151
+ let manufacturerDataFilters = self.getManufacturerDataFilters(call)
152
+ let serviceDataFilters = self.getServiceDataFilters(call)
153
+
154
+ let displayModeString = (call.getString("displayMode") ?? "alert").lowercased()
155
+ guard ["alert", "list"].contains(displayModeString) else {
156
+ call.reject("Invalid displayMode '\(call.getString("displayMode") ?? "")'. Use 'alert' or 'list'.")
157
+ return
158
+ }
159
+ let deviceListMode: DeviceListMode = displayModeString == "list" ? .list : .alert
160
+
161
+ deviceManager.startScanning(
162
+ serviceUUIDs,
163
+ name,
164
+ namePrefix,
165
+ manufacturerDataFilters,
166
+ serviceDataFilters,
167
+ false,
168
+ deviceListMode,
169
+ 30,
170
+ {(success, message) in
171
+ if success {
172
+ guard let device = deviceManager.getDevice(message) else {
173
+ call.reject("Device not found.")
174
+ return
175
+ }
176
+ let storedDevice = self.deviceMap.getOrInsert(
177
+ key: device.getId(),
178
+ create: { device },
179
+ update: { $0.updatePeripheral(device.getPeripheral()) }
180
+ ).value
181
+ let bleDevice: BleDevice = self.getBleDevice(storedDevice)
182
+ call.resolve(bleDevice)
183
+ } else {
184
+ call.reject(message)
185
+ }
186
+ },
187
+ { (_, _, _) in }
188
+ )
189
+ }
190
+
191
+ @objc func requestLEScan(_ call: CAPPluginCall) {
192
+ guard let deviceManager = self.getDeviceManager(call) else { return }
193
+
194
+ let serviceUUIDs = self.getServiceUUIDs(call)
195
+ let name = call.getString("name")
196
+ let namePrefix = call.getString("namePrefix")
197
+ let allowDuplicates = call.getBool("allowDuplicates", false)
198
+ let manufacturerDataFilters = self.getManufacturerDataFilters(call)
199
+ let serviceDataFilters = self.getServiceDataFilters(call)
200
+
201
+ deviceManager.startScanning(
202
+ serviceUUIDs,
203
+ name,
204
+ namePrefix,
205
+ manufacturerDataFilters,
206
+ serviceDataFilters,
207
+ allowDuplicates,
208
+ .none,
209
+ nil,
210
+ { (success, message) in
211
+ if success {
212
+ call.resolve()
213
+ } else {
214
+ call.reject(message)
215
+ }
216
+ }, { (device, advertisementData, rssi) in
217
+ let storedDevice = self.deviceMap.getOrInsert(
218
+ key: device.getId(),
219
+ create: { device },
220
+ update: { $0.updatePeripheral(device.getPeripheral()) }
221
+ ).value
222
+ let data = self.getScanResult(storedDevice, advertisementData, rssi)
223
+ self.notifyListeners("onScanResult", data: data)
224
+ }
225
+ )
226
+ }
227
+
228
+ @objc func stopLEScan(_ call: CAPPluginCall) {
229
+ guard let deviceManager = self.getDeviceManager(call) else { return }
230
+ deviceManager.stopScan()
231
+ call.resolve()
232
+ }
233
+
234
+ @objc func getDevices(_ call: CAPPluginCall) {
235
+ guard let deviceManager = self.getDeviceManager(call) else { return }
236
+ guard let deviceIds = call.getArray("deviceIds", String.self) else {
237
+ call.reject("deviceIds must be provided")
238
+ return
239
+ }
240
+ let deviceUUIDs: [UUID] = deviceIds.compactMap({ deviceId in
241
+ return UUID(uuidString: deviceId)
242
+ })
243
+ let peripherals = deviceManager.getDevices(deviceUUIDs)
244
+ let bleDevices: [BleDevice] = peripherals.map({peripheral in
245
+ let deviceId = peripheral.identifier.uuidString
246
+ let device = self.deviceMap.getOrInsert(
247
+ key: deviceId,
248
+ create: { Device(peripheral) },
249
+ update: { $0.updatePeripheral(peripheral) }
250
+ ).value
251
+ return self.getBleDevice(device)
252
+ })
253
+ call.resolve(["devices": bleDevices])
254
+ }
255
+
256
+ @objc func getConnectedDevices(_ call: CAPPluginCall) {
257
+ guard let deviceManager = self.getDeviceManager(call) else { return }
258
+ guard let services = call.getArray("services", String.self) else {
259
+ call.reject("services must be provided")
260
+ return
261
+ }
262
+ let serviceUUIDs: [CBUUID] = services.compactMap({ service in
263
+ return CBUUID(string: service)
264
+ })
265
+ let peripherals = deviceManager.getConnectedDevices(serviceUUIDs)
266
+ let bleDevices: [BleDevice] = peripherals.map({peripheral in
267
+ let deviceId = peripheral.identifier.uuidString
268
+ let device = self.deviceMap.getOrInsert(
269
+ key: deviceId,
270
+ create: { Device(peripheral) },
271
+ update: { $0.updatePeripheral(peripheral) }
272
+ ).value
273
+ return self.getBleDevice(device)
274
+ })
275
+ call.resolve(["devices": bleDevices])
276
+ }
277
+
278
+ @objc func connect(_ call: CAPPluginCall) {
279
+ guard self.getDeviceManager(call) != nil else { return }
280
+ guard let device = self.getDevice(call, checkConnection: false) else { return }
281
+ let timeout = self.getTimeout(call, defaultTimeout: CONNECTION_TIMEOUT)
282
+ device.setOnConnected(timeout, {(success, message) in
283
+ if success {
284
+ // only resolve after service discovery
285
+ call.resolve()
286
+ } else {
287
+ call.reject(message)
288
+ }
289
+ })
290
+ self.deviceManager?.setOnDisconnected(device, {(_, _) in
291
+ let key = "disconnected|\(device.getId())"
292
+ self.notifyListeners(key, data: nil)
293
+ })
294
+ self.deviceManager?.connect(device, timeout, {(success, message) in
295
+ if success {
296
+ log("Connected to peripheral. Waiting for service discovery.")
297
+ } else {
298
+ call.reject(message)
299
+ }
300
+ })
301
+
302
+ }
303
+
304
+ @objc func createBond(_ call: CAPPluginCall) {
305
+ call.unavailable("createBond is not available on iOS.")
306
+ }
307
+
308
+ @objc func isBonded(_ call: CAPPluginCall) {
309
+ call.unavailable("isBonded is not available on iOS.")
310
+ }
311
+
312
+ @objc func getBondedDevices(_ call: CAPPluginCall) {
313
+ call.unavailable("getBondedDevices is not available on iOS.")
314
+ }
315
+
316
+ @objc func disconnect(_ call: CAPPluginCall) {
317
+ guard self.getDeviceManager(call) != nil else { return }
318
+ guard let device = self.getDevice(call, checkConnection: false) else { return }
319
+ let timeout = self.getTimeout(call)
320
+ self.deviceManager?.disconnect(device, timeout, {(success, message) in
321
+ if success {
322
+ call.resolve()
323
+ } else {
324
+ call.reject(message)
325
+ }
326
+ })
327
+ }
328
+
329
+ @objc func getServices(_ call: CAPPluginCall) {
330
+ guard self.getDeviceManager(call) != nil else { return }
331
+ guard let device = self.getDevice(call) else { return }
332
+ let services = device.getServices()
333
+ var bleServices = [BleService]()
334
+ for service in services {
335
+ var bleCharacteristics = [BleCharacteristic]()
336
+ for characteristic in service.characteristics ?? [] {
337
+ var bleDescriptors = [BleDescriptor]()
338
+ for descriptor in characteristic.descriptors ?? [] {
339
+ bleDescriptors.append([
340
+ "uuid": cbuuidToString(descriptor.uuid)
341
+ ])
342
+ }
343
+ bleCharacteristics.append([
344
+ "uuid": cbuuidToString(characteristic.uuid),
345
+ "properties": getProperties(characteristic),
346
+ "descriptors": bleDescriptors
347
+ ])
348
+ }
349
+ bleServices.append([
350
+ "uuid": cbuuidToString(service.uuid),
351
+ "characteristics": bleCharacteristics
352
+ ])
353
+ }
354
+ call.resolve(["services": bleServices])
355
+ }
356
+
357
+ private func getProperties(_ characteristic: CBCharacteristic) -> [String: Bool] {
358
+ return [
359
+ "broadcast": characteristic.properties.contains(CBCharacteristicProperties.broadcast),
360
+ "read": characteristic.properties.contains(CBCharacteristicProperties.read),
361
+ "writeWithoutResponse": characteristic.properties.contains(CBCharacteristicProperties.writeWithoutResponse),
362
+ "write": characteristic.properties.contains(CBCharacteristicProperties.write),
363
+ "notify": characteristic.properties.contains(CBCharacteristicProperties.notify),
364
+ "indicate": characteristic.properties.contains(CBCharacteristicProperties.indicate),
365
+ "authenticatedSignedWrites": characteristic.properties.contains(CBCharacteristicProperties.authenticatedSignedWrites),
366
+ "extendedProperties": characteristic.properties.contains(CBCharacteristicProperties.extendedProperties),
367
+ "notifyEncryptionRequired": characteristic.properties.contains(CBCharacteristicProperties.notifyEncryptionRequired),
368
+ "indicateEncryptionRequired": characteristic.properties.contains(CBCharacteristicProperties.indicateEncryptionRequired)
369
+ ]
370
+ }
371
+
372
+ @objc func discoverServices(_ call: CAPPluginCall) {
373
+ guard self.getDeviceManager(call) != nil else { return }
374
+ guard let device = self.getDevice(call) else { return }
375
+ let timeout = self.getTimeout(call)
376
+ device.discoverServices(timeout, {(success, value) in
377
+ if success {
378
+ call.resolve()
379
+ } else {
380
+ call.reject(value)
381
+ }
382
+ })
383
+ }
384
+
385
+ @objc func getMtu(_ call: CAPPluginCall) {
386
+ guard self.getDeviceManager(call) != nil else { return }
387
+ guard let device = self.getDevice(call) else { return }
388
+ call.resolve([
389
+ "value": device.getMtu()
390
+ ])
391
+ }
392
+
393
+ @objc func requestConnectionPriority(_ call: CAPPluginCall) {
394
+ call.unavailable("requestConnectionPriority is not available on iOS.")
395
+ }
396
+
397
+ @objc func readRssi(_ call: CAPPluginCall) {
398
+ guard self.getDeviceManager(call) != nil else { return }
399
+ guard let device = self.getDevice(call) else { return }
400
+ let timeout = self.getTimeout(call)
401
+ device.readRssi(timeout, {(success, value) in
402
+ if success {
403
+ call.resolve([
404
+ "value": value
405
+ ])
406
+ } else {
407
+ call.reject(value)
408
+ }
409
+ })
410
+ }
411
+
412
+ @objc func read(_ call: CAPPluginCall) {
413
+ guard self.getDeviceManager(call) != nil else { return }
414
+ guard let device = self.getDevice(call) else { return }
415
+ guard let characteristic = self.getCharacteristic(call) else { return }
416
+ let timeout = self.getTimeout(call)
417
+ device.read(characteristic.0, characteristic.1, timeout, {(success, value) in
418
+ if success {
419
+ call.resolve([
420
+ "value": value
421
+ ])
422
+ } else {
423
+ call.reject(value)
424
+ }
425
+ })
426
+ }
427
+
428
+ @objc func write(_ call: CAPPluginCall) {
429
+ guard self.getDeviceManager(call) != nil else { return }
430
+ guard let device = self.getDevice(call) else { return }
431
+ guard let characteristic = self.getCharacteristic(call) else { return }
432
+ guard let value = call.getString("value") else {
433
+ call.reject("value must be provided")
434
+ return
435
+ }
436
+ let writeType = CBCharacteristicWriteType.withResponse
437
+ let timeout = self.getTimeout(call)
438
+ device.write(
439
+ characteristic.0,
440
+ characteristic.1,
441
+ value,
442
+ writeType,
443
+ timeout, {(success, value) in
444
+ if success {
445
+ call.resolve()
446
+ } else {
447
+ call.reject(value)
448
+ }
449
+ })
450
+ }
451
+
452
+ @objc func writeWithoutResponse(_ call: CAPPluginCall) {
453
+ guard self.getDeviceManager(call) != nil else { return }
454
+ guard let device = self.getDevice(call) else { return }
455
+ guard let characteristic = self.getCharacteristic(call) else { return }
456
+ guard let value = call.getString("value") else {
457
+ call.reject("value must be provided")
458
+ return
459
+ }
460
+ let writeType = CBCharacteristicWriteType.withoutResponse
461
+ let timeout = self.getTimeout(call)
462
+ device.write(
463
+ characteristic.0,
464
+ characteristic.1,
465
+ value,
466
+ writeType,
467
+ timeout, {(success, value) in
468
+ if success {
469
+ call.resolve()
470
+ } else {
471
+ call.reject(value)
472
+ }
473
+ })
474
+ }
475
+
476
+ @objc func readDescriptor(_ call: CAPPluginCall) {
477
+ guard self.getDeviceManager(call) != nil else { return }
478
+ guard let device = self.getDevice(call) else { return }
479
+ guard let descriptor = self.getDescriptor(call) else { return }
480
+ let timeout = self.getTimeout(call)
481
+ device.readDescriptor(
482
+ descriptor.0,
483
+ descriptor.1,
484
+ descriptor.2,
485
+ timeout, {(success, value) in
486
+ if success {
487
+ call.resolve([
488
+ "value": value
489
+ ])
490
+ } else {
491
+ call.reject(value)
492
+ }
493
+ })
494
+ }
495
+
496
+ @objc func writeDescriptor(_ call: CAPPluginCall) {
497
+ guard self.getDeviceManager(call) != nil else { return }
498
+ guard let device = self.getDevice(call) else { return }
499
+ guard let descriptor = self.getDescriptor(call) else { return }
500
+ guard let value = call.getString("value") else {
501
+ call.reject("value must be provided")
502
+ return
503
+ }
504
+ let timeout = self.getTimeout(call)
505
+ device.writeDescriptor(
506
+ descriptor.0,
507
+ descriptor.1,
508
+ descriptor.2,
509
+ value,
510
+ timeout, {(success, value) in
511
+ if success {
512
+ call.resolve()
513
+ } else {
514
+ call.reject(value)
515
+ }
516
+ })
517
+ }
518
+
519
+ @objc func startNotifications(_ call: CAPPluginCall) {
520
+ guard self.getDeviceManager(call) != nil else { return }
521
+ guard let device = self.getDevice(call) else { return }
522
+ guard let characteristic = self.getCharacteristic(call) else { return }
523
+ let timeout = self.getTimeout(call)
524
+ device.setNotifications(
525
+ characteristic.0,
526
+ characteristic.1,
527
+ true, {(_, value) in
528
+ let key = "notification|\(device.getId())|\(characteristic.0.uuidString.lowercased())|\(characteristic.1.uuidString.lowercased())"
529
+ self.notifyListeners(key, data: ["value": value])
530
+ },
531
+ timeout, {(success, value) in
532
+ if success {
533
+ call.resolve()
534
+ } else {
535
+ call.reject(value)
536
+ }
537
+ })
538
+ }
539
+
540
+ @objc func stopNotifications(_ call: CAPPluginCall) {
541
+ guard self.getDeviceManager(call) != nil else { return }
542
+ guard let device = self.getDevice(call) else { return }
543
+ guard let characteristic = self.getCharacteristic(call) else { return }
544
+ let timeout = self.getTimeout(call)
545
+ device.setNotifications(
546
+ characteristic.0,
547
+ characteristic.1,
548
+ false,
549
+ nil,
550
+ timeout, {(success, value) in
551
+ if success {
552
+ call.resolve()
553
+ } else {
554
+ call.reject(value)
555
+ }
556
+ })
557
+ }
558
+
559
+ private func getDisplayStrings() -> [String: String] {
560
+ let configDisplayStrings = getConfig().getObject("displayStrings") as? [String: String] ?? [String: String]()
561
+ var displayStrings = [String: String]()
562
+ displayStrings["noDeviceFound"] = configDisplayStrings["noDeviceFound"] ?? "No device found"
563
+ displayStrings["availableDevices"] = configDisplayStrings["availableDevices"] ?? "Available devices"
564
+ displayStrings["scanning"] = configDisplayStrings["scanning"] ?? "Scanning..."
565
+ displayStrings["cancel"] = configDisplayStrings["cancel"] ?? "Cancel"
566
+ return displayStrings
567
+ }
568
+
569
+ private func getDeviceManager(_ call: CAPPluginCall) -> DeviceManager? {
570
+ guard let deviceManager = self.deviceManager else {
571
+ call.reject("Bluetooth LE not initialized.")
572
+ return nil
573
+ }
574
+ return deviceManager
575
+ }
576
+
577
+ private func getServiceUUIDs(_ call: CAPPluginCall) -> [CBUUID] {
578
+ let services = call.getArray("services", String.self) ?? []
579
+ let serviceUUIDs = services.map({(service) -> CBUUID in
580
+ return CBUUID(string: service)
581
+ })
582
+ return serviceUUIDs
583
+ }
584
+
585
+ private func getManufacturerDataFilters(_ call: CAPPluginCall) -> [ManufacturerDataFilter]? {
586
+ guard let manufacturerDataArray = call.getArray("manufacturerData") else {
587
+ return nil
588
+ }
589
+
590
+ var manufacturerDataFilters: [ManufacturerDataFilter] = []
591
+
592
+ for index in 0..<manufacturerDataArray.count {
593
+ guard let dataObject = manufacturerDataArray[index] as? JSObject,
594
+ let companyIdentifier = dataObject["companyIdentifier"] as? UInt16 else {
595
+ // Invalid or missing company identifier
596
+ return nil
597
+ }
598
+
599
+ let dataPrefix: Data? = {
600
+ guard let prefixString = dataObject["dataPrefix"] as? String else { return nil }
601
+ return stringToData(prefixString)
602
+ }()
603
+
604
+ let mask: Data? = {
605
+ guard let maskString = dataObject["mask"] as? String else { return nil }
606
+ return stringToData(maskString)
607
+ }()
608
+
609
+ let manufacturerFilter = ManufacturerDataFilter(
610
+ companyIdentifier: companyIdentifier,
611
+ dataPrefix: dataPrefix,
612
+ mask: mask
613
+ )
614
+
615
+ manufacturerDataFilters.append(manufacturerFilter)
616
+ }
617
+
618
+ return manufacturerDataFilters
619
+ }
620
+
621
+ private func getServiceDataFilters(_ call: CAPPluginCall) -> [ServiceDataFilter]? {
622
+ guard let serviceDataArray = call.getArray("serviceData") else {
623
+ return nil
624
+ }
625
+
626
+ var serviceDataFilters: [ServiceDataFilter] = []
627
+
628
+ for index in 0..<serviceDataArray.count {
629
+ guard let dataObject = serviceDataArray[index] as? JSObject,
630
+ let serviceUuidString = dataObject["serviceUuid"] as? String else {
631
+ // Invalid or missing service UUID
632
+ return nil
633
+ }
634
+
635
+ let serviceUuid = CBUUID(string: serviceUuidString)
636
+
637
+ let dataPrefix: Data? = {
638
+ guard let prefixString = dataObject["dataPrefix"] as? String else { return nil }
639
+ return stringToData(prefixString)
640
+ }()
641
+
642
+ let mask: Data? = {
643
+ guard let maskString = dataObject["mask"] as? String else { return nil }
644
+ return stringToData(maskString)
645
+ }()
646
+
647
+ let serviceDataFilter = ServiceDataFilter(
648
+ serviceUuid: serviceUuid,
649
+ dataPrefix: dataPrefix,
650
+ mask: mask
651
+ )
652
+
653
+ serviceDataFilters.append(serviceDataFilter)
654
+ }
655
+
656
+ return serviceDataFilters
657
+ }
658
+
659
+ private func getDevice(_ call: CAPPluginCall, checkConnection: Bool = true) -> Device? {
660
+ guard let deviceId = call.getString("deviceId") else {
661
+ call.reject("deviceId required.")
662
+ return nil
663
+ }
664
+ guard let device = self.deviceMap[deviceId] else {
665
+ call.reject("Device not found. Call 'requestDevice', 'requestLEScan' or 'getDevices' first.")
666
+ return nil
667
+ }
668
+ if checkConnection {
669
+ guard device.isConnected() else {
670
+ call.reject("Not connected to device.")
671
+ return nil
672
+ }
673
+ }
674
+ return device
675
+ }
676
+
677
+ private func getTimeout(_ call: CAPPluginCall, defaultTimeout: Double = DEFAULT_TIMEOUT) -> Double {
678
+ guard let timeout = call.getDouble("timeout") else {
679
+ return defaultTimeout
680
+ }
681
+ return timeout / 1000
682
+ }
683
+
684
+ private func getCharacteristic(_ call: CAPPluginCall) -> (CBUUID, CBUUID)? {
685
+ guard let service = call.getString("service") else {
686
+ call.reject("Service UUID required.")
687
+ return nil
688
+ }
689
+ let serviceUUID = CBUUID(string: service)
690
+
691
+ guard let characteristic = call.getString("characteristic") else {
692
+ call.reject("Characteristic UUID required.")
693
+ return nil
694
+ }
695
+ let characteristicUUID = CBUUID(string: characteristic)
696
+ return (serviceUUID, characteristicUUID)
697
+ }
698
+
699
+ private func getDescriptor(_ call: CAPPluginCall) -> (CBUUID, CBUUID, CBUUID)? {
700
+ guard let characteristic = getCharacteristic(call) else {
701
+ return nil
702
+ }
703
+ guard let descriptor = call.getString("descriptor") else {
704
+ call.reject("Descriptor UUID required.")
705
+ return nil
706
+ }
707
+ let descriptorUUID = CBUUID(string: descriptor)
708
+
709
+ return (characteristic.0, characteristic.1, descriptorUUID)
710
+ }
711
+
712
+ private func getBleDevice(_ device: Device) -> BleDevice {
713
+ var bleDevice = [
714
+ "deviceId": device.getId()
715
+ ]
716
+ if let name = device.getName() {
717
+ bleDevice["name"] = name
718
+ }
719
+ return bleDevice
720
+ }
721
+
722
+ private func getScanResult(_ device: Device, _ advertisementData: [String: Any], _ rssi: NSNumber) -> [String: Any] {
723
+ var data = [
724
+ "device": self.getBleDevice(device),
725
+ "rssi": rssi,
726
+ "txPower": advertisementData[CBAdvertisementDataTxPowerLevelKey] ?? 127,
727
+ "uuids": (advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []).map({(uuid) -> String in
728
+ return cbuuidToString(uuid)
729
+ })
730
+ ]
731
+
732
+ if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
733
+ data["localName"] = localName
734
+ }
735
+
736
+ if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data {
737
+ data["manufacturerData"] = self.getManufacturerData(data: manufacturerData)
738
+ }
739
+
740
+ if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
741
+ data["serviceData"] = self.getServiceData(data: serviceData)
742
+ }
743
+ return data
744
+ }
745
+
746
+ private func getManufacturerData(data: Data) -> [String: String] {
747
+ var company = 0
748
+ var rest = ""
749
+ for (index, byte) in data.enumerated() {
750
+ if index == 0 {
751
+ company += Int(byte)
752
+ } else if index == 1 {
753
+ company += Int(byte) * 256
754
+ } else {
755
+ rest += String(format: "%02hhx ", byte)
756
+ }
757
+ }
758
+ return [String(company): rest]
759
+ }
760
+
761
+ private func getServiceData(data: [CBUUID: Data]) -> [String: String] {
762
+ var result: [String: String] = [:]
763
+ for (key, value) in data {
764
+ result[cbuuidToString(key)] = dataToString(value)
765
+ }
766
+ return result
767
+ }
768
+ }