@mentra/bluetooth-sdk 3.2.0-dev.200 → 3.2.0-dev.206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +63 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +5 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +298 -181
- package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +326 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbe.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamSessionState.kt +32 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbeTest.kt +24 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamSessionStateTest.kt +43 -0
- package/build/BluetoothSdk.types.d.ts +68 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -4
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +8 -13
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/DeviceManager.swift +6 -3
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +372 -220
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/sgcs/MentraLive.swift +216 -70
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +28 -0
- package/ios/Source/streaming/StreamControllerProbe.swift +18 -0
- package/ios/Source/streaming/StreamModels.swift +12 -0
- package/ios/Source/streaming/StreamSessionState.swift +29 -0
- package/ios/Tests/StreamControllerProbeTests.swift +27 -0
- package/ios/Tests/StreamSessionStateTests.swift +38 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +82 -2
- package/src/_private/BluetoothSdkModule.ts +4 -4
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +11 -0
|
@@ -8,6 +8,7 @@ import android.content.Context
|
|
|
8
8
|
import android.os.Bundle
|
|
9
9
|
import android.os.Handler
|
|
10
10
|
import android.os.Looper
|
|
11
|
+
import com.mentra.bluetoothsdk.streaming.StreamSessionState
|
|
11
12
|
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
12
13
|
import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
|
|
13
14
|
import java.util.Collections
|
|
@@ -52,8 +53,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
52
53
|
}
|
|
53
54
|
private var activityLifecycleCallbacksRegistered = false
|
|
54
55
|
private var suppressDefaultDeviceEvents = false
|
|
55
|
-
private val
|
|
56
|
-
private var activeStreamKeepAlive: ActiveStreamKeepAlive? = null
|
|
56
|
+
private val streamSession = StreamSessionState()
|
|
57
57
|
private val pendingPhotoRequests = ConcurrentHashMap<String, PendingResponse<PhotoResponseEvent>>()
|
|
58
58
|
private val pendingCameraStatusRequests = ConcurrentHashMap<String, PendingResponse<CameraStatusEvent>>()
|
|
59
59
|
private val pendingVideoRecordingRequests =
|
|
@@ -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 {
|
|
@@ -107,7 +110,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
107
110
|
private const val OTA_BES_VERSION_WAIT_MS = 5_000L
|
|
108
111
|
private const val OTA_MTK_VERSION_WAIT_MS = 2_000L
|
|
109
112
|
private const val OTA_VERSION_POLL_MS = 100L
|
|
110
|
-
private const val DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS = 5
|
|
111
113
|
private const val MAX_MISSED_STREAM_KEEP_ALIVE_ACKS = 3
|
|
112
114
|
|
|
113
115
|
// Stream states the glasses only reach after start_stream has run past
|
|
@@ -136,18 +138,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
136
138
|
): MentraBluetoothSdk = MentraBluetoothSdk(context, config, listener)
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
private data class ActiveStreamKeepAlive(
|
|
140
|
-
val streamId: String,
|
|
141
|
-
val intervalMs: Long,
|
|
142
|
-
var pendingAckId: String? = null,
|
|
143
|
-
var missedAckCount: Int = 0,
|
|
144
|
-
var nextTick: Runnable? = null,
|
|
145
|
-
// Missed-ACK counting only begins once the stream is confirmed live/coming up, so a
|
|
146
|
-
// slow startup (glasses can't ACK until they reach starting/streaming) can't trip a
|
|
147
|
-
// false keep-alive timeout before the stream is ever up.
|
|
148
|
-
var armed: Boolean = false,
|
|
149
|
-
)
|
|
150
|
-
|
|
151
141
|
// seq records send order (assigned and handed to the BLE queue under
|
|
152
142
|
// streamStartOrderLock) so that an id-carrying status for a newer start can
|
|
153
143
|
// identify which older in-flight starts it preempted.
|
|
@@ -195,15 +185,28 @@ class MentraBluetoothSdk private constructor(
|
|
|
195
185
|
)
|
|
196
186
|
|
|
197
187
|
private data class PendingWifiStatusRequest(
|
|
198
|
-
val operation: WifiStatusOperation,
|
|
199
188
|
val ssid: String,
|
|
200
189
|
val pending: PendingResponse<WifiStatusEvent>,
|
|
201
190
|
)
|
|
202
191
|
|
|
203
|
-
private
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
192
|
+
private data class PendingWifiForgetRequest(
|
|
193
|
+
val ssid: String,
|
|
194
|
+
val requestId: String,
|
|
195
|
+
var sid: String,
|
|
196
|
+
val epoch: Long,
|
|
197
|
+
val pending: PendingResponse<WifiForgetResult>,
|
|
198
|
+
var mode: WifiRequestMode,
|
|
199
|
+
var commandSent: Boolean = false,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
private data class PendingSavedWifiNetworks(
|
|
203
|
+
val requestId: String,
|
|
204
|
+
var sid: String,
|
|
205
|
+
val epoch: Long,
|
|
206
|
+
val pending: PendingResponse<SavedWifiNetworksResult>,
|
|
207
|
+
var mode: WifiRequestMode,
|
|
208
|
+
var commandSent: Boolean = false,
|
|
209
|
+
)
|
|
207
210
|
|
|
208
211
|
private data class PendingHotspotStatusRequest(
|
|
209
212
|
val enabled: Boolean,
|
|
@@ -239,6 +242,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
239
242
|
"$operation timed out waiting for glasses response.",
|
|
240
243
|
)
|
|
241
244
|
}
|
|
245
|
+
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
fun addListener(listener: MentraBluetoothSdkListener) {
|
|
@@ -880,13 +884,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
880
884
|
suspend fun sendWifiCredentials(ssid: String, password: String): WifiStatusEvent {
|
|
881
885
|
val pending = PendingResponse<WifiStatusEvent>("WiFi connect request")
|
|
882
886
|
synchronized(oneShotLock) {
|
|
883
|
-
if (pendingWifiStatus != null) {
|
|
887
|
+
if (pendingWifiStatus != null || pendingWifiForget != null) {
|
|
884
888
|
throw BluetoothSdkException(
|
|
885
889
|
"request_in_flight",
|
|
886
890
|
"A WiFi status command is already waiting for a glasses response.",
|
|
887
891
|
)
|
|
888
892
|
}
|
|
889
|
-
pendingWifiStatus = PendingWifiStatusRequest(
|
|
893
|
+
pendingWifiStatus = PendingWifiStatusRequest(ssid, pending)
|
|
890
894
|
}
|
|
891
895
|
try {
|
|
892
896
|
deviceManager.sendWifiCredentials(ssid, password)
|
|
@@ -900,24 +904,94 @@ class MentraBluetoothSdk private constructor(
|
|
|
900
904
|
}
|
|
901
905
|
}
|
|
902
906
|
|
|
903
|
-
suspend fun forgetWifiNetwork(ssid: String):
|
|
904
|
-
|
|
907
|
+
suspend fun forgetWifiNetwork(ssid: String): WifiForgetResult {
|
|
908
|
+
if (!wifiSsidIsValid(ssid)) {
|
|
909
|
+
throw BluetoothSdkException("invalid_ssid", "WiFi SSID cannot be empty.")
|
|
910
|
+
}
|
|
911
|
+
val pending = PendingResponse<WifiForgetResult>("WiFi forget request")
|
|
912
|
+
val requestId = "forget-${UUID.randomUUID()}"
|
|
913
|
+
lateinit var request: PendingWifiForgetRequest
|
|
905
914
|
synchronized(oneShotLock) {
|
|
906
|
-
if (pendingWifiStatus != null) {
|
|
915
|
+
if (pendingWifiStatus != null || pendingWifiForget != null) {
|
|
907
916
|
throw BluetoothSdkException(
|
|
908
917
|
"request_in_flight",
|
|
909
918
|
"A WiFi status command is already waiting for a glasses response.",
|
|
910
919
|
)
|
|
911
920
|
}
|
|
912
|
-
|
|
921
|
+
request =
|
|
922
|
+
PendingWifiForgetRequest(
|
|
923
|
+
ssid = ssid,
|
|
924
|
+
requestId = requestId,
|
|
925
|
+
sid = wifiSessionCapabilities.sessionId,
|
|
926
|
+
epoch = wifiSessionCapabilities.epoch,
|
|
927
|
+
pending = pending,
|
|
928
|
+
mode = wifiSessionCapabilities.forgetMode(),
|
|
929
|
+
)
|
|
930
|
+
pendingWifiForget = request
|
|
913
931
|
}
|
|
914
932
|
try {
|
|
915
|
-
|
|
933
|
+
dispatchWifiForgetIfReady(request)
|
|
916
934
|
return pending.await()
|
|
935
|
+
} catch (error: BluetoothSdkException) {
|
|
936
|
+
if (error.code == "request_timeout" && synchronized(oneShotLock) { request.mode == WifiRequestMode.DISCOVERING }) {
|
|
937
|
+
throw BluetoothSdkException(WIFI_CAPABILITY_NEGOTIATION_TIMEOUT_CODE, "WiFi capability negotiation timed out.")
|
|
938
|
+
}
|
|
939
|
+
throw error
|
|
917
940
|
} finally {
|
|
918
941
|
synchronized(oneShotLock) {
|
|
919
|
-
if (
|
|
920
|
-
|
|
942
|
+
if (pendingWifiForget === request) {
|
|
943
|
+
pendingWifiForget = null
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
suspend fun getSavedWifiNetworks(): SavedWifiNetworksResult {
|
|
950
|
+
val requestId = "saved-${UUID.randomUUID()}"
|
|
951
|
+
val pending = PendingResponse<SavedWifiNetworksResult>("Saved WiFi networks request")
|
|
952
|
+
var request: PendingSavedWifiNetworks? = null
|
|
953
|
+
var unsupported: SavedWifiNetworksResult? = null
|
|
954
|
+
synchronized(oneShotLock) {
|
|
955
|
+
if (pendingSavedWifiNetworks != null) {
|
|
956
|
+
throw BluetoothSdkException(
|
|
957
|
+
"request_in_flight",
|
|
958
|
+
"A saved WiFi networks request is already waiting for a glasses response.",
|
|
959
|
+
)
|
|
960
|
+
}
|
|
961
|
+
val snapshot = wifiSessionCapabilities.savedNetworksRequestSnapshot()
|
|
962
|
+
if (snapshot.mode == WifiRequestMode.LEGACY || snapshot.mode == WifiRequestMode.UNSUPPORTED) {
|
|
963
|
+
unsupported =
|
|
964
|
+
SavedWifiNetworksResult(
|
|
965
|
+
outcome = SavedWifiNetworksOutcome.UNSUPPORTED,
|
|
966
|
+
networks = emptyList(),
|
|
967
|
+
error = "saved_wifi_networks_unsupported",
|
|
968
|
+
)
|
|
969
|
+
return@synchronized
|
|
970
|
+
}
|
|
971
|
+
request =
|
|
972
|
+
PendingSavedWifiNetworks(
|
|
973
|
+
requestId = requestId,
|
|
974
|
+
sid = snapshot.sessionId,
|
|
975
|
+
epoch = snapshot.epoch,
|
|
976
|
+
pending = pending,
|
|
977
|
+
mode = snapshot.mode,
|
|
978
|
+
)
|
|
979
|
+
pendingSavedWifiNetworks = request
|
|
980
|
+
}
|
|
981
|
+
unsupported?.let { return it }
|
|
982
|
+
val activeRequest = checkNotNull(request)
|
|
983
|
+
try {
|
|
984
|
+
dispatchSavedWifiNetworksIfReady(activeRequest)
|
|
985
|
+
return pending.await()
|
|
986
|
+
} catch (error: BluetoothSdkException) {
|
|
987
|
+
if (error.code == "request_timeout" && synchronized(oneShotLock) { activeRequest.mode == WifiRequestMode.DISCOVERING }) {
|
|
988
|
+
throw BluetoothSdkException(WIFI_CAPABILITY_NEGOTIATION_TIMEOUT_CODE, "WiFi capability negotiation timed out.")
|
|
989
|
+
}
|
|
990
|
+
throw error
|
|
991
|
+
} finally {
|
|
992
|
+
synchronized(oneShotLock) {
|
|
993
|
+
if (pendingSavedWifiNetworks === activeRequest) {
|
|
994
|
+
pendingSavedWifiNetworks = null
|
|
921
995
|
}
|
|
922
996
|
}
|
|
923
997
|
}
|
|
@@ -1028,16 +1102,11 @@ class MentraBluetoothSdk private constructor(
|
|
|
1028
1102
|
}
|
|
1029
1103
|
}
|
|
1030
1104
|
|
|
1031
|
-
suspend fun startStream(request: StreamRequest): StreamStatusEvent
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
private suspend fun startStream(
|
|
1038
|
-
request: StreamRequest,
|
|
1039
|
-
startSdkKeepAlive: Boolean,
|
|
1040
|
-
): StreamStatusEvent {
|
|
1105
|
+
suspend fun startStream(request: StreamRequest): StreamStatusEvent {
|
|
1106
|
+
requireGlassesConnected("start stream")
|
|
1107
|
+
if (!streamSession.supported) {
|
|
1108
|
+
throw BluetoothSdkException("stream_control_unsupported", "Update the glasses software before starting a stream.")
|
|
1109
|
+
}
|
|
1041
1110
|
val message = request.toMap().toMutableMap()
|
|
1042
1111
|
val streamId = (message["streamId"] as? String)?.takeIf { it.isNotBlank() }
|
|
1043
1112
|
?: "sdk-${UUID.randomUUID()}"
|
|
@@ -1052,23 +1121,15 @@ class MentraBluetoothSdk private constructor(
|
|
|
1052
1121
|
val registered = PendingStreamStart(streamStartSeq.incrementAndGet(), pending)
|
|
1053
1122
|
pendingStreamStarts[streamId] = registered
|
|
1054
1123
|
start = registered
|
|
1055
|
-
stopStreamKeepAliveMonitor()
|
|
1056
1124
|
deviceManager.startStream(message)
|
|
1057
1125
|
}
|
|
1058
1126
|
val event = pending.await(STREAM_START_TIMEOUT_MS)
|
|
1059
|
-
if (startSdkKeepAlive) {
|
|
1060
|
-
startStreamKeepAliveMonitor(streamId, DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS)
|
|
1061
|
-
}
|
|
1062
1127
|
return event
|
|
1063
1128
|
} finally {
|
|
1064
1129
|
start?.let { pendingStreamStarts.remove(streamId, it) }
|
|
1065
1130
|
}
|
|
1066
1131
|
}
|
|
1067
1132
|
|
|
1068
|
-
internal fun sendExternallyManagedStreamKeepAlive(request: StreamKeepAliveRequest) {
|
|
1069
|
-
deviceManager.keepStreamAlive(request.toMap().toMutableMap())
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
1133
|
suspend fun rgbLedControl(request: RgbLedRequest): RgbLedControlResponseEvent {
|
|
1073
1134
|
val pending = PendingResponse<RgbLedControlResponseEvent>("RGB LED command ${request.requestId}")
|
|
1074
1135
|
pendingRgbLedRequests[request.requestId] = pending
|
|
@@ -1090,9 +1151,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1090
1151
|
|
|
1091
1152
|
suspend fun stopStream(): StreamStatusEvent {
|
|
1092
1153
|
val pending = PendingResponse<StreamStatusEvent>("stop stream")
|
|
1093
|
-
val targetStreamId =
|
|
1094
|
-
activeStreamKeepAlive?.streamId
|
|
1095
|
-
}
|
|
1154
|
+
val targetStreamId = streamSession.currentStreamId
|
|
1096
1155
|
try {
|
|
1097
1156
|
// The seq draw and the BLE hand-off share startStream's ordering
|
|
1098
1157
|
// critical section: the stop takes its own slot in the send order,
|
|
@@ -1109,7 +1168,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
1109
1168
|
pendingStreamStop =
|
|
1110
1169
|
PendingStreamStop(targetStreamId, streamStartSeq.incrementAndGet(), pending)
|
|
1111
1170
|
}
|
|
1112
|
-
stopStreamKeepAliveMonitor()
|
|
1113
1171
|
deviceManager.stopStream()
|
|
1114
1172
|
}
|
|
1115
1173
|
return pending.await(STREAM_STOP_TIMEOUT_MS)
|
|
@@ -1467,7 +1525,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
1467
1525
|
}
|
|
1468
1526
|
|
|
1469
1527
|
override fun close() {
|
|
1470
|
-
|
|
1528
|
+
resetWifiProtocolSession("", "sdk_closed")
|
|
1471
1529
|
if (activityLifecycleCallbacksRegistered) {
|
|
1472
1530
|
(appContext as? Application)?.unregisterActivityLifecycleCallbacks(
|
|
1473
1531
|
activityLifecycleCallbacks,
|
|
@@ -1483,6 +1541,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
1483
1541
|
private fun dispatchStoreUpdate(category: String, changes: Map<String, Any>) {
|
|
1484
1542
|
when (ObservableStore.normalizeCategory(category)) {
|
|
1485
1543
|
"glasses" -> {
|
|
1544
|
+
if (changes["connected"] == false) {
|
|
1545
|
+
resetWifiProtocolSession("", "wifi_session_disconnected")
|
|
1546
|
+
}
|
|
1486
1547
|
analytics.observeGlassesStatus(getRawGlassesStatus())
|
|
1487
1548
|
val state = getState()
|
|
1488
1549
|
dispatchToListeners {
|
|
@@ -1691,6 +1752,27 @@ class MentraBluetoothSdk private constructor(
|
|
|
1691
1752
|
handleWifiStatusForRequests(event)
|
|
1692
1753
|
dispatchToListeners { it.onWifiStatusChanged(event) }
|
|
1693
1754
|
}
|
|
1755
|
+
"wifi_forget_result" -> {
|
|
1756
|
+
handleWifiForgetResultForRequests(data)
|
|
1757
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1758
|
+
}
|
|
1759
|
+
"saved_wifi_networks" -> {
|
|
1760
|
+
handleSavedWifiNetworksForRequests(data)
|
|
1761
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1762
|
+
}
|
|
1763
|
+
"wifi_protocol_session_ready" -> {
|
|
1764
|
+
resetWifiProtocolSession(
|
|
1765
|
+
data["sid"] as? String ?: "",
|
|
1766
|
+
"wifi_session_restarted",
|
|
1767
|
+
)
|
|
1768
|
+
}
|
|
1769
|
+
"glasses_session_changed" -> {
|
|
1770
|
+
resetWifiProtocolSession(
|
|
1771
|
+
data["sid"] as? String ?: "",
|
|
1772
|
+
"wifi_session_changed",
|
|
1773
|
+
)
|
|
1774
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1775
|
+
}
|
|
1694
1776
|
"wifi_scan_result" -> {
|
|
1695
1777
|
val networks =
|
|
1696
1778
|
(data["networks"] as? List<*>)
|
|
@@ -1759,17 +1841,18 @@ class MentraBluetoothSdk private constructor(
|
|
|
1759
1841
|
handleRgbLedResponseForRequests(event)
|
|
1760
1842
|
dispatchToListeners { it.onRgbLedControlResponse(event) }
|
|
1761
1843
|
}
|
|
1844
|
+
"stream_control_ready" -> {
|
|
1845
|
+
streamSession.ready(data["sid"] as? String, (data["streamControlVersion"] as? Number)?.toInt())
|
|
1846
|
+
}
|
|
1762
1847
|
"stream_status" -> {
|
|
1848
|
+
if (data.containsKey("revision") && !streamSession.accept(data)) return
|
|
1763
1849
|
val event = StreamStatusEvent(data)
|
|
1764
1850
|
handleStreamStatusForRequests(event)
|
|
1765
|
-
handleStreamStatusForKeepAlive(event.status)
|
|
1766
1851
|
dispatchToListeners { it.onStreamStatus(event) }
|
|
1767
1852
|
}
|
|
1768
1853
|
"keep_alive_ack" -> {
|
|
1769
1854
|
val event = KeepAliveAckEvent(data)
|
|
1770
|
-
|
|
1771
|
-
dispatchToListeners { it.onKeepAliveAck(event) }
|
|
1772
|
-
}
|
|
1855
|
+
dispatchToListeners { it.onKeepAliveAck(event) }
|
|
1773
1856
|
}
|
|
1774
1857
|
"ota_start_ack" -> {
|
|
1775
1858
|
val event = OtaStartAckEvent.fromMap(data + mapOf("type" to "ota_start_ack"))
|
|
@@ -1800,6 +1883,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
1800
1883
|
dispatchToListeners { it.onSettingsAck(event) }
|
|
1801
1884
|
}
|
|
1802
1885
|
"version_info" -> {
|
|
1886
|
+
if (data["versionInfoType"] == "version_info_1" || data["versionInfoType"] == "version_info") {
|
|
1887
|
+
applyWifiProtocolCapabilities(data)
|
|
1888
|
+
}
|
|
1803
1889
|
val event = VersionInfoResult.fromMap(data)
|
|
1804
1890
|
handleVersionInfoForRequest(data)
|
|
1805
1891
|
dispatchToListeners { it.onVersionInfo(event) }
|
|
@@ -1859,29 +1945,6 @@ class MentraBluetoothSdk private constructor(
|
|
|
1859
1945
|
}
|
|
1860
1946
|
}
|
|
1861
1947
|
|
|
1862
|
-
private fun startStreamKeepAliveMonitor(streamId: String, requestedIntervalSeconds: Int) {
|
|
1863
|
-
val intervalSeconds =
|
|
1864
|
-
requestedIntervalSeconds.takeIf { it > 0 } ?: DEFAULT_STREAM_KEEP_ALIVE_INTERVAL_SECONDS
|
|
1865
|
-
val tracker = ActiveStreamKeepAlive(
|
|
1866
|
-
streamId = streamId,
|
|
1867
|
-
intervalMs = intervalSeconds * 1_000L,
|
|
1868
|
-
)
|
|
1869
|
-
synchronized(streamKeepAliveLock) {
|
|
1870
|
-
activeStreamKeepAlive = tracker
|
|
1871
|
-
}
|
|
1872
|
-
sendNextStreamKeepAlive(tracker)
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
private fun stopStreamKeepAliveMonitor() {
|
|
1876
|
-
val tracker =
|
|
1877
|
-
synchronized(streamKeepAliveLock) {
|
|
1878
|
-
val current = activeStreamKeepAlive
|
|
1879
|
-
activeStreamKeepAlive = null
|
|
1880
|
-
current
|
|
1881
|
-
}
|
|
1882
|
-
tracker?.nextTick?.let { mainHandler.removeCallbacks(it) }
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
1948
|
private fun handleStreamStatusForRequests(event: StreamStatusEvent) {
|
|
1886
1949
|
val startMatch = matchingStreamStart(event)
|
|
1887
1950
|
if (startMatch != null) {
|
|
@@ -2220,43 +2283,184 @@ class MentraBluetoothSdk private constructor(
|
|
|
2220
2283
|
}
|
|
2221
2284
|
|
|
2222
2285
|
private fun handleWifiStatusForRequests(event: WifiStatusEvent) {
|
|
2223
|
-
val
|
|
2286
|
+
val connectRequest = synchronized(oneShotLock) { pendingWifiStatus }
|
|
2224
2287
|
// A wifi_status carrying the explicit error field is the glasses' failure
|
|
2225
2288
|
// verdict for the in-flight connect: reject now instead of running out the
|
|
2226
2289
|
// request timeout. Only the error field counts as failure — the glasses'
|
|
2227
2290
|
// connect sequence emits a debounced bare connected=false ~1-2s after
|
|
2228
2291
|
// credentials while association is still in progress, and rejecting on that
|
|
2229
2292
|
// would kill every connect attempt early.
|
|
2230
|
-
if (
|
|
2293
|
+
if (connectRequest != null && event.error != null) {
|
|
2231
2294
|
synchronized(oneShotLock) {
|
|
2232
|
-
if (pendingWifiStatus ===
|
|
2295
|
+
if (pendingWifiStatus === connectRequest) {
|
|
2233
2296
|
pendingWifiStatus = null
|
|
2234
2297
|
}
|
|
2235
2298
|
}
|
|
2236
|
-
|
|
2299
|
+
connectRequest.pending.reject(
|
|
2237
2300
|
BluetoothSdkException(
|
|
2238
2301
|
event.error,
|
|
2239
|
-
"Glasses failed to join \"${
|
|
2302
|
+
"Glasses failed to join \"${connectRequest.ssid}\": ${event.error}",
|
|
2240
2303
|
)
|
|
2241
2304
|
)
|
|
2242
2305
|
return
|
|
2243
2306
|
}
|
|
2244
|
-
if (
|
|
2307
|
+
if (connectRequest != null && wifiStatusMatchesConnect(event.status, connectRequest.ssid)) {
|
|
2308
|
+
synchronized(oneShotLock) {
|
|
2309
|
+
if (pendingWifiStatus === connectRequest) {
|
|
2310
|
+
pendingWifiStatus = null
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
connectRequest.pending.resolve(event)
|
|
2314
|
+
return
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// Forget never settles from link state: modern requests require a correlated result,
|
|
2318
|
+
// while legacy requests resolve as unverified at accepted dispatch.
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
private fun handleWifiForgetResultForRequests(data: Map<String, Any>) {
|
|
2322
|
+
val request = synchronized(oneShotLock) { pendingWifiForget } ?: return
|
|
2323
|
+
if (request.mode != WifiRequestMode.MODERN) return
|
|
2324
|
+
val version =
|
|
2325
|
+
synchronized(oneShotLock) {
|
|
2326
|
+
if (request.epoch != wifiSessionCapabilities.epoch) return
|
|
2327
|
+
(wifiSessionCapabilities.forgetResult as? WifiProtocolCapability.Supported)?.version
|
|
2328
|
+
} ?: return
|
|
2329
|
+
val result =
|
|
2330
|
+
parseWifiForgetResult(
|
|
2331
|
+
request.requestId,
|
|
2332
|
+
request.sid,
|
|
2333
|
+
request.ssid,
|
|
2334
|
+
version,
|
|
2335
|
+
data,
|
|
2336
|
+
) ?: return
|
|
2337
|
+
|
|
2245
2338
|
synchronized(oneShotLock) {
|
|
2246
|
-
if (
|
|
2247
|
-
|
|
2339
|
+
if (pendingWifiForget !== request || request.epoch != wifiSessionCapabilities.epoch) {
|
|
2340
|
+
return
|
|
2248
2341
|
}
|
|
2342
|
+
pendingWifiForget = null
|
|
2249
2343
|
}
|
|
2250
|
-
request.pending.resolve(
|
|
2344
|
+
request.pending.resolve(result)
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
private fun handleSavedWifiNetworksForRequests(data: Map<String, Any>) {
|
|
2348
|
+
val request = synchronized(oneShotLock) { pendingSavedWifiNetworks } ?: return
|
|
2349
|
+
if (request.mode != WifiRequestMode.MODERN) return
|
|
2350
|
+
val version =
|
|
2351
|
+
synchronized(oneShotLock) {
|
|
2352
|
+
if (request.epoch != wifiSessionCapabilities.epoch) return
|
|
2353
|
+
(wifiSessionCapabilities.savedNetworks as? WifiProtocolCapability.Supported)?.version
|
|
2354
|
+
} ?: return
|
|
2355
|
+
val result =
|
|
2356
|
+
parseSavedWifiNetworks(
|
|
2357
|
+
request.requestId,
|
|
2358
|
+
request.sid,
|
|
2359
|
+
version,
|
|
2360
|
+
data,
|
|
2361
|
+
) ?: return
|
|
2362
|
+
synchronized(oneShotLock) {
|
|
2363
|
+
if (pendingSavedWifiNetworks !== request || request.epoch != wifiSessionCapabilities.epoch) {
|
|
2364
|
+
return
|
|
2365
|
+
}
|
|
2366
|
+
pendingSavedWifiNetworks = null
|
|
2367
|
+
}
|
|
2368
|
+
request.pending.resolve(result)
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
private fun wifiStatusMatchesConnect(status: WifiStatus, ssid: String): Boolean =
|
|
2372
|
+
status is WifiStatus.Connected && status.ssid == ssid
|
|
2373
|
+
|
|
2374
|
+
private fun dispatchWifiForgetIfReady(request: PendingWifiForgetRequest) {
|
|
2375
|
+
synchronized(oneShotLock) {
|
|
2376
|
+
if (pendingWifiForget !== request || request.epoch != wifiSessionCapabilities.epoch ||
|
|
2377
|
+
request.mode == WifiRequestMode.DISCOVERING || request.commandSent) return
|
|
2378
|
+
if (request.mode == WifiRequestMode.UNSUPPORTED) {
|
|
2379
|
+
request.pending.reject(BluetoothSdkException("wifi_protocol_unsupported", "Unsupported WiFi forget protocol version."))
|
|
2380
|
+
return
|
|
2381
|
+
}
|
|
2382
|
+
request.sid = wifiSessionCapabilities.sessionId
|
|
2383
|
+
request.commandSent = deviceManager.forgetWifiNetwork(
|
|
2384
|
+
request.ssid,
|
|
2385
|
+
request.requestId.takeIf { request.mode == WifiRequestMode.MODERN },
|
|
2386
|
+
request.sid.takeIf { request.mode == WifiRequestMode.MODERN },
|
|
2387
|
+
)
|
|
2388
|
+
if (!request.commandSent) {
|
|
2389
|
+
request.pending.reject(BluetoothSdkException("dispatch_failed", "No active glasses transport accepted WiFi forget."))
|
|
2390
|
+
} else if (request.mode == WifiRequestMode.LEGACY) {
|
|
2391
|
+
pendingWifiForget = null
|
|
2392
|
+
request.pending.resolve(legacyWifiForgetResult(request.ssid))
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
private fun dispatchSavedWifiNetworksIfReady(request: PendingSavedWifiNetworks) {
|
|
2398
|
+
synchronized(oneShotLock) {
|
|
2399
|
+
if (pendingSavedWifiNetworks !== request || request.epoch != wifiSessionCapabilities.epoch ||
|
|
2400
|
+
request.mode != WifiRequestMode.MODERN || request.commandSent) return
|
|
2401
|
+
request.sid = wifiSessionCapabilities.sessionId
|
|
2402
|
+
request.commandSent = deviceManager.requestSavedWifiNetworks(request.requestId, request.sid)
|
|
2403
|
+
if (!request.commandSent) {
|
|
2404
|
+
request.pending.reject(BluetoothSdkException("dispatch_failed", "No active glasses transport accepted saved WiFi request."))
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
private fun applyWifiProtocolCapabilities(data: Map<String, Any>) {
|
|
2410
|
+
var forgetToDispatch: PendingWifiForgetRequest? = null
|
|
2411
|
+
var savedToDispatch: PendingSavedWifiNetworks? = null
|
|
2412
|
+
var unsupportedSaved: PendingSavedWifiNetworks? = null
|
|
2413
|
+
synchronized(oneShotLock) {
|
|
2414
|
+
wifiSessionCapabilities.applyVersionInfo1(data)
|
|
2415
|
+
pendingWifiForget?.takeIf { it.epoch == wifiSessionCapabilities.epoch }?.let { request ->
|
|
2416
|
+
if (request.mode == WifiRequestMode.DISCOVERING) {
|
|
2417
|
+
request.mode = wifiSessionCapabilities.forgetMode()
|
|
2418
|
+
forgetToDispatch = request
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
pendingSavedWifiNetworks?.takeIf { it.epoch == wifiSessionCapabilities.epoch }?.let { request ->
|
|
2422
|
+
if (request.mode == WifiRequestMode.DISCOVERING) {
|
|
2423
|
+
request.mode = wifiSessionCapabilities.savedNetworksMode()
|
|
2424
|
+
if (request.mode == WifiRequestMode.LEGACY || request.mode == WifiRequestMode.UNSUPPORTED) {
|
|
2425
|
+
pendingSavedWifiNetworks = null
|
|
2426
|
+
unsupportedSaved = request
|
|
2427
|
+
} else {
|
|
2428
|
+
savedToDispatch = request
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
unsupportedSaved?.let { request ->
|
|
2434
|
+
request.pending.resolve(
|
|
2435
|
+
SavedWifiNetworksResult(
|
|
2436
|
+
outcome = SavedWifiNetworksOutcome.UNSUPPORTED,
|
|
2437
|
+
networks = emptyList(),
|
|
2438
|
+
error = "saved_wifi_networks_unsupported",
|
|
2439
|
+
)
|
|
2440
|
+
)
|
|
2441
|
+
}
|
|
2442
|
+
forgetToDispatch?.let(::dispatchWifiForgetIfReady)
|
|
2443
|
+
savedToDispatch?.let(::dispatchSavedWifiNetworksIfReady)
|
|
2251
2444
|
}
|
|
2252
2445
|
|
|
2253
|
-
private fun
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2446
|
+
private fun resetWifiProtocolSession(sessionId: String, code: String) {
|
|
2447
|
+
val error = BluetoothSdkException(code, "The glasses WiFi protocol session changed.")
|
|
2448
|
+
val pendingToReject = mutableListOf<PendingResponse<*>>()
|
|
2449
|
+
synchronized(oneShotLock) {
|
|
2450
|
+
wifiSessionCapabilities.reset(sessionId)
|
|
2451
|
+
pendingWifiStatus?.pending?.let(pendingToReject::add)
|
|
2452
|
+
pendingWifiForget?.pending?.let(pendingToReject::add)
|
|
2453
|
+
pendingSavedWifiNetworks?.pending?.let(pendingToReject::add)
|
|
2454
|
+
pendingWifiScan?.pending?.let(pendingToReject::add)
|
|
2455
|
+
pendingHotspotStatus?.pending?.let(pendingToReject::add)
|
|
2456
|
+
pendingWifiStatus = null
|
|
2457
|
+
pendingWifiForget = null
|
|
2458
|
+
pendingSavedWifiNetworks = null
|
|
2459
|
+
pendingWifiScan = null
|
|
2460
|
+
pendingHotspotStatus = null
|
|
2259
2461
|
}
|
|
2462
|
+
pendingToReject.forEach { it.reject(error) }
|
|
2463
|
+
}
|
|
2260
2464
|
|
|
2261
2465
|
private fun handleHotspotStatusForRequests(event: HotspotStatusEvent) {
|
|
2262
2466
|
val request = synchronized(oneShotLock) { pendingHotspotStatus } ?: return
|
|
@@ -2291,94 +2495,7 @@ class MentraBluetoothSdk private constructor(
|
|
|
2291
2495
|
)
|
|
2292
2496
|
}
|
|
2293
2497
|
|
|
2294
|
-
private fun sendNextStreamKeepAlive(tracker: ActiveStreamKeepAlive) {
|
|
2295
|
-
var timeoutEvent: StreamStatusEvent? = null
|
|
2296
|
-
var request: StreamKeepAliveRequest? = null
|
|
2297
|
-
|
|
2298
|
-
synchronized(streamKeepAliveLock) {
|
|
2299
|
-
if (activeStreamKeepAlive !== tracker) {
|
|
2300
|
-
return
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
if (tracker.armed && tracker.pendingAckId != null) {
|
|
2304
|
-
tracker.missedAckCount += 1
|
|
2305
|
-
if (tracker.missedAckCount >= MAX_MISSED_STREAM_KEEP_ALIVE_ACKS) {
|
|
2306
|
-
activeStreamKeepAlive = null
|
|
2307
|
-
tracker.nextTick?.let { mainHandler.removeCallbacks(it) }
|
|
2308
|
-
timeoutEvent =
|
|
2309
|
-
StreamStatusEvent(
|
|
2310
|
-
StreamStatus.Error(
|
|
2311
|
-
streamId = tracker.streamId,
|
|
2312
|
-
errorDetails =
|
|
2313
|
-
"Stream keep-alive timed out after ${tracker.missedAckCount} missed ACKs",
|
|
2314
|
-
timestamp = System.currentTimeMillis(),
|
|
2315
|
-
resolvedConfig = null,
|
|
2316
|
-
)
|
|
2317
|
-
)
|
|
2318
|
-
return@synchronized
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
val ackId = "ack-${System.currentTimeMillis()}"
|
|
2323
|
-
tracker.pendingAckId = ackId
|
|
2324
|
-
request = StreamKeepAliveRequest(streamId = tracker.streamId, ackId = ackId)
|
|
2325
|
-
val nextTick = Runnable { sendNextStreamKeepAlive(tracker) }
|
|
2326
|
-
tracker.nextTick = nextTick
|
|
2327
|
-
mainHandler.postDelayed(nextTick, tracker.intervalMs)
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
timeoutEvent?.let { event ->
|
|
2331
|
-
dispatchToListeners { it.onStreamStatus(event) }
|
|
2332
|
-
stopStreamKeepAliveMonitor()
|
|
2333
|
-
deviceManager.stopStream()
|
|
2334
|
-
return
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
request?.let { keepAlive ->
|
|
2338
|
-
deviceManager.keepStreamAlive(keepAlive.toMap().toMutableMap())
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
private fun handleStreamKeepAliveAck(event: KeepAliveAckEvent): Boolean {
|
|
2343
|
-
synchronized(streamKeepAliveLock) {
|
|
2344
|
-
val tracker = activeStreamKeepAlive ?: return false
|
|
2345
|
-
if (event.streamId != tracker.streamId || event.ackId != tracker.pendingAckId) {
|
|
2346
|
-
return false
|
|
2347
|
-
}
|
|
2348
|
-
tracker.pendingAckId = null
|
|
2349
|
-
tracker.missedAckCount = 0
|
|
2350
|
-
return true
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
2498
|
|
|
2354
|
-
private fun handleStreamStatusForKeepAlive(status: StreamStatus) {
|
|
2355
|
-
val streamId = status.streamId
|
|
2356
|
-
val activeStreamId = synchronized(streamKeepAliveLock) { activeStreamKeepAlive?.streamId }
|
|
2357
|
-
if (streamId == null || activeStreamId != streamId) {
|
|
2358
|
-
return
|
|
2359
|
-
}
|
|
2360
|
-
when (status.state) {
|
|
2361
|
-
StreamState.STOPPED,
|
|
2362
|
-
StreamState.STOPPING,
|
|
2363
|
-
StreamState.ERROR,
|
|
2364
|
-
StreamState.RECONNECT_FAILED -> stopStreamKeepAliveMonitor()
|
|
2365
|
-
// A non-terminal status means the stream is live or coming up and the glasses can
|
|
2366
|
-
// now ACK; arm the missed-ACK detector from here so a slow startup before the first
|
|
2367
|
-
// ACK can't trip a false keep-alive timeout. On the arming transition, drop any
|
|
2368
|
-
// pre-arm bookkeeping so a stale unacked id (sent before the glasses could ACK)
|
|
2369
|
-
// can't immediately count as a miss.
|
|
2370
|
-
else ->
|
|
2371
|
-
synchronized(streamKeepAliveLock) {
|
|
2372
|
-
activeStreamKeepAlive?.let {
|
|
2373
|
-
if (it.streamId == streamId && !it.armed) {
|
|
2374
|
-
it.armed = true
|
|
2375
|
-
it.pendingAckId = null
|
|
2376
|
-
it.missedAckCount = 0
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
2499
|
}
|
|
2383
2500
|
|
|
2384
2501
|
/** OTA status messages are not request-correlated, so only known pre-ack failures settle a start. */
|