@mentra/bluetooth-sdk 3.2.0-dev.195 → 3.2.0-dev.203

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 (63) hide show
  1. package/README.md +64 -45
  2. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -1
  3. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +77 -1
  4. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +12 -6
  5. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
  6. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  7. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +318 -36
  8. package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
  9. package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +88 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
  11. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +50 -16
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +626 -120
  14. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycle.kt +64 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +76 -0
  17. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +16 -3
  18. package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
  19. package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
  20. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoBridgeTest.kt +33 -0
  21. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +169 -0
  22. package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
  23. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceManifestTest.kt +57 -0
  24. package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +52 -0
  25. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
  26. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +94 -0
  27. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycleTest.kt +112 -0
  28. package/build/BluetoothSdk.types.d.ts +62 -1
  29. package/build/BluetoothSdk.types.d.ts.map +1 -1
  30. package/build/BluetoothSdk.types.js.map +1 -1
  31. package/build/_private/BluetoothSdkModule.d.ts +3 -2
  32. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  33. package/build/_private/BluetoothSdkModule.js.map +1 -1
  34. package/build/generated/changelogCatalog.d.ts +1 -1
  35. package/build/generated/changelogCatalog.js +1 -1
  36. package/build/generated/changelogCatalog.js.map +1 -1
  37. package/build/generated/releaseMetadata.js +5 -5
  38. package/build/generated/releaseMetadata.js.map +1 -1
  39. package/build/index.d.ts +1 -1
  40. package/build/index.d.ts.map +1 -1
  41. package/build/index.js +3 -0
  42. package/build/index.js.map +1 -1
  43. package/ios/BluetoothSdkModule.swift +8 -2
  44. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  45. package/ios/Source/Bridge.swift +78 -1
  46. package/ios/Source/DeviceManager.swift +13 -6
  47. package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
  48. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  49. package/ios/Source/MentraBluetoothSDK.swift +397 -99
  50. package/ios/Source/WifiRequestResolution.swift +251 -0
  51. package/ios/Source/requests/VersionInfoResponseAccumulator.swift +101 -0
  52. package/ios/Source/sgcs/MentraLive.swift +179 -37
  53. package/ios/Source/sgcs/SGCManager.swift +13 -1
  54. package/ios/Source/status/DeviceStatus.swift +30 -3
  55. package/ios/Tests/VersionInfoBridgeTests.swift +32 -0
  56. package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +181 -0
  57. package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
  58. package/package.json +1 -1
  59. package/src/BluetoothSdk.types.ts +76 -1
  60. package/src/_private/BluetoothSdkModule.ts +4 -1
  61. package/src/generated/changelogCatalog.ts +1 -1
  62. package/src/generated/releaseMetadata.ts +5 -5
  63. package/src/index.ts +11 -0
