@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,181 @@
|
|
|
1
|
+
@testable import MentraBluetoothSDK
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class VersionInfoResponseAccumulatorTests: XCTestCase {
|
|
5
|
+
func testMergesCurrentChunksAndCompletesOnFirmwareChunk() {
|
|
6
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
7
|
+
|
|
8
|
+
guard case .waiting = accumulator.accept(
|
|
9
|
+
chunk("version_info_1", "request-1", ["buildNumber": "42"])
|
|
10
|
+
)
|
|
11
|
+
else {
|
|
12
|
+
return XCTFail("Expected first chunk to wait")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let outcome = accumulator.accept(
|
|
16
|
+
chunk(
|
|
17
|
+
"version_info_3",
|
|
18
|
+
"request-1",
|
|
19
|
+
[
|
|
20
|
+
"buildNumber": "",
|
|
21
|
+
"besFirmwareVersion": "26.8.27.0",
|
|
22
|
+
"mtkFirmwareVersion": "MentraLive_20260709",
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
guard case let .complete(result) = outcome else {
|
|
27
|
+
return XCTFail("Expected final chunk to complete")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
XCTAssertEqual(result.buildNumber, "42")
|
|
31
|
+
XCTAssertEqual(result.besFirmwareVersion, "26.8.27.0")
|
|
32
|
+
XCTAssertEqual(result.mtkFirmwareVersion, "MentraLive_20260709")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func testIgnoresMismatchedAndTrailingStaleChunks() {
|
|
36
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
37
|
+
|
|
38
|
+
guard case .ignored = accumulator.accept(chunk("version_info_1", "other", ["buildNumber": "old"]))
|
|
39
|
+
else {
|
|
40
|
+
return XCTFail("Expected mismatched response to be ignored")
|
|
41
|
+
}
|
|
42
|
+
guard case .ignored = accumulator.accept(chunk("version_info_3", nil, ["besFirmwareVersion": "stale"]))
|
|
43
|
+
else {
|
|
44
|
+
return XCTFail("Expected stale trailing chunk to be ignored")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func testDoesNotMixCorrelatedAndUncorrelatedSequences() {
|
|
49
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
50
|
+
_ = accumulator.accept(chunk("version_info_1", "request-1", ["buildNumber": "42"]))
|
|
51
|
+
|
|
52
|
+
guard case .ignored = accumulator.accept(
|
|
53
|
+
chunk("version_info_3", nil, ["besFirmwareVersion": "stale"])
|
|
54
|
+
) else {
|
|
55
|
+
return XCTFail("Expected uncorrelated trailing chunk to be ignored")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let outcome = accumulator.accept(
|
|
59
|
+
chunk("version_info_3", "request-1", ["besFirmwareVersion": "current"])
|
|
60
|
+
)
|
|
61
|
+
guard case let .complete(result) = outcome else {
|
|
62
|
+
return XCTFail("Expected correlated final chunk to complete")
|
|
63
|
+
}
|
|
64
|
+
XCTAssertEqual(result.besFirmwareVersion, "current")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func testStaleUncorrelatedResponsesCannotReplaceCorrelatedSequence() {
|
|
68
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
69
|
+
_ = accumulator.accept(chunk("version_info_1", "request-1", ["buildNumber": "42"]))
|
|
70
|
+
|
|
71
|
+
guard case .ignored = accumulator.accept(chunk("version_info_1", nil, ["buildNumber": "stale"]))
|
|
72
|
+
else {
|
|
73
|
+
return XCTFail("Expected stale first chunk to be ignored")
|
|
74
|
+
}
|
|
75
|
+
guard case .ignored = accumulator.accept(chunk("version_info", nil, ["buildNumber": "legacy"]))
|
|
76
|
+
else {
|
|
77
|
+
return XCTFail("Expected stale legacy response to be ignored")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let outcome = accumulator.accept(
|
|
81
|
+
chunk("version_info_3", "request-1", ["besFirmwareVersion": "current"])
|
|
82
|
+
)
|
|
83
|
+
guard case let .complete(result) = outcome else {
|
|
84
|
+
return XCTFail("Expected correlated final chunk to complete")
|
|
85
|
+
}
|
|
86
|
+
XCTAssertEqual(result.buildNumber, "42")
|
|
87
|
+
XCTAssertEqual(result.besFirmwareVersion, "current")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func testRepeatedFirstChunkResetsRatherThanMixingResponses() {
|
|
91
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
92
|
+
_ = accumulator.accept(chunk("version_info_1", nil, ["appVersion": "old"]))
|
|
93
|
+
_ = accumulator.accept(chunk("version_info_1", nil, ["buildNumber": "43"]))
|
|
94
|
+
|
|
95
|
+
guard case let .complete(result) = accumulator.accept(
|
|
96
|
+
chunk("version_info_3", nil, ["besFirmwareVersion": "new"])
|
|
97
|
+
) else { return XCTFail("Legacy final chunk must complete immediately") }
|
|
98
|
+
XCTAssertEqual(result.appVersion, "")
|
|
99
|
+
XCTAssertEqual(result.buildNumber, "43")
|
|
100
|
+
XCTAssertEqual(result.besFirmwareVersion, "new")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func testLegacySingleMessageCompletesImmediately() {
|
|
104
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
105
|
+
|
|
106
|
+
let outcome = accumulator.accept(chunk("version_info", nil, ["buildNumber": "7"]))
|
|
107
|
+
guard case let .complete(result) = outcome else {
|
|
108
|
+
return XCTFail("Expected legacy response to complete")
|
|
109
|
+
}
|
|
110
|
+
XCTAssertEqual(result.buildNumber, "7")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func testLegacyFirstChunkDoesNotComplete() {
|
|
114
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
115
|
+
guard case .waiting = accumulator.accept(chunk("version_info_1", nil, ["buildNumber": "8"])) else {
|
|
116
|
+
return XCTFail("Partial response must wait for final chunk or deadline")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func testModernFinalChunkWaitsForMissingFirstChunk() {
|
|
121
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
122
|
+
guard case .waiting = accumulator.accept(chunk("version_info_3", "request-1", ["besFirmwareVersion": "new"])) else {
|
|
123
|
+
return XCTFail("Missing first chunk")
|
|
124
|
+
}
|
|
125
|
+
guard case let .complete(result) = accumulator.accept(chunk("version_info_1", "request-1", ["buildNumber": "42"])) else {
|
|
126
|
+
return XCTFail("Both chunks arrived")
|
|
127
|
+
}
|
|
128
|
+
XCTAssertEqual(result.buildNumber, "42")
|
|
129
|
+
XCTAssertEqual(result.besFirmwareVersion, "new")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func testDuplicateDoesNotCountAsMissingChunk() {
|
|
133
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
134
|
+
let first = chunk("version_info_1", "request-1", ["buildNumber": "42"])
|
|
135
|
+
_ = accumulator.accept(first)
|
|
136
|
+
guard case .waiting = accumulator.accept(first) else { return XCTFail("Duplicate is not completion") }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func testRejectsMalformedModernMetadataAndMixedProcess() {
|
|
140
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
141
|
+
let first = chunk("version_info_1", "request-1", ["buildNumber": "42"])
|
|
142
|
+
for (key, value) in [
|
|
143
|
+
VersionInfoResponseAccumulator.responseCountKey: 2.5,
|
|
144
|
+
VersionInfoResponseAccumulator.responseIndexKey: true,
|
|
145
|
+
VersionInfoResponseAccumulator.responseFinalKey: true,
|
|
146
|
+
VersionInfoResponseAccumulator.responseRequestIdKey: "",
|
|
147
|
+
] as [String: Any] {
|
|
148
|
+
var invalid = first
|
|
149
|
+
invalid[key] = value
|
|
150
|
+
guard case .ignored = accumulator.accept(invalid) else { return XCTFail("Invalid " + key) }
|
|
151
|
+
}
|
|
152
|
+
_ = accumulator.accept(first)
|
|
153
|
+
let last = chunk("version_info_3", "request-1", ["besFirmwareVersion": "new"])
|
|
154
|
+
var restarted = last
|
|
155
|
+
restarted[VersionInfoResponseAccumulator.responseSidKey] = "process-2"
|
|
156
|
+
guard case .ignored = accumulator.accept(restarted) else { return XCTFail("Mixed process") }
|
|
157
|
+
guard case .complete = accumulator.accept(last) else { return XCTFail("Matching final chunk") }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func testCorrelatedResponseWithoutCompletenessMetadataCannotComplete() {
|
|
161
|
+
let accumulator = VersionInfoResponseAccumulator(expectedRequestId: "request-1")
|
|
162
|
+
let event: [String: Any] = [
|
|
163
|
+
VersionInfoResponseAccumulator.responseChunkKey: "version_info_3",
|
|
164
|
+
VersionInfoResponseAccumulator.responseRequestIdKey: "request-1",
|
|
165
|
+
]
|
|
166
|
+
guard case .ignored = accumulator.accept(event) else { return XCTFail("Incomplete modern metadata") }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private func chunk(_ type: String, _ requestId: String?, _ values: [String: Any]) -> [String: Any] {
|
|
170
|
+
var event = values
|
|
171
|
+
event[VersionInfoResponseAccumulator.responseChunkKey] = type
|
|
172
|
+
if let requestId {
|
|
173
|
+
event[VersionInfoResponseAccumulator.responseRequestIdKey] = requestId
|
|
174
|
+
event[VersionInfoResponseAccumulator.responseIndexKey] = type == "version_info_1" ? 1 : 2
|
|
175
|
+
event[VersionInfoResponseAccumulator.responseCountKey] = 2
|
|
176
|
+
event[VersionInfoResponseAccumulator.responseFinalKey] = type == "version_info_3"
|
|
177
|
+
event[VersionInfoResponseAccumulator.responseSidKey] = "process-1"
|
|
178
|
+
}
|
|
179
|
+
return event
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -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 & {
|
|
@@ -930,6 +998,8 @@ export type BluetoothSdkModuleEvents = {
|
|
|
930
998
|
phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void
|
|
931
999
|
wifi_status_change: (event: WifiStatusChangeEvent) => void
|
|
932
1000
|
wifi_scan_result: (event: WifiScanResultEvent) => void
|
|
1001
|
+
wifi_forget_result: (event: WifiForgetResultEvent) => void
|
|
1002
|
+
saved_wifi_networks: (event: SavedWifiNetworksEvent) => void
|
|
933
1003
|
hotspot_status_change: (event: HotspotStatusChangeEvent) => void
|
|
934
1004
|
hotspot_error: (event: HotspotErrorEvent) => void
|
|
935
1005
|
photo_response: (event: PhotoResponseEvent) => void
|
|
@@ -1090,6 +1160,8 @@ export type BluetoothSdkEventMap = {
|
|
|
1090
1160
|
local_transcription: LocalTranscriptionEvent
|
|
1091
1161
|
wifi_status_change: WifiStatusChangeEvent
|
|
1092
1162
|
wifi_scan_result: WifiScanResultEvent
|
|
1163
|
+
wifi_forget_result: WifiForgetResultEvent
|
|
1164
|
+
saved_wifi_networks: SavedWifiNetworksEvent
|
|
1093
1165
|
hotspot_status_change: HotspotStatusChangeEvent
|
|
1094
1166
|
hotspot_error: HotspotErrorEvent
|
|
1095
1167
|
photo_response: PhotoResponseEvent
|
|
@@ -1182,8 +1254,11 @@ export interface BluetoothSdkPublicModule {
|
|
|
1182
1254
|
ping(): Promise<void>
|
|
1183
1255
|
|
|
1184
1256
|
requestWifiScan(): Promise<WifiSearchResult[]>
|
|
1257
|
+
/** List exact WiFi SSIDs from glasses that support reliable saved-network enumeration. */
|
|
1258
|
+
getSavedWifiNetworks(): Promise<SavedWifiNetworksResult>
|
|
1185
1259
|
sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
|
|
1186
|
-
|
|
1260
|
+
/** Forget a saved network and return its correlated semantic outcome. */
|
|
1261
|
+
forgetWifiNetwork(ssid: string): Promise<WifiForgetResult>
|
|
1187
1262
|
setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
|
|
1188
1263
|
/** Set the glasses clock from the phone after an OTA clock-skew failure. */
|
|
1189
1264
|
setSystemTime(timestampMs: number): Promise<void>
|
|
@@ -55,6 +55,8 @@ import {
|
|
|
55
55
|
VideoRecordingStatusEvent,
|
|
56
56
|
VersionInfoResult,
|
|
57
57
|
WarmUpCameraParams,
|
|
58
|
+
SavedWifiNetworksResult,
|
|
59
|
+
WifiForgetResult,
|
|
58
60
|
WifiSearchResult,
|
|
59
61
|
WifiStatusChangeEvent,
|
|
60
62
|
} from "../BluetoothSdk.types"
|
|
@@ -125,8 +127,9 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
|
|
|
125
127
|
|
|
126
128
|
// WiFi Commands
|
|
127
129
|
requestWifiScan(): Promise<WifiSearchResult[]>
|
|
130
|
+
getSavedWifiNetworks(): Promise<SavedWifiNetworksResult>
|
|
128
131
|
sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>
|
|
129
|
-
forgetWifiNetwork(ssid: string): Promise<
|
|
132
|
+
forgetWifiNetwork(ssid: string): Promise<WifiForgetResult>
|
|
130
133
|
setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>
|
|
131
134
|
/** Enable or disable Wi-Fi ADB on Mentra Live (no-op on other devices). */
|
|
132
135
|
setWifiAdbState(enabled: 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.203",
|
|
16
|
+
"releaseSetId": "mentra-3.2.0-dev.203",
|
|
17
|
+
"sourceCommit": "7eb4deefff27d498c2b4562d628607f399aa1f40",
|
|
18
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.203.json",
|
|
19
|
+
"otaManifestSha256": "0ae026238c8e70970be4e181cc1ae73dd905418292edebddb610012f4f0a9b6f"
|
|
20
20
|
})
|