@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.
- package/README.md +19 -5
- package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
- package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
- package/build/bluetooth-sdk/ota-transport.js +2 -0
- package/build/bluetooth-sdk/ota-transport.js.map +1 -0
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +1 -2
- package/build/facades/ota.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +3 -2
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
- package/build/react/MentraLiveOtaFlow.js +68 -345
- package/build/react/MentraLiveOtaFlow.js.map +1 -1
- package/build/react/index.d.ts +1 -0
- package/build/react/index.d.ts.map +1 -1
- package/build/react/index.js +1 -0
- package/build/react/index.js.map +1 -1
- package/build/react/useMentraLiveOta.d.ts +69 -0
- package/build/react/useMentraLiveOta.d.ts.map +1 -0
- package/build/react/useMentraLiveOta.js +485 -0
- package/build/react/useMentraLiveOta.js.map +1 -0
- package/build/services/GlassesStatusProjection.d.ts +15 -2
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +3 -3
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/HotspotOtaTransport.d.ts.map +1 -1
- package/build/services/HotspotOtaTransport.js +8 -7
- package/build/services/HotspotOtaTransport.js.map +1 -1
- package/build/services/OtaInstallCoordinator.d.ts +22 -1
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +8 -8
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/OtaService.d.ts.map +1 -1
- package/build/services/OtaService.js +1 -1
- package/build/services/OtaService.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +1 -1
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +6 -2
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
- package/build/services/asg/localNetworkTransport.js +13 -12
- package/build/services/asg/localNetworkTransport.js.map +1 -1
- package/build/services/glassesClockSync.js +1 -1
- package/build/services/glassesClockSync.js.map +1 -1
- package/build/stores/core.d.ts +3 -3
- package/build/stores/core.d.ts.map +1 -1
- package/build/stores/core.js.map +1 -1
- package/build/stores/glasses.d.ts +10 -4
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js.map +1 -1
- package/package.json +12 -6
- package/src/bluetooth-sdk/ota-transport.ts +1 -0
- package/src/facades/ota.ts +1 -2
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/react/MentraLiveOtaFlow.tsx +116 -466
- package/src/react/index.ts +13 -0
- package/src/react/useMentraLiveOta.ts +619 -0
- package/src/services/GlassesStatusProjection.ts +7 -6
- package/src/services/HotspotOtaTransport.ts +8 -7
- package/src/services/OtaInstallCoordinator.ts +10 -9
- package/src/services/OtaService.ts +1 -2
- package/src/services/OtaUpdateCheckService.ts +8 -5
- package/src/services/asg/localNetworkTransport.ts +20 -19
- package/src/services/glassesClockSync.ts +1 -1
- package/src/stores/core.ts +4 -4
- package/src/stores/glasses.ts +13 -6
package/src/react/index.ts
CHANGED
|
@@ -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
|
|
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<
|
|
20
|
+
let glassesStatusForwarder: ((status: Partial<PublicGlassesStatus>) => void) | null = null
|
|
22
21
|
let hydrationPromise: Promise<void> | null = null
|
|
23
22
|
|
|
24
|
-
export function startGlassesStatusProjection(
|
|
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.
|
|
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.
|
|
62
|
+
BluetoothSdk.subscribeGlassesStatus((changed) => {
|
|
62
63
|
glassesEventSeen = true
|
|
63
64
|
useGlassesStore.getState().setGlassesInfo(changed)
|
|
64
65
|
glassesStatusForwarder?.(changed)
|