@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
@@ -0,0 +1,114 @@
1
+ import Foundation
2
+ import CoreBluetooth
3
+
4
+ struct ManufacturerDataFilter {
5
+ let companyIdentifier: UInt16
6
+ let dataPrefix: Data?
7
+ let mask: Data?
8
+ }
9
+
10
+ struct ServiceDataFilter {
11
+ let serviceUuid: CBUUID
12
+ let dataPrefix: Data?
13
+ let mask: Data?
14
+ }
15
+
16
+ class ScanFilterUtils {
17
+
18
+ static func passesManufacturerDataFilter(_ advertisementData: [String: Any], filters: [ManufacturerDataFilter]?) -> Bool {
19
+ guard let filters = filters, !filters.isEmpty else {
20
+ return true // No filters means everything passes
21
+ }
22
+
23
+ guard let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
24
+ manufacturerData.count >= 2 else {
25
+ return false // If there's no valid manufacturer data, fail
26
+ }
27
+
28
+ let companyIdentifier = manufacturerData.prefix(2).withUnsafeBytes {
29
+ $0.load(as: UInt16.self).littleEndian // Manufacturer ID is little-endian
30
+ }
31
+
32
+ let payload = Data(manufacturerData.dropFirst(2))
33
+
34
+ for filter in filters {
35
+ if filter.companyIdentifier != companyIdentifier {
36
+ continue // Skip if company ID does not match
37
+ }
38
+
39
+ if let dataPrefix = filter.dataPrefix {
40
+ if payload.count < dataPrefix.count {
41
+ continue // Payload too short, does not match
42
+ }
43
+
44
+ if let mask = filter.mask {
45
+ // Validate that mask length matches dataPrefix length
46
+ if mask.count != dataPrefix.count {
47
+ continue // Skip this filter if mask length is invalid
48
+ }
49
+ var matches = true
50
+ for i in 0..<dataPrefix.count {
51
+ if (payload[i] & mask[i]) != (dataPrefix[i] & mask[i]) {
52
+ matches = false
53
+ break
54
+ }
55
+ }
56
+ if matches {
57
+ return true
58
+ }
59
+ } else if payload.starts(with: dataPrefix) {
60
+ return true
61
+ }
62
+ } else {
63
+ return true // Company ID matched, and no dataPrefix required
64
+ }
65
+ }
66
+
67
+ return false // If none matched, return false
68
+ }
69
+
70
+ static func passesServiceDataFilter(_ advertisementData: [String: Any], filters: [ServiceDataFilter]?) -> Bool {
71
+ guard let filters = filters, !filters.isEmpty else {
72
+ return true // No filters means everything passes
73
+ }
74
+
75
+ guard let serviceDataDict = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] else {
76
+ return false // If there's no service data, fail
77
+ }
78
+
79
+ for filter in filters {
80
+ guard let serviceData = serviceDataDict[filter.serviceUuid] else {
81
+ continue // Skip if service UUID does not match
82
+ }
83
+
84
+ if let dataPrefix = filter.dataPrefix {
85
+ if serviceData.count < dataPrefix.count {
86
+ continue // Service data too short, does not match
87
+ }
88
+
89
+ if let mask = filter.mask {
90
+ // Validate that mask length matches dataPrefix length
91
+ if mask.count != dataPrefix.count {
92
+ continue // Skip this filter if mask length is invalid
93
+ }
94
+ var matches = true
95
+ for i in 0..<dataPrefix.count {
96
+ if (serviceData[i] & mask[i]) != (dataPrefix[i] & mask[i]) {
97
+ matches = false
98
+ break
99
+ }
100
+ }
101
+ if matches {
102
+ return true
103
+ }
104
+ } else if serviceData.starts(with: dataPrefix) {
105
+ return true
106
+ }
107
+ } else {
108
+ return true // Service UUID matched, and no dataPrefix required
109
+ }
110
+ }
111
+
112
+ return false // If none matched, return false
113
+ }
114
+ }
@@ -1,15 +1,61 @@
1
- import Foundation
2
-
3
- class ThreadSafeDictionary<K: Hashable, T> {
4
- private var dictionary: [K: T] = [:]
5
- private let queue = DispatchQueue(label: "threadSafeDictionaryQueue", attributes: .concurrent)
6
-
7
- subscript(key: K) -> T? {
8
- get {
9
- return queue.sync { dictionary[key] }
10
- }
11
- set {
12
- queue.async(flags: .barrier) { self.dictionary[key] = newValue }
13
- }
14
- }
15
- }
1
+ import Foundation
2
+
3
+ class ThreadSafeDictionary<K: Hashable, T> {
4
+ private var dictionary: [K: T] = [:]
5
+ private let queue = DispatchQueue(label: "threadSafeDictionaryQueue", attributes: .concurrent)
6
+
7
+ subscript(key: K) -> T? {
8
+ get {
9
+ return queue.sync { dictionary[key] }
10
+ }
11
+ set {
12
+ queue.async(flags: .barrier) { self.dictionary[key] = newValue }
13
+ }
14
+ }
15
+
16
+ func removeValue(forKey key: K) -> T? {
17
+ return queue.sync(flags: .barrier) {
18
+ return dictionary.removeValue(forKey: key)
19
+ }
20
+ }
21
+
22
+ var count: Int {
23
+ return queue.sync { dictionary.count }
24
+ }
25
+
26
+ func removeAll() {
27
+ queue.async(flags: .barrier) {
28
+ self.dictionary.removeAll()
29
+ }
30
+ }
31
+
32
+ /// Atomically gets existing value or inserts and returns new value
33
+ /// The create closure is only called if the key doesn't exist
34
+ /// Returns tuple of (value, wasInserted) where wasInserted indicates if a new value was created
35
+ func getOrInsert(key: K, create: () -> T) -> (value: T, wasInserted: Bool) {
36
+ return queue.sync(flags: .barrier) {
37
+ if let existing = dictionary[key] {
38
+ return (existing, false)
39
+ }
40
+ let newValue = create()
41
+ dictionary[key] = newValue
42
+ return (newValue, true)
43
+ }
44
+ }
45
+
46
+ /// Atomically gets existing value (calling update on it) or inserts new value
47
+ /// The create closure is only called if the key doesn't exist
48
+ /// The update closure is called on existing values before returning
49
+ /// Returns tuple of (value, wasInserted) where wasInserted indicates if a new value was created
50
+ func getOrInsert(key: K, create: () -> T, update: (T) -> Void) -> (value: T, wasInserted: Bool) {
51
+ return queue.sync(flags: .barrier) {
52
+ if let existing = dictionary[key] {
53
+ update(existing)
54
+ return (existing, false)
55
+ }
56
+ let newValue = create()
57
+ dictionary[key] = newValue
58
+ return (newValue, true)
59
+ }
60
+ }
61
+ }
@@ -1,55 +1,55 @@
1
- import Foundation
2
- import CoreBluetooth
3
- import XCTest
4
- @testable import BluetoothLe
5
-
6
- class ConversionTests: XCTestCase {
7
-
8
- func testDataToString() throws {
9
- let input = Data([0xA1, 0x2E, 0x38, 0xD4, 0x89, 0xC3])
10
- let output = dataToString(input)
11
- XCTAssertEqual(output, "a12e38d489c3")
12
- }
13
-
14
- func testStringToData() throws {
15
- let input = "a12e38d489c3"
16
- let output = stringToData(input)
17
- let expected = Data([0xA1, 0x2E, 0x38, 0xD4, 0x89, 0xC3])
18
- for (index, byte) in output.enumerated() {
19
- XCTAssertEqual(byte, expected[index])
20
- }
21
- }
22
-
23
- func testEmptyStringToData() throws {
24
- let input = ""
25
- let output = stringToData(input)
26
- XCTAssertEqual(output, Data([]))
27
- }
28
-
29
- func testCbuuidToString() throws {
30
- XCTAssertEqual(cbuuidToString(CBUUID(string: "180D")), "0000180d-0000-1000-8000-00805f9b34fb")
31
- XCTAssertEqual(cbuuidToString(CBUUID(string: "AAAA180D")), "aaaa180d-0000-1000-8000-00805f9b34fb")
32
- XCTAssertEqual(cbuuidToString(CBUUID(string: "fb005c80-02e7-f387-1cad-8acd2d8df0c8")), "fb005c80-02e7-f387-1cad-8acd2d8df0c8")
33
- }
34
-
35
- func testCbuuidToStringUppercase() throws {
36
- XCTAssertEqual(cbuuidToStringUppercase(CBUUID(string: "180D")), "0000180D-0000-1000-8000-00805F9B34FB")
37
- XCTAssertEqual(cbuuidToStringUppercase(CBUUID(string: "fb005c80-02e7-f387-1cad-8acd2d8df0c8")), "FB005C80-02E7-F387-1CAD-8ACD2D8DF0C8")
38
- }
39
-
40
- func testOptionalStringConversion() throws {
41
- let str: String? = "180D"
42
- XCTAssertEqual("\(str)", "Optional(\"180D\")")
43
- XCTAssertEqual("\(str!)", "180D")
44
- }
45
-
46
- func testDescriptorValueToString() throws {
47
- XCTAssertEqual(descriptorValueToString("Hello"), "48656c6c6f")
48
- XCTAssertEqual(descriptorValueToString(Data([0, 5, 255])), "0005ff")
49
- XCTAssertEqual(descriptorValueToString(UInt16(258)), "0201")
50
- XCTAssertEqual(descriptorValueToString(UInt16(1)), "0100")
51
- XCTAssertEqual(descriptorValueToString(NSNumber(1)), "0100")
52
- XCTAssertEqual(descriptorValueToString(0), "")
53
- }
54
-
55
- }
1
+ import Foundation
2
+ import CoreBluetooth
3
+ import XCTest
4
+ @testable import BluetoothLe
5
+
6
+ class ConversionTests: XCTestCase {
7
+
8
+ func testDataToString() throws {
9
+ let input = Data([0xA1, 0x2E, 0x38, 0xD4, 0x89, 0xC3])
10
+ let output = dataToString(input)
11
+ XCTAssertEqual(output, "a12e38d489c3")
12
+ }
13
+
14
+ func testStringToData() throws {
15
+ let input = "a12e38d489c3"
16
+ let output = stringToData(input)
17
+ let expected = Data([0xA1, 0x2E, 0x38, 0xD4, 0x89, 0xC3])
18
+ for (index, byte) in output.enumerated() {
19
+ XCTAssertEqual(byte, expected[index])
20
+ }
21
+ }
22
+
23
+ func testEmptyStringToData() throws {
24
+ let input = ""
25
+ let output = stringToData(input)
26
+ XCTAssertEqual(output, Data([]))
27
+ }
28
+
29
+ func testCbuuidToString() throws {
30
+ XCTAssertEqual(cbuuidToString(CBUUID(string: "180D")), "0000180d-0000-1000-8000-00805f9b34fb")
31
+ XCTAssertEqual(cbuuidToString(CBUUID(string: "AAAA180D")), "aaaa180d-0000-1000-8000-00805f9b34fb")
32
+ XCTAssertEqual(cbuuidToString(CBUUID(string: "fb005c80-02e7-f387-1cad-8acd2d8df0c8")), "fb005c80-02e7-f387-1cad-8acd2d8df0c8")
33
+ }
34
+
35
+ func testCbuuidToStringUppercase() throws {
36
+ XCTAssertEqual(cbuuidToStringUppercase(CBUUID(string: "180D")), "0000180D-0000-1000-8000-00805F9B34FB")
37
+ XCTAssertEqual(cbuuidToStringUppercase(CBUUID(string: "fb005c80-02e7-f387-1cad-8acd2d8df0c8")), "FB005C80-02E7-F387-1CAD-8ACD2D8DF0C8")
38
+ }
39
+
40
+ func testOptionalStringConversion() throws {
41
+ let str: String? = "180D"
42
+ XCTAssertEqual("\(str)", "Optional(\"180D\")")
43
+ XCTAssertEqual("\(str!)", "180D")
44
+ }
45
+
46
+ func testDescriptorValueToString() throws {
47
+ XCTAssertEqual(descriptorValueToString("Hello"), "48656c6c6f")
48
+ XCTAssertEqual(descriptorValueToString(Data([0, 5, 255])), "0005ff")
49
+ XCTAssertEqual(descriptorValueToString(UInt16(258)), "0201")
50
+ XCTAssertEqual(descriptorValueToString(UInt16(1)), "0100")
51
+ XCTAssertEqual(descriptorValueToString(NSNumber(1)), "0100")
52
+ XCTAssertEqual(descriptorValueToString(0), "")
53
+ }
54
+
55
+ }
@@ -1,27 +1,27 @@
1
- import XCTest
2
- import Capacitor
3
- @testable import BluetoothLe
4
-
5
- class PluginTests: XCTestCase {
6
-
7
- func testEcho() {
8
- // This is an example of a functional test case for a plugin.
9
- // Use XCTAssert and related functions to verify your tests produce the correct results.
10
-
11
- let value = "Hello, World!"
12
- XCTAssertEqual(1, 1)
13
-
14
- // let plugin = MyPlugin()
15
- //
16
- // let call = CAPPluginCall(callbackId: "test", options: [
17
- // "value": value
18
- // ], success: { (result, _) in
19
- // let resultValue = result!.data["value"] as? String
20
- // XCTAssertEqual(value, resultValue)
21
- // }, error: { (_) in
22
- // XCTFail("Error shouldn't have been called")
23
- // })
24
- //
25
- // plugin.echo(call!)
26
- }
27
- }
1
+ import XCTest
2
+ import Capacitor
3
+ @testable import BluetoothLe
4
+
5
+ class PluginTests: XCTestCase {
6
+
7
+ func testEcho() {
8
+ // This is an example of a functional test case for a plugin.
9
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
10
+
11
+ let value = "Hello, World!"
12
+ XCTAssertEqual(1, 1)
13
+
14
+ // let plugin = MyPlugin()
15
+ //
16
+ // let call = CAPPluginCall(callbackId: "test", options: [
17
+ // "value": value
18
+ // ], success: { (result, _) in
19
+ // let resultValue = result!.data["value"] as? String
20
+ // XCTAssertEqual(value, resultValue)
21
+ // }, error: { (_) in
22
+ // XCTFail("Error shouldn't have been called")
23
+ // })
24
+ //
25
+ // plugin.echo(call!)
26
+ }
27
+ }
@@ -0,0 +1,153 @@
1
+ import XCTest
2
+ import CoreBluetooth
3
+ @testable import BluetoothLe
4
+
5
+ class ScanFiltersTests: XCTestCase {
6
+
7
+ // MARK: - Manufacturer Data Filter Tests
8
+
9
+ func testManufacturerDataFilter_InvalidMaskLength() {
10
+ // Test that when mask.count != dataPrefix.count, the filter is skipped
11
+ // and returns false (no match)
12
+
13
+ // Create manufacturer data: 2 bytes company ID + 4 bytes payload
14
+ var manufacturerData = Data()
15
+ manufacturerData.append(contentsOf: [0x4C, 0x00]) // Apple company ID (0x004C in little-endian)
16
+ manufacturerData.append(contentsOf: [0x01, 0x02, 0x03, 0x04]) // 4 bytes payload
17
+
18
+ let advertisementData: [String: Any] = [
19
+ CBAdvertisementDataManufacturerDataKey: manufacturerData
20
+ ]
21
+
22
+ // Create filter with dataPrefix of 4 bytes but mask of only 2 bytes
23
+ // This should be skipped due to invalid mask length
24
+ let dataPrefix = Data([0x01, 0x02, 0x03, 0x04]) // 4 bytes
25
+ let mask = Data([0xFF, 0xFF]) // Only 2 bytes - invalid!
26
+
27
+ let filter = ManufacturerDataFilter(
28
+ companyIdentifier: 0x004C,
29
+ dataPrefix: dataPrefix,
30
+ mask: mask
31
+ )
32
+
33
+ // Should return false because the filter is skipped due to invalid mask
34
+ let result = ScanFilterUtils.passesManufacturerDataFilter(advertisementData, filters: [filter])
35
+ XCTAssertFalse(result, "Should return false when mask length doesn't match dataPrefix length")
36
+ }
37
+
38
+ func testManufacturerDataFilter_ValidMaskLength() {
39
+ // Test the valid case where mask and dataPrefix have the same length
40
+ var manufacturerData = Data()
41
+ manufacturerData.append(contentsOf: [0x4C, 0x00]) // Apple company ID
42
+ manufacturerData.append(contentsOf: [0x01, 0x02, 0x03, 0x04])
43
+
44
+ let advertisementData: [String: Any] = [
45
+ CBAdvertisementDataManufacturerDataKey: manufacturerData
46
+ ]
47
+
48
+ // Mask and dataPrefix have the same length - should work correctly
49
+ let dataPrefix = Data([0x01, 0x02, 0x03, 0x04])
50
+ let mask = Data([0xFF, 0xFF, 0xFF, 0xFF]) // Same length as dataPrefix
51
+
52
+ let filter = ManufacturerDataFilter(
53
+ companyIdentifier: 0x004C,
54
+ dataPrefix: dataPrefix,
55
+ mask: mask
56
+ )
57
+
58
+ let result = ScanFilterUtils.passesManufacturerDataFilter(advertisementData, filters: [filter])
59
+ XCTAssertTrue(result, "Should match when mask and dataPrefix have same length")
60
+ }
61
+
62
+ func testManufacturerDataFilter_NoMask() {
63
+ // Test without a mask - should use simple prefix matching
64
+ var manufacturerData = Data()
65
+ manufacturerData.append(contentsOf: [0x4C, 0x00])
66
+ manufacturerData.append(contentsOf: [0x01, 0x02, 0x03, 0x04])
67
+
68
+ let advertisementData: [String: Any] = [
69
+ CBAdvertisementDataManufacturerDataKey: manufacturerData
70
+ ]
71
+
72
+ let dataPrefix = Data([0x01, 0x02])
73
+ let filter = ManufacturerDataFilter(
74
+ companyIdentifier: 0x004C,
75
+ dataPrefix: dataPrefix,
76
+ mask: nil // No mask
77
+ )
78
+
79
+ let result = ScanFilterUtils.passesManufacturerDataFilter(advertisementData, filters: [filter])
80
+ XCTAssertTrue(result, "Should match with prefix matching when no mask is provided")
81
+ }
82
+
83
+ // MARK: - Service Data Filter Tests
84
+
85
+ func testServiceDataFilter_InvalidMaskLength() {
86
+ // Test that when mask.count != dataPrefix.count, the filter is skipped
87
+ // and returns false (no match)
88
+
89
+ let serviceUUID = CBUUID(string: "1234")
90
+ let serviceData = Data([0x01, 0x02, 0x03, 0x04]) // 4 bytes
91
+
92
+ let advertisementData: [String: Any] = [
93
+ CBAdvertisementDataServiceDataKey: [serviceUUID: serviceData]
94
+ ]
95
+
96
+ // Create filter with dataPrefix of 4 bytes but mask of only 2 bytes
97
+ // This should be skipped due to invalid mask length
98
+ let dataPrefix = Data([0x01, 0x02, 0x03, 0x04]) // 4 bytes
99
+ let mask = Data([0xFF, 0xFF]) // Only 2 bytes - invalid!
100
+
101
+ let filter = ServiceDataFilter(
102
+ serviceUuid: serviceUUID,
103
+ dataPrefix: dataPrefix,
104
+ mask: mask
105
+ )
106
+
107
+ // Should return false because the filter is skipped due to invalid mask
108
+ let result = ScanFilterUtils.passesServiceDataFilter(advertisementData, filters: [filter])
109
+ XCTAssertFalse(result, "Should return false when mask length doesn't match dataPrefix length")
110
+ }
111
+
112
+ func testServiceDataFilter_ValidMaskLength() {
113
+ // Test the valid case where mask and dataPrefix have the same length
114
+ let serviceUUID = CBUUID(string: "1234")
115
+ let serviceData = Data([0x01, 0x02, 0x03, 0x04])
116
+
117
+ let advertisementData: [String: Any] = [
118
+ CBAdvertisementDataServiceDataKey: [serviceUUID: serviceData]
119
+ ]
120
+
121
+ let dataPrefix = Data([0x01, 0x02, 0x03, 0x04])
122
+ let mask = Data([0xFF, 0xFF, 0xFF, 0xFF]) // Same length
123
+
124
+ let filter = ServiceDataFilter(
125
+ serviceUuid: serviceUUID,
126
+ dataPrefix: dataPrefix,
127
+ mask: mask
128
+ )
129
+
130
+ let result = ScanFilterUtils.passesServiceDataFilter(advertisementData, filters: [filter])
131
+ XCTAssertTrue(result, "Should match when mask and dataPrefix have same length")
132
+ }
133
+
134
+ func testServiceDataFilter_NoMask() {
135
+ // Test without a mask
136
+ let serviceUUID = CBUUID(string: "1234")
137
+ let serviceData = Data([0x01, 0x02, 0x03, 0x04])
138
+
139
+ let advertisementData: [String: Any] = [
140
+ CBAdvertisementDataServiceDataKey: [serviceUUID: serviceData]
141
+ ]
142
+
143
+ let dataPrefix = Data([0x01, 0x02])
144
+ let filter = ServiceDataFilter(
145
+ serviceUuid: serviceUUID,
146
+ dataPrefix: dataPrefix,
147
+ mask: nil
148
+ )
149
+
150
+ let result = ScanFilterUtils.passesServiceDataFilter(advertisementData, filters: [filter])
151
+ XCTAssertTrue(result, "Should match with prefix matching when no mask is provided")
152
+ }
153
+ }