@mentra/engine 3.1.0-dev.7 → 3.1.0-dev.70

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.
Files changed (103) hide show
  1. package/README.md +23 -5
  2. package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
  3. package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
  4. package/build/bluetooth-sdk/ota-transport.js +2 -0
  5. package/build/bluetooth-sdk/ota-transport.js.map +1 -0
  6. package/build/engine.d.ts +5 -2
  7. package/build/engine.d.ts.map +1 -1
  8. package/build/facades/ota.d.ts +10 -6
  9. package/build/facades/ota.d.ts.map +1 -1
  10. package/build/facades/ota.js +7 -4
  11. package/build/facades/ota.js.map +1 -1
  12. package/build/facades/pairing.d.ts +4 -0
  13. package/build/facades/pairing.d.ts.map +1 -1
  14. package/build/facades/pairing.js +81 -7
  15. package/build/facades/pairing.js.map +1 -1
  16. package/build/generated/releaseMetadata.js +5 -5
  17. package/build/generated/releaseMetadata.js.map +1 -1
  18. package/build/index.d.ts +1 -1
  19. package/build/index.d.ts.map +1 -1
  20. package/build/index.js.map +1 -1
  21. package/build/react/MentraLiveOtaFlow.d.ts +8 -2
  22. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  23. package/build/react/MentraLiveOtaFlow.js +249 -354
  24. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  25. package/build/react/index.d.ts +1 -0
  26. package/build/react/index.d.ts.map +1 -1
  27. package/build/react/index.js +1 -0
  28. package/build/react/index.js.map +1 -1
  29. package/build/react/useMentraLiveOta.d.ts +84 -0
  30. package/build/react/useMentraLiveOta.d.ts.map +1 -0
  31. package/build/react/useMentraLiveOta.js +616 -0
  32. package/build/react/useMentraLiveOta.js.map +1 -0
  33. package/build/services/BlobStore.d.ts +2 -0
  34. package/build/services/BlobStore.d.ts.map +1 -1
  35. package/build/services/BlobStore.js +63 -10
  36. package/build/services/BlobStore.js.map +1 -1
  37. package/build/services/GlassesStatusProjection.d.ts +15 -2
  38. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  39. package/build/services/GlassesStatusProjection.js +3 -3
  40. package/build/services/GlassesStatusProjection.js.map +1 -1
  41. package/build/services/HotspotOtaTransport.d.ts.map +1 -1
  42. package/build/services/HotspotOtaTransport.js +8 -7
  43. package/build/services/HotspotOtaTransport.js.map +1 -1
  44. package/build/services/OtaAutoChain.d.ts +9 -2
  45. package/build/services/OtaAutoChain.d.ts.map +1 -1
  46. package/build/services/OtaAutoChain.js +10 -2
  47. package/build/services/OtaAutoChain.js.map +1 -1
  48. package/build/services/OtaInstallCoordinator.d.ts +24 -3
  49. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  50. package/build/services/OtaInstallCoordinator.js +14 -12
  51. package/build/services/OtaInstallCoordinator.js.map +1 -1
  52. package/build/services/OtaService.d.ts.map +1 -1
  53. package/build/services/OtaService.js +1 -1
  54. package/build/services/OtaService.js.map +1 -1
  55. package/build/services/OtaUpdateCheckService.d.ts +4 -1
  56. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  57. package/build/services/OtaUpdateCheckService.js +21 -5
  58. package/build/services/OtaUpdateCheckService.js.map +1 -1
  59. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  60. package/build/services/asg/localNetworkTransport.js +13 -12
  61. package/build/services/asg/localNetworkTransport.js.map +1 -1
  62. package/build/services/glassesClockSync.js +1 -1
  63. package/build/services/glassesClockSync.js.map +1 -1
  64. package/build/services/otaReleaseVersion.d.ts +10 -0
  65. package/build/services/otaReleaseVersion.d.ts.map +1 -0
  66. package/build/services/otaReleaseVersion.js +14 -0
  67. package/build/services/otaReleaseVersion.js.map +1 -0
  68. package/build/stores/appVisibility.d.ts +3 -0
  69. package/build/stores/appVisibility.d.ts.map +1 -0
  70. package/build/stores/appVisibility.js +3 -0
  71. package/build/stores/appVisibility.js.map +1 -0
  72. package/build/stores/apps.d.ts.map +1 -1
  73. package/build/stores/apps.js +7 -1
  74. package/build/stores/apps.js.map +1 -1
  75. package/build/stores/core.d.ts +3 -3
  76. package/build/stores/core.d.ts.map +1 -1
  77. package/build/stores/core.js.map +1 -1
  78. package/build/stores/glasses.d.ts +10 -4
  79. package/build/stores/glasses.d.ts.map +1 -1
  80. package/build/stores/glasses.js.map +1 -1
  81. package/package.json +13 -6
  82. package/src/bluetooth-sdk/ota-transport.ts +1 -0
  83. package/src/facades/ota.ts +17 -5
  84. package/src/facades/pairing.ts +88 -12
  85. package/src/generated/releaseMetadata.ts +5 -5
  86. package/src/index.ts +1 -0
  87. package/src/react/MentraLiveOtaFlow.tsx +370 -473
  88. package/src/react/index.ts +14 -0
  89. package/src/react/useMentraLiveOta.ts +781 -0
  90. package/src/services/BlobStore.ts +57 -10
  91. package/src/services/GlassesStatusProjection.ts +7 -6
  92. package/src/services/HotspotOtaTransport.ts +8 -7
  93. package/src/services/OtaAutoChain.ts +26 -2
  94. package/src/services/OtaInstallCoordinator.ts +16 -13
  95. package/src/services/OtaService.ts +1 -2
  96. package/src/services/OtaUpdateCheckService.ts +26 -8
  97. package/src/services/asg/localNetworkTransport.ts +20 -19
  98. package/src/services/glassesClockSync.ts +1 -1
  99. package/src/services/otaReleaseVersion.ts +22 -0
  100. package/src/stores/appVisibility.ts +3 -0
  101. package/src/stores/apps.ts +7 -1
  102. package/src/stores/core.ts +4 -4
  103. package/src/stores/glasses.ts +13 -6
