@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
|
@@ -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
|
-
private var pendingVersionInfo:
|
|
76
|
+
private var pendingVersionInfo: PendingVersionInfoRequest? = null
|
|
77
|
+
private val wifiSessionCapabilities = WifiSessionCapabilities()
|
|
75
78
|
@Volatile private var configuredOtaVersionUrl: String? = null
|
|
76
79
|
|
|
77
80
|
init {
|
|
@@ -178,6 +181,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
178
181
|
var uploadSucceeded: Boolean = false,
|
|
179
182
|
)
|
|
180
183
|
|
|
184
|
+
private class PendingVersionInfoRequest(
|
|
185
|
+
val pending: PendingResponse<VersionInfoResult>,
|
|
186
|
+
val accumulator: VersionInfoResponseAccumulator,
|
|
187
|
+
) {
|
|
188
|
+
}
|
|
189
|
+
|
|
181
190
|
private data class PendingWifiScan(
|
|
182
191
|
val pending: PendingResponse<List<WifiScanResult>>,
|
|
183
192
|
val scanId: String,
|
|
@@ -189,15 +198,28 @@ class MentraBluetoothSdk private constructor(
|
|
|
189
198
|
)
|
|
190
199
|
|
|
191
200
|
private data class PendingWifiStatusRequest(
|
|
192
|
-
val operation: WifiStatusOperation,
|
|
193
201
|
val ssid: String,
|
|
194
202
|
val pending: PendingResponse<WifiStatusEvent>,
|
|
195
203
|
)
|
|
196
204
|
|
|
197
|
-
private
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
)
|
|
201
223
|
|
|
202
224
|
private data class PendingHotspotStatusRequest(
|
|
203
225
|
val enabled: Boolean,
|
|
@@ -233,6 +255,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
233
255
|
"$operation timed out waiting for glasses response.",
|
|
234
256
|
)
|
|
235
257
|
}
|
|
258
|
+
|
|
236
259
|
}
|
|
237
260
|
|
|
238
261
|
fun addListener(listener: MentraBluetoothSdkListener) {
|
|
@@ -874,13 +897,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
874
897
|
suspend fun sendWifiCredentials(ssid: String, password: String): WifiStatusEvent {
|
|
875
898
|
val pending = PendingResponse<WifiStatusEvent>("WiFi connect request")
|
|
876
899
|
synchronized(oneShotLock) {
|
|
877
|
-
if (pendingWifiStatus != null) {
|
|
900
|
+
if (pendingWifiStatus != null || pendingWifiForget != null) {
|
|
878
901
|
throw BluetoothSdkException(
|
|
879
902
|
"request_in_flight",
|
|
880
903
|
"A WiFi status command is already waiting for a glasses response.",
|
|
881
904
|
)
|
|
882
905
|
}
|
|
883
|
-
pendingWifiStatus = PendingWifiStatusRequest(
|
|
906
|
+
pendingWifiStatus = PendingWifiStatusRequest(ssid, pending)
|
|
884
907
|
}
|
|
885
908
|
try {
|
|
886
909
|
deviceManager.sendWifiCredentials(ssid, password)
|
|
@@ -894,24 +917,94 @@ class MentraBluetoothSdk private constructor(
|
|
|
894
917
|
}
|
|
895
918
|
}
|
|
896
919
|
|
|
897
|
-
suspend fun forgetWifiNetwork(ssid: String):
|
|
898
|
-
|
|
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
|
|
899
927
|
synchronized(oneShotLock) {
|
|
900
|
-
if (pendingWifiStatus != null) {
|
|
928
|
+
if (pendingWifiStatus != null || pendingWifiForget != null) {
|
|
901
929
|
throw BluetoothSdkException(
|
|
902
930
|
"request_in_flight",
|
|
903
931
|
"A WiFi status command is already waiting for a glasses response.",
|
|
904
932
|
)
|
|
905
933
|
}
|
|
906
|
-
|
|
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
|
|
907
944
|
}
|
|
908
945
|
try {
|
|
909
|
-
|
|
946
|
+
dispatchWifiForgetIfReady(request)
|
|
910
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
|
|
911
953
|
} finally {
|
|
912
954
|
synchronized(oneShotLock) {
|
|
913
|
-
if (
|
|
914
|
-
|
|
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
|
|
915
1008
|
}
|
|
916
1009
|
}
|
|
917
1010
|
}
|
|
@@ -1195,7 +1288,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
1195
1288
|
}
|
|
1196
1289
|
|
|
1197
1290
|
suspend fun requestVersionInfo(): VersionInfoResult {
|
|
1291
|
+
val requestId = UUID.randomUUID().toString()
|
|
1198
1292
|
val pending = PendingResponse<VersionInfoResult>("version info request")
|
|
1293
|
+
val request =
|
|
1294
|
+
PendingVersionInfoRequest(
|
|
1295
|
+
pending = pending,
|
|
1296
|
+
accumulator = VersionInfoResponseAccumulator(requestId),
|
|
1297
|
+
)
|
|
1199
1298
|
synchronized(oneShotLock) {
|
|
1200
1299
|
if (pendingVersionInfo != null) {
|
|
1201
1300
|
throw BluetoothSdkException(
|
|
@@ -1203,14 +1302,14 @@ class MentraBluetoothSdk private constructor(
|
|
|
1203
1302
|
"A version info request is already waiting for a glasses response.",
|
|
1204
1303
|
)
|
|
1205
1304
|
}
|
|
1206
|
-
pendingVersionInfo =
|
|
1305
|
+
pendingVersionInfo = request
|
|
1207
1306
|
}
|
|
1208
1307
|
try {
|
|
1209
|
-
deviceManager.requestVersionInfo()
|
|
1308
|
+
deviceManager.requestVersionInfo(requestId)
|
|
1210
1309
|
return pending.await()
|
|
1211
1310
|
} finally {
|
|
1212
1311
|
synchronized(oneShotLock) {
|
|
1213
|
-
if (pendingVersionInfo ===
|
|
1312
|
+
if (pendingVersionInfo === request) {
|
|
1214
1313
|
pendingVersionInfo = null
|
|
1215
1314
|
}
|
|
1216
1315
|
}
|
|
@@ -1456,6 +1555,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1456
1555
|
|
|
1457
1556
|
override fun close() {
|
|
1458
1557
|
stopStreamKeepAliveMonitor()
|
|
1558
|
+
resetWifiProtocolSession("", "sdk_closed")
|
|
1459
1559
|
if (activityLifecycleCallbacksRegistered) {
|
|
1460
1560
|
(appContext as? Application)?.unregisterActivityLifecycleCallbacks(
|
|
1461
1561
|
activityLifecycleCallbacks,
|
|
@@ -1471,6 +1571,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
1471
1571
|
private fun dispatchStoreUpdate(category: String, changes: Map<String, Any>) {
|
|
1472
1572
|
when (ObservableStore.normalizeCategory(category)) {
|
|
1473
1573
|
"glasses" -> {
|
|
1574
|
+
if (changes["connected"] == false) {
|
|
1575
|
+
resetWifiProtocolSession("", "wifi_session_disconnected")
|
|
1576
|
+
}
|
|
1474
1577
|
analytics.observeGlassesStatus(getRawGlassesStatus())
|
|
1475
1578
|
val state = getState()
|
|
1476
1579
|
dispatchToListeners {
|
|
@@ -1607,6 +1710,22 @@ class MentraBluetoothSdk private constructor(
|
|
|
1607
1710
|
}
|
|
1608
1711
|
}
|
|
1609
1712
|
|
|
1713
|
+
private fun handleVersionInfoForRequest(data: Map<String, Any>) {
|
|
1714
|
+
synchronized(oneShotLock) {
|
|
1715
|
+
val request = pendingVersionInfo ?: return
|
|
1716
|
+
when (val outcome = request.accumulator.accept(data)) {
|
|
1717
|
+
VersionInfoAccumulatorOutcome.Ignored -> Unit
|
|
1718
|
+
VersionInfoAccumulatorOutcome.Waiting -> Unit
|
|
1719
|
+
is VersionInfoAccumulatorOutcome.Complete -> {
|
|
1720
|
+
if (pendingVersionInfo === request) {
|
|
1721
|
+
pendingVersionInfo = null
|
|
1722
|
+
request.pending.resolve(outcome.result)
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1610
1729
|
private fun dispatchBridgeEvent(eventName: String, data: Map<String, Any>) {
|
|
1611
1730
|
when (eventName) {
|
|
1612
1731
|
"log" -> dispatchToListeners { it.onLog(data["message"] as? String ?: data.toString()) }
|
|
@@ -1663,6 +1782,27 @@ class MentraBluetoothSdk private constructor(
|
|
|
1663
1782
|
handleWifiStatusForRequests(event)
|
|
1664
1783
|
dispatchToListeners { it.onWifiStatusChanged(event) }
|
|
1665
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
|
+
}
|
|
1666
1806
|
"wifi_scan_result" -> {
|
|
1667
1807
|
val networks =
|
|
1668
1808
|
(data["networks"] as? List<*>)
|
|
@@ -1772,10 +1912,11 @@ class MentraBluetoothSdk private constructor(
|
|
|
1772
1912
|
dispatchToListeners { it.onSettingsAck(event) }
|
|
1773
1913
|
}
|
|
1774
1914
|
"version_info" -> {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
pendingVersionInfo?.resolve(event)
|
|
1915
|
+
if (data["versionInfoType"] == "version_info_1" || data["versionInfoType"] == "version_info") {
|
|
1916
|
+
applyWifiProtocolCapabilities(data)
|
|
1778
1917
|
}
|
|
1918
|
+
val event = VersionInfoResult.fromMap(data)
|
|
1919
|
+
handleVersionInfoForRequest(data)
|
|
1779
1920
|
dispatchToListeners { it.onVersionInfo(event) }
|
|
1780
1921
|
}
|
|
1781
1922
|
"mic_pcm" -> {
|
|
@@ -2194,43 +2335,184 @@ class MentraBluetoothSdk private constructor(
|
|
|
2194
2335
|
}
|
|
2195
2336
|
|
|
2196
2337
|
private fun handleWifiStatusForRequests(event: WifiStatusEvent) {
|
|
2197
|
-
val
|
|
2338
|
+
val connectRequest = synchronized(oneShotLock) { pendingWifiStatus }
|
|
2198
2339
|
// A wifi_status carrying the explicit error field is the glasses' failure
|
|
2199
2340
|
// verdict for the in-flight connect: reject now instead of running out the
|
|
2200
2341
|
// request timeout. Only the error field counts as failure — the glasses'
|
|
2201
2342
|
// connect sequence emits a debounced bare connected=false ~1-2s after
|
|
2202
2343
|
// credentials while association is still in progress, and rejecting on that
|
|
2203
2344
|
// would kill every connect attempt early.
|
|
2204
|
-
if (
|
|
2345
|
+
if (connectRequest != null && event.error != null) {
|
|
2205
2346
|
synchronized(oneShotLock) {
|
|
2206
|
-
if (pendingWifiStatus ===
|
|
2347
|
+
if (pendingWifiStatus === connectRequest) {
|
|
2207
2348
|
pendingWifiStatus = null
|
|
2208
2349
|
}
|
|
2209
2350
|
}
|
|
2210
|
-
|
|
2351
|
+
connectRequest.pending.reject(
|
|
2211
2352
|
BluetoothSdkException(
|
|
2212
2353
|
event.error,
|
|
2213
|
-
"Glasses failed to join \"${
|
|
2354
|
+
"Glasses failed to join \"${connectRequest.ssid}\": ${event.error}",
|
|
2214
2355
|
)
|
|
2215
2356
|
)
|
|
2216
2357
|
return
|
|
2217
2358
|
}
|
|
2218
|
-
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
|
+
|
|
2219
2390
|
synchronized(oneShotLock) {
|
|
2220
|
-
if (
|
|
2221
|
-
|
|
2391
|
+
if (pendingWifiForget !== request || request.epoch != wifiSessionCapabilities.epoch) {
|
|
2392
|
+
return
|
|
2222
2393
|
}
|
|
2394
|
+
pendingWifiForget = null
|
|
2223
2395
|
}
|
|
2224
|
-
request.pending.resolve(
|
|
2396
|
+
request.pending.resolve(result)
|
|
2225
2397
|
}
|
|
2226
2398
|
|
|
2227
|
-
private fun
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
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
|
|
2233
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))
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
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
|
+
}
|
|
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
|
+
}
|
|
2234
2516
|
|
|
2235
2517
|
private fun handleHotspotStatusForRequests(event: HotspotStatusEvent) {
|
|
2236
2518
|
val request = synchronized(oneShotLock) { pendingHotspotStatus } ?: return
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
internal sealed interface VersionInfoAccumulatorOutcome {
|
|
4
|
+
data object Ignored : VersionInfoAccumulatorOutcome
|
|
5
|
+
data object Waiting : VersionInfoAccumulatorOutcome
|
|
6
|
+
data class Complete(val result: VersionInfoResult) : VersionInfoAccumulatorOutcome
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** One request, explicit modern completion, or the deployed legacy chunk-3 terminal boundary. */
|
|
10
|
+
internal class VersionInfoResponseAccumulator(private val expectedRequestId: String) {
|
|
11
|
+
private val values = mutableMapOf<String, Any>()
|
|
12
|
+
private val chunks = mutableMapOf<Int, Map<String, Any>>()
|
|
13
|
+
private var count: Int? = null
|
|
14
|
+
private var sid: String? = null
|
|
15
|
+
private var legacyStarted = false
|
|
16
|
+
private var completed = false
|
|
17
|
+
|
|
18
|
+
fun accept(event: Map<String, Any>): VersionInfoAccumulatorOutcome {
|
|
19
|
+
if (completed) return VersionInfoAccumulatorOutcome.Ignored
|
|
20
|
+
val hasModernMetadata = event.keys.any { it in MODERN_KEYS }
|
|
21
|
+
if (hasModernMetadata) {
|
|
22
|
+
if (event[RESPONSE_REQUEST_ID_KEY] != expectedRequestId) return VersionInfoAccumulatorOutcome.Ignored
|
|
23
|
+
val index = integer(event[RESPONSE_INDEX_KEY]) ?: return VersionInfoAccumulatorOutcome.Ignored
|
|
24
|
+
val total = integer(event[RESPONSE_COUNT_KEY]) ?: return VersionInfoAccumulatorOutcome.Ignored
|
|
25
|
+
val final = event[RESPONSE_FINAL_KEY] as? Boolean ?: return VersionInfoAccumulatorOutcome.Ignored
|
|
26
|
+
val process = (event[RESPONSE_SID_KEY] as? String)?.takeIf { it.isNotBlank() }
|
|
27
|
+
?: return VersionInfoAccumulatorOutcome.Ignored
|
|
28
|
+
if (total !in 1..16 || index !in 1..total || final != (index == total)) return VersionInfoAccumulatorOutcome.Ignored
|
|
29
|
+
if (count != null && (count != total || sid != process)) return VersionInfoAccumulatorOutcome.Ignored
|
|
30
|
+
if (count == null) {
|
|
31
|
+
values.clear()
|
|
32
|
+
count = total
|
|
33
|
+
sid = process
|
|
34
|
+
}
|
|
35
|
+
chunks[index] = event
|
|
36
|
+
if (chunks.size != total) return VersionInfoAccumulatorOutcome.Waiting
|
|
37
|
+
(1..total).forEach { merge(chunks.getValue(it)) }
|
|
38
|
+
return finish()
|
|
39
|
+
}
|
|
40
|
+
// Once a response is correlated, unsolicited legacy traffic cannot replace it.
|
|
41
|
+
if (count != null) return VersionInfoAccumulatorOutcome.Ignored
|
|
42
|
+
when (event[RESPONSE_CHUNK_KEY] as? String ?: "version_info") {
|
|
43
|
+
"version_info" -> { values.clear(); merge(event); return finish() }
|
|
44
|
+
"version_info_1" -> { values.clear(); legacyStarted = true; merge(event) }
|
|
45
|
+
"version_info_2" -> {
|
|
46
|
+
if (!legacyStarted) return VersionInfoAccumulatorOutcome.Ignored
|
|
47
|
+
merge(event)
|
|
48
|
+
}
|
|
49
|
+
"version_info_3" -> {
|
|
50
|
+
if (!legacyStarted) return VersionInfoAccumulatorOutcome.Ignored
|
|
51
|
+
merge(event)
|
|
52
|
+
return finish()
|
|
53
|
+
}
|
|
54
|
+
else -> return VersionInfoAccumulatorOutcome.Ignored
|
|
55
|
+
}
|
|
56
|
+
// Silence is never evidence of completion. The request's normal deadline handles loss.
|
|
57
|
+
return VersionInfoAccumulatorOutcome.Waiting
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private fun finish(): VersionInfoAccumulatorOutcome.Complete {
|
|
61
|
+
completed = true
|
|
62
|
+
return VersionInfoAccumulatorOutcome.Complete(VersionInfoResult.fromMap(values))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private fun merge(event: Map<String, Any>) {
|
|
66
|
+
event.forEach { (key, value) ->
|
|
67
|
+
if (!key.startsWith("_response") && key != "type" && (value !is String || value.isNotEmpty())) {
|
|
68
|
+
values[key] = value
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private fun integer(value: Any?): Int? {
|
|
74
|
+
val number = value as? Number ?: return null
|
|
75
|
+
val int = number.toInt()
|
|
76
|
+
return int.takeIf { number.toDouble() == it.toDouble() }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
companion object {
|
|
80
|
+
internal const val RESPONSE_CHUNK_KEY = "_responseChunk"
|
|
81
|
+
internal const val RESPONSE_REQUEST_ID_KEY = "_responseRequestId"
|
|
82
|
+
internal const val RESPONSE_INDEX_KEY = "_responseChunkIndex"
|
|
83
|
+
internal const val RESPONSE_COUNT_KEY = "_responseChunkCount"
|
|
84
|
+
internal const val RESPONSE_FINAL_KEY = "_responseFinal"
|
|
85
|
+
internal const val RESPONSE_SID_KEY = "_responseSid"
|
|
86
|
+
private val MODERN_KEYS = setOf(RESPONSE_REQUEST_ID_KEY, RESPONSE_INDEX_KEY, RESPONSE_COUNT_KEY, RESPONSE_FINAL_KEY)
|
|
87
|
+
}
|
|
88
|
+
}
|