@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
|
@@ -69,9 +69,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
69
69
|
private var pendingOtaStart: PendingResponse<OtaStartAckEvent>? = null
|
|
70
70
|
private var pendingStreamStop: PendingStreamStop? = null
|
|
71
71
|
private var pendingWifiScan: PendingWifiScan? = null
|
|
72
|
+
private var pendingSavedWifiNetworks: PendingSavedWifiNetworks? = null
|
|
72
73
|
private var pendingWifiStatus: PendingWifiStatusRequest? = null
|
|
74
|
+
private var pendingWifiForget: PendingWifiForgetRequest? = null
|
|
73
75
|
private var pendingHotspotStatus: PendingHotspotStatusRequest? = null
|
|
74
76
|
private var pendingVersionInfo: PendingVersionInfoRequest? = null
|
|
77
|
+
private val wifiSessionCapabilities = WifiSessionCapabilities()
|
|
75
78
|
@Volatile private var configuredOtaVersionUrl: String? = null
|
|
76
79
|
|
|
77
80
|
init {
|
|
@@ -195,15 +198,28 @@ class MentraBluetoothSdk private constructor(
|
|
|
195
198
|
)
|
|
196
199
|
|
|
197
200
|
private data class PendingWifiStatusRequest(
|
|
198
|
-
val operation: WifiStatusOperation,
|
|
199
201
|
val ssid: String,
|
|
200
202
|
val pending: PendingResponse<WifiStatusEvent>,
|
|
201
203
|
)
|
|
202
204
|
|
|
203
|
-
private
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
private data class PendingWifiForgetRequest(
|
|
206
|
+
val ssid: String,
|
|
207
|
+
val requestId: String,
|
|
208
|
+
var sid: String,
|
|
209
|
+
val epoch: Long,
|
|
210
|
+
val pending: PendingResponse<WifiForgetResult>,
|
|
211
|
+
var mode: WifiRequestMode,
|
|
212
|
+
var commandSent: Boolean = false,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
private data class PendingSavedWifiNetworks(
|
|
216
|
+
val requestId: String,
|
|
217
|
+
var sid: String,
|
|
218
|
+
val epoch: Long,
|
|
219
|
+
val pending: PendingResponse<SavedWifiNetworksResult>,
|
|
220
|
+
var mode: WifiRequestMode,
|
|
221
|
+
var commandSent: Boolean = false,
|
|
222
|
+
)
|
|
207
223
|
|
|
208
224
|
private data class PendingHotspotStatusRequest(
|
|
209
225
|
val enabled: Boolean,
|
|
@@ -239,6 +255,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
239
255
|
"$operation timed out waiting for glasses response.",
|
|
240
256
|
)
|
|
241
257
|
}
|
|
258
|
+
|
|
242
259
|
}
|
|
243
260
|
|
|
244
261
|
fun addListener(listener: MentraBluetoothSdkListener) {
|
|
@@ -880,13 +897,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
880
897
|
suspend fun sendWifiCredentials(ssid: String, password: String): WifiStatusEvent {
|
|
881
898
|
val pending = PendingResponse<WifiStatusEvent>("WiFi connect request")
|
|
882
899
|
synchronized(oneShotLock) {
|
|
883
|
-
if (pendingWifiStatus != null) {
|
|
900
|
+
if (pendingWifiStatus != null || pendingWifiForget != null) {
|
|
884
901
|
throw BluetoothSdkException(
|
|
885
902
|
"request_in_flight",
|
|
886
903
|
"A WiFi status command is already waiting for a glasses response.",
|
|
887
904
|
)
|
|
888
905
|
}
|
|
889
|
-
pendingWifiStatus = PendingWifiStatusRequest(
|
|
906
|
+
pendingWifiStatus = PendingWifiStatusRequest(ssid, pending)
|
|
890
907
|
}
|
|
891
908
|
try {
|
|
892
909
|
deviceManager.sendWifiCredentials(ssid, password)
|
|
@@ -900,24 +917,94 @@ class MentraBluetoothSdk private constructor(
|
|
|
900
917
|
}
|
|
901
918
|
}
|
|
902
919
|
|
|
903
|
-
suspend fun forgetWifiNetwork(ssid: String):
|
|
904
|
-
|
|
920
|
+
suspend fun forgetWifiNetwork(ssid: String): WifiForgetResult {
|
|
921
|
+
if (!wifiSsidIsValid(ssid)) {
|
|
922
|
+
throw BluetoothSdkException("invalid_ssid", "WiFi SSID cannot be empty.")
|
|
923
|
+
}
|
|
924
|
+
val pending = PendingResponse<WifiForgetResult>("WiFi forget request")
|
|
925
|
+
val requestId = "forget-${UUID.randomUUID()}"
|
|
926
|
+
lateinit var request: PendingWifiForgetRequest
|
|
905
927
|
synchronized(oneShotLock) {
|
|
906
|
-
if (pendingWifiStatus != null) {
|
|
928
|
+
if (pendingWifiStatus != null || pendingWifiForget != null) {
|
|
907
929
|
throw BluetoothSdkException(
|
|
908
930
|
"request_in_flight",
|
|
909
931
|
"A WiFi status command is already waiting for a glasses response.",
|
|
910
932
|
)
|
|
911
933
|
}
|
|
912
|
-
|
|
934
|
+
request =
|
|
935
|
+
PendingWifiForgetRequest(
|
|
936
|
+
ssid = ssid,
|
|
937
|
+
requestId = requestId,
|
|
938
|
+
sid = wifiSessionCapabilities.sessionId,
|
|
939
|
+
epoch = wifiSessionCapabilities.epoch,
|
|
940
|
+
pending = pending,
|
|
941
|
+
mode = wifiSessionCapabilities.forgetMode(),
|
|
942
|
+
)
|
|
943
|
+
pendingWifiForget = request
|
|
913
944
|
}
|
|
914
945
|
try {
|
|
915
|
-
|
|
946
|
+
dispatchWifiForgetIfReady(request)
|
|
916
947
|
return pending.await()
|
|
948
|
+
} catch (error: BluetoothSdkException) {
|
|
949
|
+
if (error.code == "request_timeout" && synchronized(oneShotLock) { request.mode == WifiRequestMode.DISCOVERING }) {
|
|
950
|
+
throw BluetoothSdkException(WIFI_CAPABILITY_NEGOTIATION_TIMEOUT_CODE, "WiFi capability negotiation timed out.")
|
|
951
|
+
}
|
|
952
|
+
throw error
|
|
917
953
|
} finally {
|
|
918
954
|
synchronized(oneShotLock) {
|
|
919
|
-
if (
|
|
920
|
-
|
|
955
|
+
if (pendingWifiForget === request) {
|
|
956
|
+
pendingWifiForget = null
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
suspend fun getSavedWifiNetworks(): SavedWifiNetworksResult {
|
|
963
|
+
val requestId = "saved-${UUID.randomUUID()}"
|
|
964
|
+
val pending = PendingResponse<SavedWifiNetworksResult>("Saved WiFi networks request")
|
|
965
|
+
var request: PendingSavedWifiNetworks? = null
|
|
966
|
+
var unsupported: SavedWifiNetworksResult? = null
|
|
967
|
+
synchronized(oneShotLock) {
|
|
968
|
+
if (pendingSavedWifiNetworks != null) {
|
|
969
|
+
throw BluetoothSdkException(
|
|
970
|
+
"request_in_flight",
|
|
971
|
+
"A saved WiFi networks request is already waiting for a glasses response.",
|
|
972
|
+
)
|
|
973
|
+
}
|
|
974
|
+
val snapshot = wifiSessionCapabilities.savedNetworksRequestSnapshot()
|
|
975
|
+
if (snapshot.mode == WifiRequestMode.LEGACY || snapshot.mode == WifiRequestMode.UNSUPPORTED) {
|
|
976
|
+
unsupported =
|
|
977
|
+
SavedWifiNetworksResult(
|
|
978
|
+
outcome = SavedWifiNetworksOutcome.UNSUPPORTED,
|
|
979
|
+
networks = emptyList(),
|
|
980
|
+
error = "saved_wifi_networks_unsupported",
|
|
981
|
+
)
|
|
982
|
+
return@synchronized
|
|
983
|
+
}
|
|
984
|
+
request =
|
|
985
|
+
PendingSavedWifiNetworks(
|
|
986
|
+
requestId = requestId,
|
|
987
|
+
sid = snapshot.sessionId,
|
|
988
|
+
epoch = snapshot.epoch,
|
|
989
|
+
pending = pending,
|
|
990
|
+
mode = snapshot.mode,
|
|
991
|
+
)
|
|
992
|
+
pendingSavedWifiNetworks = request
|
|
993
|
+
}
|
|
994
|
+
unsupported?.let { return it }
|
|
995
|
+
val activeRequest = checkNotNull(request)
|
|
996
|
+
try {
|
|
997
|
+
dispatchSavedWifiNetworksIfReady(activeRequest)
|
|
998
|
+
return pending.await()
|
|
999
|
+
} catch (error: BluetoothSdkException) {
|
|
1000
|
+
if (error.code == "request_timeout" && synchronized(oneShotLock) { activeRequest.mode == WifiRequestMode.DISCOVERING }) {
|
|
1001
|
+
throw BluetoothSdkException(WIFI_CAPABILITY_NEGOTIATION_TIMEOUT_CODE, "WiFi capability negotiation timed out.")
|
|
1002
|
+
}
|
|
1003
|
+
throw error
|
|
1004
|
+
} finally {
|
|
1005
|
+
synchronized(oneShotLock) {
|
|
1006
|
+
if (pendingSavedWifiNetworks === activeRequest) {
|
|
1007
|
+
pendingSavedWifiNetworks = null
|
|
921
1008
|
}
|
|
922
1009
|
}
|
|
923
1010
|
}
|
|
@@ -1468,6 +1555,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1468
1555
|
|
|
1469
1556
|
override fun close() {
|
|
1470
1557
|
stopStreamKeepAliveMonitor()
|
|
1558
|
+
resetWifiProtocolSession("", "sdk_closed")
|
|
1471
1559
|
if (activityLifecycleCallbacksRegistered) {
|
|
1472
1560
|
(appContext as? Application)?.unregisterActivityLifecycleCallbacks(
|
|
1473
1561
|
activityLifecycleCallbacks,
|
|
@@ -1483,6 +1571,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
1483
1571
|
private fun dispatchStoreUpdate(category: String, changes: Map<String, Any>) {
|
|
1484
1572
|
when (ObservableStore.normalizeCategory(category)) {
|
|
1485
1573
|
"glasses" -> {
|
|
1574
|
+
if (changes["connected"] == false) {
|
|
1575
|
+
resetWifiProtocolSession("", "wifi_session_disconnected")
|
|
1576
|
+
}
|
|
1486
1577
|
analytics.observeGlassesStatus(getRawGlassesStatus())
|
|
1487
1578
|
val state = getState()
|
|
1488
1579
|
dispatchToListeners {
|
|
@@ -1691,6 +1782,27 @@ class MentraBluetoothSdk private constructor(
|
|
|
1691
1782
|
handleWifiStatusForRequests(event)
|
|
1692
1783
|
dispatchToListeners { it.onWifiStatusChanged(event) }
|
|
1693
1784
|
}
|
|
1785
|
+
"wifi_forget_result" -> {
|
|
1786
|
+
handleWifiForgetResultForRequests(data)
|
|
1787
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1788
|
+
}
|
|
1789
|
+
"saved_wifi_networks" -> {
|
|
1790
|
+
handleSavedWifiNetworksForRequests(data)
|
|
1791
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1792
|
+
}
|
|
1793
|
+
"wifi_protocol_session_ready" -> {
|
|
1794
|
+
resetWifiProtocolSession(
|
|
1795
|
+
data["sid"] as? String ?: "",
|
|
1796
|
+
"wifi_session_restarted",
|
|
1797
|
+
)
|
|
1798
|
+
}
|
|
1799
|
+
"glasses_session_changed" -> {
|
|
1800
|
+
resetWifiProtocolSession(
|
|
1801
|
+
data["sid"] as? String ?: "",
|
|
1802
|
+
"wifi_session_changed",
|
|
1803
|
+
)
|
|
1804
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1805
|
+
}
|
|
1694
1806
|
"wifi_scan_result" -> {
|
|
1695
1807
|
val networks =
|
|
1696
1808
|
(data["networks"] as? List<*>)
|
|
@@ -1800,6 +1912,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
1800
1912
|
dispatchToListeners { it.onSettingsAck(event) }
|
|
1801
1913
|
}
|
|
1802
1914
|
"version_info" -> {
|
|
1915
|
+
if (data["versionInfoType"] == "version_info_1" || data["versionInfoType"] == "version_info") {
|
|
1916
|
+
applyWifiProtocolCapabilities(data)
|
|
1917
|
+
}
|
|
1803
1918
|
val event = VersionInfoResult.fromMap(data)
|
|
1804
1919
|
handleVersionInfoForRequest(data)
|
|
1805
1920
|
dispatchToListeners { it.onVersionInfo(event) }
|
|
@@ -2220,43 +2335,184 @@ class MentraBluetoothSdk private constructor(
|
|
|
2220
2335
|
}
|
|
2221
2336
|
|
|
2222
2337
|
private fun handleWifiStatusForRequests(event: WifiStatusEvent) {
|
|
2223
|
-
val
|
|
2338
|
+
val connectRequest = synchronized(oneShotLock) { pendingWifiStatus }
|
|
2224
2339
|
// A wifi_status carrying the explicit error field is the glasses' failure
|
|
2225
2340
|
// verdict for the in-flight connect: reject now instead of running out the
|
|
2226
2341
|
// request timeout. Only the error field counts as failure — the glasses'
|
|
2227
2342
|
// connect sequence emits a debounced bare connected=false ~1-2s after
|
|
2228
2343
|
// credentials while association is still in progress, and rejecting on that
|
|
2229
2344
|
// would kill every connect attempt early.
|
|
2230
|
-
if (
|
|
2345
|
+
if (connectRequest != null && event.error != null) {
|
|
2231
2346
|
synchronized(oneShotLock) {
|
|
2232
|
-
if (pendingWifiStatus ===
|
|
2347
|
+
if (pendingWifiStatus === connectRequest) {
|
|
2233
2348
|
pendingWifiStatus = null
|
|
2234
2349
|
}
|
|
2235
2350
|
}
|
|
2236
|
-
|
|
2351
|
+
connectRequest.pending.reject(
|
|
2237
2352
|
BluetoothSdkException(
|
|
2238
2353
|
event.error,
|
|
2239
|
-
"Glasses failed to join \"${
|
|
2354
|
+
"Glasses failed to join \"${connectRequest.ssid}\": ${event.error}",
|
|
2240
2355
|
)
|
|
2241
2356
|
)
|
|
2242
2357
|
return
|
|
2243
2358
|
}
|
|
2244
|
-
if (
|
|
2359
|
+
if (connectRequest != null && wifiStatusMatchesConnect(event.status, connectRequest.ssid)) {
|
|
2360
|
+
synchronized(oneShotLock) {
|
|
2361
|
+
if (pendingWifiStatus === connectRequest) {
|
|
2362
|
+
pendingWifiStatus = null
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
connectRequest.pending.resolve(event)
|
|
2366
|
+
return
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// Forget never settles from link state: modern requests require a correlated result,
|
|
2370
|
+
// while legacy requests resolve as unverified at accepted dispatch.
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
private fun handleWifiForgetResultForRequests(data: Map<String, Any>) {
|
|
2374
|
+
val request = synchronized(oneShotLock) { pendingWifiForget } ?: return
|
|
2375
|
+
if (request.mode != WifiRequestMode.MODERN) return
|
|
2376
|
+
val version =
|
|
2377
|
+
synchronized(oneShotLock) {
|
|
2378
|
+
if (request.epoch != wifiSessionCapabilities.epoch) return
|
|
2379
|
+
(wifiSessionCapabilities.forgetResult as? WifiProtocolCapability.Supported)?.version
|
|
2380
|
+
} ?: return
|
|
2381
|
+
val result =
|
|
2382
|
+
parseWifiForgetResult(
|
|
2383
|
+
request.requestId,
|
|
2384
|
+
request.sid,
|
|
2385
|
+
request.ssid,
|
|
2386
|
+
version,
|
|
2387
|
+
data,
|
|
2388
|
+
) ?: return
|
|
2389
|
+
|
|
2245
2390
|
synchronized(oneShotLock) {
|
|
2246
|
-
if (
|
|
2247
|
-
|
|
2391
|
+
if (pendingWifiForget !== request || request.epoch != wifiSessionCapabilities.epoch) {
|
|
2392
|
+
return
|
|
2393
|
+
}
|
|
2394
|
+
pendingWifiForget = null
|
|
2395
|
+
}
|
|
2396
|
+
request.pending.resolve(result)
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
private fun handleSavedWifiNetworksForRequests(data: Map<String, Any>) {
|
|
2400
|
+
val request = synchronized(oneShotLock) { pendingSavedWifiNetworks } ?: return
|
|
2401
|
+
if (request.mode != WifiRequestMode.MODERN) return
|
|
2402
|
+
val version =
|
|
2403
|
+
synchronized(oneShotLock) {
|
|
2404
|
+
if (request.epoch != wifiSessionCapabilities.epoch) return
|
|
2405
|
+
(wifiSessionCapabilities.savedNetworks as? WifiProtocolCapability.Supported)?.version
|
|
2406
|
+
} ?: return
|
|
2407
|
+
val result =
|
|
2408
|
+
parseSavedWifiNetworks(
|
|
2409
|
+
request.requestId,
|
|
2410
|
+
request.sid,
|
|
2411
|
+
version,
|
|
2412
|
+
data,
|
|
2413
|
+
) ?: return
|
|
2414
|
+
synchronized(oneShotLock) {
|
|
2415
|
+
if (pendingSavedWifiNetworks !== request || request.epoch != wifiSessionCapabilities.epoch) {
|
|
2416
|
+
return
|
|
2417
|
+
}
|
|
2418
|
+
pendingSavedWifiNetworks = null
|
|
2419
|
+
}
|
|
2420
|
+
request.pending.resolve(result)
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
private fun wifiStatusMatchesConnect(status: WifiStatus, ssid: String): Boolean =
|
|
2424
|
+
status is WifiStatus.Connected && status.ssid == ssid
|
|
2425
|
+
|
|
2426
|
+
private fun dispatchWifiForgetIfReady(request: PendingWifiForgetRequest) {
|
|
2427
|
+
synchronized(oneShotLock) {
|
|
2428
|
+
if (pendingWifiForget !== request || request.epoch != wifiSessionCapabilities.epoch ||
|
|
2429
|
+
request.mode == WifiRequestMode.DISCOVERING || request.commandSent) return
|
|
2430
|
+
if (request.mode == WifiRequestMode.UNSUPPORTED) {
|
|
2431
|
+
request.pending.reject(BluetoothSdkException("wifi_protocol_unsupported", "Unsupported WiFi forget protocol version."))
|
|
2432
|
+
return
|
|
2433
|
+
}
|
|
2434
|
+
request.sid = wifiSessionCapabilities.sessionId
|
|
2435
|
+
request.commandSent = deviceManager.forgetWifiNetwork(
|
|
2436
|
+
request.ssid,
|
|
2437
|
+
request.requestId.takeIf { request.mode == WifiRequestMode.MODERN },
|
|
2438
|
+
request.sid.takeIf { request.mode == WifiRequestMode.MODERN },
|
|
2439
|
+
)
|
|
2440
|
+
if (!request.commandSent) {
|
|
2441
|
+
request.pending.reject(BluetoothSdkException("dispatch_failed", "No active glasses transport accepted WiFi forget."))
|
|
2442
|
+
} else if (request.mode == WifiRequestMode.LEGACY) {
|
|
2443
|
+
pendingWifiForget = null
|
|
2444
|
+
request.pending.resolve(legacyWifiForgetResult(request.ssid))
|
|
2248
2445
|
}
|
|
2249
2446
|
}
|
|
2250
|
-
request.pending.resolve(event)
|
|
2251
2447
|
}
|
|
2252
2448
|
|
|
2253
|
-
private fun
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2449
|
+
private fun dispatchSavedWifiNetworksIfReady(request: PendingSavedWifiNetworks) {
|
|
2450
|
+
synchronized(oneShotLock) {
|
|
2451
|
+
if (pendingSavedWifiNetworks !== request || request.epoch != wifiSessionCapabilities.epoch ||
|
|
2452
|
+
request.mode != WifiRequestMode.MODERN || request.commandSent) return
|
|
2453
|
+
request.sid = wifiSessionCapabilities.sessionId
|
|
2454
|
+
request.commandSent = deviceManager.requestSavedWifiNetworks(request.requestId, request.sid)
|
|
2455
|
+
if (!request.commandSent) {
|
|
2456
|
+
request.pending.reject(BluetoothSdkException("dispatch_failed", "No active glasses transport accepted saved WiFi request."))
|
|
2457
|
+
}
|
|
2259
2458
|
}
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
private fun applyWifiProtocolCapabilities(data: Map<String, Any>) {
|
|
2462
|
+
var forgetToDispatch: PendingWifiForgetRequest? = null
|
|
2463
|
+
var savedToDispatch: PendingSavedWifiNetworks? = null
|
|
2464
|
+
var unsupportedSaved: PendingSavedWifiNetworks? = null
|
|
2465
|
+
synchronized(oneShotLock) {
|
|
2466
|
+
wifiSessionCapabilities.applyVersionInfo1(data)
|
|
2467
|
+
pendingWifiForget?.takeIf { it.epoch == wifiSessionCapabilities.epoch }?.let { request ->
|
|
2468
|
+
if (request.mode == WifiRequestMode.DISCOVERING) {
|
|
2469
|
+
request.mode = wifiSessionCapabilities.forgetMode()
|
|
2470
|
+
forgetToDispatch = request
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
pendingSavedWifiNetworks?.takeIf { it.epoch == wifiSessionCapabilities.epoch }?.let { request ->
|
|
2474
|
+
if (request.mode == WifiRequestMode.DISCOVERING) {
|
|
2475
|
+
request.mode = wifiSessionCapabilities.savedNetworksMode()
|
|
2476
|
+
if (request.mode == WifiRequestMode.LEGACY || request.mode == WifiRequestMode.UNSUPPORTED) {
|
|
2477
|
+
pendingSavedWifiNetworks = null
|
|
2478
|
+
unsupportedSaved = request
|
|
2479
|
+
} else {
|
|
2480
|
+
savedToDispatch = request
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
unsupportedSaved?.let { request ->
|
|
2486
|
+
request.pending.resolve(
|
|
2487
|
+
SavedWifiNetworksResult(
|
|
2488
|
+
outcome = SavedWifiNetworksOutcome.UNSUPPORTED,
|
|
2489
|
+
networks = emptyList(),
|
|
2490
|
+
error = "saved_wifi_networks_unsupported",
|
|
2491
|
+
)
|
|
2492
|
+
)
|
|
2493
|
+
}
|
|
2494
|
+
forgetToDispatch?.let(::dispatchWifiForgetIfReady)
|
|
2495
|
+
savedToDispatch?.let(::dispatchSavedWifiNetworksIfReady)
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
private fun resetWifiProtocolSession(sessionId: String, code: String) {
|
|
2499
|
+
val error = BluetoothSdkException(code, "The glasses WiFi protocol session changed.")
|
|
2500
|
+
val pendingToReject = mutableListOf<PendingResponse<*>>()
|
|
2501
|
+
synchronized(oneShotLock) {
|
|
2502
|
+
wifiSessionCapabilities.reset(sessionId)
|
|
2503
|
+
pendingWifiStatus?.pending?.let(pendingToReject::add)
|
|
2504
|
+
pendingWifiForget?.pending?.let(pendingToReject::add)
|
|
2505
|
+
pendingSavedWifiNetworks?.pending?.let(pendingToReject::add)
|
|
2506
|
+
pendingWifiScan?.pending?.let(pendingToReject::add)
|
|
2507
|
+
pendingHotspotStatus?.pending?.let(pendingToReject::add)
|
|
2508
|
+
pendingWifiStatus = null
|
|
2509
|
+
pendingWifiForget = null
|
|
2510
|
+
pendingSavedWifiNetworks = null
|
|
2511
|
+
pendingWifiScan = null
|
|
2512
|
+
pendingHotspotStatus = null
|
|
2513
|
+
}
|
|
2514
|
+
pendingToReject.forEach { it.reject(error) }
|
|
2515
|
+
}
|
|
2260
2516
|
|
|
2261
2517
|
private fun handleHotspotStatusForRequests(event: HotspotStatusEvent) {
|
|
2262
2518
|
val request = synchronized(oneShotLock) { pendingHotspotStatus } ?: return
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
internal fun wifiSsidIsValid(ssid: String): Boolean = ssid.trim().isNotEmpty()
|
|
4
|
+
|
|
5
|
+
/** Validate raw fields before JSON convenience getters can coerce numbers or erase presence. */
|
|
6
|
+
internal fun wifiResponseEnvelopeIsValid(values: Map<String, Any>, allowLegacy: Boolean): Boolean {
|
|
7
|
+
if (values.containsKey("connected") && values["connected"] !is Boolean) return false
|
|
8
|
+
if (values.containsKey("dispatched") && values["dispatched"] !is Boolean) return false
|
|
9
|
+
val hasTuple = listOf("protocol_version", "requestId", "sid").any(values::containsKey)
|
|
10
|
+
if (!hasTuple) return allowLegacy && !values.containsKey("outcome")
|
|
11
|
+
return (values["protocol_version"] as? Number)?.toDouble() == 1.0 &&
|
|
12
|
+
(values["requestId"] as? String)?.isNotBlank() == true &&
|
|
13
|
+
(values["sid"] as? String)?.isNotBlank() == true &&
|
|
14
|
+
!values.containsKey("dispatched")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
internal const val WIFI_CAPABILITY_NEGOTIATION_TIMEOUT_CODE = "capability_negotiation_timeout"
|
|
18
|
+
|
|
19
|
+
internal sealed class WifiProtocolCapability {
|
|
20
|
+
data object Unknown : WifiProtocolCapability()
|
|
21
|
+
|
|
22
|
+
data class Supported(val version: Int) : WifiProtocolCapability()
|
|
23
|
+
|
|
24
|
+
data object Unsupported : WifiProtocolCapability()
|
|
25
|
+
|
|
26
|
+
data object Legacy : WifiProtocolCapability()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
internal enum class WifiRequestMode {
|
|
30
|
+
DISCOVERING,
|
|
31
|
+
MODERN,
|
|
32
|
+
LEGACY,
|
|
33
|
+
UNSUPPORTED,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
internal data class WifiSessionRequestSnapshot(
|
|
37
|
+
val mode: WifiRequestMode,
|
|
38
|
+
val sessionId: String,
|
|
39
|
+
val epoch: Long,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
internal class WifiSessionCapabilities {
|
|
43
|
+
var sessionId: String = ""
|
|
44
|
+
private set
|
|
45
|
+
var epoch: Long = 0
|
|
46
|
+
private set
|
|
47
|
+
var forgetResult: WifiProtocolCapability = WifiProtocolCapability.Unknown
|
|
48
|
+
private set
|
|
49
|
+
var savedNetworks: WifiProtocolCapability = WifiProtocolCapability.Unknown
|
|
50
|
+
private set
|
|
51
|
+
|
|
52
|
+
fun reset(sessionId: String = "") {
|
|
53
|
+
epoch += 1
|
|
54
|
+
this.sessionId = sessionId
|
|
55
|
+
forgetResult = WifiProtocolCapability.Unknown
|
|
56
|
+
savedNetworks = WifiProtocolCapability.Unknown
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun applyVersionInfo1(values: Map<String, Any>) {
|
|
60
|
+
(values["sid"] as? String)?.takeIf { it.isNotEmpty() }?.let { sessionId = it }
|
|
61
|
+
forgetResult = capability(values["wifiForgetResultVersion"])
|
|
62
|
+
savedNetworks = capability(values["savedWifiNetworksVersion"])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fun forgetMode(): WifiRequestMode = requestMode(forgetResult)
|
|
66
|
+
|
|
67
|
+
fun savedNetworksMode(): WifiRequestMode = requestMode(savedNetworks)
|
|
68
|
+
|
|
69
|
+
fun savedNetworksRequestSnapshot(): WifiSessionRequestSnapshot =
|
|
70
|
+
WifiSessionRequestSnapshot(savedNetworksMode(), sessionId, epoch)
|
|
71
|
+
|
|
72
|
+
private fun capability(raw: Any?): WifiProtocolCapability {
|
|
73
|
+
if (raw == null) return WifiProtocolCapability.Legacy
|
|
74
|
+
return if (raw is Number && raw.toDouble() == 1.0 && sessionId.isNotEmpty()) {
|
|
75
|
+
WifiProtocolCapability.Supported(1)
|
|
76
|
+
} else WifiProtocolCapability.Unsupported
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun requestMode(capability: WifiProtocolCapability): WifiRequestMode =
|
|
80
|
+
when (capability) {
|
|
81
|
+
WifiProtocolCapability.Unknown -> WifiRequestMode.DISCOVERING
|
|
82
|
+
is WifiProtocolCapability.Supported -> WifiRequestMode.MODERN
|
|
83
|
+
WifiProtocolCapability.Legacy -> WifiRequestMode.LEGACY
|
|
84
|
+
WifiProtocolCapability.Unsupported -> WifiRequestMode.UNSUPPORTED
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
enum class WifiForgetOutcome(val wireValue: String) {
|
|
89
|
+
CONFIRMED("confirmed"),
|
|
90
|
+
DISPATCHED("dispatched"),
|
|
91
|
+
NOT_FOUND("not_found"),
|
|
92
|
+
UNSUPPORTED("unsupported"),
|
|
93
|
+
FAILED("failed"),
|
|
94
|
+
LEGACY_UNVERIFIED("legacy_unverified");
|
|
95
|
+
|
|
96
|
+
companion object {
|
|
97
|
+
internal fun fromWire(value: String): WifiForgetOutcome? =
|
|
98
|
+
entries.find { it.wireValue == value }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
internal fun normalizeWifiForgetResultEvent(
|
|
103
|
+
requestId: String,
|
|
104
|
+
sid: String,
|
|
105
|
+
ssid: String,
|
|
106
|
+
protocolVersion: Int,
|
|
107
|
+
outcome: String,
|
|
108
|
+
legacyDispatched: Boolean?,
|
|
109
|
+
connected: Boolean?,
|
|
110
|
+
currentSsid: String,
|
|
111
|
+
localIp: String,
|
|
112
|
+
error: String?,
|
|
113
|
+
): Map<String, Any>? {
|
|
114
|
+
if (!wifiSsidIsValid(ssid)) return null
|
|
115
|
+
val modernOutcome = WifiForgetOutcome.fromWire(outcome)
|
|
116
|
+
if (requestId.isNotEmpty() && sid.isNotEmpty() && protocolVersion == 1 &&
|
|
117
|
+
modernOutcome != null && modernOutcome != WifiForgetOutcome.LEGACY_UNVERIFIED
|
|
118
|
+
) {
|
|
119
|
+
return buildMap {
|
|
120
|
+
put("mode", "modern")
|
|
121
|
+
put("requestId", requestId)
|
|
122
|
+
put("sid", sid)
|
|
123
|
+
put("ssid", ssid)
|
|
124
|
+
put("protocolVersion", protocolVersion)
|
|
125
|
+
put("outcome", modernOutcome.wireValue)
|
|
126
|
+
connected?.let { put("connected", it) }
|
|
127
|
+
if (currentSsid.isNotEmpty()) put("currentSsid", currentSsid)
|
|
128
|
+
if (localIp.isNotEmpty()) put("localIp", localIp)
|
|
129
|
+
error?.let { put("error", it) }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (requestId.isEmpty() && sid.isEmpty() && protocolVersion == 0 && outcome.isEmpty() && legacyDispatched != null) {
|
|
133
|
+
return buildMap {
|
|
134
|
+
put("mode", "legacy")
|
|
135
|
+
put("ssid", ssid)
|
|
136
|
+
put("dispatched", legacyDispatched)
|
|
137
|
+
connected?.let { put("connected", it) }
|
|
138
|
+
if (currentSsid.isNotEmpty()) put("currentSsid", currentSsid)
|
|
139
|
+
if (localIp.isNotEmpty()) put("localIp", localIp)
|
|
140
|
+
error?.let { put("error", it) }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
data class WifiForgetResult(
|
|
147
|
+
val ssid: String,
|
|
148
|
+
val outcome: WifiForgetOutcome,
|
|
149
|
+
val connected: Boolean?,
|
|
150
|
+
val currentSsid: String?,
|
|
151
|
+
val localIp: String?,
|
|
152
|
+
val error: String? = null,
|
|
153
|
+
) {
|
|
154
|
+
internal fun toMap(): Map<String, Any> =
|
|
155
|
+
buildMap {
|
|
156
|
+
put("ssid", ssid)
|
|
157
|
+
put("outcome", outcome.wireValue)
|
|
158
|
+
connected?.let { put("connected", it) }
|
|
159
|
+
currentSsid?.let { put("currentSsid", it) }
|
|
160
|
+
localIp?.let { put("localIp", it) }
|
|
161
|
+
error?.let { put("error", it) }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
enum class SavedWifiNetworksOutcome(val wireValue: String) {
|
|
166
|
+
CONFIRMED("confirmed"),
|
|
167
|
+
UNSUPPORTED("unsupported"),
|
|
168
|
+
FAILED("failed");
|
|
169
|
+
|
|
170
|
+
companion object {
|
|
171
|
+
internal fun fromWire(value: String): SavedWifiNetworksOutcome? =
|
|
172
|
+
entries.find { it.wireValue == value }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
data class SavedWifiNetworksResult(
|
|
177
|
+
val outcome: SavedWifiNetworksOutcome,
|
|
178
|
+
val networks: List<String>,
|
|
179
|
+
val error: String? = null,
|
|
180
|
+
) {
|
|
181
|
+
internal fun toMap(): Map<String, Any> =
|
|
182
|
+
buildMap {
|
|
183
|
+
put("outcome", outcome.wireValue)
|
|
184
|
+
put("networks", networks)
|
|
185
|
+
error?.let { put("error", it) }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
internal fun parseWifiForgetResult(
|
|
190
|
+
expectedRequestId: String,
|
|
191
|
+
expectedSid: String,
|
|
192
|
+
expectedSsid: String,
|
|
193
|
+
capabilityVersion: Int,
|
|
194
|
+
data: Map<String, Any>,
|
|
195
|
+
): WifiForgetResult? {
|
|
196
|
+
if (capabilityVersion != 1 || expectedRequestId.isEmpty() || expectedSid.isEmpty()) return null
|
|
197
|
+
if (data["requestId"] as? String != expectedRequestId) return null
|
|
198
|
+
if (data["sid"] as? String != expectedSid) return null
|
|
199
|
+
if (data["ssid"] as? String != expectedSsid) return null
|
|
200
|
+
if ((data["protocolVersion"] as? Number)?.toDouble() != 1.0) return null
|
|
201
|
+
val outcome = WifiForgetOutcome.fromWire(data["outcome"] as? String ?: return null) ?: return null
|
|
202
|
+
if (outcome == WifiForgetOutcome.LEGACY_UNVERIFIED) return null
|
|
203
|
+
return WifiForgetResult(
|
|
204
|
+
ssid = expectedSsid,
|
|
205
|
+
outcome = outcome,
|
|
206
|
+
connected = data["connected"] as? Boolean,
|
|
207
|
+
currentSsid = data["currentSsid"] as? String,
|
|
208
|
+
localIp = data["localIp"] as? String,
|
|
209
|
+
error = (data["error"] as? String)?.takeIf { it.isNotEmpty() },
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
internal fun parseSavedWifiNetworks(
|
|
214
|
+
expectedRequestId: String,
|
|
215
|
+
expectedSid: String,
|
|
216
|
+
capabilityVersion: Int,
|
|
217
|
+
data: Map<String, Any>,
|
|
218
|
+
): SavedWifiNetworksResult? {
|
|
219
|
+
if (capabilityVersion != 1 || expectedRequestId.isEmpty() || expectedSid.isEmpty()) return null
|
|
220
|
+
if (data["requestId"] as? String != expectedRequestId) return null
|
|
221
|
+
if (data["sid"] as? String != expectedSid) return null
|
|
222
|
+
if ((data["protocolVersion"] as? Number)?.toDouble() != 1.0) return null
|
|
223
|
+
val outcome =
|
|
224
|
+
SavedWifiNetworksOutcome.fromWire(data["outcome"] as? String ?: return null) ?: return null
|
|
225
|
+
val rawNetworks = data["networks"] as? List<*> ?: return null
|
|
226
|
+
if (rawNetworks.any { it !is String }) return null
|
|
227
|
+
if (outcome != SavedWifiNetworksOutcome.CONFIRMED && rawNetworks.isNotEmpty()) return null
|
|
228
|
+
val networks = rawNetworks.map { it as String }.filter { it.trim().isNotEmpty() }.distinct()
|
|
229
|
+
return SavedWifiNetworksResult(
|
|
230
|
+
outcome = outcome,
|
|
231
|
+
networks = networks,
|
|
232
|
+
error = (data["error"] as? String)?.takeIf { it.isNotEmpty() },
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
internal fun legacyWifiForgetResult(
|
|
237
|
+
ssid: String,
|
|
238
|
+
): WifiForgetResult {
|
|
239
|
+
return WifiForgetResult(
|
|
240
|
+
ssid = ssid,
|
|
241
|
+
outcome = WifiForgetOutcome.LEGACY_UNVERIFIED,
|
|
242
|
+
connected = null,
|
|
243
|
+
currentSsid = null,
|
|
244
|
+
localIp = null,
|
|
245
|
+
)
|
|
246
|
+
}
|