@mentra/engine 3.1.0-dev.20 → 3.1.0-dev.21

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