@mentra/bluetooth-sdk 0.1.21-beta.2 → 0.1.21-beta.5
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 +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +17 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +222 -110
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
- package/build/BluetoothSdk.types.d.ts +2 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +17 -11
- package/ios/Source/DeviceStore.swift +3 -0
- package/ios/Source/sgcs/G2.swift +108 -41
- package/ios/Source/sgcs/MentraLive.swift +76 -61
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +2 -0
|
@@ -27,6 +27,7 @@ import android.os.Build
|
|
|
27
27
|
import android.os.Handler
|
|
28
28
|
import android.os.HandlerThread
|
|
29
29
|
import android.os.Looper
|
|
30
|
+
import android.os.SystemClock
|
|
30
31
|
import android.util.Log
|
|
31
32
|
import androidx.core.app.ActivityCompat
|
|
32
33
|
import com.mentra.bluetoothsdk.BluetoothSdkDefaults
|
|
@@ -83,6 +84,7 @@ import org.json.JSONObject
|
|
|
83
84
|
* display-related methods are stubbed out and will log a message but not actually display anything.
|
|
84
85
|
*/
|
|
85
86
|
class MentraLive : SGCManager() {
|
|
87
|
+
private val besOtaHeartbeatGuard = BesOtaHeartbeatGuard()
|
|
86
88
|
|
|
87
89
|
companion object {
|
|
88
90
|
private const val TAG = "Live"
|
|
@@ -310,7 +312,11 @@ class MentraLive : SGCManager() {
|
|
|
310
312
|
val queuedAtMs: Long
|
|
311
313
|
)
|
|
312
314
|
|
|
313
|
-
private data class QueuedBleWrite(
|
|
315
|
+
private data class QueuedBleWrite(
|
|
316
|
+
val data: ByteArray,
|
|
317
|
+
val trace: BleWriteTrace?,
|
|
318
|
+
val sessionGeneration: Long
|
|
319
|
+
)
|
|
314
320
|
|
|
315
321
|
private var sendQueue = ConcurrentLinkedQueue<QueuedBleWrite>()
|
|
316
322
|
private val bleWriteTraceSequence = AtomicLong(1)
|
|
@@ -612,6 +618,7 @@ class MentraLive : SGCManager() {
|
|
|
612
618
|
// Message tracking for reliable delivery
|
|
613
619
|
private val pendingMessages = ConcurrentHashMap<Long, PendingMessage>()
|
|
614
620
|
private val messageIdCounter = AtomicLong(1)
|
|
621
|
+
private val bleSessionGeneration = AtomicLong(0)
|
|
615
622
|
|
|
616
623
|
// Esoteric message ID generation
|
|
617
624
|
private val secureRandom = SecureRandom()
|
|
@@ -637,7 +644,8 @@ class MentraLive : SGCManager() {
|
|
|
637
644
|
val messageData: String,
|
|
638
645
|
val timestamp: Long,
|
|
639
646
|
val retryCount: Int,
|
|
640
|
-
val retryRunnable: Runnable
|
|
647
|
+
val retryRunnable: Runnable,
|
|
648
|
+
val sessionGeneration: Long
|
|
641
649
|
)
|
|
642
650
|
|
|
643
651
|
private var lc3AudioFileStream: FileOutputStream? = null
|
|
@@ -807,6 +815,7 @@ class MentraLive : SGCManager() {
|
|
|
807
815
|
if (isEqual) {
|
|
808
816
|
if (state == ConnTypes.DISCONNECTED) {
|
|
809
817
|
resetWireNegotiationState()
|
|
818
|
+
resetPendingAckState()
|
|
810
819
|
// Queued writes are session-bound: transmitting them into the NEXT session
|
|
811
820
|
// (e.g. a stale handshake whose reply activates v2 before the new session
|
|
812
821
|
// negotiated) is the bug class this clear removes. Higher layers re-send
|
|
@@ -817,12 +826,13 @@ class MentraLive : SGCManager() {
|
|
|
817
826
|
}
|
|
818
827
|
|
|
819
828
|
if (state == ConnTypes.DISCONNECTED) {
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
// clear
|
|
823
|
-
// clearing on CONNECTED would wipe
|
|
824
|
-
// glasses_ready (e.g. ASG restart) re-publishes CONNECTED.
|
|
829
|
+
// Device identity is session-bound. Clear it on disconnect so a previous pair's
|
|
830
|
+
// identifiers can never be associated with the next connection. Connect must NOT
|
|
831
|
+
// clear them: DeviceManager.disconnect already wipes them before any new connection,
|
|
832
|
+
// and clearing on CONNECTED would wipe still-valid identity mid-session when a
|
|
833
|
+
// same-link glasses_ready (e.g. ASG restart) re-publishes CONNECTED.
|
|
825
834
|
DeviceStore.apply("glasses", "serialNumber", "")
|
|
835
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", "")
|
|
826
836
|
}
|
|
827
837
|
|
|
828
838
|
// Actually update the connection state!
|
|
@@ -853,6 +863,7 @@ class MentraLive : SGCManager() {
|
|
|
853
863
|
DeviceStore.apply("glasses", "signalStrength", -1)
|
|
854
864
|
DeviceStore.apply("glasses", "signalStrengthUpdatedAt", 0L)
|
|
855
865
|
resetWireNegotiationState()
|
|
866
|
+
resetPendingAckState()
|
|
856
867
|
sendQueue.clear() // see the disconnect reset above: stale writes die with the session
|
|
857
868
|
|
|
858
869
|
// Drop OTA caches when fully disconnected — avoids leaking session/step state
|
|
@@ -873,6 +884,17 @@ class MentraLive : SGCManager() {
|
|
|
873
884
|
cachedOtaCurrentStep = 0
|
|
874
885
|
cachedOtaStepSequence = null
|
|
875
886
|
lastBesOtaProgress = -1
|
|
887
|
+
// The bounded UART-ownership lease is transport state, not session-cache state. Keep it
|
|
888
|
+
// through a BLE reconnect so heartbeats cannot resume while BES still owns the UART.
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private fun updateBesOtaHeartbeatGuard(stepType: String, phase: String, status: String) {
|
|
892
|
+
besOtaHeartbeatGuard.observeOtaStatus(
|
|
893
|
+
stepType,
|
|
894
|
+
phase,
|
|
895
|
+
status,
|
|
896
|
+
SystemClock.elapsedRealtime(),
|
|
897
|
+
)
|
|
876
898
|
}
|
|
877
899
|
|
|
878
900
|
protected fun setFontSizes() {
|
|
@@ -1175,6 +1197,26 @@ class MentraLive : SGCManager() {
|
|
|
1175
1197
|
}
|
|
1176
1198
|
}
|
|
1177
1199
|
|
|
1200
|
+
/**
|
|
1201
|
+
* Reject callbacks delivered by a GATT object from an earlier connection attempt. Without
|
|
1202
|
+
* this identity check, a late disconnect can tear down the active connection and clear its
|
|
1203
|
+
* session-bound send state.
|
|
1204
|
+
*/
|
|
1205
|
+
private fun isCurrentGattCallback(gatt: BluetoothGatt): Boolean {
|
|
1206
|
+
if (gatt === bluetoothGatt) {
|
|
1207
|
+
return true
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
Log.w(TAG, "🔌 Ignoring callback from stale BluetoothGatt")
|
|
1211
|
+
Bridge.log("LIVE: 🔌 Ignoring callback from stale BluetoothGatt session")
|
|
1212
|
+
try {
|
|
1213
|
+
gatt.close()
|
|
1214
|
+
} catch (e: Exception) {
|
|
1215
|
+
Log.w(TAG, "🔌 Failed to close stale BluetoothGatt: " + e)
|
|
1216
|
+
}
|
|
1217
|
+
return false
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1178
1220
|
/** Connect to a specific BLE device */
|
|
1179
1221
|
private fun connectToDevice(device: BluetoothDevice?) {
|
|
1180
1222
|
if (device == null) {
|
|
@@ -1497,6 +1539,10 @@ class MentraLive : SGCManager() {
|
|
|
1497
1539
|
status: Int,
|
|
1498
1540
|
newState: Int
|
|
1499
1541
|
) {
|
|
1542
|
+
if (!isCurrentGattCallback(gatt)) {
|
|
1543
|
+
return
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1500
1546
|
// Cancel the connection timeout
|
|
1501
1547
|
if (connectionTimeoutRunnable != null) {
|
|
1502
1548
|
connectionTimeoutHandler.removeCallbacks(connectionTimeoutRunnable!!)
|
|
@@ -2309,8 +2355,20 @@ class MentraLive : SGCManager() {
|
|
|
2309
2355
|
return
|
|
2310
2356
|
}
|
|
2311
2357
|
|
|
2312
|
-
// Send the next item from
|
|
2313
|
-
|
|
2358
|
+
// Send the next item from this BLE session. A retry can race disconnect teardown and land
|
|
2359
|
+
// after sendQueue.clear(); the generation check is the deterministic backstop.
|
|
2360
|
+
var queuedWrite = sendQueue.poll()
|
|
2361
|
+
val currentGeneration = bleSessionGeneration.get()
|
|
2362
|
+
while (queuedWrite != null && queuedWrite.sessionGeneration != currentGeneration) {
|
|
2363
|
+
Bridge.log(
|
|
2364
|
+
"LIVE: Dropping stale queued write from BLE session " +
|
|
2365
|
+
queuedWrite.sessionGeneration +
|
|
2366
|
+
" (current: " +
|
|
2367
|
+
currentGeneration +
|
|
2368
|
+
")"
|
|
2369
|
+
)
|
|
2370
|
+
queuedWrite = sendQueue.poll()
|
|
2371
|
+
}
|
|
2314
2372
|
if (queuedWrite != null) {
|
|
2315
2373
|
// Update last send time before sending
|
|
2316
2374
|
lastSendTimeMs = currentTimeMs
|
|
@@ -2337,25 +2395,35 @@ class MentraLive : SGCManager() {
|
|
|
2337
2395
|
|
|
2338
2396
|
/** Send data through BLE */
|
|
2339
2397
|
private fun sendDataInternal(write: QueuedBleWrite?) {
|
|
2340
|
-
if (!isConnected ||
|
|
2398
|
+
if (!isConnected || write == null) {
|
|
2399
|
+
return
|
|
2400
|
+
}
|
|
2401
|
+
if (write.sessionGeneration != bleSessionGeneration.get()) {
|
|
2402
|
+
Bridge.log("LIVE: Dropping stale BLE write before GATT transmission")
|
|
2403
|
+
return
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
val gatt = bluetoothGatt
|
|
2407
|
+
val characteristic = txCharacteristic
|
|
2408
|
+
if (gatt == null || characteristic == null) {
|
|
2341
2409
|
return
|
|
2342
2410
|
}
|
|
2343
2411
|
|
|
2344
2412
|
try {
|
|
2345
2413
|
val writeStartedAtMs = System.currentTimeMillis()
|
|
2346
|
-
|
|
2414
|
+
characteristic.value = write.data
|
|
2347
2415
|
inFlightBleWriteTrace = write.trace
|
|
2348
2416
|
inFlightBleWriteStartedAtMs = writeStartedAtMs
|
|
2349
|
-
val writeAccepted =
|
|
2417
|
+
val writeAccepted = gatt.writeCharacteristic(characteristic)
|
|
2350
2418
|
logBleWriteTrace(
|
|
2351
2419
|
"write_call",
|
|
2352
2420
|
write.trace,
|
|
2353
2421
|
mapOf(
|
|
2354
2422
|
"writeAccepted" to writeAccepted,
|
|
2355
2423
|
"currentMtu" to currentMtu,
|
|
2356
|
-
"writeType" to
|
|
2424
|
+
"writeType" to characteristic.writeType,
|
|
2357
2425
|
"queueSize" to sendQueue.size,
|
|
2358
|
-
"characteristicUuid" to
|
|
2426
|
+
"characteristicUuid" to characteristic.uuid.toString()
|
|
2359
2427
|
)
|
|
2360
2428
|
)
|
|
2361
2429
|
if (!writeAccepted) {
|
|
@@ -2378,7 +2446,11 @@ class MentraLive : SGCManager() {
|
|
|
2378
2446
|
}
|
|
2379
2447
|
|
|
2380
2448
|
/** Queue data to be sent */
|
|
2381
|
-
private fun queueData(
|
|
2449
|
+
private fun queueData(
|
|
2450
|
+
data: ByteArray?,
|
|
2451
|
+
trace: BleWriteTrace? = null,
|
|
2452
|
+
sessionGeneration: Long = bleSessionGeneration.get()
|
|
2453
|
+
) {
|
|
2382
2454
|
if (data != null) {
|
|
2383
2455
|
val queuedTrace =
|
|
2384
2456
|
trace?.copy(
|
|
@@ -2387,7 +2459,7 @@ class MentraLive : SGCManager() {
|
|
|
2387
2459
|
trace.queuedAtMs
|
|
2388
2460
|
else System.currentTimeMillis()
|
|
2389
2461
|
)
|
|
2390
|
-
sendQueue.add(QueuedBleWrite(data, queuedTrace))
|
|
2462
|
+
sendQueue.add(QueuedBleWrite(data, queuedTrace, sessionGeneration))
|
|
2391
2463
|
logBleChunkTrace(
|
|
2392
2464
|
"queued",
|
|
2393
2465
|
queuedTrace,
|
|
@@ -2431,6 +2503,7 @@ class MentraLive : SGCManager() {
|
|
|
2431
2503
|
private fun sendJson(json: JSONObject?, wakeup: Boolean) {
|
|
2432
2504
|
if (json != null) {
|
|
2433
2505
|
try {
|
|
2506
|
+
val sessionGeneration = bleSessionGeneration.get()
|
|
2434
2507
|
if (buildNumberInt < 5) {
|
|
2435
2508
|
val jsonStr = json.toString()
|
|
2436
2509
|
// Bridge.log("LIVE: 📤 Sending JSON with esoteric message ID: " + jsonStr);
|
|
@@ -2446,7 +2519,7 @@ class MentraLive : SGCManager() {
|
|
|
2446
2519
|
json,
|
|
2447
2520
|
jsonStr.length
|
|
2448
2521
|
)
|
|
2449
|
-
sendDataToGlasses(jsonStr, wakeup)
|
|
2522
|
+
sendDataToGlasses(jsonStr, wakeup, sessionGeneration)
|
|
2450
2523
|
} else {
|
|
2451
2524
|
// Add esoteric message ID to the JSON
|
|
2452
2525
|
val messageId = generateEsotericMessageId()
|
|
@@ -2488,7 +2561,7 @@ class MentraLive : SGCManager() {
|
|
|
2488
2561
|
}
|
|
2489
2562
|
|
|
2490
2563
|
// Track the message for ACK with appropriate timeout
|
|
2491
|
-
trackMessageForAck(messageId, jsonStr, ackTimeout)
|
|
2564
|
+
trackMessageForAck(messageId, jsonStr, ackTimeout, sessionGeneration)
|
|
2492
2565
|
|
|
2493
2566
|
// Send the data
|
|
2494
2567
|
if ("take_photo" == json.optString("type", "")) {
|
|
@@ -2507,7 +2580,7 @@ class MentraLive : SGCManager() {
|
|
|
2507
2580
|
json,
|
|
2508
2581
|
jsonStr.length
|
|
2509
2582
|
)
|
|
2510
|
-
sendDataToGlasses(jsonStr, wakeup)
|
|
2583
|
+
sendDataToGlasses(jsonStr, wakeup, sessionGeneration)
|
|
2511
2584
|
}
|
|
2512
2585
|
} catch (e: JSONException) {
|
|
2513
2586
|
Log.e(TAG, "Error adding message ID to JSON", e)
|
|
@@ -2527,14 +2600,14 @@ class MentraLive : SGCManager() {
|
|
|
2527
2600
|
return list
|
|
2528
2601
|
}
|
|
2529
2602
|
|
|
2530
|
-
/** Track a message for ACK response */
|
|
2531
|
-
private fun trackMessageForAck(messageId: Long, messageData: String) {
|
|
2532
|
-
trackMessageForAck(messageId, messageData, ACK_TIMEOUT_MS)
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
2603
|
/** Track a message for ACK response with custom timeout */
|
|
2536
|
-
private fun trackMessageForAck(
|
|
2537
|
-
|
|
2604
|
+
private fun trackMessageForAck(
|
|
2605
|
+
messageId: Long,
|
|
2606
|
+
messageData: String,
|
|
2607
|
+
timeoutMs: Long,
|
|
2608
|
+
sessionGeneration: Long
|
|
2609
|
+
) {
|
|
2610
|
+
if (!isConnected || sessionGeneration != bleSessionGeneration.get()) {
|
|
2538
2611
|
Bridge.log("LIVE: Not connected, skipping ACK tracking for message " + messageId)
|
|
2539
2612
|
return
|
|
2540
2613
|
}
|
|
@@ -2551,25 +2624,50 @@ class MentraLive : SGCManager() {
|
|
|
2551
2624
|
}
|
|
2552
2625
|
|
|
2553
2626
|
// Create retry runnable
|
|
2554
|
-
val retryRunnable = Runnable { retryMessage(messageId) }
|
|
2627
|
+
val retryRunnable = Runnable { retryMessage(messageId, sessionGeneration) }
|
|
2555
2628
|
|
|
2556
2629
|
// Create pending message
|
|
2557
2630
|
val pendingMessage =
|
|
2558
|
-
PendingMessage(
|
|
2631
|
+
PendingMessage(
|
|
2632
|
+
messageData,
|
|
2633
|
+
System.currentTimeMillis(),
|
|
2634
|
+
0,
|
|
2635
|
+
retryRunnable,
|
|
2636
|
+
sessionGeneration
|
|
2637
|
+
)
|
|
2559
2638
|
pendingMessages[messageId] = pendingMessage
|
|
2560
2639
|
|
|
2561
2640
|
// Schedule ACK timeout with custom timeout
|
|
2562
|
-
handler.postDelayed({ checkMessageAck(messageId) }, timeoutMs)
|
|
2641
|
+
handler.postDelayed({ checkMessageAck(messageId, sessionGeneration) }, timeoutMs)
|
|
2563
2642
|
|
|
2564
2643
|
Bridge.log(
|
|
2565
2644
|
"LIVE: 📋 Tracking message " + messageId + " for ACK (timeout: " + timeoutMs + "ms)"
|
|
2566
2645
|
)
|
|
2567
2646
|
}
|
|
2568
2647
|
|
|
2648
|
+
/**
|
|
2649
|
+
* Ends the current BLE send session before clearing its ACK state. Scheduled callbacks capture
|
|
2650
|
+
* the old generation, and queued retries retain it, so a retry racing this clear cannot become
|
|
2651
|
+
* valid in the next session.
|
|
2652
|
+
*/
|
|
2653
|
+
private fun resetPendingAckState() {
|
|
2654
|
+
val nextGeneration = bleSessionGeneration.incrementAndGet()
|
|
2655
|
+
val pendingCount = pendingMessages.size
|
|
2656
|
+
pendingMessages.clear()
|
|
2657
|
+
if (pendingCount > 0) {
|
|
2658
|
+
Bridge.log(
|
|
2659
|
+
"LIVE: Cleared $pendingCount pending ACK message(s); BLE session is now $nextGeneration"
|
|
2660
|
+
)
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2569
2664
|
/** Check if a message has been acknowledged */
|
|
2570
|
-
private fun checkMessageAck(messageId: Long) {
|
|
2665
|
+
private fun checkMessageAck(messageId: Long, sessionGeneration: Long) {
|
|
2571
2666
|
val pendingMessage = pendingMessages[messageId]
|
|
2572
|
-
if (pendingMessage != null
|
|
2667
|
+
if (pendingMessage != null &&
|
|
2668
|
+
pendingMessage.sessionGeneration == sessionGeneration &&
|
|
2669
|
+
sessionGeneration == bleSessionGeneration.get()
|
|
2670
|
+
) {
|
|
2573
2671
|
Log.w(
|
|
2574
2672
|
TAG,
|
|
2575
2673
|
"⏰ ACK timeout for message " +
|
|
@@ -2590,7 +2688,7 @@ class MentraLive : SGCManager() {
|
|
|
2590
2688
|
MAX_RETRY_ATTEMPTS +
|
|
2591
2689
|
")"
|
|
2592
2690
|
)
|
|
2593
|
-
retryMessage(messageId)
|
|
2691
|
+
retryMessage(messageId, sessionGeneration)
|
|
2594
2692
|
} else {
|
|
2595
2693
|
// Max retries reached
|
|
2596
2694
|
Log.e(
|
|
@@ -2601,22 +2699,31 @@ class MentraLive : SGCManager() {
|
|
|
2601
2699
|
MAX_RETRY_ATTEMPTS +
|
|
2602
2700
|
" attempts"
|
|
2603
2701
|
)
|
|
2604
|
-
pendingMessages.remove(messageId)
|
|
2702
|
+
pendingMessages.remove(messageId, pendingMessage)
|
|
2605
2703
|
}
|
|
2704
|
+
} else if (pendingMessage != null && pendingMessage.sessionGeneration == sessionGeneration) {
|
|
2705
|
+
pendingMessages.remove(messageId, pendingMessage)
|
|
2606
2706
|
}
|
|
2607
2707
|
}
|
|
2608
2708
|
|
|
2609
2709
|
/** Retry a message */
|
|
2610
|
-
private fun retryMessage(messageId: Long) {
|
|
2710
|
+
private fun retryMessage(messageId: Long, sessionGeneration: Long) {
|
|
2611
2711
|
val pendingMessage = pendingMessages[messageId]
|
|
2612
2712
|
if (pendingMessage == null) {
|
|
2613
2713
|
Log.w(TAG, "Message " + messageId + " no longer tracked for retry")
|
|
2614
2714
|
return
|
|
2615
2715
|
}
|
|
2716
|
+
if (pendingMessage.sessionGeneration != sessionGeneration ||
|
|
2717
|
+
sessionGeneration != bleSessionGeneration.get()
|
|
2718
|
+
) {
|
|
2719
|
+
pendingMessages.remove(messageId, pendingMessage)
|
|
2720
|
+
Bridge.log("LIVE: Dropping stale ACK retry for message $messageId")
|
|
2721
|
+
return
|
|
2722
|
+
}
|
|
2616
2723
|
|
|
2617
2724
|
if (pendingMessage.retryCount >= MAX_RETRY_ATTEMPTS) {
|
|
2618
2725
|
Log.e(TAG, "Max retries reached for message " + messageId)
|
|
2619
|
-
pendingMessages.remove(messageId)
|
|
2726
|
+
pendingMessages.remove(messageId, pendingMessage)
|
|
2620
2727
|
return
|
|
2621
2728
|
}
|
|
2622
2729
|
|
|
@@ -2626,11 +2733,18 @@ class MentraLive : SGCManager() {
|
|
|
2626
2733
|
pendingMessage.messageData,
|
|
2627
2734
|
System.currentTimeMillis(),
|
|
2628
2735
|
pendingMessage.retryCount + 1,
|
|
2629
|
-
pendingMessage.retryRunnable
|
|
2736
|
+
pendingMessage.retryRunnable,
|
|
2737
|
+
sessionGeneration
|
|
2630
2738
|
)
|
|
2631
2739
|
|
|
2632
|
-
//
|
|
2633
|
-
pendingMessages
|
|
2740
|
+
// Replace only the entry inspected above. An ACK or disconnect may have removed it.
|
|
2741
|
+
if (!pendingMessages.replace(messageId, pendingMessage, retryMessage)) {
|
|
2742
|
+
return
|
|
2743
|
+
}
|
|
2744
|
+
if (sessionGeneration != bleSessionGeneration.get()) {
|
|
2745
|
+
pendingMessages.remove(messageId, retryMessage)
|
|
2746
|
+
return
|
|
2747
|
+
}
|
|
2634
2748
|
|
|
2635
2749
|
// Send the message again
|
|
2636
2750
|
Bridge.log(
|
|
@@ -2640,10 +2754,10 @@ class MentraLive : SGCManager() {
|
|
|
2640
2754
|
retryMessage.retryCount +
|
|
2641
2755
|
")"
|
|
2642
2756
|
)
|
|
2643
|
-
sendDataToGlasses(pendingMessage.messageData, false)
|
|
2757
|
+
sendDataToGlasses(pendingMessage.messageData, false, sessionGeneration)
|
|
2644
2758
|
|
|
2645
2759
|
// Schedule next ACK check
|
|
2646
|
-
handler.postDelayed({ checkMessageAck(messageId) }, ACK_TIMEOUT_MS)
|
|
2760
|
+
handler.postDelayed({ checkMessageAck(messageId, sessionGeneration) }, ACK_TIMEOUT_MS)
|
|
2647
2761
|
}
|
|
2648
2762
|
|
|
2649
2763
|
/** Process ACK response from glasses */
|
|
@@ -3327,6 +3441,7 @@ class MentraLive : SGCManager() {
|
|
|
3327
3441
|
if (osStepSequence != null && osStepSequence.length() > 0) {
|
|
3328
3442
|
cachedOtaStepSequence = osStepSequence
|
|
3329
3443
|
}
|
|
3444
|
+
updateBesOtaHeartbeatGuard(osStepType, osPhase, osStatus)
|
|
3330
3445
|
|
|
3331
3446
|
Bridge.log(
|
|
3332
3447
|
"LIVE: 📱 OTA status - step " +
|
|
@@ -3377,6 +3492,7 @@ class MentraLive : SGCManager() {
|
|
|
3377
3492
|
} else {
|
|
3378
3493
|
unified = "in_progress"
|
|
3379
3494
|
}
|
|
3495
|
+
updateBesOtaHeartbeatGuard(currentUpdate, legacyPhase, unified)
|
|
3380
3496
|
Bridge.log(
|
|
3381
3497
|
"LIVE: 📱 Legacy ota_progress → ota_status: " +
|
|
3382
3498
|
legacyStage +
|
|
@@ -3729,9 +3845,11 @@ class MentraLive : SGCManager() {
|
|
|
3729
3845
|
if (otaVersionUrlLegacy != null) otaVersionUrlLegacy else ""
|
|
3730
3846
|
)
|
|
3731
3847
|
DeviceStore.apply("glasses", "firmwareVersion", firmwareVersionLegacy)
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3848
|
+
btMacAddressLegacy.trim().takeIf { it.isNotEmpty() }?.let {
|
|
3849
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", it)
|
|
3850
|
+
}
|
|
3851
|
+
serialNumberLegacy.trim().takeIf { it.isNotEmpty() }?.let {
|
|
3852
|
+
DeviceStore.apply("glasses", "serialNumber", it)
|
|
3735
3853
|
}
|
|
3736
3854
|
|
|
3737
3855
|
val versionInfoLegacy = HashMap<String, Any>()
|
|
@@ -3994,16 +4112,11 @@ class MentraLive : SGCManager() {
|
|
|
3994
4112
|
fields["mtk_fw_version"] as String
|
|
3995
4113
|
)
|
|
3996
4114
|
}
|
|
3997
|
-
|
|
3998
|
-
DeviceStore.apply(
|
|
3999
|
-
"glasses",
|
|
4000
|
-
"bluetoothMacAddress",
|
|
4001
|
-
fields["bt_mac_address"] as String
|
|
4002
|
-
)
|
|
4115
|
+
(fields["bt_mac_address"] as? String)?.trim()?.takeIf { it.isNotEmpty() }?.let {
|
|
4116
|
+
DeviceStore.apply("glasses", "bluetoothMacAddress", it)
|
|
4003
4117
|
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
DeviceStore.apply("glasses", "serialNumber", serialNumber)
|
|
4118
|
+
(fields["serial_number"] as? String)?.trim()?.takeIf { it.isNotEmpty() }?.let {
|
|
4119
|
+
DeviceStore.apply("glasses", "serialNumber", it)
|
|
4007
4120
|
}
|
|
4008
4121
|
if (fields.containsKey("system_time_ms")) {
|
|
4009
4122
|
val v = fields["system_time_ms"]
|
|
@@ -4470,15 +4583,28 @@ class MentraLive : SGCManager() {
|
|
|
4470
4583
|
var progress = ((rawProgress + 2) / 5) * 5
|
|
4471
4584
|
if (progress > 100) progress = 100
|
|
4472
4585
|
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4586
|
+
val mapping =
|
|
4587
|
+
mapBesOtaProgress(
|
|
4588
|
+
type,
|
|
4589
|
+
rawProgress,
|
|
4590
|
+
bodyObj.optString("message", "BES update failed"),
|
|
4591
|
+
)
|
|
4592
|
+
val besOtaStatus = mapping.status
|
|
4593
|
+
val besOtaProgressVal = mapping.progress
|
|
4594
|
+
val besOtaErrorMessage = mapping.errorMessage
|
|
4595
|
+
|
|
4596
|
+
if ("FINISHED" == besOtaStatus || "FAILED" == besOtaStatus) {
|
|
4597
|
+
besOtaHeartbeatGuard.clear()
|
|
4598
|
+
} else {
|
|
4599
|
+
// Refresh before progress de-duplication: raw progress proves the BES
|
|
4600
|
+
// transfer is active even when the rounded UI value is unchanged.
|
|
4601
|
+
besOtaHeartbeatGuard.refresh(SystemClock.elapsedRealtime())
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
// Only send if nonterminal display progress changed to a new 5% increment.
|
|
4605
|
+
if ("PROGRESS" == besOtaStatus && besOtaProgressVal == lastBesOtaProgress) {
|
|
4606
|
+
return
|
|
4480
4607
|
}
|
|
4481
|
-
lastBesOtaProgress = progress
|
|
4482
4608
|
|
|
4483
4609
|
Bridge.log(
|
|
4484
4610
|
"LIVE: 📱 BES OTA progress via sr_adota - type: " +
|
|
@@ -4487,58 +4613,21 @@ class MentraLive : SGCManager() {
|
|
|
4487
4613
|
rawProgress +
|
|
4488
4614
|
"%, rounded: " +
|
|
4489
4615
|
progress +
|
|
4616
|
+
"%, display: " +
|
|
4617
|
+
besOtaProgressVal +
|
|
4490
4618
|
"%"
|
|
4491
4619
|
)
|
|
4492
4620
|
|
|
4493
|
-
|
|
4494
|
-
val besOtaStatus: String
|
|
4495
|
-
val besOtaProgressVal: Int
|
|
4496
|
-
var besOtaErrorMessage: String? = null
|
|
4497
|
-
|
|
4498
|
-
// Order matters here: check completion (rawProgress >= 100 OR success)
|
|
4499
|
-
// BEFORE
|
|
4500
|
-
// type=="update", because some BES firmware emits the final 100% tick with
|
|
4501
|
-
// type=="update" rather than type=="success". Treating that as PROGRESS
|
|
4502
|
-
// would
|
|
4503
|
-
// leave the UI stuck at 100% forever.
|
|
4504
|
-
if ("success" == type || rawProgress >= 100) {
|
|
4505
|
-
besOtaStatus = "FINISHED"
|
|
4506
|
-
besOtaProgressVal = 100
|
|
4507
|
-
lastBesOtaProgress = -1 // Reset for next OTA
|
|
4508
|
-
} else if ("error" == type || "fail" == type) {
|
|
4509
|
-
besOtaStatus = "FAILED"
|
|
4510
|
-
besOtaProgressVal = progress
|
|
4511
|
-
besOtaErrorMessage = bodyObj.optString("message", "BES update failed")
|
|
4621
|
+
if ("FINISHED" == besOtaStatus || "FAILED" == besOtaStatus) {
|
|
4512
4622
|
lastBesOtaProgress = -1 // Reset for next OTA
|
|
4513
|
-
} else if ("update" == type) {
|
|
4514
|
-
besOtaStatus = "PROGRESS"
|
|
4515
|
-
besOtaProgressVal = progress
|
|
4516
4623
|
} else {
|
|
4517
|
-
|
|
4518
|
-
besOtaStatus = "PROGRESS"
|
|
4519
|
-
besOtaProgressVal = progress
|
|
4624
|
+
lastBesOtaProgress = besOtaProgressVal
|
|
4520
4625
|
}
|
|
4521
4626
|
|
|
4522
|
-
val syntheticStatus
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
// ota_status from the glasses — consumers mapping on this synthetic
|
|
4527
|
-
// status would otherwise never see a terminal state. Emit "complete"
|
|
4528
|
-
// for the final step; mid-session BES steps keep "step_complete" so
|
|
4529
|
-
// session-level trackers advance normally. Unknown sessions
|
|
4530
|
-
// (cachedOtaTotalSteps == 0, e.g. legacy glasses that never sent an
|
|
4531
|
-
// ota_status) conservatively keep "step_complete".
|
|
4532
|
-
syntheticStatus =
|
|
4533
|
-
if (cachedOtaTotalSteps > 0 &&
|
|
4534
|
-
cachedOtaCurrentStep >= cachedOtaTotalSteps
|
|
4535
|
-
)
|
|
4536
|
-
"complete"
|
|
4537
|
-
else "step_complete"
|
|
4538
|
-
} else if ("FAILED" == besOtaStatus) {
|
|
4539
|
-
syntheticStatus = "failed"
|
|
4540
|
-
} else {
|
|
4541
|
-
syntheticStatus = "in_progress"
|
|
4627
|
+
val syntheticStatus = when (besOtaStatus) {
|
|
4628
|
+
"FINISHED" -> "step_complete"
|
|
4629
|
+
"FAILED" -> "failed"
|
|
4630
|
+
else -> "in_progress"
|
|
4542
4631
|
}
|
|
4543
4632
|
val sid = if (cachedOtaSessionId != null) cachedOtaSessionId!! else ""
|
|
4544
4633
|
val totalSteps = if (cachedOtaTotalSteps > 0) cachedOtaTotalSteps else 1
|
|
@@ -5003,6 +5092,10 @@ class MentraLive : SGCManager() {
|
|
|
5003
5092
|
Bridge.log("LIVE: Skipping heartbeat - glasses not ready or not connected")
|
|
5004
5093
|
return
|
|
5005
5094
|
}
|
|
5095
|
+
if (besOtaHeartbeatGuard.shouldSuppress(SystemClock.elapsedRealtime())) {
|
|
5096
|
+
Bridge.log("LIVE: Skipping heartbeat while BES OTA owns the UART")
|
|
5097
|
+
return
|
|
5098
|
+
}
|
|
5006
5099
|
|
|
5007
5100
|
try {
|
|
5008
5101
|
// Send ping message (no ACK needed for heartbeats)
|
|
@@ -7572,10 +7665,22 @@ class MentraLive : SGCManager() {
|
|
|
7572
7665
|
* @param data The string data to be sent to the glasses
|
|
7573
7666
|
*/
|
|
7574
7667
|
fun sendDataToGlasses(data: String?, wakeup: Boolean) {
|
|
7668
|
+
sendDataToGlasses(data, wakeup, bleSessionGeneration.get())
|
|
7669
|
+
}
|
|
7670
|
+
|
|
7671
|
+
private fun sendDataToGlasses(
|
|
7672
|
+
data: String?,
|
|
7673
|
+
wakeup: Boolean,
|
|
7674
|
+
sessionGeneration: Long
|
|
7675
|
+
) {
|
|
7575
7676
|
if (data == null || data.isEmpty()) {
|
|
7576
7677
|
Log.e(TAG, "Cannot send empty data to glasses")
|
|
7577
7678
|
return
|
|
7578
7679
|
}
|
|
7680
|
+
if (sessionGeneration != bleSessionGeneration.get()) {
|
|
7681
|
+
Bridge.log("LIVE: Dropping send request from stale BLE session $sessionGeneration")
|
|
7682
|
+
return
|
|
7683
|
+
}
|
|
7579
7684
|
|
|
7580
7685
|
val wireData =
|
|
7581
7686
|
try {
|
|
@@ -7602,7 +7707,13 @@ class MentraLive : SGCManager() {
|
|
|
7602
7707
|
}
|
|
7603
7708
|
|
|
7604
7709
|
if (useBinaryWireProtocol && buildNumberInt >= 5) {
|
|
7605
|
-
sendDataToGlassesBinary(
|
|
7710
|
+
sendDataToGlassesBinary(
|
|
7711
|
+
wireData,
|
|
7712
|
+
wakeup,
|
|
7713
|
+
commandTraceInfo,
|
|
7714
|
+
isPhotoRequest,
|
|
7715
|
+
sessionGeneration
|
|
7716
|
+
)
|
|
7606
7717
|
return
|
|
7607
7718
|
}
|
|
7608
7719
|
|
|
@@ -7681,7 +7792,7 @@ class MentraLive : SGCManager() {
|
|
|
7681
7792
|
)
|
|
7682
7793
|
|
|
7683
7794
|
// Queue the chunk for sending
|
|
7684
|
-
queueData(packedData, trace)
|
|
7795
|
+
queueData(packedData, trace, sessionGeneration)
|
|
7685
7796
|
|
|
7686
7797
|
// Add small delay between chunks to avoid overwhelming the connection
|
|
7687
7798
|
if (i < chunks.size - 1) {
|
|
@@ -7722,7 +7833,7 @@ class MentraLive : SGCManager() {
|
|
|
7722
7833
|
)
|
|
7723
7834
|
|
|
7724
7835
|
// Queue the data for sending
|
|
7725
|
-
queueData(packedData, trace)
|
|
7836
|
+
queueData(packedData, trace, sessionGeneration)
|
|
7726
7837
|
if (isPhotoRequest) {
|
|
7727
7838
|
Bridge.log(
|
|
7728
7839
|
"LIVE: PHOTO PIPELINE BLE handoff — packedLen=" +
|
|
@@ -7740,7 +7851,8 @@ class MentraLive : SGCManager() {
|
|
|
7740
7851
|
data: String,
|
|
7741
7852
|
wakeup: Boolean,
|
|
7742
7853
|
commandTraceInfo: OutgoingBleCommandTraceInfo,
|
|
7743
|
-
isPhotoRequest: Boolean
|
|
7854
|
+
isPhotoRequest: Boolean,
|
|
7855
|
+
sessionGeneration: Long
|
|
7744
7856
|
) {
|
|
7745
7857
|
val payloadBytes = data.toByteArray(StandardCharsets.UTF_8)
|
|
7746
7858
|
var messageId = 0
|
|
@@ -7792,7 +7904,7 @@ class MentraLive : SGCManager() {
|
|
|
7792
7904
|
"wireProtocol" to "v2"
|
|
7793
7905
|
)
|
|
7794
7906
|
)
|
|
7795
|
-
queueData(packedData, trace)
|
|
7907
|
+
queueData(packedData, trace, sessionGeneration)
|
|
7796
7908
|
|
|
7797
7909
|
if (i < fragments.size - 1) {
|
|
7798
7910
|
try {
|