@mentra/bluetooth-sdk 0.1.20 → 0.1.21-beta.0
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 +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +127 -24
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +65 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +46 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +369 -97
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +183 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +4 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/status/WifiHotspotStatus.kt +19 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +15 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +15 -0
- package/build/BluetoothSdk.types.d.ts +46 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +23 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +20 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
- package/build/_private/MentraLocalNetworkModule.js +3 -0
- package/build/_private/MentraLocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +1 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +1 -0
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/expo-module.config.json +1 -0
- package/ios/BluetoothSdkModule.swift +72 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +72 -18
- package/ios/Source/DeviceManager.swift +51 -3
- package/ios/Source/DeviceStore.swift +1 -1
- package/ios/Source/MentraBluetoothSDK.swift +289 -25
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +8 -3
- package/ios/Source/sgcs/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +19 -7
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +318 -37
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/Nimo.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +5 -2
- package/ios/Source/sgcs/Simulated.swift +1 -1
- package/ios/Source/status/WifiHotspotStatus.swift +21 -2
- package/ios/Source/streaming/StreamModels.swift +9 -0
- package/ios/Source/tts/TTSTools.swift +1 -1
- package/package.json +1 -2
- package/src/BluetoothSdk.types.ts +49 -0
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +64 -13
- package/src/_private/MentraLocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +1 -0
- package/src/_private/photoRequestPayload.ts +2 -3
- package/src/index.ts +4 -0
|
@@ -34,6 +34,7 @@ import com.mentra.bluetoothsdk.Bridge
|
|
|
34
34
|
import com.mentra.bluetoothsdk.DeviceManager
|
|
35
35
|
import com.mentra.bluetoothsdk.PhotoRequest
|
|
36
36
|
import com.mentra.bluetoothsdk.PhotoSize
|
|
37
|
+
import com.mentra.bluetoothsdk.PhotoMode
|
|
37
38
|
import com.mentra.bluetoothsdk.DeviceStore
|
|
38
39
|
import com.mentra.bluetoothsdk.ObservableStore
|
|
39
40
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
@@ -146,6 +147,10 @@ class MentraLive : SGCManager() {
|
|
|
146
147
|
|
|
147
148
|
// Heartbeat parameters
|
|
148
149
|
private const val HEARTBEAT_INTERVAL_MS = 30000 // 30 seconds
|
|
150
|
+
// Grace period before an unanswered BLE wire v2 handshake is re-armed for retry,
|
|
151
|
+
// and the per-session cap on automatic retries (see sendWireHandshake).
|
|
152
|
+
private const val WIRE_HANDSHAKE_RETRY_GRACE_MS = 5000L
|
|
153
|
+
private const val WIRE_HANDSHAKE_MAX_ATTEMPTS = 3
|
|
149
154
|
private const val BATTERY_REQUEST_EVERY_N_HEARTBEATS = 10 // Every 10 heartbeats (5 minutes)
|
|
150
155
|
private const val RSSI_READ_INTERVAL_MS = 10000L // 10 seconds
|
|
151
156
|
|
|
@@ -213,6 +218,15 @@ class MentraLive : SGCManager() {
|
|
|
213
218
|
private var peerWireProtocolVersion = 0
|
|
214
219
|
private var useBinaryWireProtocol = false
|
|
215
220
|
private var wireHandshakeQueued = false
|
|
221
|
+
private var wireHandshakeAttempts = 0
|
|
222
|
+
// Session generation in which the LAST outgoing v2 handshake was sent. A handshake
|
|
223
|
+
// reply only activates v2 when it answers a handshake from the CURRENT epoch - a
|
|
224
|
+
// stale handshake that survived in flight across an epoch reset must not flip the
|
|
225
|
+
// new session to v2 before it negotiated its own build and capabilities.
|
|
226
|
+
private var wireHandshakeSentGeneration = -1
|
|
227
|
+
// Bumped on every BLE session reset; scheduled handshake-retry callbacks capture it
|
|
228
|
+
// and no-op if the session changed, so a timer from a dead session can't poke a new one.
|
|
229
|
+
private var wireSessionGeneration = 0
|
|
216
230
|
// Negotiated K900 STRING length endianness for the phone<->glasses BLE link. Defaults to
|
|
217
231
|
// legacy big-endian; upgraded to little-endian only when the glasses advertise wire_caps.k900_le
|
|
218
232
|
// (or a v2 binary handshake succeeds, which implies wire-v2 LE).
|
|
@@ -313,6 +327,8 @@ class MentraLive : SGCManager() {
|
|
|
313
327
|
// So we temporarily suspend the LC3 mic during phone audio playback
|
|
314
328
|
private var micIntentEnabled = false // User/system WANTS mic enabled
|
|
315
329
|
private var micSuspendedForAudio = false // Mic temporarily suspended due to phone audio
|
|
330
|
+
override val isMicSuspendedForAudio: Boolean
|
|
331
|
+
get() = micSuspendedForAudio
|
|
316
332
|
private var phoneAudioMonitor: PhoneAudioMonitor? = null
|
|
317
333
|
private var micOnCount = 0
|
|
318
334
|
private var micOffCount = 0
|
|
@@ -780,14 +796,12 @@ class MentraLive : SGCManager() {
|
|
|
780
796
|
val isEqual = state == connectionState
|
|
781
797
|
if (isEqual) {
|
|
782
798
|
if (state == ConnTypes.DISCONNECTED) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
peerFilePayloadV2 = false
|
|
790
|
-
BleJsonCompact.resetSession()
|
|
799
|
+
resetWireNegotiationState()
|
|
800
|
+
// Queued writes are session-bound: transmitting them into the NEXT session
|
|
801
|
+
// (e.g. a stale handshake whose reply activates v2 before the new session
|
|
802
|
+
// negotiated) is the bug class this clear removes. Higher layers re-send
|
|
803
|
+
// what still matters via their own ACK/retry tracking.
|
|
804
|
+
sendQueue.clear()
|
|
791
805
|
}
|
|
792
806
|
return
|
|
793
807
|
}
|
|
@@ -815,14 +829,9 @@ class MentraLive : SGCManager() {
|
|
|
815
829
|
DeviceStore.apply("glasses", "connected", false)
|
|
816
830
|
DeviceStore.apply("glasses", "signalStrength", -1)
|
|
817
831
|
DeviceStore.apply("glasses", "signalStrengthUpdatedAt", 0L)
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
wireHandshakeQueued = false
|
|
822
|
-
peerK900Le = false
|
|
823
|
-
peerWireCapsBinary = false
|
|
824
|
-
peerFilePayloadV2 = false
|
|
825
|
-
BleJsonCompact.resetSession()
|
|
832
|
+
resetWireNegotiationState()
|
|
833
|
+
sendQueue.clear() // see the disconnect reset above: stale writes die with the session
|
|
834
|
+
|
|
826
835
|
// Drop OTA caches when fully disconnected — avoids leaking session/step state
|
|
827
836
|
// from a previous pairing into the next one.
|
|
828
837
|
resetOtaCache()
|
|
@@ -3136,7 +3145,12 @@ class MentraLive : SGCManager() {
|
|
|
3136
3145
|
DeviceStore.apply("glasses", "wifiError", "")
|
|
3137
3146
|
}
|
|
3138
3147
|
|
|
3139
|
-
updateWifiStatus(
|
|
3148
|
+
updateWifiStatus(
|
|
3149
|
+
wifiConnectedStatus,
|
|
3150
|
+
ssid,
|
|
3151
|
+
localIp,
|
|
3152
|
+
wifiError.takeIf { it.isNotEmpty() }
|
|
3153
|
+
)
|
|
3140
3154
|
}
|
|
3141
3155
|
"hotspot_status_update" -> {
|
|
3142
3156
|
// Process hotspot status information (same pattern as "wifi_status")
|
|
@@ -3239,7 +3253,8 @@ class MentraLive : SGCManager() {
|
|
|
3239
3253
|
|
|
3240
3254
|
val scanComplete =
|
|
3241
3255
|
json.optBoolean("scan_complete", json.optBoolean("scanComplete", false))
|
|
3242
|
-
|
|
3256
|
+
val scanId = json.optString("scanId", "").ifEmpty { null }
|
|
3257
|
+
Bridge.updateWifiScanResults(networks, scanComplete, scanId)
|
|
3243
3258
|
}
|
|
3244
3259
|
"token_status" -> {
|
|
3245
3260
|
// Process coreToken acknowledgment
|
|
@@ -3492,8 +3507,14 @@ class MentraLive : SGCManager() {
|
|
|
3492
3507
|
// Glasses SOC has booted and is ready for communication
|
|
3493
3508
|
Bridge.log("LIVE: 🎉 Received glasses_ready message - SOC is booted and ready!")
|
|
3494
3509
|
|
|
3495
|
-
//
|
|
3510
|
+
// glasses_ready is a REMOTE wire-session reset: the glasses ran
|
|
3511
|
+
// onTransportReset() before sending it, so their side is back on legacy
|
|
3512
|
+
// framing regardless of what this side negotiated earlier. Start a fresh
|
|
3513
|
+
// wire epoch (clears v2-active state that would otherwise gate the
|
|
3514
|
+
// handshake off), then negotiate from the caps this message advertises.
|
|
3515
|
+
resetWireNegotiationState()
|
|
3496
3516
|
parsePeerWireCaps(json)
|
|
3517
|
+
maybeSendWireHandshake()
|
|
3497
3518
|
|
|
3498
3519
|
// Set the ready flag to stop any future readiness checks
|
|
3499
3520
|
glassesReady = true
|
|
@@ -3884,8 +3905,6 @@ class MentraLive : SGCManager() {
|
|
|
3884
3905
|
val buildNumInt = Integer.parseInt(buildNum)
|
|
3885
3906
|
buildNumberInt = buildNumInt
|
|
3886
3907
|
Bridge.log("LIVE: Parsed build number as integer: " + buildNumInt)
|
|
3887
|
-
parsePeerWireCaps(json)
|
|
3888
|
-
maybeSendWireHandshake()
|
|
3889
3908
|
} catch (e: NumberFormatException) {
|
|
3890
3909
|
buildNumberInt = 0
|
|
3891
3910
|
Log.e(TAG, "Failed to parse build number as integer: " + buildNum)
|
|
@@ -3953,6 +3972,16 @@ class MentraLive : SGCManager() {
|
|
|
3953
3972
|
}
|
|
3954
3973
|
}
|
|
3955
3974
|
|
|
3975
|
+
// Negotiate wire caps from ANY version_info chunk, not only the one
|
|
3976
|
+
// carrying build_number: the glasses put wire_caps in version_info_3
|
|
3977
|
+
// (firmware chunk) while build_number travels in version_info_1, so
|
|
3978
|
+
// gating negotiation on build_number silently discards the caps.
|
|
3979
|
+
// parsePeerWireCaps no-ops without a wire_caps key and
|
|
3980
|
+
// maybeSendWireHandshake gates on caps + build + not-yet-queued, so
|
|
3981
|
+
// running them per chunk is safe.
|
|
3982
|
+
parsePeerWireCaps(json)
|
|
3983
|
+
maybeSendWireHandshake()
|
|
3984
|
+
|
|
3956
3985
|
Bridge.log("LIVE: Processed version_info fields and sent to RN")
|
|
3957
3986
|
Bridge.sendVersionInfo(fields)
|
|
3958
3987
|
} else {
|
|
@@ -4457,7 +4486,20 @@ class MentraLive : SGCManager() {
|
|
|
4457
4486
|
|
|
4458
4487
|
val syntheticStatus: String
|
|
4459
4488
|
if ("FINISHED" == besOtaStatus) {
|
|
4460
|
-
|
|
4489
|
+
// The glasses power-cycle right after the final BES tick, so a
|
|
4490
|
+
// session whose BES step is the LAST step never gets a follow-up
|
|
4491
|
+
// ota_status from the glasses — consumers mapping on this synthetic
|
|
4492
|
+
// status would otherwise never see a terminal state. Emit "complete"
|
|
4493
|
+
// for the final step; mid-session BES steps keep "step_complete" so
|
|
4494
|
+
// session-level trackers advance normally. Unknown sessions
|
|
4495
|
+
// (cachedOtaTotalSteps == 0, e.g. legacy glasses that never sent an
|
|
4496
|
+
// ota_status) conservatively keep "step_complete".
|
|
4497
|
+
syntheticStatus =
|
|
4498
|
+
if (cachedOtaTotalSteps > 0 &&
|
|
4499
|
+
cachedOtaCurrentStep >= cachedOtaTotalSteps
|
|
4500
|
+
)
|
|
4501
|
+
"complete"
|
|
4502
|
+
else "step_complete"
|
|
4461
4503
|
} else if ("FAILED" == besOtaStatus) {
|
|
4462
4504
|
syntheticStatus = "failed"
|
|
4463
4505
|
} else {
|
|
@@ -4701,7 +4743,12 @@ class MentraLive : SGCManager() {
|
|
|
4701
4743
|
/**
|
|
4702
4744
|
* Update WiFi status and notify listeners Matches iOS MentraLive.swift updateWifiStatus pattern
|
|
4703
4745
|
*/
|
|
4704
|
-
private fun updateWifiStatus(
|
|
4746
|
+
private fun updateWifiStatus(
|
|
4747
|
+
connected: Boolean,
|
|
4748
|
+
ssid: String,
|
|
4749
|
+
localIp: String,
|
|
4750
|
+
error: String? = null
|
|
4751
|
+
) {
|
|
4705
4752
|
Bridge.log("LIVE: 🌐 Updating WiFi status - connected: " + connected + ", SSID: " + ssid)
|
|
4706
4753
|
|
|
4707
4754
|
// Update parent SGCManager fields
|
|
@@ -4710,7 +4757,7 @@ class MentraLive : SGCManager() {
|
|
|
4710
4757
|
DeviceStore.apply("glasses", "wifiLocalIp", localIp)
|
|
4711
4758
|
|
|
4712
4759
|
// Send event to bridge for cloud communication
|
|
4713
|
-
Bridge.sendWifiStatusChange(connected, ssid, localIp)
|
|
4760
|
+
Bridge.sendWifiStatusChange(connected, ssid, localIp, error)
|
|
4714
4761
|
}
|
|
4715
4762
|
|
|
4716
4763
|
/**
|
|
@@ -4784,10 +4831,13 @@ class MentraLive : SGCManager() {
|
|
|
4784
4831
|
/**
|
|
4785
4832
|
* Request WiFi scan from the glasses This will ask the glasses to scan for available networks
|
|
4786
4833
|
*/
|
|
4787
|
-
override fun requestWifiScan() {
|
|
4834
|
+
override fun requestWifiScan(scanId: String?) {
|
|
4788
4835
|
try {
|
|
4789
4836
|
val json = JSONObject()
|
|
4790
4837
|
json.put("type", "request_wifi_scan")
|
|
4838
|
+
if (!scanId.isNullOrEmpty()) {
|
|
4839
|
+
json.put("scanId", scanId)
|
|
4840
|
+
}
|
|
4791
4841
|
sendJson(json, true)
|
|
4792
4842
|
Bridge.log("LIVE: Sending WiFi scan request to glasses")
|
|
4793
4843
|
} catch (e: JSONException) {
|
|
@@ -5458,8 +5508,9 @@ class MentraLive : SGCManager() {
|
|
|
5458
5508
|
val bleImgId = "I" + String.format("%09d", System.currentTimeMillis() % 1000000000)
|
|
5459
5509
|
json.put("bleImgId", bleImgId)
|
|
5460
5510
|
|
|
5461
|
-
//
|
|
5462
|
-
|
|
5511
|
+
// Miniapps may explicitly skip direct Wi-Fi upload and force the
|
|
5512
|
+
// phone-relayed BLE path. Auto remains the default.
|
|
5513
|
+
json.put("transferMethod", request.transferMethod)
|
|
5463
5514
|
|
|
5464
5515
|
// Always prepare for potential BLE transfer
|
|
5465
5516
|
if (webhookUrl != null && !webhookUrl.isEmpty()) {
|
|
@@ -5469,7 +5520,7 @@ class MentraLive : SGCManager() {
|
|
|
5469
5520
|
blePhotoTransfers[bleImgId] = transfer
|
|
5470
5521
|
}
|
|
5471
5522
|
|
|
5472
|
-
Bridge.log("LIVE: Using
|
|
5523
|
+
Bridge.log("LIVE: Using " + request.transferMethod + " transfer mode with BLE fallback ID: " + bleImgId)
|
|
5473
5524
|
Bridge.log(
|
|
5474
5525
|
"LIVE: PHOTO PIPELINE [5b/6] JSON ready mode=" +
|
|
5475
5526
|
mode +
|
|
@@ -5488,6 +5539,7 @@ class MentraLive : SGCManager() {
|
|
|
5488
5539
|
fun warmUpCamera(
|
|
5489
5540
|
requestId: String,
|
|
5490
5541
|
size: PhotoSize,
|
|
5542
|
+
mode: PhotoMode = PhotoMode.PHOTO,
|
|
5491
5543
|
exposureTimeNs: Long?,
|
|
5492
5544
|
durationMs: Int,
|
|
5493
5545
|
) {
|
|
@@ -5496,6 +5548,8 @@ class MentraLive : SGCManager() {
|
|
|
5496
5548
|
requestId +
|
|
5497
5549
|
", size=" +
|
|
5498
5550
|
size.value +
|
|
5551
|
+
", mode=" +
|
|
5552
|
+
mode.value +
|
|
5499
5553
|
", durationMs=" +
|
|
5500
5554
|
durationMs
|
|
5501
5555
|
)
|
|
@@ -5506,6 +5560,7 @@ class MentraLive : SGCManager() {
|
|
|
5506
5560
|
json.put("requestId", requestId)
|
|
5507
5561
|
val sizeValue = size.value
|
|
5508
5562
|
json.put("size", if (sizeValue.isNotEmpty()) sizeValue else "medium")
|
|
5563
|
+
json.put("mode", mode.value)
|
|
5509
5564
|
if (exposureTimeNs != null && exposureTimeNs > 0L) {
|
|
5510
5565
|
json.put("exposureTimeNs", exposureTimeNs)
|
|
5511
5566
|
}
|
|
@@ -5516,6 +5571,16 @@ class MentraLive : SGCManager() {
|
|
|
5516
5571
|
}
|
|
5517
5572
|
}
|
|
5518
5573
|
|
|
5574
|
+
fun stopCameraWarmUp(requestId: String) {
|
|
5575
|
+
if (!isConnected) {
|
|
5576
|
+
throw IllegalStateException("not_connected")
|
|
5577
|
+
}
|
|
5578
|
+
val json = JSONObject()
|
|
5579
|
+
json.put("type", "camera_warm_up_stop")
|
|
5580
|
+
json.put("requestId", requestId)
|
|
5581
|
+
sendJson(json, true)
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5519
5584
|
override fun startStream(message: MutableMap<String, Any>) {
|
|
5520
5585
|
Bridge.log("LIVE: Starting RTMP stream")
|
|
5521
5586
|
|
|
@@ -7162,6 +7227,28 @@ class MentraLive : SGCManager() {
|
|
|
7162
7227
|
* negotiated per-link endianness and binary support. Missing wire_caps leaves the legacy
|
|
7163
7228
|
* defaults (BE, no binary) untouched so older glasses keep working.
|
|
7164
7229
|
*/
|
|
7230
|
+
/**
|
|
7231
|
+
* Drop every negotiated wire-session artifact and bump the session generation so
|
|
7232
|
+
* scheduled callbacks from the old session stand down. Called on BLE disconnect and on
|
|
7233
|
+
* glasses_ready: the glasses run onTransportReset() before sending glasses_ready, so a
|
|
7234
|
+
* mid-link ASG restart (no BLE disconnect) resets THEIR side to legacy framing - the
|
|
7235
|
+
* phone must treat every glasses_ready as a new wire epoch and renegotiate (bounded by
|
|
7236
|
+
* the per-session handshake attempt cap) instead of trusting stale v2-active state.
|
|
7237
|
+
*/
|
|
7238
|
+
private fun resetWireNegotiationState() {
|
|
7239
|
+
incomingChunkReassembler.clear()
|
|
7240
|
+
peerWireProtocolVersion = 0
|
|
7241
|
+
useBinaryWireProtocol = false
|
|
7242
|
+
wireHandshakeQueued = false
|
|
7243
|
+
wireHandshakeAttempts = 0
|
|
7244
|
+
wireSessionGeneration++
|
|
7245
|
+
peerK900Le = false
|
|
7246
|
+
peerWireCapsBinary = false
|
|
7247
|
+
peerFilePayloadV2 = false
|
|
7248
|
+
BleJsonCompact.resetSession()
|
|
7249
|
+
wireHandshakeSentGeneration = -1
|
|
7250
|
+
}
|
|
7251
|
+
|
|
7165
7252
|
private fun parsePeerWireCaps(json: JSONObject) {
|
|
7166
7253
|
val caps = json.optJSONObject("wire_caps") ?: return
|
|
7167
7254
|
if (caps.optBoolean("k900_le", false)) {
|
|
@@ -7233,7 +7320,39 @@ class MentraLive : SGCManager() {
|
|
|
7233
7320
|
)
|
|
7234
7321
|
Bridge.log("LIVE: Sending BLE wire v2 handshake")
|
|
7235
7322
|
wireHandshakeQueued = true
|
|
7323
|
+
wireHandshakeSentGeneration = wireSessionGeneration
|
|
7236
7324
|
queueData(packed, null)
|
|
7325
|
+
// The handshake and its reply are fire-and-forget binary frames with no ACK
|
|
7326
|
+
// tracking; if either is lost, wireHandshakeQueued would block every future
|
|
7327
|
+
// attempt and the session would silently stay on the legacy string wire.
|
|
7328
|
+
// Re-arm after a grace period so negotiation can retry, bounded per session
|
|
7329
|
+
// so a peer that advertises binary but never answers doesn't get pinged
|
|
7330
|
+
// forever (later caps/version triggers may still retry explicitly).
|
|
7331
|
+
wireHandshakeAttempts++
|
|
7332
|
+
val scheduledGeneration = wireSessionGeneration
|
|
7333
|
+
handler.postDelayed(
|
|
7334
|
+
{
|
|
7335
|
+
if (scheduledGeneration == wireSessionGeneration &&
|
|
7336
|
+
wireHandshakeQueued &&
|
|
7337
|
+
peerWireProtocolVersion < BleWireProtocol.PROTOCOL_V2
|
|
7338
|
+
) {
|
|
7339
|
+
Bridge.log(
|
|
7340
|
+
"LIVE: BLE wire v2 handshake unanswered after " +
|
|
7341
|
+
WIRE_HANDSHAKE_RETRY_GRACE_MS +
|
|
7342
|
+
"ms (attempt " +
|
|
7343
|
+
wireHandshakeAttempts +
|
|
7344
|
+
"/" +
|
|
7345
|
+
WIRE_HANDSHAKE_MAX_ATTEMPTS +
|
|
7346
|
+
") - re-arming"
|
|
7347
|
+
)
|
|
7348
|
+
wireHandshakeQueued = false
|
|
7349
|
+
if (wireHandshakeAttempts < WIRE_HANDSHAKE_MAX_ATTEMPTS) {
|
|
7350
|
+
maybeSendWireHandshake()
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
},
|
|
7354
|
+
WIRE_HANDSHAKE_RETRY_GRACE_MS
|
|
7355
|
+
)
|
|
7237
7356
|
} catch (e: Exception) {
|
|
7238
7357
|
Log.e(TAG, "Failed to send wire handshake", e)
|
|
7239
7358
|
}
|
|
@@ -7243,12 +7362,17 @@ class MentraLive : SGCManager() {
|
|
|
7243
7362
|
peerWireProtocolVersion = BleWireProtocol.PROTOCOL_V2
|
|
7244
7363
|
useBinaryWireProtocol = true
|
|
7245
7364
|
wireHandshakeQueued = false
|
|
7365
|
+
wireHandshakeAttempts = 0
|
|
7246
7366
|
peerK900Le = true
|
|
7247
7367
|
BleJsonCompact.markSessionConnected(System.currentTimeMillis())
|
|
7248
7368
|
Bridge.log(logMessage)
|
|
7249
7369
|
}
|
|
7250
7370
|
|
|
7251
7371
|
private fun handlePeerWireHandshake() {
|
|
7372
|
+
if (wireHandshakeSentGeneration != wireSessionGeneration) {
|
|
7373
|
+
Bridge.log("LIVE: Ignoring wire v2 handshake reply from a previous session epoch")
|
|
7374
|
+
return
|
|
7375
|
+
}
|
|
7252
7376
|
activateBinaryWireV2Session("LIVE: Peer confirmed BLE wire protocol v2")
|
|
7253
7377
|
}
|
|
7254
7378
|
|
|
@@ -7947,7 +8071,7 @@ class MentraLive : SGCManager() {
|
|
|
7947
8071
|
val type = json.optString("type", "")
|
|
7948
8072
|
|
|
7949
8073
|
when (type) {
|
|
7950
|
-
"request_wifi_scan" -> requestWifiScan()
|
|
8074
|
+
"request_wifi_scan" -> requestWifiScan(null)
|
|
7951
8075
|
"rgb_led_control_on", "rgb_led_control_off" -> {
|
|
7952
8076
|
// Forward LED control commands directly to glasses via BLE
|
|
7953
8077
|
Log.d(TAG, "💡 Forwarding LED control command to glasses: " + type)
|
|
@@ -8826,7 +8950,7 @@ class MentraLive : SGCManager() {
|
|
|
8826
8950
|
|
|
8827
8951
|
/** Send camera FOV setting to glasses (K900 / Mentra Live). */
|
|
8828
8952
|
override fun sendCameraFovSetting() {
|
|
8829
|
-
var fov =
|
|
8953
|
+
var fov = 102
|
|
8830
8954
|
var roiPosition = 0
|
|
8831
8955
|
try {
|
|
8832
8956
|
val raw = DeviceStore.get("bluetooth", "camera_fov")
|
|
@@ -8868,6 +8992,35 @@ class MentraLive : SGCManager() {
|
|
|
8868
8992
|
}
|
|
8869
8993
|
}
|
|
8870
8994
|
|
|
8995
|
+
fun sendCameraFovOverride(
|
|
8996
|
+
requestId: String,
|
|
8997
|
+
leaseId: String,
|
|
8998
|
+
fov: Int,
|
|
8999
|
+
roiPosition: Int,
|
|
9000
|
+
ttlMs: Int,
|
|
9001
|
+
) {
|
|
9002
|
+
val json = JSONObject()
|
|
9003
|
+
json.put("type", "camera_fov_override")
|
|
9004
|
+
json.put("request_id", requestId)
|
|
9005
|
+
json.put(
|
|
9006
|
+
"params",
|
|
9007
|
+
JSONObject()
|
|
9008
|
+
.put("lease_id", leaseId)
|
|
9009
|
+
.put("fov", fov)
|
|
9010
|
+
.put("roi_position", roiPosition)
|
|
9011
|
+
.put("ttl_ms", ttlMs),
|
|
9012
|
+
)
|
|
9013
|
+
sendJson(json, true)
|
|
9014
|
+
}
|
|
9015
|
+
|
|
9016
|
+
fun releaseCameraFovOverride(requestId: String, leaseId: String) {
|
|
9017
|
+
val json = JSONObject()
|
|
9018
|
+
json.put("type", "camera_fov_override_release")
|
|
9019
|
+
json.put("request_id", requestId)
|
|
9020
|
+
json.put("params", JSONObject().put("lease_id", leaseId))
|
|
9021
|
+
sendJson(json, true)
|
|
9022
|
+
}
|
|
9023
|
+
|
|
8871
9024
|
/**
|
|
8872
9025
|
* Send camera tuning config (ANR / gain) to the glasses via the {@code camera_tuning_config}
|
|
8873
9026
|
* command. The ASG client relays this as a {@code camconfig} broadcast to the camera HAL.
|
|
@@ -334,7 +334,7 @@ class MentraNex : SGCManager() {
|
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
// Network Management
|
|
337
|
-
override fun requestWifiScan () { Bridge.log("Nex: requestWifiScan operation not supported") }
|
|
337
|
+
override fun requestWifiScan (scanId: String?) { Bridge.log("Nex: requestWifiScan operation not supported") }
|
|
338
338
|
override fun sendWifiCredentials(ssid: String, password: String) { Bridge.log("Nex: sendWifiCredentials operation not supported") }
|
|
339
339
|
override fun forgetWifiNetwork(ssid: String) { }
|
|
340
340
|
override fun sendHotspotState(enabled: Boolean) { Bridge.log("Nex: sendHotspotState operation not supported") }
|
|
@@ -1427,7 +1427,7 @@ class Nimo : SGCManager() {
|
|
|
1427
1427
|
|
|
1428
1428
|
// ---------- SGCManager: Network (no WiFi) ----------
|
|
1429
1429
|
|
|
1430
|
-
override fun requestWifiScan() {}
|
|
1430
|
+
override fun requestWifiScan(scanId: String?) {}
|
|
1431
1431
|
override fun sendWifiCredentials(ssid: String, password: String) {}
|
|
1432
1432
|
override fun forgetWifiNetwork(ssid: String) {}
|
|
1433
1433
|
override fun sendHotspotState(enabled: Boolean) {}
|
|
@@ -46,6 +46,8 @@ abstract class SGCManager {
|
|
|
46
46
|
|
|
47
47
|
// Audio Control
|
|
48
48
|
abstract fun setMicEnabled(enabled: Boolean)
|
|
49
|
+
open val isMicSuspendedForAudio: Boolean
|
|
50
|
+
get() = false
|
|
49
51
|
abstract fun sortMicRanking(list: MutableList<String>): MutableList<String>
|
|
50
52
|
|
|
51
53
|
// Camera & Media
|
|
@@ -296,12 +298,12 @@ abstract class SGCManager {
|
|
|
296
298
|
abstract fun dbg2()
|
|
297
299
|
|
|
298
300
|
// Network Management
|
|
299
|
-
abstract fun requestWifiScan()
|
|
301
|
+
abstract fun requestWifiScan(scanId: String?)
|
|
300
302
|
abstract fun sendWifiCredentials(ssid: String, password: String)
|
|
301
303
|
abstract fun forgetWifiNetwork(ssid: String)
|
|
302
304
|
abstract fun sendHotspotState(enabled: Boolean)
|
|
303
305
|
|
|
304
|
-
/** Set glasses system clock (Mentra Live
|
|
306
|
+
/** Set glasses system clock (Mentra Live and G2; no-op on other devices). */
|
|
305
307
|
open fun sendSetSystemTime(timestampMs: Long) {
|
|
306
308
|
Bridge.log("SGC: sendSetSystemTime not supported on $type")
|
|
307
309
|
}
|
|
@@ -66,15 +66,31 @@ sealed interface WifiStatus {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
data class WifiStatusEvent(
|
|
69
|
+
data class WifiStatusEvent @JvmOverloads constructor(
|
|
70
70
|
val status: WifiStatus,
|
|
71
|
+
/**
|
|
72
|
+
* Glasses-reported provisioning failure reason when THIS event is the verdict of a
|
|
73
|
+
* failed connect attempt; null for routine link-state updates. An attempt property,
|
|
74
|
+
* not a link property — which is why it lives on the event and not on [WifiStatus]:
|
|
75
|
+
* "connect_timeout" arrives with a disconnected status (never associated), while
|
|
76
|
+
* "connected_to_other_network" arrives with a *connected* status — the attempt
|
|
77
|
+
* failed and Android's auto-join left the glasses on (or returned them to) a
|
|
78
|
+
* different SSID than requested, so the link is genuinely up while the request
|
|
79
|
+
* genuinely failed. Sent by ASG client builds that include the WiFi error
|
|
80
|
+
* surfacing (v40+); older glasses never set it.
|
|
81
|
+
*/
|
|
82
|
+
val error: String? = null,
|
|
71
83
|
) {
|
|
72
|
-
internal constructor(values: Map<String, Any>) : this(
|
|
84
|
+
internal constructor(values: Map<String, Any>) : this(
|
|
85
|
+
WifiStatus.fromMap(values) ?: WifiStatus.Disconnected,
|
|
86
|
+
stringValue(values, "error")?.takeIf { it.isNotEmpty() },
|
|
87
|
+
)
|
|
73
88
|
internal constructor(connected: Boolean, ssid: String?, localIp: String?) : this(
|
|
74
89
|
WifiStatus.fromStoreFields(connected, ssid, localIp) ?: WifiStatus.Disconnected
|
|
75
90
|
)
|
|
76
91
|
|
|
77
|
-
val values: Map<String, Any>
|
|
92
|
+
val values: Map<String, Any>
|
|
93
|
+
get() = status.toEventMap() + (error?.let { mapOf("error" to it) } ?: emptyMap())
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
sealed interface HotspotStatus {
|
|
@@ -436,12 +436,25 @@ sealed interface StreamStatus {
|
|
|
436
436
|
data class StreamStatusEvent(
|
|
437
437
|
val status: StreamStatus,
|
|
438
438
|
) {
|
|
439
|
-
|
|
439
|
+
// True when the glasses will retry the failed publisher themselves
|
|
440
|
+
// (emitting side lands in PR #3488); absent on older firmware and on
|
|
441
|
+
// events not parsed from a glasses status map. Carried here instead of
|
|
442
|
+
// on StreamStatus.Error so the public Error shape stays unchanged.
|
|
443
|
+
var willRetry: Boolean? = null
|
|
444
|
+
private set
|
|
445
|
+
|
|
446
|
+
constructor(values: Map<String, Any>) : this(StreamStatus.fromMap(values)) {
|
|
447
|
+
willRetry = boolValue(values, "willRetry")
|
|
448
|
+
}
|
|
440
449
|
|
|
441
450
|
val state: StreamState get() = status.state
|
|
442
451
|
val streamId: String? get() = status.streamId
|
|
443
452
|
val resolvedConfig: StreamResolvedConfig? get() = status.resolvedConfig
|
|
444
|
-
val values: Map<String, Any>
|
|
453
|
+
val values: Map<String, Any>
|
|
454
|
+
get() = buildMap {
|
|
455
|
+
putAll(status.toEventMap())
|
|
456
|
+
willRetry?.let { put("willRetry", it) }
|
|
457
|
+
}
|
|
445
458
|
}
|
|
446
459
|
|
|
447
460
|
data class KeepAliveAckEvent(
|