@@ -0,0 +1,251 @@
1
+ import CoreFoundation
2
+ import Foundation
3
+
4
+ private func isWifiProtocolV1(_ raw: Any?) -> Bool {
5
+ guard let number = raw as? NSNumber,
6
+ CFGetTypeID(number) != CFBooleanGetTypeID()
7
+ else { return false }
8
+ return number.doubleValue == 1
9
+ }
10
+
11
+ /// Validate raw fields before convenience conversions erase malformed or partial tuples.
12
+ func wifiResponseEnvelopeIsValid(_ values: [String: Any], allowLegacy: Bool) -> Bool {
13
+ for key in ["connected", "dispatched"] where values[key] != nil {
14
+ guard let number = values[key] as? NSNumber, CFGetTypeID(number) == CFBooleanGetTypeID()
15
+ else { return false }
16
+ }
17
+ let hasTuple = ["protocol_version", "requestId", "sid"].contains { values[$0] != nil }
18
+ if !hasTuple { return allowLegacy && values["outcome"] == nil }
19
+ guard let requestId = values["requestId"] as? String,
20
+ let sid = values["sid"] as? String
21
+ else { return false }
22
+ return isWifiProtocolV1(values["protocol_version"]) && wifiSsidIsValid(requestId) && wifiSsidIsValid(sid)
23
+ && values["dispatched"] == nil
24
+ }
25
+
26
+ func wifiSsidIsValid(_ ssid: String) -> Bool {
27
+ !ssid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
28
+ }
29
+
30
+ let wifiCapabilityNegotiationTimeoutCode = "capability_negotiation_timeout"
31
+
32
+ enum WifiProtocolCapability: Equatable {
33
+ case unknown
34
+ case supported(version: Int)
35
+ case unsupported
36
+ case legacy
37
+ }
38
+
39
+ enum WifiRequestMode: Equatable {
40
+ case discovering
41
+ case modern
42
+ case legacy
43
+ case unsupported
44
+ }
45
+
46
+ final class WifiSessionCapabilities {
47
+ private(set) var sessionId = ""
48
+ private(set) var epoch: UInt64 = 0
49
+ private(set) var forgetResult: WifiProtocolCapability = .unknown
50
+ private(set) var savedNetworks: WifiProtocolCapability = .unknown
51
+
52
+ func reset(sessionId: String = "") {
53
+ epoch &+= 1
54
+ self.sessionId = sessionId
55
+ forgetResult = .unknown
56
+ savedNetworks = .unknown
57
+ }
58
+
59
+ func applyVersionInfo1(_ values: [String: Any]) {
60
+ if let sid = values["sid"] as? String, !sid.isEmpty {
61
+ sessionId = sid
62
+ }
63
+ forgetResult = capability(values["wifiForgetResultVersion"])
64
+ savedNetworks = capability(values["savedWifiNetworksVersion"])
65
+ }
66
+
67
+ func forgetMode() -> WifiRequestMode {
68
+ Self.requestMode(forgetResult)
69
+ }
70
+
71
+ func savedNetworksMode() -> WifiRequestMode {
72
+ Self.requestMode(savedNetworks)
73
+ }
74
+
75
+ private func capability(_ raw: Any?) -> WifiProtocolCapability {
76
+ guard let raw else { return .legacy }
77
+ return isWifiProtocolV1(raw) && !sessionId.isEmpty ? .supported(version: 1) : .unsupported
78
+ }
79
+
80
+ private static func requestMode(_ capability: WifiProtocolCapability) -> WifiRequestMode {
81
+ switch capability {
82
+ case .unknown: .discovering
83
+ case .supported: .modern
84
+ case .legacy: .legacy
85
+ case .unsupported: .unsupported
86
+ }
87
+ }
88
+ }
89
+
90
+ public enum WifiForgetOutcome: String {
91
+ case confirmed
92
+ case dispatched
93
+ case notFound = "not_found"
94
+ case unsupported
95
+ case failed
96
+ case legacyUnverified = "legacy_unverified"
97
+ }
98
+
99
+ func normalizeWifiForgetResultEvent(
100
+ requestId: String,
101
+ sid: String,
102
+ ssid: String,
103
+ protocolVersion: Int,
104
+ outcome: String,
105
+ legacyDispatched: Bool?,
106
+ connected: Bool?,
107
+ currentSsid: String,
108
+ localIp: String,
109
+ error: String?
110
+ ) -> [String: Any]? {
111
+ guard wifiSsidIsValid(ssid) else { return nil }
112
+ if !requestId.isEmpty, !sid.isEmpty,
113
+ protocolVersion == 1,
114
+ let modernOutcome = WifiForgetOutcome(rawValue: outcome),
115
+ modernOutcome != .legacyUnverified
116
+ {
117
+ var body: [String: Any] = [
118
+ "mode": "modern",
119
+ "requestId": requestId,
120
+ "sid": sid,
121
+ "ssid": ssid,
122
+ "protocolVersion": protocolVersion,
123
+ "outcome": modernOutcome.rawValue,
124
+ ]
125
+ if let connected { body["connected"] = connected }
126
+ if !currentSsid.isEmpty { body["currentSsid"] = currentSsid }
127
+ if !localIp.isEmpty { body["localIp"] = localIp }
128
+ if let error { body["error"] = error }
129
+ return body
130
+ }
131
+ if requestId.isEmpty, sid.isEmpty, protocolVersion == 0, outcome.isEmpty, let legacyDispatched {
132
+ var body: [String: Any] = [
133
+ "mode": "legacy",
134
+ "ssid": ssid,
135
+ "dispatched": legacyDispatched,
136
+ ]
137
+ if let connected { body["connected"] = connected }
138
+ if !currentSsid.isEmpty { body["currentSsid"] = currentSsid }
139
+ if !localIp.isEmpty { body["localIp"] = localIp }
140
+ if let error { body["error"] = error }
141
+ return body
142
+ }
143
+ return nil
144
+ }
145
+
146
+ public struct WifiForgetResult {
147
+ public let ssid: String
148
+ public let outcome: WifiForgetOutcome
149
+ public let connected: Bool?
150
+ public let currentSsid: String?
151
+ public let localIp: String?
152
+ public let error: String?
153
+
154
+ public var values: [String: Any] {
155
+ var result: [String: Any] = [
156
+ "ssid": ssid,
157
+ "outcome": outcome.rawValue,
158
+ ]
159
+ if let connected { result["connected"] = connected }
160
+ if let currentSsid { result["currentSsid"] = currentSsid }
161
+ if let localIp { result["localIp"] = localIp }
162
+ if let error { result["error"] = error }
163
+ return result
164
+ }
165
+ }
166
+
167
+ public enum SavedWifiNetworksOutcome: String {
168
+ case confirmed
169
+ case unsupported
170
+ case failed
171
+ }
172
+
173
+ public struct SavedWifiNetworksResult {
174
+ public let outcome: SavedWifiNetworksOutcome
175
+ public let networks: [String]
176
+ public let error: String?
177
+
178
+ public var values: [String: Any] {
179
+ var result: [String: Any] = [
180
+ "outcome": outcome.rawValue,
181
+ "networks": networks,
182
+ ]
183
+ if let error { result["error"] = error }
184
+ return result
185
+ }
186
+ }
187
+
188
+ func parseWifiForgetResult(
189
+ expectedRequestId: String,
190
+ expectedSid: String,
191
+ expectedSsid: String,
192
+ capabilityVersion: Int,
193
+ data: [String: Any]
194
+ ) -> WifiForgetResult? {
195
+ guard capabilityVersion == 1, !expectedRequestId.isEmpty, !expectedSid.isEmpty,
196
+ data["requestId"] as? String == expectedRequestId,
197
+ data["sid"] as? String == expectedSid,
198
+ data["ssid"] as? String == expectedSsid,
199
+ isWifiProtocolV1(data["protocolVersion"]),
200
+ let rawOutcome = data["outcome"] as? String,
201
+ let outcome = WifiForgetOutcome(rawValue: rawOutcome),
202
+ outcome != .legacyUnverified
203
+ else { return nil }
204
+ return WifiForgetResult(
205
+ ssid: expectedSsid,
206
+ outcome: outcome,
207
+ connected: data["connected"] as? Bool,
208
+ currentSsid: data["currentSsid"] as? String,
209
+ localIp: data["localIp"] as? String,
210
+ error: (data["error"] as? String).flatMap { $0.isEmpty ? nil : $0 }
211
+ )
212
+ }
213
+
214
+ func parseSavedWifiNetworks(
215
+ expectedRequestId: String,
216
+ expectedSid: String,
217
+ capabilityVersion: Int,
218
+ data: [String: Any]
219
+ ) -> SavedWifiNetworksResult? {
220
+ guard capabilityVersion == 1, !expectedRequestId.isEmpty, !expectedSid.isEmpty,
221
+ data["requestId"] as? String == expectedRequestId,
222
+ data["sid"] as? String == expectedSid,
223
+ isWifiProtocolV1(data["protocolVersion"]),
224
+ let rawOutcome = data["outcome"] as? String,
225
+ let outcome = SavedWifiNetworksOutcome(rawValue: rawOutcome)
226
+ else { return nil }
227
+ guard let rawNetworks = data["networks"] as? [String], outcome == .confirmed || rawNetworks.isEmpty else { return nil }
228
+ var seen = Set<String>()
229
+ let networks = rawNetworks.filter { network in
230
+ !network.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
231
+ && seen.insert(network).inserted
232
+ }
233
+ return SavedWifiNetworksResult(
234
+ outcome: outcome,
235
+ networks: networks,
236
+ error: (data["error"] as? String).flatMap { $0.isEmpty ? nil : $0 }
237
+ )
238
+ }
239
+
240
+ func legacyWifiForgetResult(
241
+ ssid: String
242
+ ) -> WifiForgetResult {
243
+ return WifiForgetResult(
244
+ ssid: ssid,
245
+ outcome: .legacyUnverified,
246
+ connected: nil,
247
+ currentSsid: nil,
248
+ localIp: nil,
249
+ error: nil
250
+ )
251
+ }
@@ -0,0 +1,101 @@
1
+ import CoreFoundation
2
+ import Foundation
3
+
4
+ enum VersionInfoAccumulatorOutcome {
5
+ case ignored
6
+ case waiting
7
+ case complete(VersionInfoResult)
8
+ }
9
+
10
+ /// One request, explicit modern completion, or the deployed legacy chunk-3 terminal boundary.
11
+ final class VersionInfoResponseAccumulator {
12
+ static let responseChunkKey = "_responseChunk"
13
+ static let responseRequestIdKey = "_responseRequestId"
14
+ static let responseIndexKey = "_responseChunkIndex"
15
+ static let responseCountKey = "_responseChunkCount"
16
+ static let responseFinalKey = "_responseFinal"
17
+ static let responseSidKey = "_responseSid"
18
+ private static let modernKeys = [responseRequestIdKey, responseIndexKey, responseCountKey, responseFinalKey]
19
+
20
+ private let expectedRequestId: String
21
+ private var values: [String: Any] = [:]
22
+ private var chunks: [Int: [String: Any]] = [:]
23
+ private var count: Int?
24
+ private var sid: String?
25
+ private var legacyStarted = false
26
+ private var completed = false
27
+
28
+ init(expectedRequestId: String) {
29
+ self.expectedRequestId = expectedRequestId
30
+ }
31
+
32
+ func accept(_ event: [String: Any]) -> VersionInfoAccumulatorOutcome {
33
+ guard !completed else { return .ignored }
34
+ if Self.modernKeys.contains(where: { event[$0] != nil }) {
35
+ guard event[Self.responseRequestIdKey] as? String == expectedRequestId,
36
+ let index = integer(event[Self.responseIndexKey]),
37
+ let total = integer(event[Self.responseCountKey]),
38
+ let final = event[Self.responseFinalKey] as? NSNumber,
39
+ CFGetTypeID(final) == CFBooleanGetTypeID(),
40
+ let process = event[Self.responseSidKey] as? String, !process.isEmpty,
41
+ (1 ... 16).contains(total), (1 ... total).contains(index),
42
+ final.boolValue == (index == total)
43
+ else { return .ignored }
44
+ if let count, count != total || sid != process { return .ignored }
45
+ if count == nil {
46
+ values.removeAll()
47
+ count = total
48
+ sid = process
49
+ }
50
+ chunks[index] = event
51
+ guard chunks.count == total else { return .waiting }
52
+ for index in 1 ... total {
53
+ merge(chunks[index]!)
54
+ }
55
+ return finish()
56
+ }
57
+ guard count == nil else { return .ignored }
58
+ switch event[Self.responseChunkKey] as? String ?? "version_info" {
59
+ case "version_info":
60
+ values.removeAll()
61
+ merge(event)
62
+ return finish()
63
+ case "version_info_1":
64
+ values.removeAll()
65
+ legacyStarted = true
66
+ merge(event)
67
+ case "version_info_2":
68
+ guard legacyStarted else { return .ignored }
69
+ merge(event)
70
+ case "version_info_3":
71
+ guard legacyStarted else { return .ignored }
72
+ merge(event)
73
+ return finish()
74
+ default: return .ignored
75
+ }
76
+ // Silence is never evidence of completion. The request's normal deadline handles loss.
77
+ return .waiting
78
+ }
79
+
80
+ private func finish() -> VersionInfoAccumulatorOutcome {
81
+ completed = true
82
+ return .complete(VersionInfoResult(values: values))
83
+ }
84
+
85
+ private func merge(_ event: [String: Any]) {
86
+ for (key, value) in event where !key.hasPrefix("_response") && key != "type" {
87
+ if let string = value as? String, string.isEmpty { continue }
88
+ values[key] = value
89
+ }
90
+ }
91
+
92
+ private func integer(_ value: Any?) -> Int? {
93
+ guard let number = value as? NSNumber,
94
+ CFGetTypeID(number) != CFBooleanGetTypeID(),
95
+ number.doubleValue.isFinite,
96
+ number.doubleValue >= 1, number.doubleValue <= 16,
97
+ number.doubleValue == Double(number.intValue)
98
+ else { return nil }
99
+ return number.intValue
100
+ }
101
+ }