@capacitor-community/bluetooth-le 7.2.0 → 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.
- package/README.md +68 -161
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +43 -19
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +82 -78
- package/dist/docs.json +906 -849
- package/dist/esm/bleClient.js +15 -1
- package/dist/esm/bleClient.js.map +1 -1
- package/dist/esm/conversion.d.ts +12 -0
- package/dist/esm/conversion.js +33 -0
- package/dist/esm/conversion.js.map +1 -1
- package/dist/esm/definitions.d.ts +41 -2
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -0
- package/dist/esm/web.js +66 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +115 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +115 -3
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/DeviceListView.swift +121 -0
- package/ios/Plugin/DeviceManager.swift +114 -13
- package/ios/Plugin/Plugin.swift +61 -6
- package/package.json +13 -3
|
@@ -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
|
|
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
|
-
_
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
self
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
self.
|
|
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 {
|
package/ios/Plugin/Plugin.swift
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
550
|
-
return
|
|
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
|
|
555
|
-
return
|
|
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.
|
|
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",
|