@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.
@@ -1297,6 +1297,7 @@ class MentraLive: NSObject, SGCManager {
1297
1297
  // a previous pairing into the next one (would otherwise surface as wrong overall_percent
1298
1298
  // or stale lastBesOtaProgress on the next OTA).
1299
1299
  if state == ConnTypes.DISCONNECTED {
1300
+ resetPendingAckState(reason: "BLE session ended")
1300
1301
  resetWireNegotiationState()
1301
1302
  resetAncsRelayState()
1302
1303
  // Queued writes are session-bound: transmitting them into the NEXT session
@@ -1452,6 +1453,7 @@ class MentraLive: NSObject, SGCManager {
1452
1453
  private let KEEP_ALIVE_INTERVAL_MS: UInt64 = 5_000_000_000 // 5 seconds
1453
1454
  private let CONNECTION_TIMEOUT_MS: UInt64 = 100_000_000_000 // 100 seconds
1454
1455
  private let HEARTBEAT_INTERVAL_MS: TimeInterval = 30.0 // 30 seconds
1456
+ private let BES_OTA_HEARTBEAT_LEASE_SEC: TimeInterval = 120.0
1455
1457
  private let BATTERY_REQUEST_EVERY_N_HEARTBEATS = 10
1456
1458
  private let SIGNAL_STRENGTH_READ_INTERVAL_MS: TimeInterval = 10.0
1457
1459
  private let MIN_SEND_DELAY_MS: UInt64 = 160_000_000 // 160ms in nanoseconds
@@ -1546,6 +1548,7 @@ class MentraLive: NSObject, SGCManager {
1546
1548
 
1547
1549
  /// BES OTA progress tracking - only send to UI on 5% increments
1548
1550
  private var lastBesOtaProgress = -1
1551
+ private var besOtaHeartbeatSuppressedUntil: TimeInterval = 0
1549
1552
 
1550
1553
  // Cached OTA session context from last ota_status — used to fill in session fields for sr_adota
1551
1554
  private var cachedOtaSessionId: String?
@@ -1941,6 +1944,18 @@ class MentraLive: NSObject, SGCManager {
1941
1944
  private var pending: PendingMessage?
1942
1945
  private var pendingMessageTimer: Timer?
1943
1946
 
1947
+ /// ACK state belongs to the BLE session that sent the message. If the link drops while an ACK
1948
+ /// is outstanding, keeping `pending` set permanently blocks the command queue because the
1949
+ /// timeout timer is stopped during disconnect. Release both together before reconnecting.
1950
+ private func resetPendingAckState(reason: String) {
1951
+ pendingMessageTimer?.invalidate()
1952
+ pendingMessageTimer = nil
1953
+
1954
+ guard let pendingMessage = pending else { return }
1955
+ pending = nil
1956
+ Bridge.log("LIVE: Cleared pending ACK mId \(pendingMessage.id) because \(reason)")
1957
+ }
1958
+
1944
1959
  actor CommandQueue {
1945
1960
  private var commands: [PendingMessage] = []
1946
1961
 
@@ -2669,6 +2684,7 @@ class MentraLive: NSObject, SGCManager {
2669
2684
  if let seq = json["sq"] as? [String] ?? json["step_sequence"] as? [String], !seq.isEmpty {
2670
2685
  cachedOtaStepSequence = seq
2671
2686
  }
2687
+ updateBesOtaHeartbeatGuard(stepType: osStepType, phase: osPhase, status: osStatus)
2672
2688
 
2673
2689
  Bridge.log("LIVE: 📱 OTA status - step \(osCurrentStep)/\(osTotalSteps) \(osPhase) \(osStatus) \(osOverallPercent)%")
2674
2690
 
@@ -2702,6 +2718,7 @@ class MentraLive: NSObject, SGCManager {
2702
2718
  } else {
2703
2719
  unified = "in_progress"
2704
2720
  }
2721
+ updateBesOtaHeartbeatGuard(stepType: currentUpdate, phase: legacyPhase, status: unified)
2705
2722
  Bridge.log(
2706
2723
  "LIVE: 📱 Legacy ota_progress → ota_status: \(legacyStage) \(legacyStatus) \(legacyProgress)%"
2707
2724
  )
@@ -2829,6 +2846,19 @@ class MentraLive: NSObject, SGCManager {
2829
2846
  cachedOtaCurrentStep = 0
2830
2847
  cachedOtaStepSequence = nil
2831
2848
  lastBesOtaProgress = -1
2849
+ // The bounded UART-ownership lease is transport state, not session-cache state. Keep it
2850
+ // through a BLE reconnect so heartbeats cannot resume while BES still owns the UART.
2851
+ }
2852
+
2853
+ private func updateBesOtaHeartbeatGuard(stepType: String, phase: String, status: String) {
2854
+ guard stepType.lowercased() == "bes", phase.lowercased() == "install" else { return }
2855
+ switch status.lowercased() {
2856
+ case "failed", "complete", "step_complete", "finished", "success":
2857
+ besOtaHeartbeatSuppressedUntil = 0
2858
+ default:
2859
+ besOtaHeartbeatSuppressedUntil =
2860
+ ProcessInfo.processInfo.systemUptime + BES_OTA_HEARTBEAT_LEASE_SEC
2861
+ }
2832
2862
  }
2833
2863
 
2834
2864
  /// Falls back to raw besProgress when step sequence is unavailable.
@@ -3000,62 +3030,41 @@ class MentraLive: NSObject, SGCManager {
3000
3030
  var progress = ((rawProgress + 2) / 5) * 5
3001
3031
  if progress > 100 { progress = 100 }
3002
3032
 
3003
- // Only send if progress changed to a new 5% increment
3004
3033
  let isTerminalStatus = type == "success" || type == "error" || type == "fail"
3005
- if progress == lastBesOtaProgress && !isTerminalStatus {
3006
- break // Skip duplicate progress
3007
- }
3008
- lastBesOtaProgress = progress
3009
-
3010
- Bridge.log(
3011
- "LIVE: 📱 BES OTA progress via sr_adota - type: \(type), raw: \(rawProgress)%, rounded: \(progress)%"
3012
- )
3013
-
3014
- // Determine status and error message based on type
3015
- var besOtaStatus: String
3016
3034
  var besOtaProgressVal: Int
3017
3035
  var besOtaErrorMessage: String? = nil
3018
3036
 
3019
- // Order matters here: check completion (rawProgress >= 100 OR success) BEFORE
3020
- // type=="update", because some BES firmware emits the final 100% tick with
3021
- // type=="update" rather than type=="success". Treating that as PROGRESS would
3022
- // leave the UI stuck at 100% forever.
3023
- if type == "success" || rawProgress >= 100 {
3024
- besOtaStatus = "FINISHED"
3037
+ let failed = type == "error" || type == "fail"
3038
+ let succeeded = type == "success"
3039
+ if isTerminalStatus {
3040
+ besOtaHeartbeatSuppressedUntil = 0
3041
+ } else {
3042
+ // Refresh before progress de-duplication: raw progress proves the BES
3043
+ // transfer is active even when the rounded UI value is unchanged.
3044
+ besOtaHeartbeatSuppressedUntil =
3045
+ ProcessInfo.processInfo.systemUptime + BES_OTA_HEARTBEAT_LEASE_SEC
3046
+ }
3047
+ if succeeded {
3025
3048
  besOtaProgressVal = 100
3026
- lastBesOtaProgress = -1 // Reset for next OTA
3027
- } else if type == "error" || type == "fail" {
3028
- besOtaStatus = "FAILED"
3049
+ } else if failed {
3029
3050
  besOtaProgressVal = progress
3030
3051
  besOtaErrorMessage = bodyObj["message"] as? String ?? "BES update failed"
3031
- lastBesOtaProgress = -1 // Reset for next OTA
3032
- } else if type == "update" {
3033
- besOtaStatus = "PROGRESS"
3034
- besOtaProgressVal = progress
3035
3052
  } else {
3036
- // Unknown type, treat as progress
3037
- besOtaStatus = "PROGRESS"
3038
- besOtaProgressVal = progress
3053
+ // A raw update:100 precedes whole-image CRC/apply and is not terminal.
3054
+ besOtaProgressVal = min(progress, 95)
3039
3055
  }
3040
3056
 
3041
- let syntheticStatus: String
3042
- if besOtaStatus == "FINISHED" {
3043
- // The glasses power-cycle right after the final BES tick, so a session
3044
- // whose BES step is the LAST step never gets a follow-up ota_status from
3045
- // the glasses — consumers mapping on this synthetic status would otherwise
3046
- // never see a terminal state. Emit "complete" for the final step;
3047
- // mid-session BES steps keep "step_complete" so session-level trackers
3048
- // advance normally. Unknown sessions (cachedOtaTotalSteps == 0, e.g.
3049
- // legacy glasses that never sent an ota_status) conservatively keep
3050
- // "step_complete".
3051
- syntheticStatus = (cachedOtaTotalSteps > 0 && cachedOtaCurrentStep >= cachedOtaTotalSteps)
3052
- ? "complete"
3053
- : "step_complete"
3054
- } else if besOtaStatus == "FAILED" {
3055
- syntheticStatus = "failed"
3056
- } else {
3057
- syntheticStatus = "in_progress"
3057
+ // Only send if nonterminal display progress changed to a new 5% increment.
3058
+ if besOtaProgressVal == lastBesOtaProgress && !isTerminalStatus {
3059
+ break
3058
3060
  }
3061
+ lastBesOtaProgress = isTerminalStatus ? -1 : besOtaProgressVal
3062
+
3063
+ Bridge.log(
3064
+ "LIVE: 📱 BES OTA progress via sr_adota - type: \(type), raw: \(rawProgress)%, rounded: \(progress)%, display: \(besOtaProgressVal)%"
3065
+ )
3066
+
3067
+ let syntheticStatus = succeeded ? "step_complete" : (failed ? "failed" : "in_progress")
3059
3068
  let sid = cachedOtaSessionId ?? ""
3060
3069
  let totalSteps = cachedOtaTotalSteps > 0 ? cachedOtaTotalSteps : 1
3061
3070
  let currentStep = cachedOtaCurrentStep > 0 ? cachedOtaCurrentStep : 1
@@ -5068,6 +5077,10 @@ class MentraLive: NSObject, SGCManager {
5068
5077
  Bridge.log("LIVE: Skipping heartbeat - glasses not fully booted or not connected")
5069
5078
  return
5070
5079
  }
5080
+ if ProcessInfo.processInfo.systemUptime < besOtaHeartbeatSuppressedUntil {
5081
+ Bridge.log("LIVE: Skipping heartbeat while BES OTA owns the UART")
5082
+ return
5083
+ }
5071
5084
 
5072
5085
  // Send ping message to glasses hardware (no ACK needed for heartbeats)
5073
5086
  let pingJson: [String: Any] = ["type": "ping"]
@@ -5181,8 +5194,7 @@ class MentraLive: NSObject, SGCManager {
5181
5194
  stopReadinessCheckLoop()
5182
5195
  stopConnectionTimeout()
5183
5196
  stopMicBeat() // Stop LC3 audio micbeat
5184
- pendingMessageTimer?.invalidate()
5185
- pendingMessageTimer = nil
5197
+ resetPendingAckState(reason: "transport timers stopped")
5186
5198
  reconnectionWorkItem?.cancel()
5187
5199
  reconnectionWorkItem = nil
5188
5200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/bluetooth-sdk",
3
- "version": "0.1.21-beta.3",
3
+ "version": "0.1.21-beta.5",
4
4
  "description": "SDK for communicating with smart glasses",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -1201,6 +1201,8 @@ export interface OtaUpdateInfo {
1201
1201
  updates: string[] // ["apk", "mtk", "bes"]
1202
1202
  totalSize: number
1203
1203
  cacheReady?: boolean
1204
+ /** Exact BES target selected from the OTA manifest, when a BES step is pending. */
1205
+ besVersion?: string
1204
1206
  /** True when the APK step installs an older build than the glasses currently run (exact-pin manifests only). */
1205
1207
  isDowngrade?: boolean
1206
1208
  }