@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.
- package/README.md +64 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +77 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +12 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +318 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +88 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +50 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +626 -120
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycle.kt +64 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +76 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +16 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoBridgeTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +169 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceManifestTest.kt +57 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +52 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +94 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycleTest.kt +112 -0
- package/build/BluetoothSdk.types.d.ts +62 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +8 -2
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +78 -1
- package/ios/Source/DeviceManager.swift +13 -6
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +397 -99
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +101 -0
- package/ios/Source/sgcs/MentraLive.swift +179 -37
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +30 -3
- package/ios/Tests/VersionInfoBridgeTests.swift +32 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +181 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +76 -1
- package/src/_private/BluetoothSdkModule.ts +4 -1
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- 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
|
+
}
|