@capacitor-community/bluetooth-le 7.1.1 → 7.3.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.
@@ -1,6 +1,12 @@
1
1
  import Foundation
2
2
  import CoreBluetooth
3
3
 
4
+ enum DeviceListMode {
5
+ case none
6
+ case alert
7
+ case list
8
+ }
9
+
4
10
  class DeviceManager: NSObject, CBCentralManagerDelegate {
5
11
  typealias Callback = (_ success: Bool, _ message: String) -> Void
6
12
  typealias StateReceiver = (_ enabled: Bool) -> Void
@@ -15,12 +21,15 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
15
21
  private var timeoutMap = [String: DispatchWorkItem]()
16
22
  private var stopScanWorkItem: DispatchWorkItem?
17
23
  private var alertController: UIAlertController?
24
+ private var deviceListView: DeviceListView?
25
+ private var popoverController: UIPopoverPresentationController?
18
26
  private var discoveredDevices = [String: Device]()
19
27
  private var deviceNameFilter: String?
20
28
  private var deviceNamePrefixFilter: String?
21
- private var shouldShowDeviceList = false
29
+ private var deviceListMode: DeviceListMode = .none
22
30
  private var allowDuplicates = false
23
31
  private var manufacturerDataFilters: [ManufacturerDataFilter]?
32
+ private var serviceDataFilters: [ServiceDataFilter]?
24
33
 
25
34
  init(_ viewController: UIViewController?, _ displayStrings: [String: String], _ callback: @escaping Callback) {
26
35
  super.init()
@@ -81,8 +90,9 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
81
90
  _ name: String?,
82
91
  _ namePrefix: String?,
83
92
  _ manufacturerDataFilters: [ManufacturerDataFilter]?,
93
+ _ serviceDataFilters: [ServiceDataFilter]?,
84
94
  _ allowDuplicates: Bool,
85
- _ shouldShowDeviceList: Bool,
95
+ _ deviceListMode: DeviceListMode,
86
96
  _ scanDuration: Double?,
87
97
  _ callback: @escaping Callback,
88
98
  _ scanResultCallback: @escaping ScanResultCallback
@@ -92,13 +102,14 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
92
102
 
93
103
  if self.centralManager.isScanning == false {
94
104
  self.discoveredDevices = [String: Device]()
95
- self.shouldShowDeviceList = shouldShowDeviceList
105
+ self.deviceListMode = deviceListMode
96
106
  self.allowDuplicates = allowDuplicates
97
107
  self.deviceNameFilter = name
98
108
  self.deviceNamePrefixFilter = namePrefix
99
109
  self.manufacturerDataFilters = manufacturerDataFilters
110
+ self.serviceDataFilters = serviceDataFilters
100
111
 
101
- if shouldShowDeviceList {
112
+ if deviceListMode != .none {
102
113
  self.showDeviceList()
103
114
  }
104
115
 
@@ -113,7 +124,7 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
113
124
  options: [CBCentralManagerScanOptionAllowDuplicatesKey: allowDuplicates]
114
125
  )
115
126
 
116
- if shouldShowDeviceList == false {
127
+ if deviceListMode == .none {
117
128
  self.resolve("startScanning", "Scan started.")
118
129
  }
119
130
  } else {
@@ -128,10 +139,22 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
128
139
  self.stopScanWorkItem?.cancel()
129
140
  self.stopScanWorkItem = nil
130
141
  DispatchQueue.main.async { [weak self] in
131
- if self?.discoveredDevices.count == 0 {
132
- self?.alertController?.title = self?.displayStrings["noDeviceFound"]
133
- } else {
134
- self?.alertController?.title = self?.displayStrings["availableDevices"]
142
+ guard let self = self else { return }
143
+ switch self.deviceListMode {
144
+ case .alert:
145
+ if self.discoveredDevices.count == 0 {
146
+ self.alertController?.title = self.displayStrings["noDeviceFound"]
147
+ } else {
148
+ self.alertController?.title = self.displayStrings["availableDevices"]
149
+ }
150
+ case .list:
151
+ if self.discoveredDevices.count == 0 {
152
+ self.deviceListView?.setTitle(self.displayStrings["noDeviceFound"])
153
+ } else {
154
+ self.deviceListView?.setTitle(self.displayStrings["availableDevices"])
155
+ }
156
+ case .none:
157
+ break
135
158
  }
136
159
  }
137
160
  }
@@ -156,6 +179,7 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
156
179
  guard self.passesNameFilter(peripheralName: peripheral.name) else { return }
157
180
  guard self.passesNamePrefixFilter(peripheralName: peripheral.name) else { return }
158
181
  guard self.passesManufacturerDataFilter(advertisementData) else { return }
182
+ guard self.passesServiceDataFilter(advertisementData) else { return }
159
183
 
160
184
  let device: Device
161
185
  if self.allowDuplicates, let knownDevice = discoveredDevices.first(where: { $0.key == peripheral.identifier.uuidString })?.value {
@@ -166,7 +190,12 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
166
190
  }
167
191
  log("New device found: ", device.getName() ?? "Unknown")
168
192
 
169
- if shouldShowDeviceList {
193
+ switch deviceListMode {
194
+ case .none:
195
+ if self.scanResultCallback != nil {
196
+ self.scanResultCallback!(device, advertisementData, RSSI)
197
+ }
198
+ case .alert:
170
199
  DispatchQueue.main.async { [weak self] in
171
200
  self?.alertController?.addAction(UIAlertAction(title: device.getName() ?? "Unknown", style: UIAlertAction.Style.default, handler: { (_) in
172
201
  log("Selected device")
@@ -174,14 +203,29 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
174
203
  self?.resolve("startScanning", device.getId())
175
204
  }))
176
205
  }
177
- } else {
178
- if self.scanResultCallback != nil {
179
- self.scanResultCallback!(device, advertisementData, RSSI)
206
+ case .list:
207
+ DispatchQueue.main.async { [weak self] in
208
+ self?.deviceListView?.addItem(device.getName() ?? "Unknown", action: {
209
+ log("Selected device")
210
+ self?.stopScan()
211
+ self?.resolve("startScanning", device.getId())
212
+ })
180
213
  }
181
214
  }
182
215
  }
183
216
 
184
217
  func showDeviceList() {
218
+ switch deviceListMode {
219
+ case .none:
220
+ break
221
+ case .alert:
222
+ showDeviceListAlert()
223
+ case .list:
224
+ showDeviceListView()
225
+ }
226
+ }
227
+
228
+ func showDeviceListAlert() {
185
229
  DispatchQueue.main.async { [weak self] in
186
230
  self?.alertController = UIAlertController(title: self?.displayStrings["scanning"], message: nil, preferredStyle: UIAlertController.Style.alert)
187
231
  self?.alertController?.addAction(UIAlertAction(title: self?.displayStrings["cancel"], style: UIAlertAction.Style.cancel, handler: { (_) in
@@ -193,6 +237,22 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
193
237
  }
194
238
  }
195
239
 
240
+ func showDeviceListView() {
241
+ DispatchQueue.main.async { [weak self] in
242
+ self?.deviceListView = DeviceListView()
243
+ if #available(macCatalyst 15.0, iOS 15.0, *) {
244
+ self?.deviceListView?.sheetPresentationController?.detents = [.medium()]
245
+ }
246
+ self?.viewController?.present((self?.deviceListView)!, animated: true, completion: nil)
247
+ self?.deviceListView?.setTitle(self?.displayStrings["scanning"])
248
+ self?.deviceListView?.setCancelButton(self?.displayStrings["cancel"], action: {
249
+ log("Cancelled request device.")
250
+ self?.stopScan()
251
+ self?.reject("startScanning", "requestDevice cancelled.")
252
+ })
253
+ }
254
+ }
255
+
196
256
  func getDevices(
197
257
  _ deviceUUIDs: [UUID]
198
258
  ) -> [CBPeripheral] {
@@ -348,6 +408,47 @@ class DeviceManager: NSObject, CBCentralManagerDelegate {
348
408
  return false // If none matched, return false
349
409
  }
350
410
 
411
+ private func passesServiceDataFilter(_ advertisementData: [String: Any]) -> Bool {
412
+ guard let filters = self.serviceDataFilters, !filters.isEmpty else {
413
+ return true // No filters means everything passes
414
+ }
415
+
416
+ guard let serviceDataDict = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] else {
417
+ return false // If there's no service data, fail
418
+ }
419
+
420
+ for filter in filters {
421
+ guard let serviceData = serviceDataDict[filter.serviceUuid] else {
422
+ continue // Skip if service UUID does not match
423
+ }
424
+
425
+ if let dataPrefix = filter.dataPrefix {
426
+ if serviceData.count < dataPrefix.count {
427
+ continue // Service data too short, does not match
428
+ }
429
+
430
+ if let mask = filter.mask {
431
+ var matches = true
432
+ for i in 0..<dataPrefix.count {
433
+ if (serviceData[i] & mask[i]) != (dataPrefix[i] & mask[i]) {
434
+ matches = false
435
+ break
436
+ }
437
+ }
438
+ if matches {
439
+ return true
440
+ }
441
+ } else if serviceData.starts(with: dataPrefix) {
442
+ return true
443
+ }
444
+ } else {
445
+ return true // Service UUID matched, and no dataPrefix required
446
+ }
447
+ }
448
+
449
+ return false // If none matched, return false
450
+ }
451
+
351
452
  private func resolve(_ key: String, _ value: String) {
352
453
  let callback = self.callbackMap[key]
353
454
  if callback != nil {
@@ -13,6 +13,12 @@ struct ManufacturerDataFilter {
13
13
  let mask: Data?
14
14
  }
15
15
 
16
+ struct ServiceDataFilter {
17
+ let serviceUuid: CBUUID
18
+ let dataPrefix: Data?
19
+ let mask: Data?
20
+ }
21
+
16
22
  @objc(BluetoothLe)
17
23
  public class BluetoothLe: CAPPlugin {
18
24
  typealias BleDevice = [String: Any]
@@ -117,14 +123,23 @@ public class BluetoothLe: CAPPlugin {
117
123
  let name = call.getString("name")
118
124
  let namePrefix = call.getString("namePrefix")
119
125
  let manufacturerDataFilters = self.getManufacturerDataFilters(call)
126
+ let serviceDataFilters = self.getServiceDataFilters(call)
127
+
128
+ let displayModeString = (call.getString("displayMode") ?? "alert").lowercased()
129
+ guard ["alert", "list"].contains(displayModeString) else {
130
+ call.reject("Invalid displayMode '\(call.getString("displayMode") ?? "")'. Use 'alert' or 'list'.")
131
+ return
132
+ }
133
+ let deviceListMode: DeviceListMode = displayModeString == "list" ? .list : .alert
120
134
 
121
135
  deviceManager.startScanning(
122
136
  serviceUUIDs,
123
137
  name,
124
138
  namePrefix,
125
139
  manufacturerDataFilters,
140
+ serviceDataFilters,
126
141
  false,
127
- true,
142
+ deviceListMode,
128
143
  30,
129
144
  {(success, message) in
130
145
  if success {
@@ -151,14 +166,16 @@ public class BluetoothLe: CAPPlugin {
151
166
  let namePrefix = call.getString("namePrefix")
152
167
  let allowDuplicates = call.getBool("allowDuplicates", false)
153
168
  let manufacturerDataFilters = self.getManufacturerDataFilters(call)
169
+ let serviceDataFilters = self.getServiceDataFilters(call)
154
170
 
155
171
  deviceManager.startScanning(
156
172
  serviceUUIDs,
157
173
  name,
158
174
  namePrefix,
159
175
  manufacturerDataFilters,
176
+ serviceDataFilters,
160
177
  allowDuplicates,
161
- false,
178
+ .none,
162
179
  nil,
163
180
  { (success, message) in
164
181
  if success {
@@ -546,13 +563,13 @@ public class BluetoothLe: CAPPlugin {
546
563
  }
547
564
 
548
565
  let dataPrefix: Data? = {
549
- guard let prefixArray = dataObject["dataPrefix"] as? [Int] else { return nil }
550
- return Data(prefixArray.map { UInt8($0 & 0xFF) })
566
+ guard let prefixString = dataObject["dataPrefix"] as? String else { return nil }
567
+ return stringToData(prefixString)
551
568
  }()
552
569
 
553
570
  let mask: Data? = {
554
- guard let maskArray = dataObject["mask"] as? [Int] else { return nil }
555
- return Data(maskArray.map { UInt8($0 & 0xFF) })
571
+ guard let maskString = dataObject["mask"] as? String else { return nil }
572
+ return stringToData(maskString)
556
573
  }()
557
574
 
558
575
  let manufacturerFilter = ManufacturerDataFilter(
@@ -567,6 +584,44 @@ public class BluetoothLe: CAPPlugin {
567
584
  return manufacturerDataFilters
568
585
  }
569
586
 
587
+ private func getServiceDataFilters(_ call: CAPPluginCall) -> [ServiceDataFilter]? {
588
+ guard let serviceDataArray = call.getArray("serviceData") else {
589
+ return nil
590
+ }
591
+
592
+ var serviceDataFilters: [ServiceDataFilter] = []
593
+
594
+ for index in 0..<serviceDataArray.count {
595
+ guard let dataObject = serviceDataArray[index] as? JSObject,
596
+ let serviceUuidString = dataObject["serviceUuid"] as? String else {
597
+ // Invalid or missing service UUID
598
+ return nil
599
+ }
600
+
601
+ let serviceUuid = CBUUID(string: serviceUuidString)
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 serviceDataFilter = ServiceDataFilter(
614
+ serviceUuid: serviceUuid,
615
+ dataPrefix: dataPrefix,
616
+ mask: mask
617
+ )
618
+
619
+ serviceDataFilters.append(serviceDataFilter)
620
+ }
621
+
622
+ return serviceDataFilters
623
+ }
624
+
570
625
  private func getDevice(_ call: CAPPluginCall, checkConnection: Bool = true) -> Device? {
571
626
  guard let deviceId = call.getString("deviceId") else {
572
627
  call.reject("deviceId required.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capacitor-community/bluetooth-le",
3
- "version": "7.1.1",
3
+ "version": "7.3.0",
4
4
  "description": "Capacitor plugin for Bluetooth Low Energy ",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -19,7 +19,7 @@
19
19
  "prettier": "prettier \"**/*.{css,html,ts,js}\"",
20
20
  "swiftlint": "node-swiftlint",
21
21
  "docgen": "docgen --api BleClientInterface --output-readme README.md --output-json dist/docs.json",
22
- "postdocgen": "prettier README.md --write",
22
+ "postdocgen": "node scripts/fix-docgen-types.js && prettier README.md --write",
23
23
  "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
24
24
  "clean": "rimraf ./dist",
25
25
  "watch": "tsc --watch",
@@ -85,7 +85,17 @@
85
85
  "prettier": "@ionic/prettier-config",
86
86
  "swiftlint": "@ionic/swiftlint-config",
87
87
  "eslintConfig": {
88
- "extends": "@ionic/eslint-config/recommended"
88
+ "extends": "@ionic/eslint-config/recommended",
89
+ "rules": {
90
+ "@typescript-eslint/no-unused-vars": [
91
+ "error",
92
+ {
93
+ "argsIgnorePattern": "^_",
94
+ "varsIgnorePattern": "^_",
95
+ "caughtErrorsIgnorePattern": "^_"
96
+ }
97
+ ]
98
+ }
89
99
  },
90
100
  "repository": {
91
101
  "type": "git",