@mentra/engine 3.1.0-dev.4 → 3.1.0-dev.42

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