@mentra/bluetooth-sdk 3.2.0-dev.200 → 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 +46 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -1
- 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 +285 -29
- 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 +312 -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/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/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 +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 +365 -93
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/sgcs/MentraLive.swift +170 -35
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +28 -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
|
@@ -313,8 +313,10 @@ abstract class SGCManager {
|
|
|
313
313
|
|
|
314
314
|
// Network Management
|
|
315
315
|
abstract fun requestWifiScan(scanId: String?)
|
|
316
|
+
open fun requestSavedWifiNetworks(requestId: String, sid: String): Boolean = false
|
|
316
317
|
abstract fun sendWifiCredentials(ssid: String, password: String)
|
|
317
318
|
abstract fun forgetWifiNetwork(ssid: String)
|
|
319
|
+
open fun forgetWifiNetwork(ssid: String, requestId: String?, sid: String?): Boolean = false
|
|
318
320
|
abstract fun sendHotspotState(enabled: Boolean)
|
|
319
321
|
|
|
320
322
|
/** Set glasses system clock (Mentra Live and G2; no-op on other devices). */
|
|
@@ -189,6 +189,10 @@ data class VersionInfoResult(
|
|
|
189
189
|
val appVersion: String,
|
|
190
190
|
val packageName: String,
|
|
191
191
|
val hotspotOtaVersion: Int,
|
|
192
|
+
val wifiForgetResultVersion: Int? = null,
|
|
193
|
+
val savedWifiNetworksVersion: Int? = null,
|
|
194
|
+
val versionInfoType: String? = null,
|
|
195
|
+
val sid: String? = null,
|
|
192
196
|
) {
|
|
193
197
|
internal fun toMap(): Map<String, Any> =
|
|
194
198
|
buildMap {
|
|
@@ -206,6 +210,10 @@ data class VersionInfoResult(
|
|
|
206
210
|
put("packageName", packageName)
|
|
207
211
|
}
|
|
208
212
|
put("hotspotOtaVersion", hotspotOtaVersion)
|
|
213
|
+
wifiForgetResultVersion?.let { put("wifiForgetResultVersion", it) }
|
|
214
|
+
savedWifiNetworksVersion?.let { put("savedWifiNetworksVersion", it) }
|
|
215
|
+
versionInfoType?.let { put("versionInfoType", it) }
|
|
216
|
+
sid?.let { put("sid", it) }
|
|
209
217
|
}
|
|
210
218
|
|
|
211
219
|
companion object {
|
|
@@ -236,6 +244,12 @@ data class VersionInfoResult(
|
|
|
236
244
|
packageName = stringValue(values, "packageName", "package_name") ?: "",
|
|
237
245
|
hotspotOtaVersion =
|
|
238
246
|
numberValue(values, "hotspotOtaVersion", "hotspot_ota_version") ?: 0,
|
|
247
|
+
wifiForgetResultVersion =
|
|
248
|
+
numberValue(values, "wifiForgetResultVersion", "wifi_forget_result_version"),
|
|
249
|
+
savedWifiNetworksVersion =
|
|
250
|
+
numberValue(values, "savedWifiNetworksVersion", "saved_wifi_networks_version"),
|
|
251
|
+
versionInfoType = stringValue(values, "versionInfoType", "version_info_type"),
|
|
252
|
+
sid = stringValue(values, "sid"),
|
|
239
253
|
)
|
|
240
254
|
}
|
|
241
255
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class GlassesStatusClassicAudioTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun `serializes connected Classic audio state for React Native`() {
|
|
9
|
+
val serialized =
|
|
10
|
+
GlassesStatus.fromMap(mapOf("bluetoothClassicConnected" to true)).toMap()
|
|
11
|
+
|
|
12
|
+
assertThat(serialized["bluetoothClassicConnected"]).isEqualTo(true)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertNull
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class MessageAckPolicyTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun `every non-ack message id is acknowledged regardless of type`() {
|
|
10
|
+
assertEquals(17L, incomingGlassesMessageAckId("wifi_forget_result", 17L))
|
|
11
|
+
assertEquals(18L, incomingGlassesMessageAckId("saved_wifi_networks", 18L))
|
|
12
|
+
assertEquals(19L, incomingGlassesMessageAckId("future_terminal_type", 19L))
|
|
13
|
+
assertNull(incomingGlassesMessageAckId("msg_ack", 20L))
|
|
14
|
+
assertNull(incomingGlassesMessageAckId("wifi_forget_result", null))
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertNull
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
class WifiRequestResolutionTest {
|
|
10
|
+
@Test
|
|
11
|
+
fun `future fractional and malformed capabilities are unsupported not legacy`() {
|
|
12
|
+
for (version in listOf(0, 2, -1, 1.5, "1", true)) {
|
|
13
|
+
val capabilities = WifiSessionCapabilities()
|
|
14
|
+
capabilities.reset("sid")
|
|
15
|
+
capabilities.applyVersionInfo1(mapOf("wifiForgetResultVersion" to version, "savedWifiNetworksVersion" to version))
|
|
16
|
+
assertEquals(WifiRequestMode.UNSUPPORTED, capabilities.forgetMode())
|
|
17
|
+
assertEquals(WifiRequestMode.UNSUPPORTED, capabilities.savedNetworksMode())
|
|
18
|
+
}
|
|
19
|
+
val missingSession = WifiSessionCapabilities()
|
|
20
|
+
missingSession.applyVersionInfo1(mapOf("wifiForgetResultVersion" to 1))
|
|
21
|
+
assertEquals(WifiRequestMode.UNSUPPORTED, missingSession.forgetMode())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Test
|
|
25
|
+
fun `raw response rejects partial tuples and coercion before normalization`() {
|
|
26
|
+
val tuple = mapOf<String, Any>("protocol_version" to 1, "requestId" to "request", "sid" to "sid")
|
|
27
|
+
assertTrue(wifiResponseEnvelopeIsValid(tuple, false))
|
|
28
|
+
for (key in tuple.keys) assertFalse(wifiResponseEnvelopeIsValid(tuple - key, true))
|
|
29
|
+
for (version in listOf(0, 2, 1.5, "1", true)) {
|
|
30
|
+
assertFalse(wifiResponseEnvelopeIsValid(tuple + ("protocol_version" to version), true))
|
|
31
|
+
}
|
|
32
|
+
assertFalse(wifiResponseEnvelopeIsValid(tuple + ("connected" to 0), true))
|
|
33
|
+
assertFalse(wifiResponseEnvelopeIsValid(tuple + ("dispatched" to true), true))
|
|
34
|
+
assertTrue(wifiResponseEnvelopeIsValid(mapOf("dispatched" to true), true))
|
|
35
|
+
assertFalse(wifiResponseEnvelopeIsValid(mapOf("requestId" to "", "dispatched" to true), true))
|
|
36
|
+
assertFalse(wifiResponseEnvelopeIsValid(emptyMap(), false))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
fun `semantic results omit transport metadata`() {
|
|
41
|
+
val data = mapOf<String, Any>("requestId" to "request", "sid" to "sid", "protocolVersion" to 1,
|
|
42
|
+
"ssid" to "AP", "outcome" to "confirmed", "networks" to listOf("AP"))
|
|
43
|
+
val forbidden = setOf("mode", "capabilityVersion", "requestId", "sid", "protocolVersion")
|
|
44
|
+
assertTrue(parseWifiForgetResult("request", "sid", "AP", 1, data)!!.toMap().keys.intersect(forbidden).isEmpty())
|
|
45
|
+
assertTrue(parseSavedWifiNetworks("request", "sid", 1, data)!!.toMap().keys.intersect(forbidden).isEmpty())
|
|
46
|
+
assertNull(parseWifiForgetResult("request", "sid", "AP", 1, data + ("protocolVersion" to 1.5)))
|
|
47
|
+
assertNull(parseSavedWifiNetworks("request", "sid", 1, data + ("protocolVersion" to 1.5)))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test
|
|
51
|
+
fun `capabilities are unknown per session until version info finalizes them`() {
|
|
52
|
+
val capabilities = WifiSessionCapabilities()
|
|
53
|
+
capabilities.reset("sid-1")
|
|
54
|
+
|
|
55
|
+
assertEquals(WifiRequestMode.DISCOVERING, capabilities.forgetMode())
|
|
56
|
+
assertEquals(WifiRequestMode.DISCOVERING, capabilities.savedNetworksMode())
|
|
57
|
+
capabilities.applyVersionInfo1(mapOf("sid" to "sid-1", "wifiForgetResultVersion" to 1))
|
|
58
|
+
|
|
59
|
+
assertEquals(WifiProtocolCapability.Supported(1), capabilities.forgetResult)
|
|
60
|
+
assertEquals(WifiProtocolCapability.Legacy, capabilities.savedNetworks)
|
|
61
|
+
assertEquals(WifiRequestMode.MODERN, capabilities.forgetMode())
|
|
62
|
+
assertEquals(WifiRequestMode.LEGACY, capabilities.savedNetworksMode())
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Test
|
|
66
|
+
fun `new session resets capabilities and advances epoch`() {
|
|
67
|
+
val capabilities = WifiSessionCapabilities()
|
|
68
|
+
capabilities.reset("sid-1")
|
|
69
|
+
capabilities.applyVersionInfo1(
|
|
70
|
+
mapOf("wifiForgetResultVersion" to 1, "savedWifiNetworksVersion" to 1)
|
|
71
|
+
)
|
|
72
|
+
val oldEpoch = capabilities.epoch
|
|
73
|
+
|
|
74
|
+
capabilities.reset("sid-2")
|
|
75
|
+
|
|
76
|
+
assertTrue(capabilities.epoch > oldEpoch)
|
|
77
|
+
assertEquals("sid-2", capabilities.sessionId)
|
|
78
|
+
assertEquals(WifiProtocolCapability.Unknown, capabilities.forgetResult)
|
|
79
|
+
assertEquals(WifiProtocolCapability.Unknown, capabilities.savedNetworks)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
fun `saved request snapshot cannot mix mode with a later session`() {
|
|
84
|
+
val capabilities = WifiSessionCapabilities()
|
|
85
|
+
capabilities.reset("sid-1")
|
|
86
|
+
capabilities.applyVersionInfo1(mapOf("savedWifiNetworksVersion" to 1))
|
|
87
|
+
val first = capabilities.savedNetworksRequestSnapshot()
|
|
88
|
+
|
|
89
|
+
capabilities.reset("sid-2")
|
|
90
|
+
val second = capabilities.savedNetworksRequestSnapshot()
|
|
91
|
+
|
|
92
|
+
assertEquals(WifiRequestMode.MODERN, first.mode)
|
|
93
|
+
assertEquals("sid-1", first.sessionId)
|
|
94
|
+
assertEquals(WifiRequestMode.DISCOVERING, second.mode)
|
|
95
|
+
assertEquals("sid-2", second.sessionId)
|
|
96
|
+
assertTrue(second.epoch > first.epoch)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test
|
|
100
|
+
fun `SSID validation rejects blank input without changing identity`() {
|
|
101
|
+
assertFalse(wifiSsidIsValid(" "))
|
|
102
|
+
assertTrue(wifiSsidIsValid(" Field AP "))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
fun `forget result requires exact id session ssid and protocol`() {
|
|
107
|
+
val exact =
|
|
108
|
+
mapOf<String, Any>(
|
|
109
|
+
"requestId" to "forget-1",
|
|
110
|
+
"sid" to "sid-1",
|
|
111
|
+
"ssid" to " Field AP ",
|
|
112
|
+
"protocolVersion" to 1,
|
|
113
|
+
"outcome" to "dispatched",
|
|
114
|
+
"connected" to false,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
assertNull(parseWifiForgetResult("other", "sid-1", " Field AP ", 1, exact))
|
|
118
|
+
assertNull(parseWifiForgetResult("forget-1", "sid-1", " Field AP ", 1, exact - "requestId"))
|
|
119
|
+
assertNull(parseWifiForgetResult("forget-1", "other", " Field AP ", 1, exact))
|
|
120
|
+
assertNull(parseWifiForgetResult("forget-1", "sid-1", "Field AP", 1, exact))
|
|
121
|
+
assertNull(parseWifiForgetResult("forget-1", "sid-1", " Field AP ", 2, exact))
|
|
122
|
+
assertEquals(
|
|
123
|
+
WifiForgetOutcome.DISPATCHED,
|
|
124
|
+
parseWifiForgetResult("forget-1", "sid-1", " Field AP ", 1, exact)?.outcome,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
fun `forget parser preserves each honest terminal outcome`() {
|
|
130
|
+
listOf("confirmed", "dispatched", "not_found", "unsupported", "failed").forEach { outcome ->
|
|
131
|
+
val parsed =
|
|
132
|
+
parseWifiForgetResult(
|
|
133
|
+
"forget-1",
|
|
134
|
+
"sid-1",
|
|
135
|
+
"AP",
|
|
136
|
+
1,
|
|
137
|
+
mapOf(
|
|
138
|
+
"requestId" to "forget-1",
|
|
139
|
+
"sid" to "sid-1",
|
|
140
|
+
"ssid" to "AP",
|
|
141
|
+
"protocolVersion" to 1,
|
|
142
|
+
"outcome" to outcome,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
assertEquals(outcome, parsed?.outcome?.wireValue)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Test
|
|
150
|
+
fun `missing connectivity snapshot remains unknown`() {
|
|
151
|
+
val parsed =
|
|
152
|
+
parseWifiForgetResult(
|
|
153
|
+
"forget-1",
|
|
154
|
+
"sid-1",
|
|
155
|
+
"AP",
|
|
156
|
+
1,
|
|
157
|
+
mapOf(
|
|
158
|
+
"requestId" to "forget-1",
|
|
159
|
+
"sid" to "sid-1",
|
|
160
|
+
"ssid" to "AP",
|
|
161
|
+
"protocolVersion" to 1,
|
|
162
|
+
"outcome" to "dispatched",
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
assertNull(parsed?.connected)
|
|
167
|
+
assertFalse(parsed?.toMap()?.containsKey("connected") ?: true)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@Test
|
|
171
|
+
fun `legacy forget is explicitly unverified`() {
|
|
172
|
+
val result =
|
|
173
|
+
legacyWifiForgetResult(
|
|
174
|
+
"AP",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
assertFalse(result.toMap().keys.any { it in setOf("mode", "capabilityVersion", "requestId", "sid") })
|
|
178
|
+
assertEquals(WifiForgetOutcome.LEGACY_UNVERIFIED, result.outcome)
|
|
179
|
+
assertNull(result.connected)
|
|
180
|
+
assertNull(result.currentSsid)
|
|
181
|
+
assertNull(result.localIp)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@Test
|
|
185
|
+
fun `saved list preserves exact identity and requires correlation tuple`() {
|
|
186
|
+
val exact =
|
|
187
|
+
mapOf<String, Any>(
|
|
188
|
+
"requestId" to "saved-1",
|
|
189
|
+
"sid" to "sid-1",
|
|
190
|
+
"protocolVersion" to 1,
|
|
191
|
+
"outcome" to "confirmed",
|
|
192
|
+
"networks" to listOf(" Field AP ", "", "Field AP", " Field AP "),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
assertNull(parseSavedWifiNetworks("other", "sid-1", 1, exact))
|
|
196
|
+
assertNull(parseSavedWifiNetworks("saved-1", "sid-1", 1, exact - "requestId"))
|
|
197
|
+
assertNull(parseSavedWifiNetworks("saved-1", "other", 1, exact))
|
|
198
|
+
assertNull(parseSavedWifiNetworks("saved-1", "sid-1", 2, exact))
|
|
199
|
+
assertEquals(
|
|
200
|
+
listOf(" Field AP ", "Field AP"),
|
|
201
|
+
parseSavedWifiNetworks("saved-1", "sid-1", 1, exact)?.networks,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
val failure =
|
|
205
|
+
parseSavedWifiNetworks(
|
|
206
|
+
"saved-1",
|
|
207
|
+
"sid-1",
|
|
208
|
+
1,
|
|
209
|
+
exact + ("outcome" to "failed") + ("error" to "backend_failed") + ("networks" to emptyList<String>()),
|
|
210
|
+
)
|
|
211
|
+
assertEquals(SavedWifiNetworksOutcome.FAILED, failure?.outcome)
|
|
212
|
+
assertEquals("backend_failed", failure?.error)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@Test
|
|
217
|
+
fun `raw forget event preserves modern and legacy wire truth`() {
|
|
218
|
+
val modern =
|
|
219
|
+
normalizeWifiForgetResultEvent(
|
|
220
|
+
"forget-1",
|
|
221
|
+
"sid-1",
|
|
222
|
+
"AP",
|
|
223
|
+
1,
|
|
224
|
+
"dispatched",
|
|
225
|
+
null,
|
|
226
|
+
false,
|
|
227
|
+
"",
|
|
228
|
+
"",
|
|
229
|
+
null,
|
|
230
|
+
)
|
|
231
|
+
val legacy =
|
|
232
|
+
normalizeWifiForgetResultEvent(
|
|
233
|
+
"",
|
|
234
|
+
"",
|
|
235
|
+
"AP",
|
|
236
|
+
0,
|
|
237
|
+
"",
|
|
238
|
+
true,
|
|
239
|
+
false,
|
|
240
|
+
"",
|
|
241
|
+
"",
|
|
242
|
+
null,
|
|
243
|
+
)
|
|
244
|
+
val withoutSnapshot =
|
|
245
|
+
normalizeWifiForgetResultEvent(
|
|
246
|
+
"forget-unknown",
|
|
247
|
+
"sid-1",
|
|
248
|
+
"AP",
|
|
249
|
+
1,
|
|
250
|
+
"dispatched",
|
|
251
|
+
null,
|
|
252
|
+
null,
|
|
253
|
+
"",
|
|
254
|
+
"",
|
|
255
|
+
null,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
assertEquals("modern", modern?.get("mode"))
|
|
259
|
+
assertEquals("dispatched", modern?.get("outcome"))
|
|
260
|
+
assertEquals("legacy", legacy?.get("mode"))
|
|
261
|
+
assertEquals(true, legacy?.get("dispatched"))
|
|
262
|
+
assertFalse(legacy?.containsKey("sid") ?: true)
|
|
263
|
+
assertFalse(withoutSnapshot?.containsKey("connected") ?: true)
|
|
264
|
+
assertNull(
|
|
265
|
+
normalizeWifiForgetResultEvent(
|
|
266
|
+
"bad", "", "AP", 0, "", null, false, "", "", null
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
}
|
package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class ClassicAudioConnectionTrackerTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun `available connected profile is sufficient even if the other query fails or never answers`() {
|
|
9
|
+
assertThat(readyClassicAudioSnapshot(mapOf(ClassicAudioProfile.A2DP to true)))
|
|
10
|
+
.containsExactly(ClassicAudioProfile.A2DP)
|
|
11
|
+
assertThat(readyClassicAudioSnapshot(mapOf(ClassicAudioProfile.HEADSET to true)))
|
|
12
|
+
.containsExactly(ClassicAudioProfile.HEADSET)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Test
|
|
16
|
+
fun `missing profile never fabricates a disconnect during a profile switch`() {
|
|
17
|
+
assertThat(readyClassicAudioSnapshot(mapOf(ClassicAudioProfile.A2DP to false))).isNull()
|
|
18
|
+
assertThat(readyClassicAudioSnapshot(mapOf(ClassicAudioProfile.A2DP to false,
|
|
19
|
+
ClassicAudioProfile.HEADSET to false))).isEmpty()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private val address = "AA:BB:CC:DD:EE:FF"
|
|
23
|
+
|
|
24
|
+
private fun snapshot(tracker: ClassicAudioConnectionTracker, vararg profiles: ClassicAudioProfile) {
|
|
25
|
+
val ticket = tracker.beginSnapshot(address)!!
|
|
26
|
+
assertThat(tracker.applySnapshot(ticket, address, profiles.toSet())).isTrue()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `publishes complete A2DP and HFP snapshots without intermediate false negative`() {
|
|
31
|
+
val changes = mutableListOf<Boolean>()
|
|
32
|
+
val tracker = ClassicAudioConnectionTracker(changes::add)
|
|
33
|
+
tracker.setTarget(address)
|
|
34
|
+
snapshot(tracker, ClassicAudioProfile.A2DP)
|
|
35
|
+
snapshot(tracker, ClassicAudioProfile.HEADSET)
|
|
36
|
+
snapshot(tracker)
|
|
37
|
+
assertThat(changes).containsExactly(true, false)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test
|
|
41
|
+
fun `address comparison is case insensitive`() {
|
|
42
|
+
val tracker = ClassicAudioConnectionTracker {}
|
|
43
|
+
tracker.setTarget(address.lowercase())
|
|
44
|
+
snapshot(tracker, ClassicAudioProfile.A2DP)
|
|
45
|
+
assertThat(tracker.connected).isTrue()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
fun `another device cannot request a snapshot`() {
|
|
50
|
+
val tracker = ClassicAudioConnectionTracker {}
|
|
51
|
+
tracker.setTarget(address)
|
|
52
|
+
assertThat(tracker.beginSnapshot("11:22:33:44:55:66")).isNull()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `late result for previous device cannot replace current profiles`() {
|
|
57
|
+
val tracker = ClassicAudioConnectionTracker {}
|
|
58
|
+
tracker.setTarget(address)
|
|
59
|
+
val ticket = tracker.beginSnapshot(address)!!
|
|
60
|
+
tracker.setTarget("11:22:33:44:55:66")
|
|
61
|
+
assertThat(tracker.applySnapshot(ticket, address, setOf(ClassicAudioProfile.A2DP))).isFalse()
|
|
62
|
+
assertThat(tracker.connected).isFalse()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Test
|
|
66
|
+
fun `old response cannot resurrect reconnected session with same MAC`() {
|
|
67
|
+
val tracker = ClassicAudioConnectionTracker {}
|
|
68
|
+
tracker.setTarget(address)
|
|
69
|
+
val ticket = tracker.beginSnapshot(address)!!
|
|
70
|
+
tracker.invalidate(address)
|
|
71
|
+
tracker.setTarget(address)
|
|
72
|
+
snapshot(tracker)
|
|
73
|
+
assertThat(tracker.applySnapshot(ticket, address, setOf(ClassicAudioProfile.A2DP))).isFalse()
|
|
74
|
+
assertThat(tracker.connected).isFalse()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
fun `newer query invalidates in flight snapshot even before its response arrives`() {
|
|
79
|
+
val tracker = ClassicAudioConnectionTracker {}
|
|
80
|
+
tracker.setTarget(address)
|
|
81
|
+
val first = tracker.beginSnapshot(address)!!
|
|
82
|
+
val second = tracker.beginSnapshot(address)!!
|
|
83
|
+
assertThat(tracker.applySnapshot(first, address, setOf(ClassicAudioProfile.A2DP))).isFalse()
|
|
84
|
+
assertThat(tracker.applySnapshot(second, address, emptySet())).isTrue()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Test
|
|
88
|
+
fun `late disconnect snapshot cannot overwrite newer connected snapshot`() {
|
|
89
|
+
val tracker = ClassicAudioConnectionTracker {}
|
|
90
|
+
tracker.setTarget(address)
|
|
91
|
+
val old = tracker.beginSnapshot(address)!!
|
|
92
|
+
snapshot(tracker, ClassicAudioProfile.HEADSET)
|
|
93
|
+
assertThat(tracker.applySnapshot(old, address, emptySet())).isFalse()
|
|
94
|
+
assertThat(tracker.connected).isTrue()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
fun `clear invalidates queries but retains target for retry`() {
|
|
99
|
+
val tracker = ClassicAudioConnectionTracker {}
|
|
100
|
+
tracker.setTarget(address)
|
|
101
|
+
val old = tracker.beginSnapshot(address)!!
|
|
102
|
+
assertThat(tracker.clear(address)).isTrue()
|
|
103
|
+
assertThat(tracker.applySnapshot(old, address, setOf(ClassicAudioProfile.A2DP))).isFalse()
|
|
104
|
+
snapshot(tracker, ClassicAudioProfile.A2DP)
|
|
105
|
+
assertThat(tracker.connected).isTrue()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Test
|
|
109
|
+
fun `reset clears truth and prevents pending results from applying`() {
|
|
110
|
+
val changes = mutableListOf<Boolean>()
|
|
111
|
+
val tracker = ClassicAudioConnectionTracker(changes::add)
|
|
112
|
+
tracker.setTarget(address)
|
|
113
|
+
snapshot(tracker, ClassicAudioProfile.A2DP)
|
|
114
|
+
val ticket = tracker.beginSnapshot(address)!!
|
|
115
|
+
tracker.reset()
|
|
116
|
+
assertThat(tracker.applySnapshot(ticket, address, setOf(ClassicAudioProfile.A2DP))).isFalse()
|
|
117
|
+
assertThat(tracker.beginSnapshot(address)).isNull()
|
|
118
|
+
assertThat(changes).containsExactly(true, false)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test
|
|
122
|
+
fun `stale snapshot cannot emit audio routing notifications`() {
|
|
123
|
+
val tracker = ClassicAudioConnectionTracker {}
|
|
124
|
+
tracker.setTarget(address)
|
|
125
|
+
val old = tracker.beginSnapshot(address)!!
|
|
126
|
+
tracker.setTarget(address)
|
|
127
|
+
var notifications = 0
|
|
128
|
+
tracker.applySnapshot(old, address, setOf(ClassicAudioProfile.A2DP)) { notifications++ }
|
|
129
|
+
assertThat(notifications).isZero()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -99,6 +99,54 @@ export type WifiStatusChangeEvent = WifiStatus & {
|
|
|
99
99
|
*/
|
|
100
100
|
error?: string;
|
|
101
101
|
};
|
|
102
|
+
export type WifiForgetOutcome = "confirmed" | "dispatched" | "not_found" | "unsupported" | "failed" | "legacy_unverified";
|
|
103
|
+
export type WifiForgetResult = {
|
|
104
|
+
ssid: string;
|
|
105
|
+
outcome: WifiForgetOutcome;
|
|
106
|
+
connected?: boolean;
|
|
107
|
+
currentSsid?: string;
|
|
108
|
+
localIp?: string;
|
|
109
|
+
error?: string;
|
|
110
|
+
};
|
|
111
|
+
export type ModernWifiForgetResultEvent = {
|
|
112
|
+
type: "wifi_forget_result";
|
|
113
|
+
mode: "modern";
|
|
114
|
+
requestId: string;
|
|
115
|
+
sid: string;
|
|
116
|
+
ssid: string;
|
|
117
|
+
protocolVersion: number;
|
|
118
|
+
outcome: Exclude<WifiForgetOutcome, "legacy_unverified">;
|
|
119
|
+
connected?: boolean;
|
|
120
|
+
currentSsid?: string;
|
|
121
|
+
localIp?: string;
|
|
122
|
+
error?: string;
|
|
123
|
+
};
|
|
124
|
+
export type LegacyWifiForgetResultEvent = {
|
|
125
|
+
type: "wifi_forget_result";
|
|
126
|
+
mode: "legacy";
|
|
127
|
+
ssid: string;
|
|
128
|
+
dispatched: boolean;
|
|
129
|
+
connected?: boolean;
|
|
130
|
+
currentSsid?: string;
|
|
131
|
+
localIp?: string;
|
|
132
|
+
error?: string;
|
|
133
|
+
};
|
|
134
|
+
export type WifiForgetResultEvent = ModernWifiForgetResultEvent | LegacyWifiForgetResultEvent;
|
|
135
|
+
export type SavedWifiNetworksOutcome = "confirmed" | "unsupported" | "failed";
|
|
136
|
+
export type SavedWifiNetworksResult = {
|
|
137
|
+
outcome: SavedWifiNetworksOutcome;
|
|
138
|
+
networks: string[];
|
|
139
|
+
error?: string;
|
|
140
|
+
};
|
|
141
|
+
export type SavedWifiNetworksEvent = {
|
|
142
|
+
type: "saved_wifi_networks";
|
|
143
|
+
requestId: string;
|
|
144
|
+
sid: string;
|
|
145
|
+
protocolVersion: number;
|
|
146
|
+
outcome: SavedWifiNetworksOutcome;
|
|
147
|
+
networks: string[];
|
|
148
|
+
error?: string;
|
|
149
|
+
};
|
|
102
150
|
export type HotspotStatus = {
|
|
103
151
|
state: "disabled";
|
|
104
152
|
} | {
|
|
@@ -120,6 +168,8 @@ export type HotspotErrorEvent = {
|
|
|
120
168
|
timestamp: number;
|
|
121
169
|
};
|
|
122
170
|
export type VersionInfoResult = {
|
|
171
|
+
versionInfoType?: string;
|
|
172
|
+
sid?: string;
|
|
123
173
|
androidVersion: string;
|
|
124
174
|
firmwareVersion: string;
|
|
125
175
|
besFirmwareVersion: string;
|
|
@@ -142,6 +192,10 @@ export type VersionInfoResult = {
|
|
|
142
192
|
packageName?: string;
|
|
143
193
|
/** Phone-served hotspot OTA protocol version; 0 means unsupported/legacy glasses. */
|
|
144
194
|
hotspotOtaVersion: number;
|
|
195
|
+
/** Session-correlated WiFi forget result protocol; absent/0 means unsupported. */
|
|
196
|
+
wifiForgetResultVersion?: number;
|
|
197
|
+
/** Session-correlated saved-network listing protocol; absent/0 means unsupported. */
|
|
198
|
+
savedWifiNetworksVersion?: number;
|
|
145
199
|
};
|
|
146
200
|
export type VersionInfoEvent = VersionInfoResult & {
|
|
147
201
|
type: "version_info";
|
|
@@ -782,6 +836,8 @@ export type BluetoothSdkModuleEvents = {
|
|
|
782
836
|
phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void;
|
|
783
837
|
wifi_status_change: (event: WifiStatusChangeEvent) => void;
|
|
784
838
|
wifi_scan_result: (event: WifiScanResultEvent) => void;
|
|
839
|
+
wifi_forget_result: (event: WifiForgetResultEvent) => void;
|
|
840
|
+
saved_wifi_networks: (event: SavedWifiNetworksEvent) => void;
|
|
785
841
|
hotspot_status_change: (event: HotspotStatusChangeEvent) => void;
|
|
786
842
|
hotspot_error: (event: HotspotErrorEvent) => void;
|
|
787
843
|
photo_response: (event: PhotoResponseEvent) => void;
|
|
@@ -918,6 +974,8 @@ export type BluetoothSdkEventMap = {
|
|
|
918
974
|
local_transcription: LocalTranscriptionEvent;
|
|
919
975
|
wifi_status_change: WifiStatusChangeEvent;
|
|
920
976
|
wifi_scan_result: WifiScanResultEvent;
|
|
977
|
+
wifi_forget_result: WifiForgetResultEvent;
|
|
978
|
+
saved_wifi_networks: SavedWifiNetworksEvent;
|
|
921
979
|
hotspot_status_change: HotspotStatusChangeEvent;
|
|
922
980
|
hotspot_error: HotspotErrorEvent;
|
|
923
981
|
photo_response: PhotoResponseEvent;
|
|
@@ -995,8 +1053,11 @@ export interface BluetoothSdkPublicModule {
|
|
|
995
1053
|
/** Keep legacy Mentra Live OTA sessions awake. Modern sessions normally do not require this. */
|
|
996
1054
|
ping(): Promise<void>;
|
|
997
1055
|
requestWifiScan(): Promise<WifiSearchResult[]>;
|
|
1056
|
+
/** List exact WiFi SSIDs from glasses that support reliable saved-network enumeration. */
|
|
1057
|
+
getSavedWifiNetworks(): Promise<SavedWifiNetworksResult>;
|
|
998
1058
|
sendWifiCredentials(ssid: string, password: string): Promise<WifiStatusChangeEvent>;
|
|
999
|
-
|
|
1059
|
+
/** Forget a saved network and return its correlated semantic outcome. */
|
|
1060
|
+
forgetWifiNetwork(ssid: string): Promise<WifiForgetResult>;
|
|
1000
1061
|
setHotspotState(enabled: boolean): Promise<HotspotStatusChangeEvent>;
|
|
1001
1062
|
/** Set the glasses clock from the phone after an OTA clock-skew failure. */
|
|
1002
1063
|
setSystemTime(timestampMs: number): Promise<void>;
|