@capacitor-community/bluetooth-le 7.2.0 → 8.0.0-0

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