@mentra/bluetooth-sdk 3.2.0-dev.200 → 3.2.0-dev.206
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 +46 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +63 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +5 -4
- 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 +298 -181
- package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +326 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbe.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamSessionState.kt +32 -0
- 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/WifiRequestResolutionTest.kt +271 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbeTest.kt +24 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamSessionStateTest.kt +43 -0
- package/build/BluetoothSdk.types.d.ts +68 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -4
- 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 -13
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/DeviceManager.swift +6 -3
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +372 -220
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/sgcs/MentraLive.swift +216 -70
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +28 -0
- package/ios/Source/streaming/StreamControllerProbe.swift +18 -0
- package/ios/Source/streaming/StreamModels.swift +12 -0
- package/ios/Source/streaming/StreamSessionState.swift +29 -0
- package/ios/Tests/StreamControllerProbeTests.swift +27 -0
- package/ios/Tests/StreamSessionStateTests.swift +38 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +82 -2
- package/src/_private/BluetoothSdkModule.ts +4 -4
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +11 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class StreamSessionStateTests: XCTestCase {
|
|
5
|
+
func testReadinessRequiresKnownProtocolAndProcessIdentity() {
|
|
6
|
+
let state = StreamSessionState()
|
|
7
|
+
state.ready(sessionId: "asg", controlVersion: nil)
|
|
8
|
+
XCTAssertFalse(state.supported)
|
|
9
|
+
state.ready(sessionId: "asg", controlVersion: 2)
|
|
10
|
+
XCTAssertFalse(state.supported)
|
|
11
|
+
state.ready(sessionId: "", controlVersion: 1)
|
|
12
|
+
XCTAssertFalse(state.supported)
|
|
13
|
+
state.ready(sessionId: "asg", controlVersion: 1)
|
|
14
|
+
XCTAssertTrue(state.supported)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func testReconnectionReconcilesTerminalStateWithoutHeartbeatTimeouts() {
|
|
18
|
+
let state = StreamSessionState()
|
|
19
|
+
state.ready(sessionId: "asg", controlVersion: 1)
|
|
20
|
+
XCTAssertTrue(state.accept(["sid": "asg", "revision": 1, "streamId": "stream", "terminal": false]))
|
|
21
|
+
state.ready(sessionId: "asg", controlVersion: 1)
|
|
22
|
+
XCTAssertEqual(state.currentStreamId, "stream")
|
|
23
|
+
XCTAssertTrue(state.accept(["sid": "asg", "revision": 3, "streamId": "stream", "terminal": true]))
|
|
24
|
+
XCTAssertNil(state.currentStreamId)
|
|
25
|
+
XCTAssertFalse(state.accept(["sid": "asg", "revision": 2, "streamId": "stream", "terminal": false]))
|
|
26
|
+
XCTAssertNil(state.currentStreamId)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func testProcessRestartRejectsOldStatusAndAcceptsNewStoppedSnapshot() {
|
|
30
|
+
let state = StreamSessionState()
|
|
31
|
+
state.ready(sessionId: "old", controlVersion: 1)
|
|
32
|
+
XCTAssertTrue(state.accept(["sid": "old", "revision": 8, "streamId": "stream", "terminal": false]))
|
|
33
|
+
state.ready(sessionId: "new", controlVersion: 1)
|
|
34
|
+
XCTAssertFalse(state.accept(["sid": "old", "revision": 9, "streamId": "stream", "terminal": false]))
|
|
35
|
+
XCTAssertTrue(state.accept(["sid": "new", "revision": 0, "terminal": true]))
|
|
36
|
+
XCTAssertNil(state.currentStreamId)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class WifiRequestResolutionTests: XCTestCase {
|
|
5
|
+
@MainActor
|
|
6
|
+
func testUnsupportedControllerDoesNotClaimWifiDispatch() {
|
|
7
|
+
let keys = ["fullyBooted", "connected", "connectionState", "micEnabled", "voiceActivityDetectionEnabled", "bluetoothClassicConnected"]
|
|
8
|
+
let snapshot = keys.compactMap { key in DeviceStore.shared.get("glasses", key).map { (key, $0) } }
|
|
9
|
+
defer { for (key, value) in snapshot {
|
|
10
|
+
DeviceStore.shared.apply("glasses", key, value)
|
|
11
|
+
} }
|
|
12
|
+
let controller: SGCManager = Simulated()
|
|
13
|
+
XCTAssertFalse(controller.forgetWifiNetwork("AP", requestId: nil, sid: nil))
|
|
14
|
+
XCTAssertFalse(controller.forgetWifiNetwork("AP", requestId: "request", sid: "sid"))
|
|
15
|
+
XCTAssertFalse(controller.requestSavedWifiNetworks(requestId: "request", sid: "sid"))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func testFutureFractionalAndMalformedCapabilitiesAreUnsupportedNotLegacy() {
|
|
19
|
+
for version in [0, 2, -1, 1.5, "1", true, NSNull()] as [Any] {
|
|
20
|
+
let capabilities = WifiSessionCapabilities()
|
|
21
|
+
capabilities.reset(sessionId: "sid")
|
|
22
|
+
capabilities.applyVersionInfo1(["wifiForgetResultVersion": version, "savedWifiNetworksVersion": version])
|
|
23
|
+
XCTAssertEqual(capabilities.forgetMode(), .unsupported)
|
|
24
|
+
XCTAssertEqual(capabilities.savedNetworksMode(), .unsupported)
|
|
25
|
+
}
|
|
26
|
+
let missingSession = WifiSessionCapabilities()
|
|
27
|
+
missingSession.applyVersionInfo1(["wifiForgetResultVersion": 1])
|
|
28
|
+
XCTAssertEqual(missingSession.forgetMode(), .unsupported)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func testRawResponseRejectsPartialTuplesAndCoercionBeforeNormalization() {
|
|
32
|
+
let tuple: [String: Any] = ["protocol_version": 1, "requestId": "request", "sid": "sid"]
|
|
33
|
+
XCTAssertTrue(wifiResponseEnvelopeIsValid(tuple, allowLegacy: false))
|
|
34
|
+
for key in tuple.keys {
|
|
35
|
+
XCTAssertFalse(wifiResponseEnvelopeIsValid(tuple.filter { $0.key != key }, allowLegacy: true))
|
|
36
|
+
}
|
|
37
|
+
for version in [0, 2, 1.5, "1", true, NSNull()] as [Any] {
|
|
38
|
+
var malformed = tuple
|
|
39
|
+
malformed["protocol_version"] = version
|
|
40
|
+
XCTAssertFalse(wifiResponseEnvelopeIsValid(malformed, allowLegacy: true))
|
|
41
|
+
}
|
|
42
|
+
XCTAssertFalse(wifiResponseEnvelopeIsValid(tuple.merging(["connected": 0]) { _, new in new }, allowLegacy: true))
|
|
43
|
+
XCTAssertFalse(wifiResponseEnvelopeIsValid(tuple.merging(["dispatched": true]) { _, new in new }, allowLegacy: true))
|
|
44
|
+
XCTAssertTrue(wifiResponseEnvelopeIsValid(["dispatched": true], allowLegacy: true))
|
|
45
|
+
XCTAssertFalse(wifiResponseEnvelopeIsValid(["requestId": "", "dispatched": true], allowLegacy: true))
|
|
46
|
+
XCTAssertFalse(wifiResponseEnvelopeIsValid([:], allowLegacy: false))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func testSemanticResultsOmitTransportMetadata() throws {
|
|
50
|
+
let data: [String: Any] = ["requestId": "request", "sid": "sid", "protocolVersion": 1,
|
|
51
|
+
"ssid": "AP", "outcome": "confirmed", "networks": ["AP"]]
|
|
52
|
+
let forbidden: Set = ["mode", "capabilityVersion", "requestId", "sid", "protocolVersion"]
|
|
53
|
+
let forget = try XCTUnwrap(parseWifiForgetResult(expectedRequestId: "request", expectedSid: "sid", expectedSsid: "AP", capabilityVersion: 1, data: data))
|
|
54
|
+
let saved = try XCTUnwrap(parseSavedWifiNetworks(expectedRequestId: "request", expectedSid: "sid", capabilityVersion: 1, data: data))
|
|
55
|
+
XCTAssertTrue(Set(forget.values.keys).isDisjoint(with: forbidden))
|
|
56
|
+
XCTAssertTrue(Set(saved.values.keys).isDisjoint(with: forbidden))
|
|
57
|
+
for version in [1.5, true] as [Any] {
|
|
58
|
+
var malformed = data
|
|
59
|
+
malformed["protocolVersion"] = version
|
|
60
|
+
XCTAssertNil(parseWifiForgetResult(expectedRequestId: "request", expectedSid: "sid", expectedSsid: "AP", capabilityVersion: 1, data: malformed))
|
|
61
|
+
XCTAssertNil(parseSavedWifiNetworks(expectedRequestId: "request", expectedSid: "sid", capabilityVersion: 1, data: malformed))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@MainActor
|
|
66
|
+
func testPendingResponseCancellationResumesExactlyOnceWithoutTimeout() async {
|
|
67
|
+
let pending = PendingResponse<Int>(operation: "WiFi capability negotiation")
|
|
68
|
+
let waiter = Task { @MainActor in
|
|
69
|
+
try await pending.wait(timeoutMs: nil)
|
|
70
|
+
}
|
|
71
|
+
await Task.yield()
|
|
72
|
+
|
|
73
|
+
waiter.cancel()
|
|
74
|
+
|
|
75
|
+
do {
|
|
76
|
+
_ = try await waiter.value
|
|
77
|
+
XCTFail("Expected cancellation")
|
|
78
|
+
} catch let error as BluetoothSdkError {
|
|
79
|
+
XCTAssertEqual(error.code, "request_cancelled")
|
|
80
|
+
} catch {
|
|
81
|
+
XCTFail("Unexpected error: \(error)")
|
|
82
|
+
}
|
|
83
|
+
pending.resolve(42)
|
|
84
|
+
do {
|
|
85
|
+
_ = try await pending.wait(timeoutMs: nil)
|
|
86
|
+
XCTFail("Late resolve must not replace the cancellation result")
|
|
87
|
+
} catch let error as BluetoothSdkError {
|
|
88
|
+
XCTAssertEqual(error.code, "request_cancelled")
|
|
89
|
+
} catch {
|
|
90
|
+
XCTFail("Unexpected error: \(error)")
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func testCapabilitiesAreUnknownPerSessionUntilVersionInfoFinalizesThem() {
|
|
95
|
+
let capabilities = WifiSessionCapabilities()
|
|
96
|
+
capabilities.reset(sessionId: "sid-1")
|
|
97
|
+
|
|
98
|
+
XCTAssertEqual(capabilities.forgetResult, .unknown)
|
|
99
|
+
XCTAssertEqual(capabilities.savedNetworks, .unknown)
|
|
100
|
+
capabilities.applyVersionInfo1(["sid": "sid-1", "wifiForgetResultVersion": 1])
|
|
101
|
+
|
|
102
|
+
XCTAssertEqual(capabilities.forgetResult, .supported(version: 1))
|
|
103
|
+
XCTAssertEqual(capabilities.savedNetworks, .legacy)
|
|
104
|
+
XCTAssertEqual(capabilities.forgetMode(), .modern)
|
|
105
|
+
XCTAssertEqual(capabilities.savedNetworksMode(), .legacy)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func testNewSessionResetsCapabilitiesAndAdvancesEpoch() {
|
|
109
|
+
let capabilities = WifiSessionCapabilities()
|
|
110
|
+
capabilities.reset(sessionId: "sid-1")
|
|
111
|
+
capabilities.applyVersionInfo1([
|
|
112
|
+
"wifiForgetResultVersion": 1,
|
|
113
|
+
"savedWifiNetworksVersion": 1,
|
|
114
|
+
])
|
|
115
|
+
let oldEpoch = capabilities.epoch
|
|
116
|
+
|
|
117
|
+
capabilities.reset(sessionId: "sid-2")
|
|
118
|
+
|
|
119
|
+
XCTAssertGreaterThan(capabilities.epoch, oldEpoch)
|
|
120
|
+
XCTAssertEqual(capabilities.sessionId, "sid-2")
|
|
121
|
+
XCTAssertEqual(capabilities.forgetResult, .unknown)
|
|
122
|
+
XCTAssertEqual(capabilities.savedNetworks, .unknown)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func testSsidValidationRejectsBlankInputWithoutChangingIdentity() {
|
|
126
|
+
XCTAssertFalse(wifiSsidIsValid(" "))
|
|
127
|
+
XCTAssertTrue(wifiSsidIsValid(" Field AP "))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func testForgetResultRequiresExactIdSessionSsidAndProtocol() {
|
|
131
|
+
let exact: [String: Any] = [
|
|
132
|
+
"requestId": "forget-1",
|
|
133
|
+
"sid": "sid-1",
|
|
134
|
+
"ssid": " Field AP ",
|
|
135
|
+
"protocolVersion": 1,
|
|
136
|
+
"outcome": "dispatched",
|
|
137
|
+
"connected": false,
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
XCTAssertNil(parseWifiForgetResult(
|
|
141
|
+
expectedRequestId: "other", expectedSid: "sid-1", expectedSsid: " Field AP ",
|
|
142
|
+
capabilityVersion: 1, data: exact
|
|
143
|
+
))
|
|
144
|
+
XCTAssertNil(parseWifiForgetResult(
|
|
145
|
+
expectedRequestId: "forget-1", expectedSid: "sid-1", expectedSsid: " Field AP ",
|
|
146
|
+
capabilityVersion: 1, data: exact.filter { $0.key != "requestId" }
|
|
147
|
+
))
|
|
148
|
+
XCTAssertNil(parseWifiForgetResult(
|
|
149
|
+
expectedRequestId: "forget-1", expectedSid: "other", expectedSsid: " Field AP ",
|
|
150
|
+
capabilityVersion: 1, data: exact
|
|
151
|
+
))
|
|
152
|
+
XCTAssertNil(parseWifiForgetResult(
|
|
153
|
+
expectedRequestId: "forget-1", expectedSid: "sid-1", expectedSsid: "Field AP",
|
|
154
|
+
capabilityVersion: 1, data: exact
|
|
155
|
+
))
|
|
156
|
+
XCTAssertNil(parseWifiForgetResult(
|
|
157
|
+
expectedRequestId: "forget-1", expectedSid: "sid-1", expectedSsid: " Field AP ",
|
|
158
|
+
capabilityVersion: 2, data: exact
|
|
159
|
+
))
|
|
160
|
+
XCTAssertEqual(
|
|
161
|
+
parseWifiForgetResult(
|
|
162
|
+
expectedRequestId: "forget-1", expectedSid: "sid-1", expectedSsid: " Field AP ",
|
|
163
|
+
capabilityVersion: 1, data: exact
|
|
164
|
+
)?.outcome,
|
|
165
|
+
.dispatched
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func testForgetParserPreservesEveryHonestTerminalOutcome() {
|
|
170
|
+
for outcome in ["confirmed", "dispatched", "not_found", "unsupported", "failed"] {
|
|
171
|
+
let parsed = parseWifiForgetResult(
|
|
172
|
+
expectedRequestId: "forget-1",
|
|
173
|
+
expectedSid: "sid-1",
|
|
174
|
+
expectedSsid: "AP",
|
|
175
|
+
capabilityVersion: 1,
|
|
176
|
+
data: [
|
|
177
|
+
"requestId": "forget-1",
|
|
178
|
+
"sid": "sid-1",
|
|
179
|
+
"ssid": "AP",
|
|
180
|
+
"protocolVersion": 1,
|
|
181
|
+
"outcome": outcome,
|
|
182
|
+
]
|
|
183
|
+
)
|
|
184
|
+
XCTAssertEqual(parsed?.outcome.rawValue, outcome)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func testMissingConnectivitySnapshotRemainsUnknown() {
|
|
189
|
+
let parsed = parseWifiForgetResult(
|
|
190
|
+
expectedRequestId: "forget-1",
|
|
191
|
+
expectedSid: "sid-1",
|
|
192
|
+
expectedSsid: "AP",
|
|
193
|
+
capabilityVersion: 1,
|
|
194
|
+
data: [
|
|
195
|
+
"requestId": "forget-1",
|
|
196
|
+
"sid": "sid-1",
|
|
197
|
+
"ssid": "AP",
|
|
198
|
+
"protocolVersion": 1,
|
|
199
|
+
"outcome": "dispatched",
|
|
200
|
+
]
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
XCTAssertNil(parsed?.connected)
|
|
204
|
+
XCTAssertNil(parsed?.values["connected"])
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func testLegacyForgetIsExplicitlyUnverified() {
|
|
208
|
+
let result = legacyWifiForgetResult(
|
|
209
|
+
ssid: "AP"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
XCTAssertTrue(Set(result.values.keys).isDisjoint(with: ["mode", "capabilityVersion", "requestId", "sid"]))
|
|
213
|
+
XCTAssertEqual(result.outcome, .legacyUnverified)
|
|
214
|
+
XCTAssertNil(result.connected)
|
|
215
|
+
XCTAssertNil(result.currentSsid)
|
|
216
|
+
XCTAssertNil(result.localIp)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func testSavedListPreservesIdentityAndRequiresCorrelationTuple() {
|
|
220
|
+
let exact: [String: Any] = [
|
|
221
|
+
"requestId": "saved-1",
|
|
222
|
+
"sid": "sid-1",
|
|
223
|
+
"protocolVersion": 1,
|
|
224
|
+
"outcome": "confirmed",
|
|
225
|
+
"networks": [" Field AP ", "", "Field AP", " Field AP "],
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
XCTAssertNil(parseSavedWifiNetworks(
|
|
229
|
+
expectedRequestId: "other", expectedSid: "sid-1", capabilityVersion: 1, data: exact
|
|
230
|
+
))
|
|
231
|
+
XCTAssertNil(parseSavedWifiNetworks(
|
|
232
|
+
expectedRequestId: "saved-1", expectedSid: "sid-1", capabilityVersion: 1,
|
|
233
|
+
data: exact.filter { $0.key != "requestId" }
|
|
234
|
+
))
|
|
235
|
+
XCTAssertNil(parseSavedWifiNetworks(
|
|
236
|
+
expectedRequestId: "saved-1", expectedSid: "other", capabilityVersion: 1, data: exact
|
|
237
|
+
))
|
|
238
|
+
XCTAssertNil(parseSavedWifiNetworks(
|
|
239
|
+
expectedRequestId: "saved-1", expectedSid: "sid-1", capabilityVersion: 2, data: exact
|
|
240
|
+
))
|
|
241
|
+
XCTAssertEqual(
|
|
242
|
+
parseSavedWifiNetworks(
|
|
243
|
+
expectedRequestId: "saved-1", expectedSid: "sid-1", capabilityVersion: 1, data: exact
|
|
244
|
+
)?.networks,
|
|
245
|
+
[" Field AP ", "Field AP"]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
var failure = exact
|
|
249
|
+
failure["outcome"] = "failed"
|
|
250
|
+
failure["error"] = "backend_failed"
|
|
251
|
+
failure["networks"] = [String]()
|
|
252
|
+
let parsedFailure = parseSavedWifiNetworks(
|
|
253
|
+
expectedRequestId: "saved-1",
|
|
254
|
+
expectedSid: "sid-1",
|
|
255
|
+
capabilityVersion: 1,
|
|
256
|
+
data: failure
|
|
257
|
+
)
|
|
258
|
+
XCTAssertEqual(parsedFailure?.outcome, .failed)
|
|
259
|
+
XCTAssertEqual(parsedFailure?.error, "backend_failed")
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
func testRawForgetEventPreservesModernAndLegacyWireTruth() {
|
|
264
|
+
let modern = normalizeWifiForgetResultEvent(
|
|
265
|
+
requestId: "forget-1",
|
|
266
|
+
sid: "sid-1",
|
|
267
|
+
ssid: "AP",
|
|
268
|
+
protocolVersion: 1,
|
|
269
|
+
outcome: "dispatched",
|
|
270
|
+
legacyDispatched: nil,
|
|
271
|
+
connected: false,
|
|
272
|
+
currentSsid: "",
|
|
273
|
+
localIp: "",
|
|
274
|
+
error: nil
|
|
275
|
+
)
|
|
276
|
+
let legacy = normalizeWifiForgetResultEvent(
|
|
277
|
+
requestId: "",
|
|
278
|
+
sid: "",
|
|
279
|
+
ssid: "AP",
|
|
280
|
+
protocolVersion: 0,
|
|
281
|
+
outcome: "",
|
|
282
|
+
legacyDispatched: true,
|
|
283
|
+
connected: false,
|
|
284
|
+
currentSsid: "",
|
|
285
|
+
localIp: "",
|
|
286
|
+
error: nil
|
|
287
|
+
)
|
|
288
|
+
let withoutSnapshot = normalizeWifiForgetResultEvent(
|
|
289
|
+
requestId: "forget-unknown",
|
|
290
|
+
sid: "sid-1",
|
|
291
|
+
ssid: "AP",
|
|
292
|
+
protocolVersion: 1,
|
|
293
|
+
outcome: "dispatched",
|
|
294
|
+
legacyDispatched: nil,
|
|
295
|
+
connected: nil,
|
|
296
|
+
currentSsid: "",
|
|
297
|
+
localIp: "",
|
|
298
|
+
error: nil
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
XCTAssertEqual(modern?["mode"] as? String, "modern")
|
|
302
|
+
XCTAssertEqual(modern?["outcome"] as? String, "dispatched")
|
|
303
|
+
XCTAssertEqual(legacy?["mode"] as? String, "legacy")
|
|
304
|
+
XCTAssertEqual(legacy?["dispatched"] as? Bool, true)
|
|
305
|
+
XCTAssertNil(legacy?["sid"])
|
|
306
|
+
XCTAssertNil(withoutSnapshot?["connected"])
|
|
307
|
+
XCTAssertNil(normalizeWifiForgetResultEvent(
|
|
308
|
+
requestId: "bad",
|
|
309
|
+
sid: "",
|
|
310
|
+
ssid: "AP",
|
|
311
|
+
protocolVersion: 0,
|
|
312
|
+
outcome: "",
|
|
313
|
+
legacyDispatched: nil,
|
|
314
|
+
connected: false,
|
|
315
|
+
currentSsid: "",
|
|
316
|
+
localIp: "",
|
|
317
|
+
error: nil
|
|
318
|
+
))
|
|
319
|
+
}
|
|
320
|
+
}
|
package/package.json
CHANGED
|
@@ -128,6 +128,68 @@ export type WifiStatusChangeEvent = WifiStatus & {
|
|
|
128
128
|
error?: string
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
export type WifiForgetOutcome =
|
|
132
|
+
| "confirmed"
|
|
133
|
+
| "dispatched"
|
|
134
|
+
| "not_found"
|
|
135
|
+
| "unsupported"
|
|
136
|
+
| "failed"
|
|
137
|
+
| "legacy_unverified"
|
|
138
|
+
|
|
139
|
+
export type WifiForgetResult = {
|
|
140
|
+
ssid: string
|
|
141
|
+
outcome: WifiForgetOutcome
|
|
142
|
+
connected?: boolean
|
|
143
|
+
currentSsid?: string
|
|
144
|
+
localIp?: string
|
|
145
|
+
error?: string
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type ModernWifiForgetResultEvent = {
|
|
149
|
+
type: "wifi_forget_result"
|
|
150
|
+
mode: "modern"
|
|
151
|
+
requestId: string
|
|
152
|
+
sid: string
|
|
153
|
+
ssid: string
|
|
154
|
+
protocolVersion: number
|
|
155
|
+
outcome: Exclude<WifiForgetOutcome, "legacy_unverified">
|
|
156
|
+
connected?: boolean
|
|
157
|
+
currentSsid?: string
|
|
158
|
+
localIp?: string
|
|
159
|
+
error?: string
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type LegacyWifiForgetResultEvent = {
|
|
163
|
+
type: "wifi_forget_result"
|
|
164
|
+
mode: "legacy"
|
|
165
|
+
ssid: string
|
|
166
|
+
dispatched: boolean
|
|
167
|
+
connected?: boolean
|
|
168
|
+
currentSsid?: string
|
|
169
|
+
localIp?: string
|
|
170
|
+
error?: string
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export type WifiForgetResultEvent = ModernWifiForgetResultEvent | LegacyWifiForgetResultEvent
|
|
174
|
+
|
|
175
|
+
export type SavedWifiNetworksOutcome = "confirmed" | "unsupported" | "failed"
|
|
176
|
+
|
|
177
|
+
export type SavedWifiNetworksResult = {
|
|
178
|
+
outcome: SavedWifiNetworksOutcome
|
|
179
|
+
networks: string[]
|
|
180
|
+
error?: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type SavedWifiNetworksEvent = {
|
|
184
|
+
type: "saved_wifi_networks"
|
|
185
|
+
requestId: string
|
|
186
|
+
sid: string
|
|
187
|
+
protocolVersion: number
|
|
188
|
+
outcome: SavedWifiNetworksOutcome
|
|
189
|
+
networks: string[]
|
|
190
|
+
error?: string
|
|
191
|
+
}
|
|
192
|
+
|
|
131
193
|
export type HotspotStatus = {state: "disabled"} | {state: "enabled"; ssid: string; password: string; localIp: string}
|
|
132
194
|
|
|
133
195
|
export type EnabledHotspotStatus = Extract<HotspotStatus, {state: "enabled"}>
|
|
@@ -147,6 +209,8 @@ export type HotspotErrorEvent = {
|
|
|
147
209
|
}
|
|
148
210
|
|
|
149
211
|
export type VersionInfoResult = {
|
|
212
|
+
versionInfoType?: string
|
|
213
|
+
sid?: string
|
|
150
214
|
androidVersion: string
|
|
151
215
|
firmwareVersion: string
|
|
152
216
|
besFirmwareVersion: string
|
|
@@ -169,6 +233,10 @@ export type VersionInfoResult = {
|
|
|
169
233
|
packageName?: string
|
|
170
234
|
/** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
|
|
171
235
|
hotspotOtaVersion: number
|
|
236
|
+
/** Session-correlated WiFi forget result protocol; absent/0 means unsupported. */
|
|
237
|
+
wifiForgetResultVersion?: number
|
|
238
|
+
/** Session-correlated saved-network listing protocol; absent/0 means unsupported. */
|
|
239
|
+
savedWifiNetworksVersion?: number
|
|
172
240
|
}
|
|
173
241
|
|
|
174
242
|
export type VersionInfoEvent = VersionInfoResult & {
|
|
@@ -796,6 +864,11 @@ export type StreamLiveStats = {
|
|
|
796
864
|
type StreamStatusCommon = {
|
|
797
865
|
type: "stream_status"
|
|
798
866
|
streamId?: string
|
|
867
|
+
/** ASG process identity and monotonic revision for reconnect reconciliation. */
|
|
868
|
+
sid?: string
|
|
869
|
+
revision?: number
|
|
870
|
+
terminal?: boolean
|
|
871
|
+
errorDetails?: string
|
|
799
872
|
timestamp?: number
|
|
800
873
|
resolvedConfig?: StreamResolvedConfig
|
|
801
874
|
stats?: StreamLiveStats
|
|
@@ -830,7 +903,7 @@ export type StreamStatusEvent =
|
|
|
830
903
|
})
|
|
831
904
|
| (StreamStatusCommon & {
|
|
832
905
|
kind: "snapshot"
|
|
833
|
-
status:
|
|
906
|
+
status: StreamStatusState
|
|
834
907
|
streaming: boolean
|
|
835
908
|
reconnecting: boolean
|
|
836
909
|
attempt?: number
|
|
@@ -930,6 +1003,8 @@ export type BluetoothSdkModuleEvents = {
|
|
|
930
1003
|
phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void
|
|
931
1004
|
wifi_status_change: (event: WifiStatusChangeEvent) => void
|
|
932
1005
|
wifi_scan_result: (event: WifiScanResultEvent) => void
|
|
1006
|
+
wifi_forget_result: (event: WifiForgetResultEvent) => void
|
|
1007
|
+
saved_wifi_networks: (event: SavedWifiNetworksEvent) => void
|
|
933
1008
|
hotspot_status_change: (event: HotspotStatusChangeEvent) => void
|
|
934
1009
|
hotspot_error: (event: HotspotErrorEvent) => void
|
|
935
1010
|
photo_response: (event: PhotoResponseEvent) => void
|
|
@@ -1090,6 +1165,8 @@ export type BluetoothSdkEventMap = {
|
|
|
1090
1165
|
local_transcription: LocalTranscriptionEvent
|
|
1091
1166
|
wifi_status_change: WifiStatusChangeEvent
|
|
1092
1167
|
wifi_scan_result: WifiScanResultEvent
|
|
1168
|
+
wifi_forget_result: WifiForgetResultEvent
|
|
1169
|
+
saved_wifi_networks: SavedWifiNetworksEvent
|
|
1093
1170
|
hotspot_status_change: HotspotStatusChangeEvent
|
|
1094
1171
|
hotspot_error: HotspotErrorEvent
|
|
1095
1172
|
photo_response: PhotoResponseEvent
|
|
@@ -1182,8 +1259,11 @@ export interface BluetoothSdkPublicModule {
|
|
|
1182
1259
|
ping(): Promise<void>
|
|
1183
1260
|
|
|
1184
1261
|
requestWifiScan(): Promise<WifiSearchResult[]>
|
|
1262
|
+
/** List exact WiFi SSIDs from glasses that support reliable saved-network enumeration. */
|
|
1263
|
+
getSavedWifiNetworks(): Promise<SavedWifiNetworksResult>
|
|
1185
1264
|
sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
|
|
1186
|
-
|
|
1265
|
+
/** Forget a saved network and return its correlated semantic outcome. */
|
|
1266
|
+
forgetWifiNetwork(ssid: string): Promise<WifiForgetResult>
|
|
1187
1267
|
setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
|
|
1188
1268
|
/** Set the glasses clock from the phone after an OTA clock-skew failure. */
|
|
1189
1269
|
setSystemTime(timestampMs: number): Promise<void>
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
ScanModelOptions,
|
|
47
47
|
ScanOptions,
|
|
48
48
|
SettingsAckSuccessEvent,
|
|
49
|
-
StreamKeepAliveRequest,
|
|
50
49
|
StreamStartRequest,
|
|
51
50
|
StreamStatusEvent,
|
|
52
51
|
VideoRecordingStartedStatusEvent,
|
|
@@ -55,6 +54,8 @@ import {
|
|
|
55
54
|
VideoRecordingStatusEvent,
|
|
56
55
|
VersionInfoResult,
|
|
57
56
|
WarmUpCameraParams,
|
|
57
|
+
SavedWifiNetworksResult,
|
|
58
|
+
WifiForgetResult,
|
|
58
59
|
WifiSearchResult,
|
|
59
60
|
WifiStatusChangeEvent,
|
|
60
61
|
} from "../BluetoothSdk.types"
|
|
@@ -125,8 +126,9 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
|
|
|
125
126
|
|
|
126
127
|
// WiFi Commands
|
|
127
128
|
requestWifiScan(): Promise<WifiSearchResult[]>
|
|
129
|
+
getSavedWifiNetworks(): Promise<SavedWifiNetworksResult>
|
|
128
130
|
sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
|
|
129
|
-
forgetWifiNetwork(ssid: string): Promise<
|
|
131
|
+
forgetWifiNetwork(ssid: string): Promise<WifiForgetResult>
|
|
130
132
|
setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
|
|
131
133
|
/** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
|
|
132
134
|
setWifiAdbState(enabled: boolean): Promise<void>
|
|
@@ -202,9 +204,7 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
|
|
|
202
204
|
|
|
203
205
|
// Stream Commands
|
|
204
206
|
startStream(params: StreamStartRequest): Promise<StreamStatusEvent>
|
|
205
|
-
startExternallyManagedStream(params: StreamStartRequest): Promise<StreamStatusEvent>
|
|
206
207
|
stopStream(): Promise<StreamStatusEvent>
|
|
207
|
-
sendExternallyManagedStreamKeepAlive(params: StreamKeepAliveRequest): Promise<void>
|
|
208
208
|
|
|
209
209
|
// Microphone Commands
|
|
210
210
|
setMicState(enabled: boolean, useGlassesMic?: boolean, sendTranscript?: boolean, sendLc3Data?: boolean): Promise<void>
|
|
@@ -3,7 +3,7 @@ export const GENERATED_RELEASE_CHANGELOGS = Object.freeze(
|
|
|
3
3
|
[
|
|
4
4
|
{
|
|
5
5
|
"version": "3.2.0",
|
|
6
|
-
"markdown": "This release is under active development. User-facing changes will be documented as they land
|
|
6
|
+
"markdown": "This release is under active development. User-facing changes will be documented as they land.\n\n- Breaking Bluetooth SDK change: `forgetWifiNetwork(ssid)` now returns `WifiForgetResult` instead of `WifiStatusChangeEvent`. Migrate status-only consumers to the semantic `outcome`; the requested network remains `ssid`, while optional post-command connectivity is reported by `connected`, `currentSsid`, and `localIp`."
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"version": "3.1.0",
|
|
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
|
|
|
12
12
|
export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "3.2.0",
|
|
15
|
-
"releaseIdentity": "3.2.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.2.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.2.0-dev.206",
|
|
16
|
+
"releaseSetId": "mentra-3.2.0-dev.206",
|
|
17
|
+
"sourceCommit": "3c22b92a5a53ce608c087503417ca5b3d16cc926",
|
|
18
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.206.json",
|
|
19
|
+
"otaManifestSha256": "7bc5e09f2757b2c11da58c93459bb0a554f406dd0ae5b50b58db28b64b92c838"
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,8 @@ const PUBLIC_EVENT_NAMES = new Set<BluetoothSdkEventName>([
|
|
|
26
26
|
"local_transcription",
|
|
27
27
|
"wifi_status_change",
|
|
28
28
|
"wifi_scan_result",
|
|
29
|
+
"wifi_forget_result",
|
|
30
|
+
"saved_wifi_networks",
|
|
29
31
|
"hotspot_status_change",
|
|
30
32
|
"hotspot_error",
|
|
31
33
|
"photo_response",
|
|
@@ -117,6 +119,7 @@ export const BluetoothSdk: BluetoothSdkPublicModule = Object.freeze({
|
|
|
117
119
|
setScreenDisabled: bindPublicMethod("setScreenDisabled"),
|
|
118
120
|
ping: bindPublicMethod("ping"),
|
|
119
121
|
requestWifiScan: bindPublicMethod("requestWifiScan"),
|
|
122
|
+
getSavedWifiNetworks: bindPublicMethod("getSavedWifiNetworks"),
|
|
120
123
|
sendWifiCredentials: bindPublicMethod("sendWifiCredentials"),
|
|
121
124
|
forgetWifiNetwork: bindPublicMethod("forgetWifiNetwork"),
|
|
122
125
|
setHotspotState: bindPublicMethod("setHotspotState"),
|
|
@@ -308,6 +311,14 @@ export type {
|
|
|
308
311
|
VideoRecordingSuccessStatusEvent,
|
|
309
312
|
VersionInfoEvent,
|
|
310
313
|
VersionInfoResult,
|
|
314
|
+
SavedWifiNetworksEvent,
|
|
315
|
+
SavedWifiNetworksOutcome,
|
|
316
|
+
SavedWifiNetworksResult,
|
|
317
|
+
WifiForgetOutcome,
|
|
318
|
+
WifiForgetResult,
|
|
319
|
+
WifiForgetResultEvent,
|
|
320
|
+
LegacyWifiForgetResultEvent,
|
|
321
|
+
ModernWifiForgetResultEvent,
|
|
311
322
|
WifiScanResultEvent,
|
|
312
323
|
VoiceActivityDetectionStatusEvent,
|
|
313
324
|
WifiSearchResult,
|