@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
|
@@ -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
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
3037
|
-
|
|
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
|
-
|
|
3042
|
-
if
|
|
3043
|
-
|
|
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
|
-
|
|
5185
|
-
pendingMessageTimer = nil
|
|
5197
|
+
resetPendingAckState(reason: "transport timers stopped")
|
|
5186
5198
|
reconnectionWorkItem?.cancel()
|
|
5187
5199
|
reconnectionWorkItem = nil
|
|
5188
5200
|
}
|
package/package.json
CHANGED
|
@@ -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
|
}
|