@mentra/engine 3.1.0-dev.31 → 3.1.0-dev.37
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/build/engine.d.ts +3 -0
- package/build/engine.d.ts.map +1 -1
- package/build/facades/ota.d.ts +5 -2
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +3 -0
- package/build/facades/ota.js.map +1 -1
- package/build/facades/pairing.d.ts +4 -0
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +81 -7
- package/build/facades/pairing.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
- package/build/react/MentraLiveOtaFlow.js +39 -1
- package/build/react/MentraLiveOtaFlow.js.map +1 -1
- package/build/react/index.d.ts +1 -1
- package/build/react/index.d.ts.map +1 -1
- package/build/react/index.js.map +1 -1
- package/build/react/useMentraLiveOta.d.ts +11 -0
- package/build/react/useMentraLiveOta.d.ts.map +1 -1
- package/build/react/useMentraLiveOta.js +73 -8
- package/build/react/useMentraLiveOta.js.map +1 -1
- package/build/services/OtaAutoChain.d.ts +9 -2
- package/build/services/OtaAutoChain.d.ts.map +1 -1
- package/build/services/OtaAutoChain.js +10 -2
- package/build/services/OtaAutoChain.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +3 -0
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +12 -0
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/otaReleaseVersion.d.ts +10 -0
- package/build/services/otaReleaseVersion.d.ts.map +1 -0
- package/build/services/otaReleaseVersion.js +14 -0
- package/build/services/otaReleaseVersion.js.map +1 -0
- package/package.json +6 -6
- package/src/facades/ota.ts +13 -1
- package/src/facades/pairing.ts +88 -12
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
- package/src/react/MentraLiveOtaFlow.tsx +72 -9
- package/src/react/index.ts +1 -0
- package/src/react/useMentraLiveOta.ts +106 -18
- package/src/services/OtaAutoChain.ts +26 -2
- package/src/services/OtaUpdateCheckService.ts +15 -0
- package/src/services/otaReleaseVersion.ts +22 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otaReleaseVersion.js","sourceRoot":"","sources":["../../src/services/otaReleaseVersion.ts"],"names":[],"mappings":"AAAA,MAAM,2BAA2B,GAAG,6CAA6C,CAAA;AASjF,SAAS,mBAAmB,CAAC,OAAkC;IAC7D,MAAM,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,CAAA;IAC/B,OAAO,OAAO,IAAI,2BAA2B,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;AAC9E,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,wBAAwB,CAAC,KAA6B;IACpE,MAAM,eAAe,GAAG,mBAAmB,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAA;IAC1E,IAAI,eAAe,IAAI,KAAK,CAAC,mBAAmB,EAAE,IAAI,EAAE,KAAK,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC;QACtF,OAAO,eAAe,CAAA;IACxB,CAAC;IACD,OAAO,mBAAmB,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAA;AAC1D,CAAC","sourcesContent":["const COORDINATED_RELEASE_VERSION = /^\\d+\\.\\d+\\.\\d+(?:-(?:dev|beta)\\.[1-9]\\d*)?$/\n\ntype OtaReleaseVersionInput = {\n manifestReleaseVersion?: string | null\n manifestUrl: string\n packagedManifestUrl?: string | null\n packagedReleaseIdentity?: string | null\n}\n\nfunction validReleaseVersion(version: string | null | undefined): string | null {\n const trimmed = version?.trim()\n return trimmed && COORDINATED_RELEASE_VERSION.test(trimmed) ? trimmed : null\n}\n\n/** Resolve the label for the selected OTA pin, including stable promotion of exact beta OTA bytes. */\nexport function resolveOtaReleaseVersion(input: OtaReleaseVersionInput): string | null {\n const packagedRelease = validReleaseVersion(input.packagedReleaseIdentity)\n if (packagedRelease && input.packagedManifestUrl?.trim() === input.manifestUrl.trim()) {\n return packagedRelease\n }\n return validReleaseVersion(input.manifestReleaseVersion)\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/engine",
|
|
3
|
-
"version": "3.1.0-dev.
|
|
3
|
+
"version": "3.1.0-dev.37",
|
|
4
4
|
"description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,11 +97,11 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@mentra/bluetooth-sdk": "3.1.0-dev.
|
|
101
|
-
"@mentra/cloud-client": "3.1.0-dev.
|
|
102
|
-
"@mentra/cloud-protocol": "3.1.0-dev.
|
|
103
|
-
"@mentra/crust": "3.1.0-dev.
|
|
104
|
-
"@mentra/miniapp": "3.1.0-dev.
|
|
100
|
+
"@mentra/bluetooth-sdk": "3.1.0-dev.37",
|
|
101
|
+
"@mentra/cloud-client": "3.1.0-dev.37",
|
|
102
|
+
"@mentra/cloud-protocol": "3.1.0-dev.37",
|
|
103
|
+
"@mentra/crust": "3.1.0-dev.37",
|
|
104
|
+
"@mentra/miniapp": "3.1.0-dev.37",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
107
|
"semver": "^7.7.4",
|
package/src/facades/ota.ts
CHANGED
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
* progress screen is a pure renderer over its snapshot + attach/detach/retry/finish.
|
|
10
10
|
*/
|
|
11
11
|
import BluetoothSdk from "@mentra/bluetooth-sdk"
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
OtaProgress,
|
|
14
|
+
OtaProgressStatus,
|
|
15
|
+
OtaStartAckEvent,
|
|
16
|
+
OtaStatus,
|
|
17
|
+
OtaUpdateInfo,
|
|
18
|
+
ReleaseChangelog,
|
|
19
|
+
} from "@mentra/bluetooth-sdk"
|
|
13
20
|
import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
|
|
14
21
|
import {resolveOtaManifestUrl} from "../services/otaManifestUrl"
|
|
15
22
|
import {otaInstallCoordinator, type OtaInstallSnapshot} from "../services/OtaInstallCoordinator"
|
|
@@ -26,6 +33,7 @@ function projectSnapshot() {
|
|
|
26
33
|
return {
|
|
27
34
|
connected: isGlassesConnected(s.connection),
|
|
28
35
|
buildNumber: s.buildNumber || null,
|
|
36
|
+
appVersion: s.appVersion || null,
|
|
29
37
|
mtkFirmwareVersion: s.mtkFirmwareVersion || null,
|
|
30
38
|
besFirmwareVersion: s.besFirmwareVersion || null,
|
|
31
39
|
hotspotOtaVersion: s.hotspotOtaVersion ?? 0,
|
|
@@ -46,6 +54,7 @@ export type {
|
|
|
46
54
|
OtaProgressStatus,
|
|
47
55
|
OtaStatus,
|
|
48
56
|
OtaUpdateInfo,
|
|
57
|
+
ReleaseChangelog,
|
|
49
58
|
OtaInstallSnapshot,
|
|
50
59
|
OtaCheckCurrentGlassesOptions,
|
|
51
60
|
OtaCheckCurrentGlassesResult,
|
|
@@ -84,6 +93,9 @@ export const ota = {
|
|
|
84
93
|
ping: () => BluetoothSdk.ping(),
|
|
85
94
|
/** Resolve and compare the current glasses against the OTA manifest, then update the OTA snapshot. */
|
|
86
95
|
checkForUpdates: (options?: OtaCheckCurrentGlassesOptions) => checkCurrentGlassesForUpdate(options),
|
|
96
|
+
/** Return bundled release changelogs crossed between two coordinated product versions, newest first. */
|
|
97
|
+
getReleaseChangelogs: (fromVersion?: string | null, toVersion?: string | null): ReleaseChangelog[] =>
|
|
98
|
+
BluetoothSdk.getReleaseChangelogs(fromVersion, toVersion),
|
|
87
99
|
/** Clear the available-update prompt state. */
|
|
88
100
|
clearUpdateAvailable: () => useGlassesStore.getState().setOtaUpdateAvailable(null),
|
|
89
101
|
/** Clear active progress/status before entering a fresh install flow. */
|
package/src/facades/pairing.ts
CHANGED
|
@@ -13,6 +13,7 @@ import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
|
13
13
|
import type {ConnectOptions, Device, DeviceModel, PairFailureEvent, GlassesNotReadyEvent} from "@mentra/bluetooth-sdk"
|
|
14
14
|
import {useCoreStore} from "../stores/core"
|
|
15
15
|
import {useGlassesStore} from "../stores/glasses"
|
|
16
|
+
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
16
17
|
import {hasDefaultDevice} from "../services/DeviceStoreHydration"
|
|
17
18
|
import {
|
|
18
19
|
markPendingSelection,
|
|
@@ -20,18 +21,14 @@ import {
|
|
|
20
21
|
subscribePairingIdentity,
|
|
21
22
|
type IdentitySnapshot,
|
|
22
23
|
} from "../services/PairingIdentity"
|
|
23
|
-
import {
|
|
24
|
-
isGlassesConnected,
|
|
25
|
-
isGlassesLinkLayerBusy,
|
|
26
|
-
isGlassesReady,
|
|
27
|
-
waitForGlassesReady,
|
|
28
|
-
} from "../services/GlassesReadiness"
|
|
24
|
+
import {isGlassesConnected, isGlassesLinkLayerBusy, isGlassesReady} from "../services/GlassesReadiness"
|
|
29
25
|
import {
|
|
30
26
|
logAutomaticReportSubmissionStatus,
|
|
31
27
|
toAutomaticReportSubmissionStatus,
|
|
32
28
|
type AutomaticReportSubmissionStatus,
|
|
33
29
|
} from "../services/AutomaticReportResult"
|
|
34
30
|
import {pushAllBluetoothSettings} from "../services/GlassesSettingsSync"
|
|
31
|
+
import {ControllerTypes} from "../types"
|
|
35
32
|
import {submitAutomaticReport} from "./reports"
|
|
36
33
|
|
|
37
34
|
export type {IdentitySnapshot} from "../services/PairingIdentity"
|
|
@@ -60,6 +57,82 @@ function projectReadiness() {
|
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
|
|
60
|
+
function projectTargetReady(options: Pick<PairingReadyWaitOptions, "deviceModel" | "deviceName">): boolean {
|
|
61
|
+
const {deviceModel, deviceName} = options
|
|
62
|
+
const glasses = useGlassesStore.getState()
|
|
63
|
+
|
|
64
|
+
if (deviceModel === ControllerTypes.R1) {
|
|
65
|
+
if (!glasses.controllerConnected || !glasses.controllerFullyBooted) return false
|
|
66
|
+
const settings = useSettingsStore.getState()
|
|
67
|
+
const pairedModel = settings.getSetting(SETTINGS.default_controller.key)
|
|
68
|
+
const pairedName = settings.getSetting(SETTINGS.controller_device_name.key)
|
|
69
|
+
return (!deviceModel || pairedModel === deviceModel) && (!deviceName || pairedName === deviceName)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isGlassesReady(glasses.connection)) return false
|
|
73
|
+
const identity = projectPairingIdentity()
|
|
74
|
+
if (!deviceModel && !deviceName) return true
|
|
75
|
+
return (
|
|
76
|
+
identity.kind === "paired" &&
|
|
77
|
+
(!deviceModel || identity.model === deviceModel) &&
|
|
78
|
+
(!deviceName || identity.name === deviceName)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function subscribeTargetReady(
|
|
83
|
+
options: Pick<PairingReadyWaitOptions, "deviceModel" | "deviceName">,
|
|
84
|
+
cb: (ready: boolean) => void,
|
|
85
|
+
): () => void {
|
|
86
|
+
let last = projectTargetReady(options)
|
|
87
|
+
const notify = () => {
|
|
88
|
+
const ready = projectTargetReady(options)
|
|
89
|
+
if (ready === last) return
|
|
90
|
+
last = ready
|
|
91
|
+
cb(ready)
|
|
92
|
+
}
|
|
93
|
+
const unsubscribeGlasses = useGlassesStore.subscribe(notify)
|
|
94
|
+
const unsubscribeSettings = useSettingsStore.subscribe(notify)
|
|
95
|
+
return () => {
|
|
96
|
+
unsubscribeGlasses()
|
|
97
|
+
unsubscribeSettings()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function waitForTargetReady(options: PairingReadyWaitOptions, timeoutMs: number): Promise<boolean> {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
if (options.signal?.aborted) {
|
|
104
|
+
resolve(false)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (projectTargetReady(options)) {
|
|
108
|
+
resolve(true)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let settled = false
|
|
113
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
114
|
+
let unsubscribe: (() => void) | null = null
|
|
115
|
+
let onAbort: (() => void) | null = null
|
|
116
|
+
const finish = (ready: boolean) => {
|
|
117
|
+
if (settled) return
|
|
118
|
+
settled = true
|
|
119
|
+
if (timeout) clearTimeout(timeout)
|
|
120
|
+
if (unsubscribe) unsubscribe()
|
|
121
|
+
if (onAbort && options.signal) options.signal.removeEventListener("abort", onAbort)
|
|
122
|
+
resolve(ready)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
unsubscribe = subscribeTargetReady(options, (ready) => {
|
|
126
|
+
if (ready) finish(true)
|
|
127
|
+
})
|
|
128
|
+
if (options.signal) {
|
|
129
|
+
onAbort = () => finish(false)
|
|
130
|
+
options.signal.addEventListener("abort", onAbort)
|
|
131
|
+
}
|
|
132
|
+
timeout = setTimeout(() => finish(projectTargetReady(options)), timeoutMs)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
63
136
|
export type PairingReadinessSnapshot = ReturnType<typeof projectReadiness>
|
|
64
137
|
|
|
65
138
|
export async function submitPairingBootTimeoutReport(params: {
|
|
@@ -108,12 +181,7 @@ async function waitForReadyDuringPairing(options: PairingReadyWaitOptions = {}):
|
|
|
108
181
|
})
|
|
109
182
|
|
|
110
183
|
try {
|
|
111
|
-
const ready = await
|
|
112
|
-
getConnection: () => useGlassesStore.getState().connection,
|
|
113
|
-
subscribe: (listener) => useGlassesStore.subscribe((state) => state.connection, listener),
|
|
114
|
-
timeoutMs,
|
|
115
|
-
signal: options.signal,
|
|
116
|
-
})
|
|
184
|
+
const ready = await waitForTargetReady(options, timeoutMs)
|
|
117
185
|
|
|
118
186
|
if (!ready && !options.signal?.aborted) {
|
|
119
187
|
void submitPairingBootTimeoutReport({
|
|
@@ -193,6 +261,14 @@ export const pairing = {
|
|
|
193
261
|
cb(snap)
|
|
194
262
|
})
|
|
195
263
|
},
|
|
264
|
+
/** Whether the exact wearable or controller selected on the scan screen is ready. */
|
|
265
|
+
targetReady: (options: Pick<PairingReadyWaitOptions, "deviceModel" | "deviceName">): boolean =>
|
|
266
|
+
projectTargetReady(options),
|
|
267
|
+
/** Subscribe to readiness for the exact selected target, excluding stale devices. */
|
|
268
|
+
onTargetReady: (
|
|
269
|
+
options: Pick<PairingReadyWaitOptions, "deviceModel" | "deviceName">,
|
|
270
|
+
cb: (ready: boolean) => void,
|
|
271
|
+
): (() => void) => subscribeTargetReady(options, cb),
|
|
196
272
|
|
|
197
273
|
// --- pairing-identity lifecycle (the PairingIdentity read-model + the JS-owned
|
|
198
274
|
// identity writes; promotion to `paired` only ever happens natively) ---
|
|
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
|
|
|
12
12
|
export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "3.1.0",
|
|
15
|
-
"releaseIdentity": "3.1.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.1.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.1.0/mentra-live-ota-3.1.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.1.0-dev.37",
|
|
16
|
+
"releaseSetId": "mentra-3.1.0-dev.37",
|
|
17
|
+
"sourceCommit": "86e6323ec7ce7730aa33b55479c40b02c4af9bc4",
|
|
18
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.1.0/mentra-live-ota-3.1.0-dev.37.json",
|
|
19
|
+
"otaManifestSha256": "f496445881297a4be07bb1533cb9ac49dfbaea6fb5aa9d25605550406d045279"
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/* eslint-disable react-native/no-raw-text -- BodyText and PercentText are local Text wrappers. */
|
|
2
2
|
import React, {useMemo} from "react"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ActivityIndicator,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
View,
|
|
10
|
+
type StyleProp,
|
|
11
|
+
type ViewStyle,
|
|
12
|
+
} from "react-native"
|
|
4
13
|
import {SafeAreaView} from "react-native-safe-area-context"
|
|
5
14
|
import Svg, {Path, Rect} from "react-native-svg"
|
|
6
15
|
|
|
@@ -63,6 +72,9 @@ const ENGLISH_COPY: Record<string, string> = {
|
|
|
63
72
|
"ota:updateConnectWifi": "Connect your {{deviceName}} to WiFi to install the update.",
|
|
64
73
|
"ota:updateDescription":
|
|
65
74
|
"A new update is available for your glasses. We recommend updating now for the best experience.",
|
|
75
|
+
"ota:releaseTransition": "{{fromVersion}} → {{toVersion}}",
|
|
76
|
+
"ota:releaseTransitionUnknown": "Current version unknown → {{toVersion}}",
|
|
77
|
+
"ota:updatedToVersion": "Updated to {{version}}",
|
|
66
78
|
"ota:downgradeAvailable": "{{deviceName}} Version Change Required",
|
|
67
79
|
"ota:downgradeDescription":
|
|
68
80
|
"This app requires an earlier glasses software version. Your photos and videos will be preserved, but glasses settings will be reset and restored automatically after the change.",
|
|
@@ -195,6 +207,16 @@ function OtaFlowContent({
|
|
|
195
207
|
) : null}
|
|
196
208
|
</>
|
|
197
209
|
}>
|
|
210
|
+
{state.releaseTransition ? (
|
|
211
|
+
<BodyText colors={colors}>
|
|
212
|
+
{state.releaseTransition.fromVersion
|
|
213
|
+
? translate("ota:releaseTransition", {
|
|
214
|
+
fromVersion: state.releaseTransition.fromVersion,
|
|
215
|
+
toVersion: state.releaseTransition.toVersion,
|
|
216
|
+
})
|
|
217
|
+
: translate("ota:releaseTransitionUnknown", {toVersion: state.releaseTransition.toVersion})}
|
|
218
|
+
</BodyText>
|
|
219
|
+
) : null}
|
|
198
220
|
<BodyText colors={colors}>
|
|
199
221
|
{state.screen === "wifi_required"
|
|
200
222
|
? translate("ota:updateConnectWifi", {deviceName})
|
|
@@ -224,6 +246,12 @@ function OtaFlowContent({
|
|
|
224
246
|
icon="check"
|
|
225
247
|
title={translate("ota:upToDate")}>
|
|
226
248
|
<BodyText colors={colors}>{translate("ota:noUpdatesAvailable")}</BodyText>
|
|
249
|
+
{state.releaseTransition ? (
|
|
250
|
+
<BodyText colors={colors}>
|
|
251
|
+
{translate("ota:updatedToVersion", {version: state.releaseTransition.toVersion})}
|
|
252
|
+
</BodyText>
|
|
253
|
+
) : null}
|
|
254
|
+
<ChangelogList changelogs={state.changelogs} colors={colors} />
|
|
227
255
|
</FlowPage>
|
|
228
256
|
)
|
|
229
257
|
}
|
|
@@ -279,10 +307,10 @@ function OtaFlowContent({
|
|
|
279
307
|
state.hotspotPhase === "downloading"
|
|
280
308
|
? "Downloading update to phone..."
|
|
281
309
|
: state.hotspotPhase === "starting_hotspot"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
310
|
+
? "Starting glasses hotspot..."
|
|
311
|
+
: state.hotspotPhase === "joining_hotspot"
|
|
312
|
+
? "Connecting phone to glasses..."
|
|
313
|
+
: "Starting update..."
|
|
286
314
|
return (
|
|
287
315
|
<FlowPage colors={colors} icon="download" title={title}>
|
|
288
316
|
{state.hotspotPhase === "downloading" && state.hotspotArtifactPercent !== null ? (
|
|
@@ -334,13 +362,13 @@ function OtaFlowContent({
|
|
|
334
362
|
const title = state.versionChangeConverged
|
|
335
363
|
? translate("ota:versionChangeComplete")
|
|
336
364
|
: state.versionChange
|
|
337
|
-
|
|
338
|
-
|
|
365
|
+
? translate("ota:versionChangeFirmwarePassComplete")
|
|
366
|
+
: "Update complete!"
|
|
339
367
|
const message = state.versionChangeConverged
|
|
340
368
|
? translate("ota:versionChangeCompleteMessage")
|
|
341
369
|
: state.versionChange
|
|
342
|
-
|
|
343
|
-
|
|
370
|
+
? translate("ota:versionChangeFirmwarePassCompleteMessage")
|
|
371
|
+
: "Your glasses are up to date."
|
|
344
372
|
return (
|
|
345
373
|
<FlowPage
|
|
346
374
|
actions={
|
|
@@ -354,6 +382,12 @@ function OtaFlowContent({
|
|
|
354
382
|
icon="check"
|
|
355
383
|
title={title}>
|
|
356
384
|
<BodyText colors={colors}>{message}</BodyText>
|
|
385
|
+
{state.releaseTransition ? (
|
|
386
|
+
<BodyText colors={colors}>
|
|
387
|
+
{translate("ota:updatedToVersion", {version: state.releaseTransition.toVersion})}
|
|
388
|
+
</BodyText>
|
|
389
|
+
) : null}
|
|
390
|
+
<ChangelogList changelogs={state.changelogs} colors={colors} />
|
|
357
391
|
</FlowPage>
|
|
358
392
|
)
|
|
359
393
|
}
|
|
@@ -426,6 +460,30 @@ function PercentText({colors, percent}: {colors: MentraLiveOtaFlowTheme; percent
|
|
|
426
460
|
return <Text style={[styles.percent, {color: colors.primary}]}>{Math.round(percent)}%</Text>
|
|
427
461
|
}
|
|
428
462
|
|
|
463
|
+
function ChangelogList({
|
|
464
|
+
changelogs,
|
|
465
|
+
colors,
|
|
466
|
+
}: {
|
|
467
|
+
changelogs: MentraLiveOtaController["state"]["changelogs"]
|
|
468
|
+
colors: MentraLiveOtaFlowTheme
|
|
469
|
+
}) {
|
|
470
|
+
if (changelogs.length === 0) return null
|
|
471
|
+
return (
|
|
472
|
+
<ScrollView contentContainerStyle={styles.changelogContent} style={styles.changelogList}>
|
|
473
|
+
{changelogs.map((entry) => (
|
|
474
|
+
<View key={entry.version} style={styles.changelogEntry}>
|
|
475
|
+
<Text selectable style={[styles.changelogVersion, {color: colors.foreground}]}>
|
|
476
|
+
{entry.version}
|
|
477
|
+
</Text>
|
|
478
|
+
<Text selectable style={[styles.changelogMarkdown, {color: colors.textDim}]}>
|
|
479
|
+
{entry.markdown}
|
|
480
|
+
</Text>
|
|
481
|
+
</View>
|
|
482
|
+
))}
|
|
483
|
+
</ScrollView>
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
429
487
|
function FlowButton({
|
|
430
488
|
colors,
|
|
431
489
|
disabled = false,
|
|
@@ -494,6 +552,11 @@ const styles = StyleSheet.create({
|
|
|
494
552
|
percent: {fontSize: 30, fontVariant: ["tabular-nums"], fontWeight: "700"},
|
|
495
553
|
progressTrack: {borderRadius: 4, height: 8, maxWidth: 420, overflow: "hidden", width: "100%"},
|
|
496
554
|
progressFill: {borderRadius: 4, height: 8},
|
|
555
|
+
changelogList: {flexShrink: 1, maxHeight: 260, maxWidth: 420, width: "100%"},
|
|
556
|
+
changelogContent: {gap: 18, paddingVertical: 4},
|
|
557
|
+
changelogEntry: {gap: 8},
|
|
558
|
+
changelogVersion: {fontSize: 16, fontWeight: "600"},
|
|
559
|
+
changelogMarkdown: {fontSize: 14, lineHeight: 20},
|
|
497
560
|
button: {
|
|
498
561
|
alignItems: "center",
|
|
499
562
|
borderRadius: 50,
|
package/src/react/index.ts
CHANGED
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
clearOtaAutoChainReconnectWait,
|
|
7
7
|
isOtaAutoChainActive,
|
|
8
8
|
otaAutoChainFingerprint,
|
|
9
|
+
otaAutoChainReleaseRange,
|
|
9
10
|
otaAutoChainReconnectWaitRemaining,
|
|
10
11
|
stopOtaAutoChain,
|
|
11
12
|
tryAdvanceOtaAutoChain,
|
|
13
|
+
type OtaAutoChainReleaseRange,
|
|
12
14
|
} from "../services/OtaAutoChain"
|
|
13
15
|
import {
|
|
14
16
|
BES_INSTALL_RESTART_MESSAGE,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
} from "../services/OtaErrorMapping"
|
|
19
21
|
import type {OtaInstallSnapshot} from "../services/OtaInstallCoordinator"
|
|
20
22
|
import type {OtaCheckCurrentGlassesResult} from "../services/OtaUpdateCheckService"
|
|
23
|
+
import type {ReleaseChangelog} from "../facades/ota"
|
|
21
24
|
import {useEngineSnapshot} from "./useEngineSnapshot"
|
|
22
25
|
|
|
23
26
|
export type MentraLiveOtaFlowPage = "check" | "progress"
|
|
@@ -56,6 +59,13 @@ export type MentraLiveOtaHotspotPhase = "idle" | "downloading" | "starting_hotsp
|
|
|
56
59
|
export type MentraLiveOtaInstallPhase = "download" | "install"
|
|
57
60
|
export type MentraLiveOtaStep = "apk" | "mtk" | "bes"
|
|
58
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
|
+
|
|
59
69
|
export type MentraLiveOtaState = {
|
|
60
70
|
screen: MentraLiveOtaScreen
|
|
61
71
|
connected: boolean
|
|
@@ -84,6 +94,10 @@ export type MentraLiveOtaState = {
|
|
|
84
94
|
canDiscard: boolean
|
|
85
95
|
canOpenWifiSetup: boolean
|
|
86
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[]
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
export type UseMentraLiveOtaOptions = {
|
|
@@ -115,16 +129,44 @@ type CheckState = "checking" | "update_available" | "no_update" | "dev_build" |
|
|
|
115
129
|
const AUTO_CHAIN_NETWORK_RETRY_DELAY_MS = 5000
|
|
116
130
|
const AUTO_CHAIN_COMPLETE_DELAY_MS = 750
|
|
117
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
|
+
|
|
118
160
|
function installProgress(snapshot: OtaInstallSnapshot): number | null {
|
|
119
161
|
if (snapshot.displayState !== "updating") return null
|
|
120
162
|
const {otaStatus} = snapshot
|
|
121
163
|
const isDownload = otaStatus?.phase === "download"
|
|
122
164
|
const totalSteps = otaStatus?.totalSteps ?? 1
|
|
123
165
|
const rawPercent = isDownload
|
|
124
|
-
?
|
|
166
|
+
? otaStatus?.stepPercent ?? 0
|
|
125
167
|
: totalSteps >= 2
|
|
126
|
-
|
|
127
|
-
|
|
168
|
+
? otaStatus?.overallPercent ?? 0
|
|
169
|
+
: otaStatus?.stepPercent ?? 0
|
|
128
170
|
return Math.min(Math.max(rawPercent, snapshot.mtkInstallStallSimulatedPercent ?? 0, 0), 100)
|
|
129
171
|
}
|
|
130
172
|
|
|
@@ -169,6 +211,11 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
169
211
|
const [isVersionChange, setIsVersionChange] = useState(false)
|
|
170
212
|
const [errorKind, setErrorKind] = useState<"network" | "pin_unavailable">("network")
|
|
171
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[]>([])
|
|
172
219
|
const [checkGeneration, setCheckGeneration] = useState(0)
|
|
173
220
|
const performCheckGenerationRef = useRef(0)
|
|
174
221
|
const activeCheckKeyRef = useRef<string | null>(null)
|
|
@@ -313,13 +360,22 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
313
360
|
setIsVersionChange(result.updateInfo.isDowngrade === true)
|
|
314
361
|
const fingerprint = otaAutoChainFingerprint(result)
|
|
315
362
|
setUpdateFingerprint(fingerprint)
|
|
363
|
+
if (!isOtaAutoChainActive()) {
|
|
364
|
+
const fromVersion = currentReleaseVersion(ota.snapshot().appVersion)
|
|
365
|
+
setOfferedReleaseTransition(result.releaseVersion ? {fromVersion, toVersion: result.releaseVersion} : null)
|
|
366
|
+
}
|
|
316
367
|
if (isOtaAutoChainActive()) {
|
|
317
368
|
const snapshot = ota.snapshot()
|
|
318
369
|
if (!snapshot.wifiStatusKnown) return
|
|
319
370
|
if (!snapshot.wifiConnected && snapshot.hotspotOtaVersion !== 1) {
|
|
320
371
|
stopOtaAutoChain()
|
|
321
372
|
} else {
|
|
322
|
-
const admission = tryAdvanceOtaAutoChain(
|
|
373
|
+
const admission = tryAdvanceOtaAutoChain(
|
|
374
|
+
fingerprint,
|
|
375
|
+
result.updateInfo.isDowngrade === true,
|
|
376
|
+
releaseRangeTargetVersion(result),
|
|
377
|
+
result.releaseVersion,
|
|
378
|
+
)
|
|
323
379
|
if (admission.advance) {
|
|
324
380
|
checkCompletedRef.current = true
|
|
325
381
|
ota.installSession.prepare(result)
|
|
@@ -332,6 +388,10 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
332
388
|
setCheckState("update_available")
|
|
333
389
|
return
|
|
334
390
|
}
|
|
391
|
+
if (isOtaAutoChainActive()) {
|
|
392
|
+
setCompletedChangelogs(releaseChangelogsForActiveChain())
|
|
393
|
+
setCompletedReleaseTransition(releaseTransitionFromRange(otaAutoChainReleaseRange()))
|
|
394
|
+
}
|
|
335
395
|
stopOtaAutoChain()
|
|
336
396
|
checkCompletedRef.current = true
|
|
337
397
|
ota.clearUpdateAvailable()
|
|
@@ -397,6 +457,9 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
397
457
|
}, [installSnapshot.connected, installSnapshot.displayState, page, returnToCheck])
|
|
398
458
|
|
|
399
459
|
const check = useCallback(() => {
|
|
460
|
+
setOfferedReleaseTransition(null)
|
|
461
|
+
setCompletedReleaseTransition(null)
|
|
462
|
+
setCompletedChangelogs([])
|
|
400
463
|
setPage("check")
|
|
401
464
|
setCheckState("checking")
|
|
402
465
|
setCheckGeneration((generation) => generation + 1)
|
|
@@ -420,9 +483,19 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
420
483
|
return
|
|
421
484
|
}
|
|
422
485
|
ota.installSession.prepare(result)
|
|
423
|
-
|
|
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
|
+
}
|
|
424
497
|
navigateToProgress()
|
|
425
|
-
}, [check, isVersionChange, navigateToProgress, page, updateFingerprint])
|
|
498
|
+
}, [check, isVersionChange, navigateToProgress, offeredReleaseTransition, page, updateFingerprint])
|
|
426
499
|
|
|
427
500
|
const retryInstall = useCallback(() => {
|
|
428
501
|
if (page !== "progress") return
|
|
@@ -490,6 +563,8 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
490
563
|
canDiscard: false,
|
|
491
564
|
canOpenWifiSetup: false,
|
|
492
565
|
continueDisabled: false,
|
|
566
|
+
releaseTransition: null,
|
|
567
|
+
changelogs: [],
|
|
493
568
|
}
|
|
494
569
|
}
|
|
495
570
|
|
|
@@ -499,22 +574,22 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
499
574
|
checkState === "checking"
|
|
500
575
|
? "checking"
|
|
501
576
|
: checkState === "update_available"
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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"
|
|
512
587
|
const error: MentraLiveOtaError | null =
|
|
513
588
|
screen === "check_failed"
|
|
514
589
|
? {code: "check_failed", message: "Couldn't check for updates. Please check your connection and try again."}
|
|
515
590
|
: screen === "update_info_unavailable"
|
|
516
|
-
|
|
517
|
-
|
|
591
|
+
? {code: "update_info_unavailable", message: "Update information for this app version is unavailable."}
|
|
592
|
+
: null
|
|
518
593
|
return {
|
|
519
594
|
screen,
|
|
520
595
|
connected: otaSnapshot.connected,
|
|
@@ -543,6 +618,13 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
543
618
|
canDiscard: false,
|
|
544
619
|
canOpenWifiSetup: screen === "wifi_required",
|
|
545
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 : [],
|
|
546
628
|
}
|
|
547
629
|
}
|
|
548
630
|
|
|
@@ -568,6 +650,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
568
650
|
}
|
|
569
651
|
: null
|
|
570
652
|
const totalSteps = installSnapshot.otaStatus?.totalSteps ?? null
|
|
653
|
+
const changelogs = screen === "complete" ? releaseChangelogsForActiveChain() : []
|
|
571
654
|
return {
|
|
572
655
|
screen,
|
|
573
656
|
connected: installSnapshot.connected,
|
|
@@ -599,15 +682,20 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
599
682
|
canDiscard: screen === "disconnected",
|
|
600
683
|
canOpenWifiSetup: screen === "failed" && showChangeWifi,
|
|
601
684
|
continueDisabled: installSnapshot.continueButtonDisabled,
|
|
685
|
+
releaseTransition: releaseTransitionFromRange(otaAutoChainReleaseRange()),
|
|
686
|
+
changelogs,
|
|
602
687
|
}
|
|
603
688
|
}, [
|
|
604
689
|
checkState,
|
|
690
|
+
completedChangelogs,
|
|
691
|
+
completedReleaseTransition,
|
|
605
692
|
errorKind,
|
|
606
693
|
firmwareRestarting,
|
|
607
694
|
installSnapshot,
|
|
608
695
|
isUpdateRequired,
|
|
609
696
|
isVersionChange,
|
|
610
697
|
otaSnapshot,
|
|
698
|
+
offeredReleaseTransition,
|
|
611
699
|
page,
|
|
612
700
|
runtimeReady,
|
|
613
701
|
])
|