@mentra/bluetooth-sdk 3.2.0-dev.261 → 3.2.0-dev.262
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/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/GlassesLinkDiagnostics.kt +96 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +37 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +5 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesLinkDiagnosticsTest.kt +79 -0
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +18 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +1 -0
- package/ios/Source/sgcs/MentraLive.swift +18 -1
- package/ios/Source/sgcs/SGCManager.swift +4 -0
- package/package.json +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -16,6 +16,7 @@ import com.mentra.bluetoothsdk.services.ForegroundService
|
|
|
16
16
|
import com.mentra.bluetoothsdk.services.PhoneMic
|
|
17
17
|
import com.mentra.bluetoothsdk.sgcs.Ar99
|
|
18
18
|
import com.mentra.bluetoothsdk.sgcs.G1
|
|
19
|
+
import com.mentra.bluetoothsdk.sgcs.GlassesLinkDiagnostics
|
|
19
20
|
import com.mentra.bluetoothsdk.sgcs.G2
|
|
20
21
|
import com.mentra.bluetoothsdk.sgcs.SceneElement
|
|
21
22
|
import com.mentra.bluetoothsdk.sgcs.SceneFrame
|
|
@@ -879,6 +880,7 @@ class DeviceManager {
|
|
|
879
880
|
|
|
880
881
|
private fun micDbgLc3Window(lc3: ByteArray, seq: Int?, pcm: ByteArray) {
|
|
881
882
|
val now = System.currentTimeMillis()
|
|
883
|
+
GlassesLinkDiagnostics.recordMicPacket(now)
|
|
882
884
|
if (micDbgWindowStart == 0L) micDbgWindowStart = now
|
|
883
885
|
micDbgPkts++
|
|
884
886
|
micDbgLc3Bytes += lc3.size
|
|
@@ -3,9 +3,9 @@ package com.mentra.bluetoothsdk
|
|
|
3
3
|
/** Generated by release CI. Do not edit in a release checkout. */
|
|
4
4
|
internal object GeneratedReleaseMetadata {
|
|
5
5
|
const val FAMILY_BASE_VERSION: String = "3.2.0"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "3.2.0-dev.
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.2.0-dev.
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
9
|
-
const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "3.2.0-dev.262"
|
|
7
|
+
const val RELEASE_SET_ID: String = "mentra-3.2.0-dev.262"
|
|
8
|
+
const val SOURCE_COMMIT: String = "545ed70c7afa46ca09b895d2dcd651b45510dbb5"
|
|
9
|
+
const val OTA_MANIFEST_URL: String = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.262.json"
|
|
10
|
+
const val OTA_MANIFEST_SHA256: String = "d1c4e63ae2c1cd8de479d4bbc57219d2b85e904c26a491441265f5182220125f"
|
|
11
11
|
}
|
|
@@ -89,6 +89,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
89
89
|
activityLifecycleCallbacksRegistered = true
|
|
90
90
|
}
|
|
91
91
|
bridgeEventSinkId = Bridge.addEventSink { eventName, data -> dispatchBridgeEvent(eventName, data) }
|
|
92
|
+
// DeviceManager/MentraLive outlive Expo module remounts. glasses_ready already
|
|
93
|
+
// ran on this BLE session; replay it so startStream does not claim old firmware.
|
|
94
|
+
deviceManager.sgc?.replayStreamControlReady()
|
|
92
95
|
// Baseline the analytics connection state before subscribing to the store:
|
|
93
96
|
// store updates invoke listeners synchronously on the updating thread, so a
|
|
94
97
|
// connected status observed before the baseline would be reported as a fresh
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeps the handful of measurements that explain why a Mentra Live BLE link died, and renders them
|
|
5
|
+
* as one line when it does.
|
|
6
|
+
*
|
|
7
|
+
* <p>A BES watchdog reboot reaches the phone only as `GATT connection error: status=8`, which says
|
|
8
|
+
* nothing about whether the link faded, went abruptly quiet, or was dropped while healthy. The
|
|
9
|
+
* answer is always in the seconds before: how far the RSSI had fallen, how long since the glasses
|
|
10
|
+
* last said anything, and how long since a mic packet arrived. Those exist today only as thousands
|
|
11
|
+
* of separate `RSSI:` and `MICDBG-RX` lines that have to be correlated by hand after the fact.
|
|
12
|
+
*
|
|
13
|
+
* <p>Phone-side, because the glasses' own log is the artifact most likely to be missing or to have
|
|
14
|
+
* lost the window: it uploads only a 600-line logcat tail, which the post-reboot reconnect fills.
|
|
15
|
+
*/
|
|
16
|
+
object GlassesLinkDiagnostics {
|
|
17
|
+
|
|
18
|
+
/** Grep marker. Never build this by concatenation — a single grep must be exhaustive. */
|
|
19
|
+
const val MARKER = "LINK_DEATH"
|
|
20
|
+
|
|
21
|
+
private const val RSSI_HISTORY = 12
|
|
22
|
+
|
|
23
|
+
private val rssiHistory = ArrayDeque<Sample>(RSSI_HISTORY)
|
|
24
|
+
private var lastInboundMs: Long? = null
|
|
25
|
+
private var lastMicPacketMs: Long? = null
|
|
26
|
+
private var sessionStartMs: Long? = null
|
|
27
|
+
|
|
28
|
+
private data class Sample(val atMs: Long, val rssi: Int)
|
|
29
|
+
|
|
30
|
+
/** Called when a BLE session is established, so ages are measured against this link only. */
|
|
31
|
+
@Synchronized
|
|
32
|
+
fun onSessionStart(nowMs: Long = System.currentTimeMillis()) {
|
|
33
|
+
rssiHistory.clear()
|
|
34
|
+
lastInboundMs = null
|
|
35
|
+
lastMicPacketMs = null
|
|
36
|
+
sessionStartMs = nowMs
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Called for every JSON message the glasses send. */
|
|
40
|
+
@Synchronized
|
|
41
|
+
fun recordInbound(nowMs: Long = System.currentTimeMillis()) {
|
|
42
|
+
lastInboundMs = nowMs
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Called for every LC3 mic packet. Mic gaps lead the control-channel failure by seconds. */
|
|
46
|
+
@Synchronized
|
|
47
|
+
fun recordMicPacket(nowMs: Long = System.currentTimeMillis()) {
|
|
48
|
+
lastMicPacketMs = nowMs
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Synchronized
|
|
52
|
+
fun recordRssi(rssi: Int, nowMs: Long = System.currentTimeMillis()) {
|
|
53
|
+
while (rssiHistory.size >= RSSI_HISTORY) {
|
|
54
|
+
rssiHistory.removeFirst()
|
|
55
|
+
}
|
|
56
|
+
rssiHistory.addLast(Sample(nowMs, rssi))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One-line summary of the link's final seconds.
|
|
61
|
+
*
|
|
62
|
+
* @param reason the transition being explained, e.g. `gatt_error` or `gatt_disconnected`
|
|
63
|
+
* @param status the GATT status code, or null when the stack did not report one
|
|
64
|
+
*/
|
|
65
|
+
@Synchronized
|
|
66
|
+
fun summary(
|
|
67
|
+
reason: String,
|
|
68
|
+
status: Int? = null,
|
|
69
|
+
streamActive: Boolean? = null,
|
|
70
|
+
nowMs: Long = System.currentTimeMillis()
|
|
71
|
+
): String {
|
|
72
|
+
val parts = mutableListOf<String>()
|
|
73
|
+
parts.add(MARKER)
|
|
74
|
+
parts.add("reason=$reason")
|
|
75
|
+
if (status != null) parts.add("status=$status")
|
|
76
|
+
parts.add("sessionMs=${age(sessionStartMs, nowMs)}")
|
|
77
|
+
parts.add("msSinceInbound=${age(lastInboundMs, nowMs)}")
|
|
78
|
+
parts.add("msSinceMicPacket=${age(lastMicPacketMs, nowMs)}")
|
|
79
|
+
parts.add("lastRssi=${rssiHistory.lastOrNull()?.rssi ?: UNKNOWN}")
|
|
80
|
+
parts.add("minRssi=${rssiHistory.minOfOrNull { it.rssi } ?: UNKNOWN}")
|
|
81
|
+
parts.add("rssiTrend=${trend()}")
|
|
82
|
+
if (streamActive != null) parts.add("streamActive=$streamActive")
|
|
83
|
+
return parts.joinToString(" ")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Oldest-to-newest RSSI so a fade is distinguishable from an abrupt drop at full signal. */
|
|
87
|
+
private fun trend(): String =
|
|
88
|
+
if (rssiHistory.isEmpty()) "none"
|
|
89
|
+
else rssiHistory.joinToString(",") { it.rssi.toString() }
|
|
90
|
+
|
|
91
|
+
/** Reports [UNKNOWN] for a signal never seen on this link, rather than an age from epoch. */
|
|
92
|
+
private fun age(sinceMs: Long?, nowMs: Long): Long =
|
|
93
|
+
if (sinceMs == null) UNKNOWN.toLong() else nowMs - sinceMs
|
|
94
|
+
|
|
95
|
+
private const val UNKNOWN = -1
|
|
96
|
+
}
|
|
@@ -537,6 +537,9 @@ class MentraLive : SGCManager() {
|
|
|
537
537
|
// cannot signal a restart - a CHANGED (or newly appearing) sid is the restart signal.
|
|
538
538
|
// Null = no sid observed this BLE session (legacy glasses, or none seen yet).
|
|
539
539
|
private var glassesSessionId: String? = null
|
|
540
|
+
// Last `glasses_ready.streamControlVersion`. Survives BluetoothSdkModule remounts
|
|
541
|
+
// so a new MentraBluetoothSdk can seed StreamSessionState without another ready.
|
|
542
|
+
private var streamControlVersion = 0
|
|
540
543
|
// True once a glasses_ready completed on THIS physical BLE session; resets only with
|
|
541
544
|
// the physical connection (never on heartbeat readiness flaps), so a first-seen sid
|
|
542
545
|
// after an upgrade OTA is always detected as a restart.
|
|
@@ -1673,6 +1676,7 @@ class MentraLive : SGCManager() {
|
|
|
1673
1676
|
connectedDevice = null
|
|
1674
1677
|
glassesReady = false
|
|
1675
1678
|
glassesSessionId = null
|
|
1679
|
+
streamControlVersion = 0
|
|
1676
1680
|
readinessCompletedThisBleSession = false
|
|
1677
1681
|
glassesReadyReceived = false
|
|
1678
1682
|
ctkdInitiatedThisGattSession = false
|
|
@@ -1947,6 +1951,7 @@ class MentraLive : SGCManager() {
|
|
|
1947
1951
|
connectedDevice = null
|
|
1948
1952
|
glassesReady = false
|
|
1949
1953
|
glassesSessionId = null
|
|
1954
|
+
streamControlVersion = 0
|
|
1950
1955
|
readinessCompletedThisBleSession = false
|
|
1951
1956
|
glassesReadyReceived = false
|
|
1952
1957
|
ctkdInitiatedThisGattSession = false
|
|
@@ -2328,6 +2333,8 @@ class MentraLive : SGCManager() {
|
|
|
2328
2333
|
// Discover services
|
|
2329
2334
|
gatt.discoverServices()
|
|
2330
2335
|
|
|
2336
|
+
GlassesLinkDiagnostics.onSessionStart()
|
|
2337
|
+
|
|
2331
2338
|
// Do NOT reset reconnectAttempts here — ephemeral GATT CONNECTED
|
|
2332
2339
|
// followed by status 19 was zeroing the counter every ~1s and
|
|
2333
2340
|
// preventing exponential backoff. Reset at ble_chars_ready instead.
|
|
@@ -2340,6 +2347,13 @@ class MentraLive : SGCManager() {
|
|
|
2340
2347
|
Bridge.log(
|
|
2341
2348
|
"LIVE: 🔌 ⚠️ Disconnected from GATT server - Will attempt reconnection"
|
|
2342
2349
|
)
|
|
2350
|
+
Bridge.log(
|
|
2351
|
+
"LIVE: " +
|
|
2352
|
+
GlassesLinkDiagnostics.summary(
|
|
2353
|
+
"gatt_disconnected",
|
|
2354
|
+
status
|
|
2355
|
+
)
|
|
2356
|
+
)
|
|
2343
2357
|
endPairingTiming(
|
|
2344
2358
|
"gatt_disconnected",
|
|
2345
2359
|
"queueSize=${sendQueue.size}"
|
|
@@ -2350,6 +2364,7 @@ class MentraLive : SGCManager() {
|
|
|
2350
2364
|
connectedDevice = null
|
|
2351
2365
|
glassesReady = false // Reset ready state on disconnect
|
|
2352
2366
|
glassesSessionId = null // Fresh BLE session starts with no sid known
|
|
2367
|
+
streamControlVersion = 0
|
|
2353
2368
|
readinessCompletedThisBleSession = false
|
|
2354
2369
|
|
|
2355
2370
|
// Reset audio pairing flags
|
|
@@ -2401,6 +2416,9 @@ class MentraLive : SGCManager() {
|
|
|
2401
2416
|
status +
|
|
2402
2417
|
") - Will retry reconnection"
|
|
2403
2418
|
)
|
|
2419
|
+
Bridge.log(
|
|
2420
|
+
"LIVE: " + GlassesLinkDiagnostics.summary("gatt_error", status)
|
|
2421
|
+
)
|
|
2404
2422
|
endPairingTiming(
|
|
2405
2423
|
"gatt_error",
|
|
2406
2424
|
"status=$status queueSize=${sendQueue.size}"
|
|
@@ -2409,6 +2427,7 @@ class MentraLive : SGCManager() {
|
|
|
2409
2427
|
isConnecting = false
|
|
2410
2428
|
glassesReady = false
|
|
2411
2429
|
glassesSessionId = null
|
|
2430
|
+
streamControlVersion = 0
|
|
2412
2431
|
readinessCompletedThisBleSession = false
|
|
2413
2432
|
glassesReadyReceived = false
|
|
2414
2433
|
audioConnected = false
|
|
@@ -3970,6 +3989,7 @@ class MentraLive : SGCManager() {
|
|
|
3970
3989
|
Log.d(TAG, "LIVE: Got some JSON from glasses: " + json.toString())
|
|
3971
3990
|
}
|
|
3972
3991
|
BleTraceLogger.logJson("glasses_to_phone", "sdk_ble_event", json, null)
|
|
3992
|
+
GlassesLinkDiagnostics.recordInbound()
|
|
3973
3993
|
|
|
3974
3994
|
if (MessageChunker.isChunkedMessage(json)) {
|
|
3975
3995
|
processChunkedJsonMessage(json)
|
|
@@ -4571,6 +4591,7 @@ class MentraLive : SGCManager() {
|
|
|
4571
4591
|
// already runs this full remote-reset flow, so recording (not re-triggering)
|
|
4572
4592
|
// is correct here; version_info detection covers the restart case.
|
|
4573
4593
|
glassesSessionId = json.optString("sid", "").takeIf { it.isNotEmpty() }
|
|
4594
|
+
streamControlVersion = json.optInt("streamControlVersion", 0)
|
|
4574
4595
|
Bridge.sendTypedMessage(
|
|
4575
4596
|
"wifi_protocol_session_ready",
|
|
4576
4597
|
mapOf("sid" to (glassesSessionId ?: "")),
|
|
@@ -4578,7 +4599,7 @@ class MentraLive : SGCManager() {
|
|
|
4578
4599
|
readinessCompletedThisBleSession = true
|
|
4579
4600
|
Bridge.sendTypedMessage("stream_control_ready", mapOf(
|
|
4580
4601
|
"sid" to json.optString("sid", ""),
|
|
4581
|
-
"streamControlVersion" to
|
|
4602
|
+
"streamControlVersion" to streamControlVersion,
|
|
4582
4603
|
))
|
|
4583
4604
|
|
|
4584
4605
|
// Set the ready flag to stop any future readiness checks
|
|
@@ -5519,6 +5540,7 @@ class MentraLive : SGCManager() {
|
|
|
5519
5540
|
glassesReady = false
|
|
5520
5541
|
glassesReadyReceived = false
|
|
5521
5542
|
glassesSessionId = null
|
|
5543
|
+
streamControlVersion = 0
|
|
5522
5544
|
readinessCompletedThisBleSession = false
|
|
5523
5545
|
}
|
|
5524
5546
|
"sr_adota" -> {
|
|
@@ -6138,6 +6160,7 @@ class MentraLive : SGCManager() {
|
|
|
6138
6160
|
val now = System.currentTimeMillis()
|
|
6139
6161
|
DeviceStore.apply("glasses", "signalStrength", rssi)
|
|
6140
6162
|
DeviceStore.apply("glasses", "signalStrengthUpdatedAt", now)
|
|
6163
|
+
GlassesLinkDiagnostics.recordRssi(rssi, now)
|
|
6141
6164
|
Bridge.log("LIVE: 📶 RSSI: " + rssi + " dBm")
|
|
6142
6165
|
}
|
|
6143
6166
|
|
|
@@ -6821,6 +6844,19 @@ class MentraLive : SGCManager() {
|
|
|
6821
6844
|
sendJson(json, true)
|
|
6822
6845
|
}
|
|
6823
6846
|
|
|
6847
|
+
override fun replayStreamControlReady() {
|
|
6848
|
+
val sid = glassesSessionId ?: return
|
|
6849
|
+
if (streamControlVersion != 1) return
|
|
6850
|
+
Bridge.log("LIVE: Replaying stream_control_ready sid=$sid version=$streamControlVersion")
|
|
6851
|
+
Bridge.sendTypedMessage(
|
|
6852
|
+
"stream_control_ready",
|
|
6853
|
+
mapOf(
|
|
6854
|
+
"sid" to sid,
|
|
6855
|
+
"streamControlVersion" to streamControlVersion,
|
|
6856
|
+
),
|
|
6857
|
+
)
|
|
6858
|
+
}
|
|
6859
|
+
|
|
6824
6860
|
override fun startStream(message: MutableMap<String, Any>) {
|
|
6825
6861
|
Bridge.log("LIVE: Starting RTMP stream")
|
|
6826
6862
|
|
|
@@ -54,6 +54,11 @@ abstract class SGCManager {
|
|
|
54
54
|
abstract fun requestPhoto(request: PhotoRequest)
|
|
55
55
|
abstract fun startStream(message: MutableMap<String, Any>)
|
|
56
56
|
abstract fun stopStream()
|
|
57
|
+
/**
|
|
58
|
+
* Re-advertise glasses-owned stream control after the phone SDK remounts.
|
|
59
|
+
* Mentra Live overrides this; other devices stay on the no-op.
|
|
60
|
+
*/
|
|
61
|
+
open fun replayStreamControlReady() {}
|
|
57
62
|
abstract fun sendStreamKeepAlive(message: MutableMap<String, Any>)
|
|
58
63
|
abstract fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean)
|
|
59
64
|
open fun queryVideoRecordingStatus(requestId: String) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import com.mentra.bluetoothsdk.sgcs.GlassesLinkDiagnostics
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Before
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
class GlassesLinkDiagnosticsTest {
|
|
9
|
+
|
|
10
|
+
@Before
|
|
11
|
+
fun resetSession() {
|
|
12
|
+
GlassesLinkDiagnostics.onSessionStart(nowMs = 0L)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Test
|
|
16
|
+
fun `summary separates a fading link from an abrupt drop`() {
|
|
17
|
+
// Walking out of range: RSSI decays, then the glasses go quiet before GATT notices.
|
|
18
|
+
GlassesLinkDiagnostics.recordRssi(-62, nowMs = 1_000L)
|
|
19
|
+
GlassesLinkDiagnostics.recordRssi(-78, nowMs = 2_000L)
|
|
20
|
+
GlassesLinkDiagnostics.recordRssi(-92, nowMs = 3_000L)
|
|
21
|
+
GlassesLinkDiagnostics.recordInbound(nowMs = 3_200L)
|
|
22
|
+
GlassesLinkDiagnostics.recordMicPacket(nowMs = 3_400L)
|
|
23
|
+
|
|
24
|
+
val summary =
|
|
25
|
+
GlassesLinkDiagnostics.summary(
|
|
26
|
+
reason = "gatt_error",
|
|
27
|
+
status = 8,
|
|
28
|
+
streamActive = true,
|
|
29
|
+
nowMs = 12_000L
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assertTrue(summary.startsWith("LINK_DEATH "))
|
|
33
|
+
assertTrue(summary.contains("reason=gatt_error"))
|
|
34
|
+
assertTrue(summary.contains("status=8"))
|
|
35
|
+
assertTrue(summary.contains("sessionMs=12000"))
|
|
36
|
+
assertTrue(summary.contains("msSinceInbound=8800"))
|
|
37
|
+
assertTrue(summary.contains("msSinceMicPacket=8600"))
|
|
38
|
+
assertTrue(summary.contains("lastRssi=-92"))
|
|
39
|
+
assertTrue(summary.contains("minRssi=-92"))
|
|
40
|
+
assertTrue(summary.contains("rssiTrend=-62,-78,-92"))
|
|
41
|
+
assertTrue(summary.contains("streamActive=true"))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun `summary marks never-seen signals rather than reporting a bogus age`() {
|
|
46
|
+
val summary = GlassesLinkDiagnostics.summary(reason = "gatt_disconnected", nowMs = 5_000L)
|
|
47
|
+
|
|
48
|
+
assertTrue(summary.contains("msSinceInbound=-1"))
|
|
49
|
+
assertTrue(summary.contains("msSinceMicPacket=-1"))
|
|
50
|
+
assertTrue(summary.contains("lastRssi=-1"))
|
|
51
|
+
assertTrue(summary.contains("rssiTrend=none"))
|
|
52
|
+
assertTrue(!summary.contains("status="))
|
|
53
|
+
assertTrue(!summary.contains("streamActive="))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Test
|
|
57
|
+
fun `rssi history is bounded and keeps the newest samples`() {
|
|
58
|
+
repeat(30) { i -> GlassesLinkDiagnostics.recordRssi(-40 - i, nowMs = i.toLong()) }
|
|
59
|
+
|
|
60
|
+
val summary = GlassesLinkDiagnostics.summary(reason = "gatt_error", nowMs = 100L)
|
|
61
|
+
|
|
62
|
+
assertTrue(summary.contains("lastRssi=-69"))
|
|
63
|
+
assertTrue(summary.contains("minRssi=-69"))
|
|
64
|
+
assertTrue(summary.contains("rssiTrend=-58,-59,-60,-61,-62,-63,-64,-65,-66,-67,-68,-69"))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test
|
|
68
|
+
fun `a new session forgets the previous link`() {
|
|
69
|
+
GlassesLinkDiagnostics.recordRssi(-91, nowMs = 1_000L)
|
|
70
|
+
GlassesLinkDiagnostics.recordInbound(nowMs = 1_000L)
|
|
71
|
+
|
|
72
|
+
GlassesLinkDiagnostics.onSessionStart(nowMs = 2_000L)
|
|
73
|
+
val summary = GlassesLinkDiagnostics.summary(reason = "gatt_error", nowMs = 2_500L)
|
|
74
|
+
|
|
75
|
+
assertTrue(summary.contains("sessionMs=500"))
|
|
76
|
+
assertTrue(summary.contains("rssiTrend=none"))
|
|
77
|
+
assertTrue(summary.contains("msSinceInbound=-1"))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "3.2.0",
|
|
4
|
-
"releaseIdentity": "3.2.0-dev.
|
|
5
|
-
"releaseSetId": "mentra-3.2.0-dev.
|
|
6
|
-
"sourceCommit": "
|
|
7
|
-
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
8
|
-
"otaManifestSha256": "
|
|
4
|
+
"releaseIdentity": "3.2.0-dev.262",
|
|
5
|
+
"releaseSetId": "mentra-3.2.0-dev.262",
|
|
6
|
+
"sourceCommit": "545ed70c7afa46ca09b895d2dcd651b45510dbb5",
|
|
7
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.262.json",
|
|
8
|
+
"otaManifestSha256": "d1c4e63ae2c1cd8de479d4bbc57219d2b85e904c26a491441265f5182220125f"
|
|
9
9
|
});
|
|
10
10
|
//# sourceMappingURL=releaseMetadata.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,wHAAwH;IAC1I,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.2.0\",\n \"releaseIdentity\": \"3.2.0-dev.
|
|
1
|
+
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,sBAAsB;IACtC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,wHAAwH;IAC1I,mBAAmB,EAAE,kEAAkE;CACxF,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"3.2.0\",\n \"releaseIdentity\": \"3.2.0-dev.262\",\n \"releaseSetId\": \"mentra-3.2.0-dev.262\",\n \"sourceCommit\": \"545ed70c7afa46ca09b895d2dcd651b45510dbb5\",\n \"otaManifestUrl\": \"https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.262.json\",\n \"otaManifestSha256\": \"d1c4e63ae2c1cd8de479d4bbc57219d2b85e904c26a491441265f5182220125f\"\n})\n"]}
|
|
@@ -2,6 +2,16 @@ import CryptoKit
|
|
|
2
2
|
import ExpoModulesCore
|
|
3
3
|
import Foundation
|
|
4
4
|
|
|
5
|
+
/// Carry the SDK's own error code across the Expo bridge.
|
|
6
|
+
///
|
|
7
|
+
/// `BluetoothSdkError` is a plain `Error`, not an Expo `Exception`, so `Promise.reject`
|
|
8
|
+
/// wraps it in `UnexpectedException` and every code JS branches on arrives as
|
|
9
|
+
/// `ERR_UNEXPECTED`. Android keeps the code because `DecoratedException` forwards
|
|
10
|
+
/// `cause.code`; this is the iOS half of that guarantee.
|
|
11
|
+
private func codedBridgeError(_ error: BluetoothSdkError) -> Exception {
|
|
12
|
+
Exception(name: error.code, description: error.message, code: error.code)
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
|
|
6
16
|
private var sdk: MentraBluetoothSDK?
|
|
7
17
|
|
|
@@ -344,9 +354,16 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
|
|
|
344
354
|
return try await sdk.forgetWifiNetwork(ssid: ssid).values
|
|
345
355
|
}
|
|
346
356
|
|
|
357
|
+
// The host retries a hotspot command that a Wi-Fi protocol session refresh
|
|
358
|
+
// cancelled, and branches on the code to tell that apart from a real failure,
|
|
359
|
+
// so this one has to reach JS with its code intact.
|
|
347
360
|
AsyncFunction("setHotspotState") { (enabled: Bool) in
|
|
348
361
|
let sdk = await MainActor.run { self.bluetoothSdk() }
|
|
349
|
-
|
|
362
|
+
do {
|
|
363
|
+
return try await sdk.setHotspotState(enabled: enabled).values
|
|
364
|
+
} catch let error as BluetoothSdkError {
|
|
365
|
+
throw codedBridgeError(error)
|
|
366
|
+
}
|
|
350
367
|
}
|
|
351
368
|
|
|
352
369
|
AsyncFunction("setWifiAdbState") { (enabled: Bool) in
|
|
@@ -11,7 +11,7 @@ enum BluetoothSdkDefaults {
|
|
|
11
11
|
static let voiceActivityDetectionEnabled = false
|
|
12
12
|
static let loudnessGateEnabled = false
|
|
13
13
|
private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
|
|
14
|
-
private static let swiftPackageSdkVersion = "3.2.0-dev.
|
|
14
|
+
private static let swiftPackageSdkVersion = "3.2.0-dev.262"
|
|
15
15
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
16
16
|
|
|
17
17
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "3.2.0"
|
|
6
|
-
static let releaseIdentity = "3.2.0-dev.
|
|
7
|
-
static let releaseSetId = "mentra-3.2.0-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
10
|
-
static let otaManifestSha256 = "
|
|
6
|
+
static let releaseIdentity = "3.2.0-dev.262"
|
|
7
|
+
static let releaseSetId = "mentra-3.2.0-dev.262"
|
|
8
|
+
static let sourceCommit = "545ed70c7afa46ca09b895d2dcd651b45510dbb5"
|
|
9
|
+
static let otaManifestUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.262.json"
|
|
10
|
+
static let otaManifestSha256 = "d1c4e63ae2c1cd8de479d4bbc57219d2b85e904c26a491441265f5182220125f"
|
|
11
11
|
}
|
|
@@ -328,6 +328,7 @@ public final class MentraBluetoothSDK {
|
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
}
|
|
331
|
+
DeviceManager.shared.sgc?.replayStreamControlReady()
|
|
331
332
|
storeListenerId = DeviceStore.shared.store.addListener { [weak self] category, changes in
|
|
332
333
|
Task { @MainActor [weak self] in
|
|
333
334
|
self?.dispatchStoreUpdate(category, changes)
|
|
@@ -1056,6 +1056,7 @@ extension MentraLive: CBCentralManagerDelegate {
|
|
|
1056
1056
|
self.fullyBooted = false
|
|
1057
1057
|
self.connected = false
|
|
1058
1058
|
self.glassesSessionId = nil // Fresh BLE session starts with no sid known
|
|
1059
|
+
self.streamControlVersion = 0
|
|
1059
1060
|
self.readinessCompletedThisBleSession = false
|
|
1060
1061
|
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
1061
1062
|
self.rgbLedAuthorityClaimed = false
|
|
@@ -1702,6 +1703,9 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1702
1703
|
/// cannot signal a restart - a CHANGED (or newly appearing) sid is the restart signal.
|
|
1703
1704
|
/// Nil = no sid observed this BLE session (legacy glasses, or none seen yet).
|
|
1704
1705
|
private var glassesSessionId: String?
|
|
1706
|
+
/// Last `glasses_ready.streamControlVersion`. Survives SDK remounts so a new
|
|
1707
|
+
/// MentraBluetoothSDK can seed StreamSessionState without another ready.
|
|
1708
|
+
private var streamControlVersion = 0
|
|
1705
1709
|
// True once a glasses_ready completed on THIS physical BLE session. Unlike
|
|
1706
1710
|
// fullyBooted, this never flaps on sr_hrt ready=0 heartbeats — it only resets with
|
|
1707
1711
|
// the physical connection — so a first-seen sid after an upgrade OTA cannot be
|
|
@@ -1845,6 +1849,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
1845
1849
|
connected = false
|
|
1846
1850
|
fullyBooted = false
|
|
1847
1851
|
glassesSessionId = nil
|
|
1852
|
+
streamControlVersion = 0
|
|
1848
1853
|
readinessCompletedThisBleSession = false
|
|
1849
1854
|
rgbLedAuthorityClaimed = false
|
|
1850
1855
|
stopAllTimers()
|
|
@@ -2152,6 +2157,15 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2152
2157
|
)
|
|
2153
2158
|
}
|
|
2154
2159
|
|
|
2160
|
+
func replayStreamControlReady() {
|
|
2161
|
+
guard let sid = glassesSessionId, streamControlVersion == 1 else { return }
|
|
2162
|
+
Bridge.log("LIVE: Replaying stream_control_ready sid=\(sid) version=\(streamControlVersion)")
|
|
2163
|
+
Bridge.sendTypedMessage("stream_control_ready", body: [
|
|
2164
|
+
"sid": sid,
|
|
2165
|
+
"streamControlVersion": streamControlVersion,
|
|
2166
|
+
])
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2155
2169
|
func startStream(_ message: [String: Any]) {
|
|
2156
2170
|
Bridge.log("Starting stream")
|
|
2157
2171
|
var json = message
|
|
@@ -2522,6 +2536,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2522
2536
|
connected = false
|
|
2523
2537
|
fullyBooted = false
|
|
2524
2538
|
glassesSessionId = nil
|
|
2539
|
+
streamControlVersion = 0
|
|
2525
2540
|
readinessCompletedThisBleSession = false
|
|
2526
2541
|
readinessCompletedThisBleSession = false // Fresh BLE session starts with no sid known
|
|
2527
2542
|
readinessCompletedThisBleSession = false
|
|
@@ -2801,6 +2816,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2801
2816
|
// already runs this full remote-reset flow, so recording (not re-triggering)
|
|
2802
2817
|
// is correct here; version_info detection covers the restart case.
|
|
2803
2818
|
glassesSessionId = (json["sid"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2819
|
+
streamControlVersion = json["streamControlVersion"] as? Int ?? 0
|
|
2804
2820
|
Bridge.sendTypedMessage(
|
|
2805
2821
|
"wifi_protocol_session_ready",
|
|
2806
2822
|
body: ["sid": glassesSessionId ?? ""]
|
|
@@ -2808,7 +2824,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
2808
2824
|
readinessCompletedThisBleSession = true
|
|
2809
2825
|
Bridge.sendTypedMessage("stream_control_ready", body: [
|
|
2810
2826
|
"sid": json["sid"] as? String ?? "",
|
|
2811
|
-
"streamControlVersion":
|
|
2827
|
+
"streamControlVersion": streamControlVersion,
|
|
2812
2828
|
])
|
|
2813
2829
|
handleGlassesReady()
|
|
2814
2830
|
|
|
@@ -5653,6 +5669,7 @@ class MentraLive: NSObject, SGCManager {
|
|
|
5653
5669
|
fullyBooted = false
|
|
5654
5670
|
connected = false
|
|
5655
5671
|
glassesSessionId = nil
|
|
5672
|
+
streamControlVersion = 0
|
|
5656
5673
|
|
|
5657
5674
|
Bridge.log("LIVE: 🔄 Starting glasses SOC readiness check loop")
|
|
5658
5675
|
|
|
@@ -66,6 +66,8 @@ protocol SGCManager {
|
|
|
66
66
|
func requestPhoto(_ request: PhotoRequest)
|
|
67
67
|
func startStream(_ message: [String: Any])
|
|
68
68
|
func stopStream()
|
|
69
|
+
/// Re-advertise glasses-owned stream control after the phone SDK remounts.
|
|
70
|
+
func replayStreamControlReady()
|
|
69
71
|
func sendStreamKeepAlive(_ message: [String: Any])
|
|
70
72
|
func startVideoRecording(requestId: String, save: Bool, sound: Bool)
|
|
71
73
|
func queryVideoRecordingStatus(requestId: String)
|
|
@@ -238,6 +240,8 @@ extension SGCManager {
|
|
|
238
240
|
false
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
func replayStreamControlReady() {}
|
|
244
|
+
|
|
241
245
|
@discardableResult func requestSavedWifiNetworks(requestId _: String, sid _: String) -> Bool {
|
|
242
246
|
false
|
|
243
247
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/bluetooth-sdk",
|
|
3
|
-
"version": "3.2.0-dev.
|
|
3
|
+
"version": "3.2.0-dev.262",
|
|
4
4
|
"description": "SDK for communicating with smart glasses",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"registry": "https://registry.npmjs.org/"
|
|
115
115
|
},
|
|
116
116
|
"dependencies": {
|
|
117
|
-
"@mentra/cloud-protocol": "3.2.0-dev.
|
|
117
|
+
"@mentra/cloud-protocol": "3.2.0-dev.262"
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|
|
120
120
|
"@types/node": "^25.9.3",
|
|
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
|
|
|
12
12
|
export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "3.2.0",
|
|
15
|
-
"releaseIdentity": "3.2.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.2.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.2.0-dev.262",
|
|
16
|
+
"releaseSetId": "mentra-3.2.0-dev.262",
|
|
17
|
+
"sourceCommit": "545ed70c7afa46ca09b895d2dcd651b45510dbb5",
|
|
18
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.262.json",
|
|
19
|
+
"otaManifestSha256": "d1c4e63ae2c1cd8de479d4bbc57219d2b85e904c26a491441265f5182220125f"
|
|
20
20
|
})
|