@mentra/bluetooth-sdk 0.1.21-beta.3 → 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/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +11 -4
- 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 +207 -93
- 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 +11 -6
- package/ios/Source/sgcs/MentraLive.swift +59 -47
- 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
|
|
@@ -854,6 +863,7 @@ class MentraLive : SGCManager() {
|
|
|
854
863
|
DeviceStore.apply("glasses", "signalStrength", -1)
|
|
855
864
|
DeviceStore.apply("glasses", "signalStrengthUpdatedAt", 0L)
|
|
856
865
|
resetWireNegotiationState()
|
|
866
|
+
resetPendingAckState()
|
|
857
867
|
sendQueue.clear() // see the disconnect reset above: stale writes die with the session
|
|
858
868
|
|
|
859
869
|
// Drop OTA caches when fully disconnected — avoids leaking session/step state
|
|
@@ -874,6 +884,17 @@ class MentraLive : SGCManager() {
|
|
|
874
884
|
cachedOtaCurrentStep = 0
|
|
875
885
|
cachedOtaStepSequence = null
|
|
876
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
|
+
)
|
|
877
898
|
}
|
|
878
899
|
|
|
879
900
|
protected fun setFontSizes() {
|
|
@@ -1176,6 +1197,26 @@ class MentraLive : SGCManager() {
|
|
|
1176
1197
|
}
|
|
1177
1198
|
}
|
|
1178
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
|
+
|
|
1179
1220
|
/** Connect to a specific BLE device */
|
|
1180
1221
|
private fun connectToDevice(device: BluetoothDevice?) {
|
|
1181
1222
|
if (device == null) {
|
|
@@ -1498,6 +1539,10 @@ class MentraLive : SGCManager() {
|
|
|
1498
1539
|
status: Int,
|
|
1499
1540
|
newState: Int
|
|
1500
1541
|
) {
|
|
1542
|
+
if (!isCurrentGattCallback(gatt)) {
|
|
1543
|
+
return
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1501
1546
|
// Cancel the connection timeout
|
|
1502
1547
|
if (connectionTimeoutRunnable != null) {
|
|
1503
1548
|
connectionTimeoutHandler.removeCallbacks(connectionTimeoutRunnable!!)
|
|
@@ -2310,8 +2355,20 @@ class MentraLive : SGCManager() {
|
|
|
2310
2355
|
return
|
|
2311
2356
|
}
|
|
2312
2357
|
|
|
2313
|
-
// Send the next item from
|
|
2314
|
-
|
|
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
|
+
}
|
|
2315
2372
|
if (queuedWrite != null) {
|
|
2316
2373
|
// Update last send time before sending
|
|
2317
2374
|
lastSendTimeMs = currentTimeMs
|
|
@@ -2338,25 +2395,35 @@ class MentraLive : SGCManager() {
|
|
|
2338
2395
|
|
|
2339
2396
|
/** Send data through BLE */
|
|
2340
2397
|
private fun sendDataInternal(write: QueuedBleWrite?) {
|
|
2341
|
-
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) {
|
|
2342
2409
|
return
|
|
2343
2410
|
}
|
|
2344
2411
|
|
|
2345
2412
|
try {
|
|
2346
2413
|
val writeStartedAtMs = System.currentTimeMillis()
|
|
2347
|
-
|
|
2414
|
+
characteristic.value = write.data
|
|
2348
2415
|
inFlightBleWriteTrace = write.trace
|
|
2349
2416
|
inFlightBleWriteStartedAtMs = writeStartedAtMs
|
|
2350
|
-
val writeAccepted =
|
|
2417
|
+
val writeAccepted = gatt.writeCharacteristic(characteristic)
|
|
2351
2418
|
logBleWriteTrace(
|
|
2352
2419
|
"write_call",
|
|
2353
2420
|
write.trace,
|
|
2354
2421
|
mapOf(
|
|
2355
2422
|
"writeAccepted" to writeAccepted,
|
|
2356
2423
|
"currentMtu" to currentMtu,
|
|
2357
|
-
"writeType" to
|
|
2424
|
+
"writeType" to characteristic.writeType,
|
|
2358
2425
|
"queueSize" to sendQueue.size,
|
|
2359
|
-
"characteristicUuid" to
|
|
2426
|
+
"characteristicUuid" to characteristic.uuid.toString()
|
|
2360
2427
|
)
|
|
2361
2428
|
)
|
|
2362
2429
|
if (!writeAccepted) {
|
|
@@ -2379,7 +2446,11 @@ class MentraLive : SGCManager() {
|
|
|
2379
2446
|
}
|
|
2380
2447
|
|
|
2381
2448
|
/** Queue data to be sent */
|
|
2382
|
-
private fun queueData(
|
|
2449
|
+
private fun queueData(
|
|
2450
|
+
data: ByteArray?,
|
|
2451
|
+
trace: BleWriteTrace? = null,
|
|
2452
|
+
sessionGeneration: Long = bleSessionGeneration.get()
|
|
2453
|
+
) {
|
|
2383
2454
|
if (data != null) {
|
|
2384
2455
|
val queuedTrace =
|
|
2385
2456
|
trace?.copy(
|
|
@@ -2388,7 +2459,7 @@ class MentraLive : SGCManager() {
|
|
|
2388
2459
|
trace.queuedAtMs
|
|
2389
2460
|
else System.currentTimeMillis()
|
|
2390
2461
|
)
|
|
2391
|
-
sendQueue.add(QueuedBleWrite(data, queuedTrace))
|
|
2462
|
+
sendQueue.add(QueuedBleWrite(data, queuedTrace, sessionGeneration))
|
|
2392
2463
|
logBleChunkTrace(
|
|
2393
2464
|
"queued",
|
|
2394
2465
|
queuedTrace,
|
|
@@ -2432,6 +2503,7 @@ class MentraLive : SGCManager() {
|
|
|
2432
2503
|
private fun sendJson(json: JSONObject?, wakeup: Boolean) {
|
|
2433
2504
|
if (json != null) {
|
|
2434
2505
|
try {
|
|
2506
|
+
val sessionGeneration = bleSessionGeneration.get()
|
|
2435
2507
|
if (buildNumberInt < 5) {
|
|
2436
2508
|
val jsonStr = json.toString()
|
|
2437
2509
|
// Bridge.log("LIVE: 📤 Sending JSON with esoteric message ID: " + jsonStr);
|
|
@@ -2447,7 +2519,7 @@ class MentraLive : SGCManager() {
|
|
|
2447
2519
|
json,
|
|
2448
2520
|
jsonStr.length
|
|
2449
2521
|
)
|
|
2450
|
-
sendDataToGlasses(jsonStr, wakeup)
|
|
2522
|
+
sendDataToGlasses(jsonStr, wakeup, sessionGeneration)
|
|
2451
2523
|
} else {
|
|
2452
2524
|
// Add esoteric message ID to the JSON
|
|
2453
2525
|
val messageId = generateEsotericMessageId()
|
|
@@ -2489,7 +2561,7 @@ class MentraLive : SGCManager() {
|
|
|
2489
2561
|
}
|
|
2490
2562
|
|
|
2491
2563
|
// Track the message for ACK with appropriate timeout
|
|
2492
|
-
trackMessageForAck(messageId, jsonStr, ackTimeout)
|
|
2564
|
+
trackMessageForAck(messageId, jsonStr, ackTimeout, sessionGeneration)
|
|
2493
2565
|
|
|
2494
2566
|
// Send the data
|
|
2495
2567
|
if ("take_photo" == json.optString("type", "")) {
|
|
@@ -2508,7 +2580,7 @@ class MentraLive : SGCManager() {
|
|
|
2508
2580
|
json,
|
|
2509
2581
|
jsonStr.length
|
|
2510
2582
|
)
|
|
2511
|
-
sendDataToGlasses(jsonStr, wakeup)
|
|
2583
|
+
sendDataToGlasses(jsonStr, wakeup, sessionGeneration)
|
|
2512
2584
|
}
|
|
2513
2585
|
} catch (e: JSONException) {
|
|
2514
2586
|
Log.e(TAG, "Error adding message ID to JSON", e)
|
|
@@ -2528,14 +2600,14 @@ class MentraLive : SGCManager() {
|
|
|
2528
2600
|
return list
|
|
2529
2601
|
}
|
|
2530
2602
|
|
|
2531
|
-
/** Track a message for ACK response */
|
|
2532
|
-
private fun trackMessageForAck(messageId: Long, messageData: String) {
|
|
2533
|
-
trackMessageForAck(messageId, messageData, ACK_TIMEOUT_MS)
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
2603
|
/** Track a message for ACK response with custom timeout */
|
|
2537
|
-
private fun trackMessageForAck(
|
|
2538
|
-
|
|
2604
|
+
private fun trackMessageForAck(
|
|
2605
|
+
messageId: Long,
|
|
2606
|
+
messageData: String,
|
|
2607
|
+
timeoutMs: Long,
|
|
2608
|
+
sessionGeneration: Long
|
|
2609
|
+
) {
|
|
2610
|
+
if (!isConnected || sessionGeneration != bleSessionGeneration.get()) {
|
|
2539
2611
|
Bridge.log("LIVE: Not connected, skipping ACK tracking for message " + messageId)
|
|
2540
2612
|
return
|
|
2541
2613
|
}
|
|
@@ -2552,25 +2624,50 @@ class MentraLive : SGCManager() {
|
|
|
2552
2624
|
}
|
|
2553
2625
|
|
|
2554
2626
|
// Create retry runnable
|
|
2555
|
-
val retryRunnable = Runnable { retryMessage(messageId) }
|
|
2627
|
+
val retryRunnable = Runnable { retryMessage(messageId, sessionGeneration) }
|
|
2556
2628
|
|
|
2557
2629
|
// Create pending message
|
|
2558
2630
|
val pendingMessage =
|
|
2559
|
-
PendingMessage(
|
|
2631
|
+
PendingMessage(
|
|
2632
|
+
messageData,
|
|
2633
|
+
System.currentTimeMillis(),
|
|
2634
|
+
0,
|
|
2635
|
+
retryRunnable,
|
|
2636
|
+
sessionGeneration
|
|
2637
|
+
)
|
|
2560
2638
|
pendingMessages[messageId] = pendingMessage
|
|
2561
2639
|
|
|
2562
2640
|
// Schedule ACK timeout with custom timeout
|
|
2563
|
-
handler.postDelayed({ checkMessageAck(messageId) }, timeoutMs)
|
|
2641
|
+
handler.postDelayed({ checkMessageAck(messageId, sessionGeneration) }, timeoutMs)
|
|
2564
2642
|
|
|
2565
2643
|
Bridge.log(
|
|
2566
2644
|
"LIVE: 📋 Tracking message " + messageId + " for ACK (timeout: " + timeoutMs + "ms)"
|
|
2567
2645
|
)
|
|
2568
2646
|
}
|
|
2569
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
|
+
|
|
2570
2664
|
/** Check if a message has been acknowledged */
|
|
2571
|
-
private fun checkMessageAck(messageId: Long) {
|
|
2665
|
+
private fun checkMessageAck(messageId: Long, sessionGeneration: Long) {
|
|
2572
2666
|
val pendingMessage = pendingMessages[messageId]
|
|
2573
|
-
if (pendingMessage != null
|
|
2667
|
+
if (pendingMessage != null &&
|
|
2668
|
+
pendingMessage.sessionGeneration == sessionGeneration &&
|
|
2669
|
+
sessionGeneration == bleSessionGeneration.get()
|
|
2670
|
+
) {
|
|
2574
2671
|
Log.w(
|
|
2575
2672
|
TAG,
|
|
2576
2673
|
"⏰ ACK timeout for message " +
|
|
@@ -2591,7 +2688,7 @@ class MentraLive : SGCManager() {
|
|
|
2591
2688
|
MAX_RETRY_ATTEMPTS +
|
|
2592
2689
|
")"
|
|
2593
2690
|
)
|
|
2594
|
-
retryMessage(messageId)
|
|
2691
|
+
retryMessage(messageId, sessionGeneration)
|
|
2595
2692
|
} else {
|
|
2596
2693
|
// Max retries reached
|
|
2597
2694
|
Log.e(
|
|
@@ -2602,22 +2699,31 @@ class MentraLive : SGCManager() {
|
|
|
2602
2699
|
MAX_RETRY_ATTEMPTS +
|
|
2603
2700
|
" attempts"
|
|
2604
2701
|
)
|
|
2605
|
-
pendingMessages.remove(messageId)
|
|
2702
|
+
pendingMessages.remove(messageId, pendingMessage)
|
|
2606
2703
|
}
|
|
2704
|
+
} else if (pendingMessage != null && pendingMessage.sessionGeneration == sessionGeneration) {
|
|
2705
|
+
pendingMessages.remove(messageId, pendingMessage)
|
|
2607
2706
|
}
|
|
2608
2707
|
}
|
|
2609
2708
|
|
|
2610
2709
|
/** Retry a message */
|
|
2611
|
-
private fun retryMessage(messageId: Long) {
|
|
2710
|
+
private fun retryMessage(messageId: Long, sessionGeneration: Long) {
|
|
2612
2711
|
val pendingMessage = pendingMessages[messageId]
|
|
2613
2712
|
if (pendingMessage == null) {
|
|
2614
2713
|
Log.w(TAG, "Message " + messageId + " no longer tracked for retry")
|
|
2615
2714
|
return
|
|
2616
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
|
+
}
|
|
2617
2723
|
|
|
2618
2724
|
if (pendingMessage.retryCount >= MAX_RETRY_ATTEMPTS) {
|
|
2619
2725
|
Log.e(TAG, "Max retries reached for message " + messageId)
|
|
2620
|
-
pendingMessages.remove(messageId)
|
|
2726
|
+
pendingMessages.remove(messageId, pendingMessage)
|
|
2621
2727
|
return
|
|
2622
2728
|
}
|
|
2623
2729
|
|
|
@@ -2627,11 +2733,18 @@ class MentraLive : SGCManager() {
|
|
|
2627
2733
|
pendingMessage.messageData,
|
|
2628
2734
|
System.currentTimeMillis(),
|
|
2629
2735
|
pendingMessage.retryCount + 1,
|
|
2630
|
-
pendingMessage.retryRunnable
|
|
2736
|
+
pendingMessage.retryRunnable,
|
|
2737
|
+
sessionGeneration
|
|
2631
2738
|
)
|
|
2632
2739
|
|
|
2633
|
-
//
|
|
2634
|
-
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
|
+
}
|
|
2635
2748
|
|
|
2636
2749
|
// Send the message again
|
|
2637
2750
|
Bridge.log(
|
|
@@ -2641,10 +2754,10 @@ class MentraLive : SGCManager() {
|
|
|
2641
2754
|
retryMessage.retryCount +
|
|
2642
2755
|
")"
|
|
2643
2756
|
)
|
|
2644
|
-
sendDataToGlasses(pendingMessage.messageData, false)
|
|
2757
|
+
sendDataToGlasses(pendingMessage.messageData, false, sessionGeneration)
|
|
2645
2758
|
|
|
2646
2759
|
// Schedule next ACK check
|
|
2647
|
-
handler.postDelayed({ checkMessageAck(messageId) }, ACK_TIMEOUT_MS)
|
|
2760
|
+
handler.postDelayed({ checkMessageAck(messageId, sessionGeneration) }, ACK_TIMEOUT_MS)
|
|
2648
2761
|
}
|
|
2649
2762
|
|
|
2650
2763
|
/** Process ACK response from glasses */
|
|
@@ -3328,6 +3441,7 @@ class MentraLive : SGCManager() {
|
|
|
3328
3441
|
if (osStepSequence != null && osStepSequence.length() > 0) {
|
|
3329
3442
|
cachedOtaStepSequence = osStepSequence
|
|
3330
3443
|
}
|
|
3444
|
+
updateBesOtaHeartbeatGuard(osStepType, osPhase, osStatus)
|
|
3331
3445
|
|
|
3332
3446
|
Bridge.log(
|
|
3333
3447
|
"LIVE: 📱 OTA status - step " +
|
|
@@ -3378,6 +3492,7 @@ class MentraLive : SGCManager() {
|
|
|
3378
3492
|
} else {
|
|
3379
3493
|
unified = "in_progress"
|
|
3380
3494
|
}
|
|
3495
|
+
updateBesOtaHeartbeatGuard(currentUpdate, legacyPhase, unified)
|
|
3381
3496
|
Bridge.log(
|
|
3382
3497
|
"LIVE: 📱 Legacy ota_progress → ota_status: " +
|
|
3383
3498
|
legacyStage +
|
|
@@ -4468,15 +4583,28 @@ class MentraLive : SGCManager() {
|
|
|
4468
4583
|
var progress = ((rawProgress + 2) / 5) * 5
|
|
4469
4584
|
if (progress > 100) progress = 100
|
|
4470
4585
|
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
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
|
|
4478
4607
|
}
|
|
4479
|
-
lastBesOtaProgress = progress
|
|
4480
4608
|
|
|
4481
4609
|
Bridge.log(
|
|
4482
4610
|
"LIVE: 📱 BES OTA progress via sr_adota - type: " +
|
|
@@ -4485,58 +4613,21 @@ class MentraLive : SGCManager() {
|
|
|
4485
4613
|
rawProgress +
|
|
4486
4614
|
"%, rounded: " +
|
|
4487
4615
|
progress +
|
|
4616
|
+
"%, display: " +
|
|
4617
|
+
besOtaProgressVal +
|
|
4488
4618
|
"%"
|
|
4489
4619
|
)
|
|
4490
4620
|
|
|
4491
|
-
|
|
4492
|
-
val besOtaStatus: String
|
|
4493
|
-
val besOtaProgressVal: Int
|
|
4494
|
-
var besOtaErrorMessage: String? = null
|
|
4495
|
-
|
|
4496
|
-
// Order matters here: check completion (rawProgress >= 100 OR success)
|
|
4497
|
-
// BEFORE
|
|
4498
|
-
// type=="update", because some BES firmware emits the final 100% tick with
|
|
4499
|
-
// type=="update" rather than type=="success". Treating that as PROGRESS
|
|
4500
|
-
// would
|
|
4501
|
-
// leave the UI stuck at 100% forever.
|
|
4502
|
-
if ("success" == type || rawProgress >= 100) {
|
|
4503
|
-
besOtaStatus = "FINISHED"
|
|
4504
|
-
besOtaProgressVal = 100
|
|
4621
|
+
if ("FINISHED" == besOtaStatus || "FAILED" == besOtaStatus) {
|
|
4505
4622
|
lastBesOtaProgress = -1 // Reset for next OTA
|
|
4506
|
-
} else if ("error" == type || "fail" == type) {
|
|
4507
|
-
besOtaStatus = "FAILED"
|
|
4508
|
-
besOtaProgressVal = progress
|
|
4509
|
-
besOtaErrorMessage = bodyObj.optString("message", "BES update failed")
|
|
4510
|
-
lastBesOtaProgress = -1 // Reset for next OTA
|
|
4511
|
-
} else if ("update" == type) {
|
|
4512
|
-
besOtaStatus = "PROGRESS"
|
|
4513
|
-
besOtaProgressVal = progress
|
|
4514
4623
|
} else {
|
|
4515
|
-
|
|
4516
|
-
besOtaStatus = "PROGRESS"
|
|
4517
|
-
besOtaProgressVal = progress
|
|
4624
|
+
lastBesOtaProgress = besOtaProgressVal
|
|
4518
4625
|
}
|
|
4519
4626
|
|
|
4520
|
-
val syntheticStatus
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
// ota_status from the glasses — consumers mapping on this synthetic
|
|
4525
|
-
// status would otherwise never see a terminal state. Emit "complete"
|
|
4526
|
-
// for the final step; mid-session BES steps keep "step_complete" so
|
|
4527
|
-
// session-level trackers advance normally. Unknown sessions
|
|
4528
|
-
// (cachedOtaTotalSteps == 0, e.g. legacy glasses that never sent an
|
|
4529
|
-
// ota_status) conservatively keep "step_complete".
|
|
4530
|
-
syntheticStatus =
|
|
4531
|
-
if (cachedOtaTotalSteps > 0 &&
|
|
4532
|
-
cachedOtaCurrentStep >= cachedOtaTotalSteps
|
|
4533
|
-
)
|
|
4534
|
-
"complete"
|
|
4535
|
-
else "step_complete"
|
|
4536
|
-
} else if ("FAILED" == besOtaStatus) {
|
|
4537
|
-
syntheticStatus = "failed"
|
|
4538
|
-
} else {
|
|
4539
|
-
syntheticStatus = "in_progress"
|
|
4627
|
+
val syntheticStatus = when (besOtaStatus) {
|
|
4628
|
+
"FINISHED" -> "step_complete"
|
|
4629
|
+
"FAILED" -> "failed"
|
|
4630
|
+
else -> "in_progress"
|
|
4540
4631
|
}
|
|
4541
4632
|
val sid = if (cachedOtaSessionId != null) cachedOtaSessionId!! else ""
|
|
4542
4633
|
val totalSteps = if (cachedOtaTotalSteps > 0) cachedOtaTotalSteps else 1
|
|
@@ -5001,6 +5092,10 @@ class MentraLive : SGCManager() {
|
|
|
5001
5092
|
Bridge.log("LIVE: Skipping heartbeat - glasses not ready or not connected")
|
|
5002
5093
|
return
|
|
5003
5094
|
}
|
|
5095
|
+
if (besOtaHeartbeatGuard.shouldSuppress(SystemClock.elapsedRealtime())) {
|
|
5096
|
+
Bridge.log("LIVE: Skipping heartbeat while BES OTA owns the UART")
|
|
5097
|
+
return
|
|
5098
|
+
}
|
|
5004
5099
|
|
|
5005
5100
|
try {
|
|
5006
5101
|
// Send ping message (no ACK needed for heartbeats)
|
|
@@ -7570,10 +7665,22 @@ class MentraLive : SGCManager() {
|
|
|
7570
7665
|
* @param data The string data to be sent to the glasses
|
|
7571
7666
|
*/
|
|
7572
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
|
+
) {
|
|
7573
7676
|
if (data == null || data.isEmpty()) {
|
|
7574
7677
|
Log.e(TAG, "Cannot send empty data to glasses")
|
|
7575
7678
|
return
|
|
7576
7679
|
}
|
|
7680
|
+
if (sessionGeneration != bleSessionGeneration.get()) {
|
|
7681
|
+
Bridge.log("LIVE: Dropping send request from stale BLE session $sessionGeneration")
|
|
7682
|
+
return
|
|
7683
|
+
}
|
|
7577
7684
|
|
|
7578
7685
|
val wireData =
|
|
7579
7686
|
try {
|
|
@@ -7600,7 +7707,13 @@ class MentraLive : SGCManager() {
|
|
|
7600
7707
|
}
|
|
7601
7708
|
|
|
7602
7709
|
if (useBinaryWireProtocol && buildNumberInt >= 5) {
|
|
7603
|
-
sendDataToGlassesBinary(
|
|
7710
|
+
sendDataToGlassesBinary(
|
|
7711
|
+
wireData,
|
|
7712
|
+
wakeup,
|
|
7713
|
+
commandTraceInfo,
|
|
7714
|
+
isPhotoRequest,
|
|
7715
|
+
sessionGeneration
|
|
7716
|
+
)
|
|
7604
7717
|
return
|
|
7605
7718
|
}
|
|
7606
7719
|
|
|
@@ -7679,7 +7792,7 @@ class MentraLive : SGCManager() {
|
|
|
7679
7792
|
)
|
|
7680
7793
|
|
|
7681
7794
|
// Queue the chunk for sending
|
|
7682
|
-
queueData(packedData, trace)
|
|
7795
|
+
queueData(packedData, trace, sessionGeneration)
|
|
7683
7796
|
|
|
7684
7797
|
// Add small delay between chunks to avoid overwhelming the connection
|
|
7685
7798
|
if (i < chunks.size - 1) {
|
|
@@ -7720,7 +7833,7 @@ class MentraLive : SGCManager() {
|
|
|
7720
7833
|
)
|
|
7721
7834
|
|
|
7722
7835
|
// Queue the data for sending
|
|
7723
|
-
queueData(packedData, trace)
|
|
7836
|
+
queueData(packedData, trace, sessionGeneration)
|
|
7724
7837
|
if (isPhotoRequest) {
|
|
7725
7838
|
Bridge.log(
|
|
7726
7839
|
"LIVE: PHOTO PIPELINE BLE handoff — packedLen=" +
|
|
@@ -7738,7 +7851,8 @@ class MentraLive : SGCManager() {
|
|
|
7738
7851
|
data: String,
|
|
7739
7852
|
wakeup: Boolean,
|
|
7740
7853
|
commandTraceInfo: OutgoingBleCommandTraceInfo,
|
|
7741
|
-
isPhotoRequest: Boolean
|
|
7854
|
+
isPhotoRequest: Boolean,
|
|
7855
|
+
sessionGeneration: Long
|
|
7742
7856
|
) {
|
|
7743
7857
|
val payloadBytes = data.toByteArray(StandardCharsets.UTF_8)
|
|
7744
7858
|
var messageId = 0
|
|
@@ -7790,7 +7904,7 @@ class MentraLive : SGCManager() {
|
|
|
7790
7904
|
"wireProtocol" to "v2"
|
|
7791
7905
|
)
|
|
7792
7906
|
)
|
|
7793
|
-
queueData(packedData, trace)
|
|
7907
|
+
queueData(packedData, trace, sessionGeneration)
|
|
7794
7908
|
|
|
7795
7909
|
if (i < fragments.size - 1) {
|
|
7796
7910
|
try {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertFalse
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class BesOtaHeartbeatGuardTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun leaseSuppressesUntilDeadlineAndThenExpires() {
|
|
10
|
+
val guard = BesOtaHeartbeatGuard(120_000)
|
|
11
|
+
guard.refresh(1_000)
|
|
12
|
+
|
|
13
|
+
assertTrue(guard.shouldSuppress(120_999))
|
|
14
|
+
assertFalse(guard.shouldSuppress(121_000))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
fun progressRenewsLease() {
|
|
19
|
+
val guard = BesOtaHeartbeatGuard(120_000)
|
|
20
|
+
guard.refresh(1_000)
|
|
21
|
+
guard.refresh(60_000)
|
|
22
|
+
|
|
23
|
+
assertTrue(guard.shouldSuppress(121_000))
|
|
24
|
+
assertFalse(guard.shouldSuppress(180_000))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
fun terminalStatusClearsLeaseImmediately() {
|
|
29
|
+
val guard = BesOtaHeartbeatGuard(120_000)
|
|
30
|
+
guard.refresh(1_000)
|
|
31
|
+
guard.clear()
|
|
32
|
+
|
|
33
|
+
assertFalse(guard.shouldSuppress(1_001))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun besInstallStatusArmsLease() {
|
|
38
|
+
val guard = BesOtaHeartbeatGuard(120_000)
|
|
39
|
+
|
|
40
|
+
guard.observeOtaStatus("BES", "INSTALL", "in_progress", 1_000)
|
|
41
|
+
|
|
42
|
+
assertTrue(guard.shouldSuppress(120_999))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test
|
|
46
|
+
fun legacyBesInstallStatusUsesSameLease() {
|
|
47
|
+
val guard = BesOtaHeartbeatGuard(120_000)
|
|
48
|
+
|
|
49
|
+
guard.observeOtaStatus("bes", "install", "in_progress", 5_000)
|
|
50
|
+
|
|
51
|
+
assertTrue(guard.shouldSuppress(124_999))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
fun unrelatedStatusDoesNotClearActiveLease() {
|
|
56
|
+
val guard = BesOtaHeartbeatGuard(120_000)
|
|
57
|
+
guard.observeOtaStatus("bes", "install", "in_progress", 1_000)
|
|
58
|
+
|
|
59
|
+
guard.observeOtaStatus("apk", "install", "complete", 2_000)
|
|
60
|
+
|
|
61
|
+
assertTrue(guard.shouldSuppress(120_999))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test
|
|
65
|
+
fun besStepCompleteClearsLease() {
|
|
66
|
+
val guard = BesOtaHeartbeatGuard(120_000)
|
|
67
|
+
guard.observeOtaStatus("bes", "install", "in_progress", 1_000)
|
|
68
|
+
|
|
69
|
+
guard.observeOtaStatus("bes", "install", "step_complete", 2_000)
|
|
70
|
+
|
|
71
|
+
assertFalse(guard.shouldSuppress(2_001))
|
|
72
|
+
}
|
|
73
|
+
}
|