@@ -0,0 +1,781 @@
1
+ import {useCallback, useEffect, useMemo, useRef, useState} from "react"
2
+
3
+ import {ota} from "../facades/ota"
4
+ import {
5
+ beginOtaAutoChain,
6
+ clearOtaAutoChainReconnectWait,
7
+ isOtaAutoChainActive,
8
+ otaAutoChainFingerprint,
9
+ otaAutoChainReleaseRange,
10
+ otaAutoChainReconnectWaitRemaining,
11
+ stopOtaAutoChain,
12
+ tryAdvanceOtaAutoChain,
13
+ type OtaAutoChainReleaseRange,
14
+ } from "../services/OtaAutoChain"
15
+ import {
16
+ BES_INSTALL_RESTART_MESSAGE,
17
+ getOtaErrorMessage,
18
+ shouldRequireGlassesRebootForBesFailure,
19
+ shouldShowChangeWifiForOtaDownloadFailure,
20
+ } from "../services/OtaErrorMapping"
21
+ import type {OtaInstallSnapshot} from "../services/OtaInstallCoordinator"
22
+ import type {OtaCheckCurrentGlassesResult} from "../services/OtaUpdateCheckService"
23
+ import type {ReleaseChangelog} from "../facades/ota"
24
+ import {useEngineSnapshot} from "./useEngineSnapshot"
25
+
26
+ export type MentraLiveOtaFlowPage = "check" | "progress"
27
+
28
+ export type MentraLiveOtaScreen =
29
+ | "initializing"
30
+ | "checking"
31
+ | "finishing"
32
+ | "update_available"
33
+ | "battery_required"
34
+ | "wifi_required"
35
+ | "up_to_date"
36
+ | "dev_build"
37
+ | "check_failed"
38
+ | "update_info_unavailable"
39
+ | "starting"
40
+ | "preparing_hotspot"
41
+ | "updating"
42
+ | "restarting"
43
+ | "verifying"
44
+ | "complete"
45
+ | "failed"
46
+ | "disconnected"
47
+
48
+ export type MentraLiveOtaErrorCode =
49
+ | "check_failed"
50
+ | "update_info_unavailable"
51
+ | "install_failed"
52
+ | "bes_restart_required"
53
+
54
+ export type MentraLiveOtaError = {
55
+ code: MentraLiveOtaErrorCode
56
+ message: string
57
+ }
58
+
59
+ export type MentraLiveOtaTransport = "wifi" | "hotspot"
60
+ export type MentraLiveOtaHotspotPhase = "idle" | "downloading" | "starting_hotspot" | "joining_hotspot" | "serving"
61
+ export type MentraLiveOtaInstallPhase = "download" | "install"
62
+ export type MentraLiveOtaStep = "apk" | "mtk" | "bes"
63
+
64
+ export type MentraLiveOtaReleaseTransition = {
65
+ /** Current glasses software label. Temporarily backed by the reported ASG app version. */
66
+ fromVersion: string | null
67
+ /** Exact coordinated release identity for the selected OTA pin. */
68
+ toVersion: string
69
+ }
70
+
71
+ export type MentraLiveOtaState = {
72
+ screen: MentraLiveOtaScreen
73
+ connected: boolean
74
+ batteryLevel: number | null
75
+ transport: MentraLiveOtaTransport | null
76
+ updateRequired: boolean
77
+ versionChange: boolean
78
+ versionChangeConverged: boolean
79
+ versionChangePhase: "installing" | "restarting" | "verifying" | null
80
+ wifiConnected: boolean
81
+ wifiStatusKnown: boolean
82
+ hotspotSupported: boolean
83
+ hotspotPhase: MentraLiveOtaHotspotPhase
84
+ hotspotArtifactPercent: number | null
85
+ phase: MentraLiveOtaInstallPhase | null
86
+ step: MentraLiveOtaStep | null
87
+ currentStep: number | null
88
+ totalSteps: number | null
89
+ progress: number | null
90
+ installingApkOnly: boolean
91
+ firmwareRestarting: boolean
92
+ error: MentraLiveOtaError | null
93
+ canInstall: boolean
94
+ canRetry: boolean
95
+ canFinish: boolean
96
+ canDismiss: boolean
97
+ canDiscard: boolean
98
+ canOpenWifiSetup: boolean
99
+ continueDisabled: boolean
100
+ /** True only after an approved update session reaches a final no-update check. */
101
+ completedUpdate: boolean
102
+ /** Release labels for the offered or just-completed coordinated update. */
103
+ releaseTransition: MentraLiveOtaReleaseTransition | null
104
+ /** Release notes crossed by this update, newest first. Populated on completion. */
105
+ changelogs: ReleaseChangelog[]
106
+ }
107
+
108
+ export type UseMentraLiveOtaOptions = {
109
+ /** Entry page. `progress` exists for interrupted-session recovery. */
110
+ initialPage?: MentraLiveOtaFlowPage
111
+ /** Start Engine's OTA-only projections. Full Engine hosts should pass false. */
112
+ initializeRuntime?: boolean
113
+ /** Called after the final check or when an optional update is dismissed. */
114
+ onFinished?: () => void
115
+ /** Host-owned Wi-Fi setup for glasses without hotspot OTA support. */
116
+ onOpenWifiSetup?: () => void
117
+ /** Lets a host coordinate its connection overlay with OTA firmware restarts. */
118
+ onFirmwareRestartingChange?: (restarting: boolean, progressActive: boolean) => void
119
+ }
120
+
121
+ export type MentraLiveOtaController = {
122
+ state: MentraLiveOtaState
123
+ check: () => void
124
+ retryCheck: () => void
125
+ install: () => void
126
+ retryInstall: () => void
127
+ finish: () => void
128
+ discard: () => void
129
+ openWifiSetup: () => void
130
+ }
131
+
132
+ type CheckState = "checking" | "update_available" | "no_update" | "dev_build" | "error"
133
+
134
+ const AUTO_CHAIN_NETWORK_RETRY_DELAY_MS = 5000
135
+ const AUTO_CHAIN_COMPLETE_DELAY_MS = 750
136
+ export const MINIMUM_OTA_BATTERY_LEVEL = 25
137
+
138
+ function releaseRangeTargetVersion(result: OtaCheckCurrentGlassesResult): string | null {
139
+ return result.releaseVersion ?? result.updateInfo?.versionName ?? result.latestVersionInfo?.versionName ?? null
140
+ }
141
+
142
+ function currentReleaseVersion(appVersion: string | null): string | null {
143
+ const version = appVersion?.trim()
144
+ return version || null
145
+ }
146
+
147
+ function releaseTransitionFromRange(range: OtaAutoChainReleaseRange | null): MentraLiveOtaReleaseTransition | null {
148
+ if (!range?.releaseVersion) return null
149
+ return {fromVersion: range.fromVersion, toVersion: range.releaseVersion}
150
+ }
151
+
152
+ function releaseChangelogsForActiveChain(): ReleaseChangelog[] {
153
+ const range = otaAutoChainReleaseRange()
154
+ if (!range?.toVersion) return []
155
+ try {
156
+ return ota.getReleaseChangelogs(range.fromVersion, range.toVersion)
157
+ } catch {
158
+ try {
159
+ return ota.getReleaseChangelogs(null, range.toVersion)
160
+ } catch {
161
+ return []
162
+ }
163
+ }
164
+ }
165
+
166
+ function installProgress(snapshot: OtaInstallSnapshot): number | null {
167
+ if (snapshot.displayState !== "updating") return null
168
+ const {otaStatus} = snapshot
169
+ const isDownload = otaStatus?.phase === "download"
170
+ const totalSteps = otaStatus?.totalSteps ?? 1
171
+ const rawPercent = isDownload
172
+ ? (otaStatus?.stepPercent ?? 0)
173
+ : totalSteps >= 2
174
+ ? (otaStatus?.overallPercent ?? 0)
175
+ : (otaStatus?.stepPercent ?? 0)
176
+ return Math.min(Math.max(rawPercent, snapshot.mtkInstallStallSimulatedPercent ?? 0, 0), 100)
177
+ }
178
+
179
+ function progressScreen(snapshot: OtaInstallSnapshot): MentraLiveOtaScreen {
180
+ if (snapshot.versionChangePhase === "restarting") return "restarting"
181
+ if (snapshot.versionChangePhase === "verifying") return "verifying"
182
+ switch (snapshot.displayState) {
183
+ case "starting":
184
+ return snapshot.hotspotPhase === "idle" || snapshot.hotspotPhase === "serving" ? "starting" : "preparing_hotspot"
185
+ case "updating":
186
+ return "updating"
187
+ case "restarting":
188
+ return "restarting"
189
+ case "complete":
190
+ return "complete"
191
+ case "failed":
192
+ return "failed"
193
+ default:
194
+ return "disconnected"
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Headless Mentra Live OTA flow. Engine owns checking, hotspot staging,
200
+ * APK/MTK/BES sequencing, restart recovery, retries, and verification; the host
201
+ * renders semantic state and invokes these idempotent actions.
202
+ */
203
+ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraLiveOtaController {
204
+ const {
205
+ initialPage = "check",
206
+ initializeRuntime = true,
207
+ onFinished,
208
+ onFirmwareRestartingChange,
209
+ onOpenWifiSetup,
210
+ } = options
211
+ const otaSnapshot = useEngineSnapshot(ota.snapshot, ota.onSnapshot)
212
+ const installSnapshot = useEngineSnapshot(ota.installSession.snapshot, ota.installSession.onSnapshot)
213
+ const [page, setPage] = useState<MentraLiveOtaFlowPage>(initialPage)
214
+ const [runtimeReady, setRuntimeReady] = useState(!initializeRuntime)
215
+ const [checkState, setCheckState] = useState<CheckState>("checking")
216
+ const [isUpdateRequired, setIsUpdateRequired] = useState(true)
217
+ const [isVersionChange, setIsVersionChange] = useState(false)
218
+ const [errorKind, setErrorKind] = useState<"network" | "pin_unavailable">("network")
219
+ const [updateFingerprint, setUpdateFingerprint] = useState<string | null>(null)
220
+ const [offeredReleaseTransition, setOfferedReleaseTransition] = useState<MentraLiveOtaReleaseTransition | null>(null)
221
+ const [completedReleaseTransition, setCompletedReleaseTransition] = useState<MentraLiveOtaReleaseTransition | null>(
222
+ null,
223
+ )
224
+ const [completedUpdate, setCompletedUpdate] = useState(false)
225
+ const [completedChangelogs, setCompletedChangelogs] = useState<ReleaseChangelog[]>([])
226
+ const [batteryBlocked, setBatteryBlocked] = useState(false)
227
+ const [checkGeneration, setCheckGeneration] = useState(0)
228
+ const performCheckGenerationRef = useRef(0)
229
+ const activeCheckKeyRef = useRef<string | null>(null)
230
+ const checkStartedRef = useRef(false)
231
+ const checkCompletedRef = useRef(false)
232
+ const selectedCheckResultRef = useRef<OtaCheckCurrentGlassesResult | null>(null)
233
+ const autoChainAdvancedRef = useRef(false)
234
+ const installActionPendingRef = useRef(false)
235
+ const onFinishedRef = useRef(onFinished)
236
+ const onOpenWifiSetupRef = useRef(onOpenWifiSetup)
237
+ const onFirmwareRestartingChangeRef = useRef(onFirmwareRestartingChange)
238
+ onFinishedRef.current = onFinished
239
+ onOpenWifiSetupRef.current = onOpenWifiSetup
240
+ onFirmwareRestartingChangeRef.current = onFirmwareRestartingChange
241
+
242
+ useEffect(() => {
243
+ if (!initializeRuntime) {
244
+ setRuntimeReady(true)
245
+ return
246
+ }
247
+ let cancelled = false
248
+ setRuntimeReady(false)
249
+ void ota.initialize().finally(() => {
250
+ if (!cancelled) setRuntimeReady(true)
251
+ })
252
+ return () => {
253
+ cancelled = true
254
+ }
255
+ }, [initializeRuntime])
256
+
257
+ const returnToCheck = useCallback(() => {
258
+ installActionPendingRef.current = false
259
+ autoChainAdvancedRef.current = false
260
+ setCheckState("checking")
261
+ setPage("check")
262
+ setCheckGeneration((generation) => generation + 1)
263
+ }, [])
264
+
265
+ const navigateToProgress = useCallback(() => {
266
+ installActionPendingRef.current = true
267
+ ota.clearProgress()
268
+ setPage("progress")
269
+ }, [])
270
+
271
+ const continueApprovedChain = useCallback(
272
+ (result: OtaCheckCurrentGlassesResult): boolean => {
273
+ if (installActionPendingRef.current || !isOtaAutoChainActive() || !result.updateInfo) return false
274
+ const snapshot = ota.snapshot()
275
+ if (!snapshot.wifiStatusKnown || (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1)) return false
276
+
277
+ const admission = tryAdvanceOtaAutoChain(
278
+ otaAutoChainFingerprint(result),
279
+ result.updateInfo.isDowngrade === true,
280
+ releaseRangeTargetVersion(result),
281
+ result.releaseVersion,
282
+ )
283
+ if (!admission.advance) return false
284
+
285
+ checkCompletedRef.current = true
286
+ ota.installSession.prepare(result)
287
+ navigateToProgress()
288
+ return true
289
+ },
290
+ [navigateToProgress],
291
+ )
292
+
293
+ useEffect(() => {
294
+ if (!runtimeReady || page !== "check") return
295
+ const MIN_DISPLAY_TIME_MS = 1100
296
+ const MAX_WAIT_FOR_VERSION_INFO_MS = 10_000
297
+ const checkKey = String(checkGeneration)
298
+ if (activeCheckKeyRef.current !== checkKey) {
299
+ activeCheckKeyRef.current = checkKey
300
+ checkStartedRef.current = false
301
+ checkCompletedRef.current = false
302
+ selectedCheckResultRef.current = null
303
+ installActionPendingRef.current = false
304
+ setCheckState("checking")
305
+ }
306
+
307
+ const myGeneration = ++performCheckGenerationRef.current
308
+ let cancelled = false
309
+ let reconnectTimeout: ReturnType<typeof setTimeout> | null = null
310
+
311
+ const performCheck = async () => {
312
+ if (checkCompletedRef.current) return
313
+ if (!otaSnapshot.connected) {
314
+ if (isOtaAutoChainActive()) {
315
+ const remainingMs = otaAutoChainReconnectWaitRemaining()
316
+ if (remainingMs === null) return
317
+ reconnectTimeout = setTimeout(() => {
318
+ if (cancelled || myGeneration !== performCheckGenerationRef.current || ota.snapshot().connected) return
319
+ stopOtaAutoChain()
320
+ checkCompletedRef.current = true
321
+ setCheckState("error")
322
+ }, remainingMs)
323
+ return
324
+ }
325
+ if (checkStartedRef.current) {
326
+ stopOtaAutoChain()
327
+ checkCompletedRef.current = true
328
+ setCheckState("error")
329
+ return
330
+ }
331
+ checkCompletedRef.current = true
332
+ onFinishedRef.current?.()
333
+ return
334
+ }
335
+
336
+ clearOtaAutoChainReconnectWait()
337
+ checkStartedRef.current = true
338
+ const startTime = Date.now()
339
+ try {
340
+ const checkOptions = {
341
+ waitForBuildNumberMs: MAX_WAIT_FOR_VERSION_INFO_MS,
342
+ waitForBesVersionMs: 5000,
343
+ waitForMtkVersionMs: 2000,
344
+ refreshVersionInfo: true,
345
+ fixClockBeforeCheck: false,
346
+ }
347
+ let result = await ota.checkForUpdates(checkOptions)
348
+ if (cancelled || myGeneration !== performCheckGenerationRef.current) return
349
+ if (!result.hasCheckCompleted && result.checkFailureReason === "network" && isOtaAutoChainActive()) {
350
+ await new Promise((resolve) => setTimeout(resolve, AUTO_CHAIN_NETWORK_RETRY_DELAY_MS))
351
+ if (cancelled || myGeneration !== performCheckGenerationRef.current) return
352
+ result = await ota.checkForUpdates(checkOptions)
353
+ }
354
+ if (cancelled || myGeneration !== performCheckGenerationRef.current) return
355
+ selectedCheckResultRef.current = result
356
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, MIN_DISPLAY_TIME_MS - (Date.now() - startTime))))
357
+ if (cancelled || myGeneration !== performCheckGenerationRef.current) return
358
+
359
+ if (result.skippedReason === "disconnected") {
360
+ stopOtaAutoChain()
361
+ checkCompletedRef.current = true
362
+ setCheckState("error")
363
+ return
364
+ }
365
+ if (result.skippedReason === "missing_build") {
366
+ checkCompletedRef.current = true
367
+ if (isOtaAutoChainActive()) {
368
+ stopOtaAutoChain()
369
+ setCheckState("error")
370
+ } else {
371
+ onFinishedRef.current?.()
372
+ }
373
+ return
374
+ }
375
+ if (result.skippedReason === "dev_build") {
376
+ stopOtaAutoChain()
377
+ checkCompletedRef.current = true
378
+ ota.clearUpdateAvailable()
379
+ setCheckState("dev_build")
380
+ return
381
+ }
382
+ if (!result.hasCheckCompleted) {
383
+ stopOtaAutoChain()
384
+ checkCompletedRef.current = true
385
+ setErrorKind(result.checkFailureReason === "pin_unavailable" ? "pin_unavailable" : "network")
386
+ setCheckState("error")
387
+ return
388
+ }
389
+ if (result.updateAvailable && result.updateInfo) {
390
+ setIsUpdateRequired(result.isRequired)
391
+ setIsVersionChange(result.updateInfo.isDowngrade === true)
392
+ const fingerprint = otaAutoChainFingerprint(result)
393
+ setUpdateFingerprint(fingerprint)
394
+ if (!isOtaAutoChainActive()) {
395
+ const fromVersion = currentReleaseVersion(ota.snapshot().appVersion)
396
+ setOfferedReleaseTransition(result.releaseVersion ? {fromVersion, toVersion: result.releaseVersion} : null)
397
+ }
398
+ if (isOtaAutoChainActive() && !ota.snapshot().wifiStatusKnown) {
399
+ checkCompletedRef.current = true
400
+ return
401
+ }
402
+ if (continueApprovedChain(result)) return
403
+ checkCompletedRef.current = true
404
+ setCheckState("update_available")
405
+ return
406
+ }
407
+ const completedApprovedSession = isOtaAutoChainActive()
408
+ if (completedApprovedSession) {
409
+ setCompletedChangelogs(releaseChangelogsForActiveChain())
410
+ setCompletedReleaseTransition(releaseTransitionFromRange(otaAutoChainReleaseRange()))
411
+ }
412
+ setCompletedUpdate(completedApprovedSession)
413
+ stopOtaAutoChain()
414
+ checkCompletedRef.current = true
415
+ ota.clearUpdateAvailable()
416
+ setCheckState("no_update")
417
+ } catch (error) {
418
+ console.error("OTA check failed:", error)
419
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, MIN_DISPLAY_TIME_MS - (Date.now() - startTime))))
420
+ if (cancelled || myGeneration !== performCheckGenerationRef.current) return
421
+ stopOtaAutoChain()
422
+ checkCompletedRef.current = true
423
+ setErrorKind("network")
424
+ setCheckState("error")
425
+ }
426
+ }
427
+
428
+ void performCheck()
429
+ return () => {
430
+ cancelled = true
431
+ if (reconnectTimeout) clearTimeout(reconnectTimeout)
432
+ }
433
+ }, [checkGeneration, continueApprovedChain, otaSnapshot.connected, otaSnapshot.wifiStatusKnown, page, runtimeReady])
434
+
435
+ useEffect(() => {
436
+ if (
437
+ !runtimeReady ||
438
+ page !== "check" ||
439
+ (checkState !== "checking" && checkState !== "update_available") ||
440
+ !otaSnapshot.wifiStatusKnown ||
441
+ !isOtaAutoChainActive()
442
+ ) {
443
+ return
444
+ }
445
+ const result = selectedCheckResultRef.current
446
+ if (!result?.updateAvailable || !result.updateInfo) return
447
+ if (!otaSnapshot.wifiConnected && otaSnapshot.hotspotOtaVersion !== 1) {
448
+ setCheckState("update_available")
449
+ return
450
+ }
451
+ continueApprovedChain(result)
452
+ }, [
453
+ checkState,
454
+ continueApprovedChain,
455
+ otaSnapshot.hotspotOtaVersion,
456
+ otaSnapshot.wifiConnected,
457
+ otaSnapshot.wifiStatusKnown,
458
+ page,
459
+ runtimeReady,
460
+ ])
461
+
462
+ useEffect(() => {
463
+ if (!runtimeReady || page !== "progress") return
464
+ ota.installSession.attach()
465
+ return () => ota.installSession.detach()
466
+ }, [page, runtimeReady])
467
+
468
+ const firmwareRestarting =
469
+ page === "progress" &&
470
+ ((!installSnapshot.connected && installSnapshot.displayState === "restarting") ||
471
+ installSnapshot.versionChangePhase === "restarting")
472
+
473
+ useEffect(() => {
474
+ if (page !== "progress") return
475
+ onFirmwareRestartingChangeRef.current?.(firmwareRestarting, true)
476
+ }, [firmwareRestarting, page])
477
+
478
+ useEffect(() => {
479
+ if (page !== "progress") return
480
+ return () => {
481
+ onFirmwareRestartingChangeRef.current?.(false, false)
482
+ }
483
+ }, [page])
484
+
485
+ useEffect(() => {
486
+ if (
487
+ page !== "progress" ||
488
+ installSnapshot.displayState !== "complete" ||
489
+ !installSnapshot.connected ||
490
+ !isOtaAutoChainActive() ||
491
+ autoChainAdvancedRef.current
492
+ ) {
493
+ return
494
+ }
495
+ const timeout = setTimeout(async () => {
496
+ if (!isOtaAutoChainActive()) return
497
+ autoChainAdvancedRef.current = true
498
+ await ota.installSession.finish()
499
+ returnToCheck()
500
+ }, AUTO_CHAIN_COMPLETE_DELAY_MS)
501
+ return () => clearTimeout(timeout)
502
+ }, [installSnapshot.connected, installSnapshot.displayState, page, returnToCheck])
503
+
504
+ const check = useCallback(() => {
505
+ setBatteryBlocked(false)
506
+ setOfferedReleaseTransition(null)
507
+ setCompletedReleaseTransition(null)
508
+ setCompletedUpdate(false)
509
+ setCompletedChangelogs([])
510
+ setPage("check")
511
+ setCheckState("checking")
512
+ setCheckGeneration((generation) => generation + 1)
513
+ }, [])
514
+
515
+ const install = useCallback(() => {
516
+ if (installActionPendingRef.current || page !== "check") return
517
+ const result = selectedCheckResultRef.current
518
+ if (!result) {
519
+ setErrorKind("network")
520
+ setCheckState("error")
521
+ return
522
+ }
523
+ const snapshot = ota.snapshot()
524
+ if (snapshot.batteryLevel !== null && snapshot.batteryLevel < MINIMUM_OTA_BATTERY_LEVEL) {
525
+ setBatteryBlocked(true)
526
+ return
527
+ }
528
+ if (!snapshot.wifiStatusKnown) {
529
+ check()
530
+ return
531
+ }
532
+ if (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1) {
533
+ onOpenWifiSetupRef.current?.()
534
+ return
535
+ }
536
+ ota.installSession.prepare(result)
537
+ setBatteryBlocked(false)
538
+ setCompletedUpdate(false)
539
+ setCompletedChangelogs([])
540
+ if (updateFingerprint) {
541
+ const fromVersion = offeredReleaseTransition
542
+ ? offeredReleaseTransition.fromVersion
543
+ : currentReleaseVersion(snapshot.appVersion)
544
+ beginOtaAutoChain(updateFingerprint, isVersionChange, {
545
+ fromVersion,
546
+ toVersion: releaseRangeTargetVersion(result),
547
+ releaseVersion: result.releaseVersion,
548
+ })
549
+ }
550
+ navigateToProgress()
551
+ }, [check, isVersionChange, navigateToProgress, offeredReleaseTransition, page, updateFingerprint])
552
+
553
+ useEffect(() => {
554
+ if (
555
+ batteryBlocked &&
556
+ (otaSnapshot.batteryLevel === null || otaSnapshot.batteryLevel >= MINIMUM_OTA_BATTERY_LEVEL)
557
+ ) {
558
+ setBatteryBlocked(false)
559
+ }
560
+ }, [batteryBlocked, otaSnapshot.batteryLevel])
561
+
562
+ const retryInstall = useCallback(() => {
563
+ if (page !== "progress") return
564
+ onFirmwareRestartingChangeRef.current?.(false, true)
565
+ ota.installSession.retry()
566
+ }, [page])
567
+
568
+ const finish = useCallback(async () => {
569
+ if (page === "check") {
570
+ onFinishedRef.current?.()
571
+ return
572
+ }
573
+ if (installSnapshot.displayState === "restarting") stopOtaAutoChain()
574
+ const requiresGlassesReboot = shouldRequireGlassesRebootForBesFailure(
575
+ installSnapshot.otaStatus,
576
+ installSnapshot.otaProgress,
577
+ installSnapshot.errorMsg,
578
+ )
579
+ if (requiresGlassesReboot) stopOtaAutoChain()
580
+ await ota.installSession.finish()
581
+ returnToCheck()
582
+ }, [installSnapshot, page, returnToCheck])
583
+
584
+ const discard = useCallback(async () => {
585
+ if (page !== "progress") return
586
+ stopOtaAutoChain()
587
+ await ota.installSession.discard()
588
+ returnToCheck()
589
+ }, [page, returnToCheck])
590
+
591
+ const openWifiSetup = useCallback(() => {
592
+ if (page === "progress") stopOtaAutoChain()
593
+ onOpenWifiSetupRef.current?.()
594
+ }, [page])
595
+
596
+ const state = useMemo<MentraLiveOtaState>(() => {
597
+ const hotspotSupported = otaSnapshot.hotspotOtaVersion === 1
598
+ const canInstall = otaSnapshot.wifiStatusKnown && (otaSnapshot.wifiConnected || hotspotSupported)
599
+ const autoChainActive = isOtaAutoChainActive()
600
+ if (!runtimeReady) {
601
+ return {
602
+ screen: "initializing",
603
+ connected: otaSnapshot.connected,
604
+ batteryLevel: otaSnapshot.batteryLevel,
605
+ transport: null,
606
+ updateRequired: isUpdateRequired,
607
+ versionChange: isVersionChange,
608
+ versionChangeConverged: false,
609
+ versionChangePhase: null,
610
+ wifiConnected: otaSnapshot.wifiConnected,
611
+ wifiStatusKnown: otaSnapshot.wifiStatusKnown,
612
+ hotspotSupported,
613
+ hotspotPhase: "idle",
614
+ hotspotArtifactPercent: null,
615
+ phase: null,
616
+ step: null,
617
+ currentStep: null,
618
+ totalSteps: null,
619
+ progress: null,
620
+ installingApkOnly: false,
621
+ firmwareRestarting: false,
622
+ error: null,
623
+ canInstall: false,
624
+ canRetry: false,
625
+ canFinish: false,
626
+ canDismiss: false,
627
+ canDiscard: false,
628
+ canOpenWifiSetup: false,
629
+ continueDisabled: false,
630
+ completedUpdate: false,
631
+ releaseTransition: null,
632
+ changelogs: [],
633
+ }
634
+ }
635
+
636
+ if (page === "check") {
637
+ const wifiRequired = otaSnapshot.wifiStatusKnown && !otaSnapshot.wifiConnected && !hotspotSupported
638
+ let screen: MentraLiveOtaScreen
639
+ if (checkState === "checking") screen = autoChainActive ? "finishing" : "checking"
640
+ else if (checkState === "update_available") {
641
+ screen = wifiRequired ? "wifi_required" : batteryBlocked ? "battery_required" : "update_available"
642
+ } else if (checkState === "no_update") screen = "up_to_date"
643
+ else if (checkState === "dev_build") screen = "dev_build"
644
+ else screen = errorKind === "pin_unavailable" ? "update_info_unavailable" : "check_failed"
645
+
646
+ let error: MentraLiveOtaError | null = null
647
+ if (screen === "check_failed") {
648
+ error = {
649
+ code: "check_failed",
650
+ message: "Couldn't check for updates. Please check your connection and try again.",
651
+ }
652
+ } else if (screen === "update_info_unavailable") {
653
+ error = {code: "update_info_unavailable", message: "Update information for this app version is unavailable."}
654
+ }
655
+ return {
656
+ screen,
657
+ connected: otaSnapshot.connected,
658
+ batteryLevel: otaSnapshot.batteryLevel,
659
+ transport: otaSnapshot.wifiConnected ? "wifi" : hotspotSupported ? "hotspot" : null,
660
+ updateRequired: isUpdateRequired,
661
+ versionChange: isVersionChange,
662
+ versionChangeConverged: false,
663
+ versionChangePhase: null,
664
+ wifiConnected: otaSnapshot.wifiConnected,
665
+ wifiStatusKnown: otaSnapshot.wifiStatusKnown,
666
+ hotspotSupported,
667
+ hotspotPhase: "idle",
668
+ hotspotArtifactPercent: null,
669
+ phase: null,
670
+ step: null,
671
+ currentStep: null,
672
+ totalSteps: null,
673
+ progress: null,
674
+ installingApkOnly: false,
675
+ firmwareRestarting: false,
676
+ error,
677
+ canInstall: screen === "update_available" && canInstall,
678
+ canRetry: screen === "check_failed",
679
+ canFinish: screen === "up_to_date" || screen === "dev_build" || screen === "update_info_unavailable",
680
+ canDismiss:
681
+ (screen === "update_available" || screen === "wifi_required" || screen === "battery_required") &&
682
+ !isUpdateRequired &&
683
+ !autoChainActive,
684
+ canDiscard: false,
685
+ canOpenWifiSetup: screen === "wifi_required",
686
+ continueDisabled: false,
687
+ completedUpdate: screen === "up_to_date" && completedUpdate,
688
+ releaseTransition:
689
+ screen === "update_available" || screen === "wifi_required"
690
+ ? autoChainActive
691
+ ? releaseTransitionFromRange(otaAutoChainReleaseRange())
692
+ : offeredReleaseTransition
693
+ : screen === "up_to_date"
694
+ ? completedReleaseTransition
695
+ : null,
696
+ changelogs: screen === "up_to_date" ? completedChangelogs : [],
697
+ }
698
+ }
699
+
700
+ const requiresGlassesReboot = shouldRequireGlassesRebootForBesFailure(
701
+ installSnapshot.otaStatus,
702
+ installSnapshot.otaProgress,
703
+ installSnapshot.errorMsg,
704
+ )
705
+ const showChangeWifi = shouldShowChangeWifiForOtaDownloadFailure(
706
+ installSnapshot.otaStatus,
707
+ installSnapshot.otaProgress,
708
+ installSnapshot.errorMsg,
709
+ )
710
+ const displayedError = requiresGlassesReboot
711
+ ? BES_INSTALL_RESTART_MESSAGE
712
+ : installSnapshot.errorMsg || getOtaErrorMessage(installSnapshot.otaStatus?.error)
713
+ const progressState = progressScreen(installSnapshot)
714
+ const screen = progressState === "complete" && autoChainActive ? "finishing" : progressState
715
+ const error =
716
+ screen === "failed"
717
+ ? {
718
+ code: requiresGlassesReboot ? ("bes_restart_required" as const) : ("install_failed" as const),
719
+ message: displayedError,
720
+ }
721
+ : null
722
+ const totalSteps = installSnapshot.otaStatus?.totalSteps ?? null
723
+ const changelogs = screen === "complete" ? releaseChangelogsForActiveChain() : []
724
+ return {
725
+ screen,
726
+ connected: installSnapshot.connected,
727
+ batteryLevel: otaSnapshot.batteryLevel,
728
+ transport: installSnapshot.transport,
729
+ updateRequired: isUpdateRequired,
730
+ versionChange: installSnapshot.isVersionChange,
731
+ versionChangeConverged: installSnapshot.versionChangeConverged,
732
+ versionChangePhase: installSnapshot.versionChangePhase,
733
+ wifiConnected: otaSnapshot.wifiConnected,
734
+ wifiStatusKnown: otaSnapshot.wifiStatusKnown,
735
+ hotspotSupported,
736
+ hotspotPhase: installSnapshot.hotspotPhase,
737
+ hotspotArtifactPercent: installSnapshot.hotspotArtifactPercent,
738
+ phase: installSnapshot.otaStatus?.phase ?? installSnapshot.otaProgress?.stage ?? null,
739
+ step: installSnapshot.otaStatus?.stepType ?? null,
740
+ currentStep: installSnapshot.otaStatus?.currentStep ?? null,
741
+ totalSteps,
742
+ progress: installProgress(installSnapshot),
743
+ installingApkOnly:
744
+ installSnapshot.otaStatus?.stepType === "apk" &&
745
+ installSnapshot.otaStatus.phase === "install" &&
746
+ totalSteps === 1,
747
+ firmwareRestarting,
748
+ error,
749
+ canInstall: false,
750
+ canRetry: screen === "failed" && !requiresGlassesReboot,
751
+ canFinish: screen === "complete" || screen === "restarting" || (screen === "failed" && requiresGlassesReboot),
752
+ canDismiss: false,
753
+ canDiscard: screen === "disconnected",
754
+ canOpenWifiSetup: screen === "failed" && showChangeWifi,
755
+ continueDisabled: installSnapshot.continueButtonDisabled,
756
+ completedUpdate: false,
757
+ releaseTransition: releaseTransitionFromRange(otaAutoChainReleaseRange()),
758
+ changelogs,
759
+ }
760
+ }, [
761
+ checkState,
762
+ batteryBlocked,
763
+ completedChangelogs,
764
+ completedReleaseTransition,
765
+ completedUpdate,
766
+ errorKind,
767
+ firmwareRestarting,
768
+ installSnapshot,
769
+ isUpdateRequired,
770
+ isVersionChange,
771
+ otaSnapshot,
772
+ offeredReleaseTransition,
773
+ page,
774
+ runtimeReady,
775
+ ])
776
+
777
+ return useMemo(
778
+ () => ({state, check, retryCheck: check, install, retryInstall, finish, discard, openWifiSetup}),
779
+ [check, discard, finish, install, openWifiSetup, retryInstall, state],
780
+ )
781
+ }