@mentra/engine 3.4.0-dev.990 → 3.4.0-dev.991
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/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
- package/build/react/MentraLiveOtaFlow.js +1 -4
- package/build/react/MentraLiveOtaFlow.js.map +1 -1
- package/build/react/useMentraLiveOta.js +2 -2
- package/build/react/useMentraLiveOta.js.map +1 -1
- package/build/services/OtaArtifactDownloader.d.ts +3 -1
- package/build/services/OtaArtifactDownloader.d.ts.map +1 -1
- package/build/services/OtaArtifactDownloader.js +57 -13
- package/build/services/OtaArtifactDownloader.js.map +1 -1
- package/build/services/OtaArtifactFailure.d.ts +18 -0
- package/build/services/OtaArtifactFailure.d.ts.map +1 -0
- package/build/services/OtaArtifactFailure.js +72 -0
- package/build/services/OtaArtifactFailure.js.map +1 -0
- package/build/services/OtaErrorMapping.d.ts +4 -2
- package/build/services/OtaErrorMapping.d.ts.map +1 -1
- package/build/services/OtaErrorMapping.js +57 -6
- package/build/services/OtaErrorMapping.js.map +1 -1
- package/build/services/OtaFailureReportService.d.ts +2 -0
- package/build/services/OtaFailureReportService.d.ts.map +1 -1
- package/build/services/OtaFailureReportService.js.map +1 -1
- package/build/services/OtaInstallCoordinator.d.ts +3 -1
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +15 -26
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/otaInstallPolicy.d.ts +2 -2
- package/build/services/otaInstallPolicy.js +2 -2
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/package.json +8 -8
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/react/MentraLiveOtaFlow.tsx +1 -6
- package/src/react/useMentraLiveOta.ts +2 -2
- package/src/services/OtaArtifactDownloader.ts +62 -15
- package/src/services/OtaArtifactFailure.ts +102 -0
- package/src/services/OtaErrorMapping.ts +60 -6
- package/src/services/OtaFailureReportService.ts +2 -0
- package/src/services/OtaInstallCoordinator.ts +17 -28
- package/src/services/otaInstallPolicy.ts +2 -2
|
@@ -87,8 +87,8 @@ export declare const OtaProgressMessages: {
|
|
|
87
87
|
readonly globalTimeout: "Update took too long. Please try again.";
|
|
88
88
|
readonly restartTimeout: "Glasses did not finish restarting. Restart them before trying the update again.";
|
|
89
89
|
readonly sendOtaStartFailed: "Failed to communicate with glasses.";
|
|
90
|
-
readonly hotspotArtifactDownloadFailed: "
|
|
91
|
-
readonly hotspotArtifactVerifyFailed: "The update
|
|
90
|
+
readonly hotspotArtifactDownloadFailed: "The update could not be downloaded to your phone. Please try again.";
|
|
91
|
+
readonly hotspotArtifactVerifyFailed: "The update file could not be verified. Please try again later.";
|
|
92
92
|
readonly hotspotWifiPermissionDenied: "Allow nearby devices access so the phone can connect to the glasses hotspot.";
|
|
93
93
|
readonly hotspotStartFailed: "Could not start the glasses hotspot. Please try again.";
|
|
94
94
|
readonly hotspotJoinFailed: "Could not connect the phone to the glasses hotspot. Please try again.";
|
|
@@ -111,8 +111,8 @@ export const OtaProgressMessages = {
|
|
|
111
111
|
globalTimeout: "Update took too long. Please try again.",
|
|
112
112
|
restartTimeout: "Glasses did not finish restarting. Restart them before trying the update again.",
|
|
113
113
|
sendOtaStartFailed: "Failed to communicate with glasses.",
|
|
114
|
-
hotspotArtifactDownloadFailed: "
|
|
115
|
-
hotspotArtifactVerifyFailed: "The update
|
|
114
|
+
hotspotArtifactDownloadFailed: "The update could not be downloaded to your phone. Please try again.",
|
|
115
|
+
hotspotArtifactVerifyFailed: "The update file could not be verified. Please try again later.",
|
|
116
116
|
hotspotWifiPermissionDenied: "Allow nearby devices access so the phone can connect to the glasses hotspot.",
|
|
117
117
|
hotspotStartFailed: "Could not start the glasses hotspot. Please try again.",
|
|
118
118
|
hotspotJoinFailed: "Could not connect the phone to the glasses hotspot. Please try again.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otaInstallPolicy.js","sourceRoot":"","sources":["../../src/services/otaInstallPolicy.ts"],"names":[],"mappings":"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","sourcesContent":["/** Shared OTA progress watchdog timings (see progress-legacy.tsx / progress.tsx). */\nimport type {OtaProgress, OtaStatus} from \"@mentra/bluetooth-sdk\"\n\n/**\n * First glasses `build_number` (ASG `versionCode`) that uses `progress.tsx` with unified\n * `ota_status` session UI. Strictly lower builds use `/ota/progress-legacy` (routed from\n * `check-for-updates.tsx` before navigation), and get the LEGACY_* padded policy below\n * in the unified OtaInstallCoordinator (WP 8C). Change only here.\n */\nexport const MINIMUM_OTA_STATUS_BUILD = 37\n\nexport const MAX_RETRIES = 3\n/** Wait between ended native ota_start requests; never overlaps a pending request. */\nexport const RETRY_INTERVAL_MS = 5000\n/** APK/BES and general install when progress events are expected */\nexport const PROGRESS_TIMEOUT_MS = 120_000\n/** Fail if still at 0% in starting/downloading */\nexport const DOWNLOAD_STUCK_TIMEOUT_MS = 70_000\n/** MTK system install often goes quiet for long periods */\nexport const MTK_INSTALL_TIMEOUT_MS = 300_000\n/** Whole multi-step session cap */\nexport const GLOBAL_OTA_TIMEOUT_MS = 20 * 60 * 1000\n/** BLE keepalive during OTA */\nexport const PING_INTERVAL_MS = 10_000\n/**\n * If we sent ota_query_status (e.g. on reconnect) and the glasses haven't\n * replied with an ota_status by then, fall back to ota_start. This recovers\n * from the case where the glasses process restarted between mount and\n * reconnect — the session is gone, ota_query_status returns nothing, and\n * the user would otherwise sit on the spinner forever.\n */\nexport const QUERY_REPLY_TIMEOUT_MS = 6000\n/** 15s lockout on the Continue button after a BES restart, to prevent an accidental tap. */\nexport const BES_CONTINUE_LOCKOUT_MS = 15_000\n/** Fail closed if a BES reboot never reconnects; normal restarts complete in a fraction of this window. */\nexport const BES_RESTART_TIMEOUT_MS = 120_000\n\n// --- Legacy old-build (< MINIMUM_OTA_STATUS_BUILD) OTA policy (WP 8C) ---\n//\n// Ported verbatim from mobile/src/app/ota/progress-legacy.tsx, which padded EVERY\n// watchdog/timer by +20s for old ASG builds (\"Legacy OTA: +20s on every watchdog /\n// timer duration\"). Old builds respond slower on every path, so where the legacy\n// screen used a longer duration the coordinator applies the longer value whenever\n// the session is legacy-shaped (see isLegacyShapedOtaStatus / the coordinator's\n// session-shape check). Builds >= 37 keep the unpadded values above.\n\n/** The uniform pad progress-legacy.tsx added to every duration for old builds. */\nexport const LEGACY_EXTRA_TIMEOUT_MS = 20_000\nexport const LEGACY_RETRY_INTERVAL_MS = RETRY_INTERVAL_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_PROGRESS_TIMEOUT_MS = PROGRESS_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS = DOWNLOAD_STUCK_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_MTK_INSTALL_TIMEOUT_MS = MTK_INSTALL_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_GLOBAL_OTA_TIMEOUT_MS = GLOBAL_OTA_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_PING_INTERVAL_MS = PING_INTERVAL_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_BES_CONTINUE_LOCKOUT_MS = BES_CONTINUE_LOCKOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_BES_RESTART_TIMEOUT_MS = BES_RESTART_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\n/**\n * After a legacy apk `install FINISHED` observed in-flight, the legacy screen held the\n * completed state for 12s (+20s pad) so the ASG process restart settles (and fresh\n * version_info lands) before the user can continue.\n */\nexport const LEGACY_APK_COMPLETION_SETTLE_MS = 12_000 + LEGACY_EXTRA_TIMEOUT_MS\n/**\n * Legacy MTK install stall simulation (display-only): the MTK system install typically\n * stalls around 49-50%. After LEGACY_MTK_STALL_DETECT_MS without a new event in the\n * [45, 55) zone, the legacy screen simulated +1% every LEGACY_MTK_SIM_TICK_MS starting\n * at max(stall+1, 51) and capped at 60%, purely to keep the user informed.\n */\nexport const LEGACY_MTK_STALL_DETECT_MS = 20_000 + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_MTK_SIM_TICK_MS = 15_000 + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_MTK_STALL_ZONE_MIN_PERCENT = 45\nexport const LEGACY_MTK_STALL_ZONE_MAX_PERCENT = 55\nexport const LEGACY_MTK_SIM_FLOOR_PERCENT = 51\nexport const LEGACY_MTK_SIM_CAP_PERCENT = 60\n\nexport type OtaProtocolProfile = \"legacy\" | \"unified\"\n\n/**\n * Legacy-shaped ota_status: old (< 37) ASG builds send `ota_progress`, which the SDK\n * (Android MentraLive.kt / iOS MentraLive.swift) maps to a unified ota_status with\n * sessionId \"\" and totalSteps 1, and FINISHED→\"complete\" regardless of phase. A real\n * (>= 37) session always carries a sessionId; an \"idle\" reply also has sessionId \"\" but\n * proves the glasses speak the unified protocol, so it is NOT legacy-shaped.\n */\nexport function isLegacyShapedOtaStatus(otaStatus: OtaStatus | null): boolean {\n return !!otaStatus && otaStatus.sessionId === \"\" && otaStatus.status !== \"idle\"\n}\n\n/**\n * Is the current install session legacy-shaped? Event shape wins when events exist\n * (per the WP 8C rule: key off legacy-shaped inputs, not a build flag, where possible);\n * before any event arrives only the glasses build number can tell.\n */\nexport function isLegacyShapedOtaSession(\n otaStatus: OtaStatus | null,\n otaProgress: OtaProgress | null,\n buildNumber: string | null | undefined,\n): boolean {\n if (otaStatus) return isLegacyShapedOtaStatus(otaStatus)\n if (otaProgress) return true\n const build = Number.parseInt(buildNumber ?? \"\", 10)\n return Number.isFinite(build) && build > 0 && build < MINIMUM_OTA_STATUS_BUILD\n}\n\n/**\n * Select the compatibility profile once at install-screen attach. The old split\n * routes made this decision from the pre-update build and kept it for the whole\n * mounted flow, even when an APK step restarted into a newer ASG build. Returning\n * null leaves the coordinator undecided until the first meaningful OTA event.\n */\nexport function selectOtaProtocolProfile(\n otaStatus: OtaStatus | null,\n otaProgress: OtaProgress | null,\n buildNumber: string | null | undefined,\n): OtaProtocolProfile | null {\n const build = Number.parseInt(buildNumber ?? \"\", 10)\n if (Number.isFinite(build) && build > 0) {\n return build < MINIMUM_OTA_STATUS_BUILD ? \"legacy\" : \"unified\"\n }\n if (otaStatus) return isLegacyShapedOtaStatus(otaStatus) ? \"legacy\" : \"unified\"\n if (otaProgress) return \"legacy\"\n return null\n}\n\nexport const OtaProgressMessages = {\n stalledOrStuck: \"Update may have failed. Ensure glasses have internet access and try again.\",\n globalTimeout: \"Update took too long. Please try again.\",\n restartTimeout: \"Glasses did not finish restarting. Restart them before trying the update again.\",\n sendOtaStartFailed: \"Failed to communicate with glasses.\",\n hotspotArtifactDownloadFailed: \"Could not download the update to your phone. Check phone internet and try again.\",\n hotspotArtifactVerifyFailed: \"The update package could not be verified. Please try again later.\",\n hotspotWifiPermissionDenied: \"Allow nearby devices access so the phone can connect to the glasses hotspot.\",\n hotspotStartFailed: \"Could not start the glasses hotspot. Please try again.\",\n hotspotJoinFailed: \"Could not connect the phone to the glasses hotspot. Please try again.\",\n hotspotServerFailed: \"Could not make the update available to the glasses. Please try again.\",\n} as const\n"]}
|
|
1
|
+
{"version":3,"file":"otaInstallPolicy.js","sourceRoot":"","sources":["../../src/services/otaInstallPolicy.ts"],"names":[],"mappings":"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","sourcesContent":["/** Shared OTA progress watchdog timings (see progress-legacy.tsx / progress.tsx). */\nimport type {OtaProgress, OtaStatus} from \"@mentra/bluetooth-sdk\"\n\n/**\n * First glasses `build_number` (ASG `versionCode`) that uses `progress.tsx` with unified\n * `ota_status` session UI. Strictly lower builds use `/ota/progress-legacy` (routed from\n * `check-for-updates.tsx` before navigation), and get the LEGACY_* padded policy below\n * in the unified OtaInstallCoordinator (WP 8C). Change only here.\n */\nexport const MINIMUM_OTA_STATUS_BUILD = 37\n\nexport const MAX_RETRIES = 3\n/** Wait between ended native ota_start requests; never overlaps a pending request. */\nexport const RETRY_INTERVAL_MS = 5000\n/** APK/BES and general install when progress events are expected */\nexport const PROGRESS_TIMEOUT_MS = 120_000\n/** Fail if still at 0% in starting/downloading */\nexport const DOWNLOAD_STUCK_TIMEOUT_MS = 70_000\n/** MTK system install often goes quiet for long periods */\nexport const MTK_INSTALL_TIMEOUT_MS = 300_000\n/** Whole multi-step session cap */\nexport const GLOBAL_OTA_TIMEOUT_MS = 20 * 60 * 1000\n/** BLE keepalive during OTA */\nexport const PING_INTERVAL_MS = 10_000\n/**\n * If we sent ota_query_status (e.g. on reconnect) and the glasses haven't\n * replied with an ota_status by then, fall back to ota_start. This recovers\n * from the case where the glasses process restarted between mount and\n * reconnect — the session is gone, ota_query_status returns nothing, and\n * the user would otherwise sit on the spinner forever.\n */\nexport const QUERY_REPLY_TIMEOUT_MS = 6000\n/** 15s lockout on the Continue button after a BES restart, to prevent an accidental tap. */\nexport const BES_CONTINUE_LOCKOUT_MS = 15_000\n/** Fail closed if a BES reboot never reconnects; normal restarts complete in a fraction of this window. */\nexport const BES_RESTART_TIMEOUT_MS = 120_000\n\n// --- Legacy old-build (< MINIMUM_OTA_STATUS_BUILD) OTA policy (WP 8C) ---\n//\n// Ported verbatim from mobile/src/app/ota/progress-legacy.tsx, which padded EVERY\n// watchdog/timer by +20s for old ASG builds (\"Legacy OTA: +20s on every watchdog /\n// timer duration\"). Old builds respond slower on every path, so where the legacy\n// screen used a longer duration the coordinator applies the longer value whenever\n// the session is legacy-shaped (see isLegacyShapedOtaStatus / the coordinator's\n// session-shape check). Builds >= 37 keep the unpadded values above.\n\n/** The uniform pad progress-legacy.tsx added to every duration for old builds. */\nexport const LEGACY_EXTRA_TIMEOUT_MS = 20_000\nexport const LEGACY_RETRY_INTERVAL_MS = RETRY_INTERVAL_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_PROGRESS_TIMEOUT_MS = PROGRESS_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS = DOWNLOAD_STUCK_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_MTK_INSTALL_TIMEOUT_MS = MTK_INSTALL_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_GLOBAL_OTA_TIMEOUT_MS = GLOBAL_OTA_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_PING_INTERVAL_MS = PING_INTERVAL_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_BES_CONTINUE_LOCKOUT_MS = BES_CONTINUE_LOCKOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_BES_RESTART_TIMEOUT_MS = BES_RESTART_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS\n/**\n * After a legacy apk `install FINISHED` observed in-flight, the legacy screen held the\n * completed state for 12s (+20s pad) so the ASG process restart settles (and fresh\n * version_info lands) before the user can continue.\n */\nexport const LEGACY_APK_COMPLETION_SETTLE_MS = 12_000 + LEGACY_EXTRA_TIMEOUT_MS\n/**\n * Legacy MTK install stall simulation (display-only): the MTK system install typically\n * stalls around 49-50%. After LEGACY_MTK_STALL_DETECT_MS without a new event in the\n * [45, 55) zone, the legacy screen simulated +1% every LEGACY_MTK_SIM_TICK_MS starting\n * at max(stall+1, 51) and capped at 60%, purely to keep the user informed.\n */\nexport const LEGACY_MTK_STALL_DETECT_MS = 20_000 + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_MTK_SIM_TICK_MS = 15_000 + LEGACY_EXTRA_TIMEOUT_MS\nexport const LEGACY_MTK_STALL_ZONE_MIN_PERCENT = 45\nexport const LEGACY_MTK_STALL_ZONE_MAX_PERCENT = 55\nexport const LEGACY_MTK_SIM_FLOOR_PERCENT = 51\nexport const LEGACY_MTK_SIM_CAP_PERCENT = 60\n\nexport type OtaProtocolProfile = \"legacy\" | \"unified\"\n\n/**\n * Legacy-shaped ota_status: old (< 37) ASG builds send `ota_progress`, which the SDK\n * (Android MentraLive.kt / iOS MentraLive.swift) maps to a unified ota_status with\n * sessionId \"\" and totalSteps 1, and FINISHED→\"complete\" regardless of phase. A real\n * (>= 37) session always carries a sessionId; an \"idle\" reply also has sessionId \"\" but\n * proves the glasses speak the unified protocol, so it is NOT legacy-shaped.\n */\nexport function isLegacyShapedOtaStatus(otaStatus: OtaStatus | null): boolean {\n return !!otaStatus && otaStatus.sessionId === \"\" && otaStatus.status !== \"idle\"\n}\n\n/**\n * Is the current install session legacy-shaped? Event shape wins when events exist\n * (per the WP 8C rule: key off legacy-shaped inputs, not a build flag, where possible);\n * before any event arrives only the glasses build number can tell.\n */\nexport function isLegacyShapedOtaSession(\n otaStatus: OtaStatus | null,\n otaProgress: OtaProgress | null,\n buildNumber: string | null | undefined,\n): boolean {\n if (otaStatus) return isLegacyShapedOtaStatus(otaStatus)\n if (otaProgress) return true\n const build = Number.parseInt(buildNumber ?? \"\", 10)\n return Number.isFinite(build) && build > 0 && build < MINIMUM_OTA_STATUS_BUILD\n}\n\n/**\n * Select the compatibility profile once at install-screen attach. The old split\n * routes made this decision from the pre-update build and kept it for the whole\n * mounted flow, even when an APK step restarted into a newer ASG build. Returning\n * null leaves the coordinator undecided until the first meaningful OTA event.\n */\nexport function selectOtaProtocolProfile(\n otaStatus: OtaStatus | null,\n otaProgress: OtaProgress | null,\n buildNumber: string | null | undefined,\n): OtaProtocolProfile | null {\n const build = Number.parseInt(buildNumber ?? \"\", 10)\n if (Number.isFinite(build) && build > 0) {\n return build < MINIMUM_OTA_STATUS_BUILD ? \"legacy\" : \"unified\"\n }\n if (otaStatus) return isLegacyShapedOtaStatus(otaStatus) ? \"legacy\" : \"unified\"\n if (otaProgress) return \"legacy\"\n return null\n}\n\nexport const OtaProgressMessages = {\n stalledOrStuck: \"Update may have failed. Ensure glasses have internet access and try again.\",\n globalTimeout: \"Update took too long. Please try again.\",\n restartTimeout: \"Glasses did not finish restarting. Restart them before trying the update again.\",\n sendOtaStartFailed: \"Failed to communicate with glasses.\",\n hotspotArtifactDownloadFailed: \"The update could not be downloaded to your phone. Please try again.\",\n hotspotArtifactVerifyFailed: \"The update file could not be verified. Please try again later.\",\n hotspotWifiPermissionDenied: \"Allow nearby devices access so the phone can connect to the glasses hotspot.\",\n hotspotStartFailed: \"Could not start the glasses hotspot. Please try again.\",\n hotspotJoinFailed: \"Could not connect the phone to the glasses hotspot. Please try again.\",\n hotspotServerFailed: \"Could not make the update available to the glasses. Please try again.\",\n} as const\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/engine",
|
|
3
|
-
"version": "3.4.0-dev.
|
|
3
|
+
"version": "3.4.0-dev.991",
|
|
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,13 +97,13 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@mentra/acs-meeting": "3.4.0-dev.
|
|
101
|
-
"@mentra/glasses-media": "3.4.0-dev.
|
|
102
|
-
"@mentra/bluetooth-sdk": "3.4.0-dev.
|
|
103
|
-
"@mentra/cloud-client": "3.4.0-dev.
|
|
104
|
-
"@mentra/cloud-protocol": "3.4.0-dev.
|
|
105
|
-
"@mentra/crust": "3.4.0-dev.
|
|
106
|
-
"@mentra/miniapp": "3.4.0-dev.
|
|
100
|
+
"@mentra/acs-meeting": "3.4.0-dev.991",
|
|
101
|
+
"@mentra/glasses-media": "3.4.0-dev.991",
|
|
102
|
+
"@mentra/bluetooth-sdk": "3.4.0-dev.991",
|
|
103
|
+
"@mentra/cloud-client": "3.4.0-dev.991",
|
|
104
|
+
"@mentra/cloud-protocol": "3.4.0-dev.991",
|
|
105
|
+
"@mentra/crust": "3.4.0-dev.991",
|
|
106
|
+
"@mentra/miniapp": "3.4.0-dev.991",
|
|
107
107
|
"buffer": "^6.0.3",
|
|
108
108
|
"events": "^3.3.0",
|
|
109
109
|
"jszip": "^3.10.1",
|
|
@@ -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.4.0",
|
|
15
|
-
"releaseIdentity": "3.4.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.4.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.4.0-dev.991",
|
|
16
|
+
"releaseSetId": "mentra-3.4.0-dev.991",
|
|
17
|
+
"sourceCommit": "0694bd698d4328d72d98e25b982e520f930039e1",
|
|
18
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.4.0/mentra-live-ota-3.4.0-dev.991.json",
|
|
19
|
+
"otaManifestSha256": "1039d426367531957e74c6a2f436f6e577198dd01df5ffb1e76a937eb6be3f9c"
|
|
20
20
|
})
|
|
@@ -618,11 +618,7 @@ function OtaFlowContent({
|
|
|
618
618
|
icon="alert"
|
|
619
619
|
title={translate("ota:updateFailed")}>
|
|
620
620
|
<BodyText colors={colors}>{failureMessage(state.error, translate)}</BodyText>
|
|
621
|
-
|
|
622
|
-
<Text style={[styles.errorCode, {color: colors.textDim}]} testID="ota-error-code">
|
|
623
|
-
{translate("ota:errorCode", {code: state.error.glassesCode})}
|
|
624
|
-
</Text>
|
|
625
|
-
) : null}
|
|
621
|
+
|
|
626
622
|
</FlowPage>
|
|
627
623
|
)
|
|
628
624
|
}
|
|
@@ -898,7 +894,6 @@ const styles = StyleSheet.create({
|
|
|
898
894
|
svgIcon: {alignItems: "center", height: 72, justifyContent: "center", width: 72},
|
|
899
895
|
title: {fontSize: 20, fontWeight: "600", textAlign: "center"},
|
|
900
896
|
body: {fontSize: 14, lineHeight: 20, maxWidth: 420, textAlign: "center"},
|
|
901
|
-
errorCode: {fontSize: 12, fontVariant: ["tabular-nums"], lineHeight: 16, opacity: 0.7, textAlign: "center"},
|
|
902
897
|
percent: {fontSize: 30, fontVariant: ["tabular-nums"], fontWeight: "700"},
|
|
903
898
|
progressTrack: {borderRadius: 4, height: 8, maxWidth: 420, overflow: "hidden", width: "100%"},
|
|
904
899
|
progressFill: {borderRadius: 4, height: 8},
|
|
@@ -838,7 +838,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
838
838
|
// phone-side copy outranks it. Only a failed ota_status carries a current code.
|
|
839
839
|
const glassesCode = installSnapshot.otaStatus?.status === "failed" ? installSnapshot.otaStatus.error || null : null
|
|
840
840
|
// Precedence mirrors the legacy screen: the BES restart instruction, then a phone-side
|
|
841
|
-
// watchdog/preflight message (
|
|
841
|
+
// watchdog/preflight message (translated when a copy key is available), then the glasses code mapped to
|
|
842
842
|
// copy (unknown codes get the generic glasses-error copy rather than the raw code).
|
|
843
843
|
let displayedError: string
|
|
844
844
|
let copyKey: string | null
|
|
@@ -847,7 +847,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
|
|
|
847
847
|
copyKey = OTA_ERROR_BES_RESTART_REQUIRED_COPY_KEY
|
|
848
848
|
} else if (installSnapshot.errorMsg) {
|
|
849
849
|
displayedError = installSnapshot.errorMsg
|
|
850
|
-
copyKey = null
|
|
850
|
+
copyKey = installSnapshot.errorCopyKey ?? null
|
|
851
851
|
} else {
|
|
852
852
|
displayedError = getOtaErrorMessage(glassesCode)
|
|
853
853
|
copyKey = otaErrorCopyKey(glassesCode)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as RNFS from "@dr.pogodin/react-native-fs"
|
|
2
|
+
import {otaArtifactFailure, type OtaArtifactFailure} from "./OtaArtifactFailure"
|
|
2
3
|
import type {OtaCheckCurrentGlassesResult, VersionJson} from "./OtaUpdateCheckService"
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -45,12 +46,33 @@ export class OtaArtifactError extends Error {
|
|
|
45
46
|
constructor(
|
|
46
47
|
public readonly code: "artifact_download_failed" | "artifact_verify_failed" | "manifest_invalid",
|
|
47
48
|
message: string,
|
|
49
|
+
public readonly failure?: OtaArtifactFailure,
|
|
50
|
+
cause?: unknown,
|
|
48
51
|
) {
|
|
49
52
|
super(message)
|
|
50
53
|
this.name = "OtaArtifactError"
|
|
54
|
+
// Native Errors may contain URLs and paths. Keep the original available locally,
|
|
55
|
+
// but only the bounded, redacted failure snapshot can enter JSON/incident evidence.
|
|
56
|
+
if (cause !== undefined) Object.defineProperty(this, "cause", {value: cause})
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
function artifactPreparationError(
|
|
61
|
+
error: unknown,
|
|
62
|
+
context: Omit<OtaArtifactFailure, "category" | "nativeErrors">,
|
|
63
|
+
): OtaArtifactError {
|
|
64
|
+
return new OtaArtifactError(
|
|
65
|
+
error instanceof OtaArtifactError
|
|
66
|
+
? error.code
|
|
67
|
+
: context.stage === "verify"
|
|
68
|
+
? "artifact_verify_failed"
|
|
69
|
+
: "artifact_download_failed",
|
|
70
|
+
error instanceof OtaArtifactError ? error.message : "Preparing the update artifact failed",
|
|
71
|
+
otaArtifactFailure(error, context),
|
|
72
|
+
error,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
const ASG_PACKAGE = "com.mentra.asg_client"
|
|
55
77
|
|
|
56
78
|
export function otaArtifactsDirectory(): string {
|
|
@@ -135,7 +157,16 @@ export async function prepareArtifacts(
|
|
|
135
157
|
) => Promise<{statusCode: number}>,
|
|
136
158
|
): Promise<PreparedOtaArtifact[]> {
|
|
137
159
|
const directory = otaArtifactsDirectory()
|
|
138
|
-
|
|
160
|
+
try {
|
|
161
|
+
await RNFS.mkdir(directory, {NSURLIsExcludedFromBackupKey: true})
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw artifactPreparationError(error, {
|
|
164
|
+
artifact: null,
|
|
165
|
+
stage: "save",
|
|
166
|
+
bytesWritten: 0,
|
|
167
|
+
contentLength: 0,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
139
170
|
|
|
140
171
|
const prepared: PreparedOtaArtifact[] = []
|
|
141
172
|
for (let index = 0; index < plan.length; index++) {
|
|
@@ -152,19 +183,34 @@ export async function prepareArtifacts(
|
|
|
152
183
|
})
|
|
153
184
|
|
|
154
185
|
const cachedPath = `${directory}/${entry.sha256}`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
186
|
+
try {
|
|
187
|
+
if (await RNFS.exists(cachedPath)) {
|
|
188
|
+
const cachedHash = (await RNFS.hash(cachedPath, "sha256")).toLowerCase()
|
|
189
|
+
if (cachedHash === entry.sha256) {
|
|
190
|
+
prepared.push({...entry, filePath: cachedPath})
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
await safeUnlink(cachedPath)
|
|
160
194
|
}
|
|
161
|
-
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw artifactPreparationError(error, {
|
|
197
|
+
artifact: entry.kind,
|
|
198
|
+
stage: "verify",
|
|
199
|
+
bytesWritten: 0,
|
|
200
|
+
contentLength: 0,
|
|
201
|
+
})
|
|
162
202
|
}
|
|
163
203
|
|
|
164
204
|
const partPath = `${directory}/download-${index}.part`
|
|
165
205
|
await safeUnlink(partPath)
|
|
206
|
+
let stage: OtaArtifactFailure["stage"] = "download"
|
|
207
|
+
let bytesWritten = 0
|
|
208
|
+
let contentLength = 0
|
|
209
|
+
let httpStatus: number | undefined
|
|
166
210
|
try {
|
|
167
|
-
const reportProgress = (
|
|
211
|
+
const reportProgress = (written: number, length: number) => {
|
|
212
|
+
bytesWritten = written
|
|
213
|
+
contentLength = length
|
|
168
214
|
onProgress?.({
|
|
169
215
|
kind: entry.kind,
|
|
170
216
|
index,
|
|
@@ -182,10 +228,15 @@ export async function prepareArtifacts(
|
|
|
182
228
|
connectionTimeout: 30_000,
|
|
183
229
|
readTimeout: 30_000,
|
|
184
230
|
progressDivider: 5,
|
|
231
|
+
begin: (res) => {
|
|
232
|
+
httpStatus = res.statusCode
|
|
233
|
+
contentLength = res.contentLength
|
|
234
|
+
},
|
|
185
235
|
progress: (res: RNFS.DownloadProgressCallbackResultT) => {
|
|
186
236
|
reportProgress(res.bytesWritten, res.contentLength)
|
|
187
237
|
},
|
|
188
238
|
}).promise
|
|
239
|
+
httpStatus = downloadResult.statusCode
|
|
189
240
|
// A resumed native download can finish with 206. Accept it only through
|
|
190
241
|
// the same full-file manifest SHA256 verification below.
|
|
191
242
|
if (downloadResult.statusCode !== 200 && downloadResult.statusCode !== 206) {
|
|
@@ -195,6 +246,7 @@ export async function prepareArtifacts(
|
|
|
195
246
|
)
|
|
196
247
|
}
|
|
197
248
|
|
|
249
|
+
stage = "verify"
|
|
198
250
|
const actualSha256 = (await RNFS.hash(partPath, "sha256")).toLowerCase()
|
|
199
251
|
if (actualSha256 !== entry.sha256) {
|
|
200
252
|
throw new OtaArtifactError(
|
|
@@ -203,19 +255,14 @@ export async function prepareArtifacts(
|
|
|
203
255
|
)
|
|
204
256
|
}
|
|
205
257
|
|
|
258
|
+
stage = "save"
|
|
206
259
|
const finalPath = `${directory}/${entry.sha256}`
|
|
207
260
|
await safeUnlink(finalPath)
|
|
208
261
|
await RNFS.moveFile(partPath, finalPath)
|
|
209
262
|
prepared.push({...entry, filePath: finalPath})
|
|
210
263
|
} catch (error) {
|
|
211
264
|
await safeUnlink(partPath)
|
|
212
|
-
|
|
213
|
-
throw error
|
|
214
|
-
}
|
|
215
|
-
throw new OtaArtifactError(
|
|
216
|
-
"artifact_download_failed",
|
|
217
|
-
`Download of ${entry.kind} artifact failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
218
|
-
)
|
|
265
|
+
throw artifactPreparationError(error, {artifact: entry.kind, stage, httpStatus, bytesWritten, contentLength})
|
|
219
266
|
}
|
|
220
267
|
}
|
|
221
268
|
return prepared
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type OtaArtifactFailureCategory =
|
|
2
|
+
| "internet"
|
|
3
|
+
| "connection"
|
|
4
|
+
| "server"
|
|
5
|
+
| "timeout"
|
|
6
|
+
| "storage"
|
|
7
|
+
| "save"
|
|
8
|
+
| "secure_connection"
|
|
9
|
+
| "download"
|
|
10
|
+
| "verify"
|
|
11
|
+
|
|
12
|
+
export interface OtaArtifactFailure {
|
|
13
|
+
artifact: "apk" | "mtk" | "bes" | null
|
|
14
|
+
stage: "download" | "verify" | "save"
|
|
15
|
+
category: OtaArtifactFailureCategory
|
|
16
|
+
httpStatus?: number
|
|
17
|
+
bytesWritten: number
|
|
18
|
+
contentLength: number
|
|
19
|
+
nativeErrors: {name?: string; code?: string; domain?: string; message?: string}[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Never retain artifact URLs, local paths, headers, or arbitrary native userInfo. */
|
|
23
|
+
function privateErrorText(value: unknown): string | undefined {
|
|
24
|
+
if (typeof value !== "string" && typeof value !== "number") return undefined
|
|
25
|
+
return String(value)
|
|
26
|
+
.replace(/(?:https?|file):\/\/[^\s"'<>]+/gi, "[REDACTED_URL]")
|
|
27
|
+
.replace(/\/(?:data|var|private|storage|Users)\/[^\s"'<>]*/g, "[REDACTED_PATH]")
|
|
28
|
+
.replace(/\b(?:authorization|proxy-authorization|cookie|set-cookie)\s*[:=][^\r\n]*/gi, "[REDACTED_HEADER]")
|
|
29
|
+
.replace(
|
|
30
|
+
/\b(?:bearer\s+\S+|(?:token|password|signature|api[_-]?key)\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+))/gi,
|
|
31
|
+
"[REDACTED]",
|
|
32
|
+
)
|
|
33
|
+
.slice(0, 500)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Preserve Error's non-enumerable fields before JSON serialization drops them. */
|
|
37
|
+
export function otaArtifactFailure(
|
|
38
|
+
error: unknown,
|
|
39
|
+
context: Omit<OtaArtifactFailure, "category" | "nativeErrors">,
|
|
40
|
+
): OtaArtifactFailure {
|
|
41
|
+
const nativeErrors: OtaArtifactFailure["nativeErrors"] = []
|
|
42
|
+
const seen = new Set<unknown>()
|
|
43
|
+
let current = error
|
|
44
|
+
for (let depth = 0; current != null && depth < 4 && !seen.has(current); depth++) {
|
|
45
|
+
seen.add(current)
|
|
46
|
+
if (typeof current !== "object") {
|
|
47
|
+
nativeErrors.push({message: privateErrorText(current)})
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
const detail = current as Record<string, unknown>
|
|
51
|
+
nativeErrors.push({
|
|
52
|
+
name: privateErrorText(detail.name),
|
|
53
|
+
code: privateErrorText(detail.code),
|
|
54
|
+
domain: privateErrorText(detail.domain),
|
|
55
|
+
message: privateErrorText(detail.message),
|
|
56
|
+
})
|
|
57
|
+
current = detail.cause
|
|
58
|
+
}
|
|
59
|
+
const text = nativeErrors.map((entry) => Object.values(entry).join(" ")).join(" ")
|
|
60
|
+
let httpStatus = context.httpStatus
|
|
61
|
+
if (context.stage === "download" && httpStatus === undefined) {
|
|
62
|
+
// BackgroundOtaArtifactDownloader.swift rejects non-2xx responses instead of
|
|
63
|
+
// resolving a status. Expo can wrap this exact producer message in another Error.
|
|
64
|
+
for (const entry of nativeErrors) {
|
|
65
|
+
const response = /\bArtifact download failed with HTTP ([1-5]\d{2})\b/.exec(entry.message ?? "")
|
|
66
|
+
if (response) {
|
|
67
|
+
httpStatus = Number(response[1])
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// A DNS error, timeout, reset, or HTTP failure alone does not establish no internet.
|
|
73
|
+
let category: OtaArtifactFailureCategory = context.stage === "verify" ? "verify" : "download"
|
|
74
|
+
if (/\bENOSPC\b|No space left on device/i.test(text)) category = "storage"
|
|
75
|
+
else if (context.stage === "save") category = "save"
|
|
76
|
+
else if (context.stage === "download") {
|
|
77
|
+
if (/\b(?:EACCES|EROFS)\b|Permission denied|Read-only file system/i.test(text)) category = "save"
|
|
78
|
+
else if (
|
|
79
|
+
httpStatus !== undefined &&
|
|
80
|
+
httpStatus >= 100 &&
|
|
81
|
+
httpStatus <= 599 &&
|
|
82
|
+
httpStatus !== 200 &&
|
|
83
|
+
httpStatus !== 206
|
|
84
|
+
)
|
|
85
|
+
category = "server"
|
|
86
|
+
else if (
|
|
87
|
+
nativeErrors.some((entry) => entry.domain === "NSURLErrorDomain" && entry.code === "-1009") ||
|
|
88
|
+
/\b(?:ENETDOWN|NSURLErrorNotConnectedToInternet)\b|Network is down|not connected to the Internet/i.test(text)
|
|
89
|
+
)
|
|
90
|
+
category = "internet"
|
|
91
|
+
else if (/\b(?:ETIMEDOUT|SocketTimeoutException)\b|timed? out/i.test(text)) category = "timeout"
|
|
92
|
+
else if (/\b(?:SSLHandshakeException|SSLPeerUnverifiedException|CERTIFICATE_VERIFY_FAILED)\b/i.test(text))
|
|
93
|
+
category = "secure_connection"
|
|
94
|
+
else if (
|
|
95
|
+
/\b(?:ENETUNREACH|ECONNRESET|ECONNABORTED|EPIPE|UnknownHostException|EAI_NODATA|EAI_AGAIN)\b|Software caused connection abort|Connection reset|Unable to resolve host/i.test(
|
|
96
|
+
text,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
category = "connection"
|
|
100
|
+
}
|
|
101
|
+
return {...context, httpStatus, category, nativeErrors}
|
|
102
|
+
}
|
|
@@ -8,8 +8,8 @@ import type {OtaProgress, OtaStatus} from "../facades/ota"
|
|
|
8
8
|
* The glasses-side producers are `sendProgressToPhone(..., "FAILED", errorCode)` in the
|
|
9
9
|
* ASG client's OtaHelper (download classification, install failures, and the downgrade
|
|
10
10
|
* handoff to the recovery worker). Keep this table in sync with those codes; anything
|
|
11
|
-
* else falls back to `OTA_ERROR_UNKNOWN_GLASSES_COPY_KEY
|
|
12
|
-
*
|
|
11
|
+
* else falls back to `OTA_ERROR_UNKNOWN_GLASSES_COPY_KEY`. Raw codes remain in private
|
|
12
|
+
* incident evidence rather than the product failure page.
|
|
13
13
|
*/
|
|
14
14
|
export const OTA_GLASSES_ERROR_COPY_KEYS: Readonly<Record<string, string>> = {
|
|
15
15
|
dns_failed: "ota:errorDnsFailed",
|
|
@@ -42,6 +42,45 @@ export const OTA_ERROR_UNKNOWN_GLASSES_COPY_KEY = "ota:errorGlassesUnknown"
|
|
|
42
42
|
export const OTA_ERROR_GENERIC_COPY_KEY = "ota:errorGeneric"
|
|
43
43
|
export const OTA_ERROR_BES_RESTART_REQUIRED_COPY_KEY = "ota:errorBesRestartRequired"
|
|
44
44
|
|
|
45
|
+
const PHONE_ARTIFACT_COPY_KEYS: Readonly<Record<string, string>> = {
|
|
46
|
+
internet: "ota:errorPhoneInternet",
|
|
47
|
+
connection: "ota:errorPhoneConnection",
|
|
48
|
+
server: "ota:errorPhoneDownloadServer",
|
|
49
|
+
timeout: "ota:errorPhoneDownloadTimeout",
|
|
50
|
+
storage: "ota:errorPhoneStorage",
|
|
51
|
+
save: "ota:errorPhoneSave",
|
|
52
|
+
secure_connection: "ota:errorPhoneSecureConnection",
|
|
53
|
+
download: "ota:errorPhoneDownload",
|
|
54
|
+
verify: "ota:errorPhoneVerify",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A known failed stage is useful; it is not evidence of an internet outage. */
|
|
58
|
+
export function phoneOtaErrorCopyKey(error: unknown): string {
|
|
59
|
+
if (typeof error !== "object" || error === null) return OTA_ERROR_GENERIC_COPY_KEY
|
|
60
|
+
const detail = error as {code?: unknown; failure?: {category?: unknown}}
|
|
61
|
+
switch (detail.code) {
|
|
62
|
+
case "artifact_download_failed":
|
|
63
|
+
case "artifact_verify_failed": {
|
|
64
|
+
const category = detail.failure?.category
|
|
65
|
+
if (typeof category === "string" && hasOwn(PHONE_ARTIFACT_COPY_KEYS, category))
|
|
66
|
+
return PHONE_ARTIFACT_COPY_KEYS[category]
|
|
67
|
+
return detail.code === "artifact_verify_failed" ? "ota:errorPhoneVerify" : "ota:errorPhoneDownload"
|
|
68
|
+
}
|
|
69
|
+
case "manifest_invalid":
|
|
70
|
+
return "ota:errorPhoneVerify"
|
|
71
|
+
case "hotspot_wifi_permission_denied":
|
|
72
|
+
return "ota:errorPhoneNearbyPermission"
|
|
73
|
+
case "hotspot_start_failed":
|
|
74
|
+
return "ota:errorGlassesHotspotStart"
|
|
75
|
+
case "hotspot_join_failed":
|
|
76
|
+
return "ota:errorPhoneHotspotJoin"
|
|
77
|
+
case "hotspot_server_failed":
|
|
78
|
+
return "ota:errorPhoneShareUpdate"
|
|
79
|
+
default:
|
|
80
|
+
return OTA_ERROR_GENERIC_COPY_KEY
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
45
84
|
/** English copy for every OTA error key above. Mirrored in the Mentra App's `ota` i18n namespace. */
|
|
46
85
|
export const OTA_ERROR_ENGLISH_COPY: Readonly<Record<string, string>> = {
|
|
47
86
|
"ota:errorDnsFailed": "Could not find the update server — retry, or check glasses Wi-Fi if this continues",
|
|
@@ -63,12 +102,12 @@ export const OTA_ERROR_ENGLISH_COPY: Readonly<Record<string, string>> = {
|
|
|
63
102
|
"ota:errorNoInternet": "Glasses Wi-Fi has no internet connection",
|
|
64
103
|
"ota:errorClockSkew": "Glasses clock is wrong — syncing time from your phone, then retrying update check",
|
|
65
104
|
"ota:errorSslError": "Secure connection failed — try a different Wi-Fi network",
|
|
66
|
-
"ota:errorDownloadFailed": "
|
|
105
|
+
"ota:errorDownloadFailed": "The update could not be downloaded to your glasses. Please try again.",
|
|
67
106
|
"ota:errorInsufficientStorage": "Not enough storage on your glasses — free up space before trying again",
|
|
68
107
|
"ota:errorFirmwareTooLarge": "Firmware file is unexpectedly large — please contact support",
|
|
69
108
|
"ota:errorFirmwareVerifyFailed": "Firmware verification failed — please try again or contact support",
|
|
70
109
|
"ota:errorApkVerifyFailed": "Update verification failed — please try again or contact support",
|
|
71
|
-
"ota:errorInstallFailed": "
|
|
110
|
+
"ota:errorInstallFailed": "The update could not be installed on your glasses. Please try again.",
|
|
72
111
|
"ota:errorApkRestartGuardNotPersisted":
|
|
73
112
|
"Your glasses could not save the update before restarting. Restart your glasses and try again.",
|
|
74
113
|
"ota:errorDowngradeHandoffRefused":
|
|
@@ -77,8 +116,23 @@ export const OTA_ERROR_ENGLISH_COPY: Readonly<Record<string, string>> = {
|
|
|
77
116
|
"The recovery service on your glasses did not respond. Restart your glasses and try again.",
|
|
78
117
|
"ota:errorDowngradeTransactionStalled":
|
|
79
118
|
"The version change on your glasses did not finish. Restart your glasses and try again.",
|
|
80
|
-
"ota:errorGlassesUnknown": "
|
|
81
|
-
"ota:errorGeneric": "
|
|
119
|
+
"ota:errorGlassesUnknown": "Something went wrong. Please try again.",
|
|
120
|
+
"ota:errorGeneric": "Something went wrong. Please try again.",
|
|
121
|
+
"ota:errorPhoneInternet": "Your phone could not connect to the internet. Check its connection and try again.",
|
|
122
|
+
"ota:errorPhoneConnection": "Your phone could not reach the update server. Check its connection and try again.",
|
|
123
|
+
"ota:errorPhoneDownloadServer": "The update server could not provide the download. Please try again later.",
|
|
124
|
+
"ota:errorPhoneDownloadTimeout": "The update download took too long. Please try again.",
|
|
125
|
+
"ota:errorPhoneStorage": "There is not enough space on your phone for the update. Free up space and try again.",
|
|
126
|
+
"ota:errorPhoneSave": "Your phone could not save the update. Please try again.",
|
|
127
|
+
"ota:errorPhoneSecureConnection":
|
|
128
|
+
"Your phone could not make a secure connection to the update server. Please try again later.",
|
|
129
|
+
"ota:errorPhoneDownload": "The update could not be downloaded to your phone. Please try again.",
|
|
130
|
+
"ota:errorPhoneVerify": "The update file could not be verified. Please try again later.",
|
|
131
|
+
"ota:errorPhoneNearbyPermission": "Allow nearby devices access so your phone can connect to your glasses.",
|
|
132
|
+
"ota:errorGlassesHotspotStart":
|
|
133
|
+
"Your glasses could not start the connection needed for the update. Please try again.",
|
|
134
|
+
"ota:errorPhoneHotspotJoin": "Your phone could not connect to your glasses for the update. Please try again.",
|
|
135
|
+
"ota:errorPhoneShareUpdate": "Your phone could not send the update to your glasses. Please try again.",
|
|
82
136
|
"ota:errorBesRestartRequired": "Restart your glasses to safely exit firmware update mode before trying again",
|
|
83
137
|
"ota:errorCode": "Error code: {{code}}",
|
|
84
138
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {OtaProgress, OtaStatus} from "@mentra/bluetooth-sdk"
|
|
2
2
|
|
|
3
|
+
import type {OtaArtifactFailure} from "./OtaArtifactFailure"
|
|
3
4
|
import {redactOtaDiagnosticError} from "../utils/otaDiagnosticRedaction"
|
|
4
5
|
|
|
5
6
|
export type OtaFailureReason =
|
|
@@ -15,6 +16,7 @@ export interface OtaFailureEvidence {
|
|
|
15
16
|
attemptId: string
|
|
16
17
|
deviceRevision: number
|
|
17
18
|
reason: OtaFailureReason
|
|
19
|
+
artifactFailure?: OtaArtifactFailure
|
|
18
20
|
transport: "wifi" | "hotspot" | null
|
|
19
21
|
hotspotPhase: string
|
|
20
22
|
connected: boolean
|
|
@@ -23,9 +23,11 @@ import GlobalEventEmitter from "../utils/GlobalEventEmitter"
|
|
|
23
23
|
import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
|
|
24
24
|
import {resolveOtaManifestUrl} from "./otaManifestUrl"
|
|
25
25
|
import {hotspotOtaTransport, type HotspotOtaPhase} from "./HotspotOtaTransport"
|
|
26
|
+
import {OTA_ERROR_ENGLISH_COPY, phoneOtaErrorCopyKey} from "./OtaErrorMapping"
|
|
27
|
+
import type {OtaArtifactFailure} from "./OtaArtifactFailure"
|
|
26
28
|
import {waitForOtaInternet} from "./OtaInternetRecovery"
|
|
27
29
|
import {submitOtaFailureReport, type OtaFailureReason} from "./OtaFailureReportService"
|
|
28
|
-
import type
|
|
30
|
+
import {OtaArtifactError, type OtaArtifactDownloadProgress} from "./OtaArtifactDownloader"
|
|
29
31
|
import {otaDeviceSessionRevision, subscribeOtaDeviceSession} from "./OtaDeviceSession"
|
|
30
32
|
import {checkCurrentGlassesForUpdate, isOtaCheckForCurrentDevice} from "./OtaUpdateCheckService"
|
|
31
33
|
import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
|
|
@@ -67,28 +69,6 @@ function isTerminalForWatchdog(d: DisplayState): boolean {
|
|
|
67
69
|
return d === "complete" || d === "failed" || d === "restarting"
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
function hotspotPreflightErrorMessage(error: unknown): string {
|
|
71
|
-
const code =
|
|
72
|
-
typeof error === "object" && error !== null && "code" in error ? String((error as {code: unknown}).code) : ""
|
|
73
|
-
switch (code) {
|
|
74
|
-
case "artifact_download_failed":
|
|
75
|
-
return OtaProgressMessages.hotspotArtifactDownloadFailed
|
|
76
|
-
case "artifact_verify_failed":
|
|
77
|
-
case "manifest_invalid":
|
|
78
|
-
return OtaProgressMessages.hotspotArtifactVerifyFailed
|
|
79
|
-
case "hotspot_wifi_permission_denied":
|
|
80
|
-
return OtaProgressMessages.hotspotWifiPermissionDenied
|
|
81
|
-
case "hotspot_start_failed":
|
|
82
|
-
return OtaProgressMessages.hotspotStartFailed
|
|
83
|
-
case "hotspot_join_failed":
|
|
84
|
-
return OtaProgressMessages.hotspotJoinFailed
|
|
85
|
-
case "hotspot_server_failed":
|
|
86
|
-
return OtaProgressMessages.hotspotServerFailed
|
|
87
|
-
default:
|
|
88
|
-
return OtaProgressMessages.hotspotServerFailed
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
72
|
/** Non-empty when glasses are actively reporting work — drives stall timeout reset. */
|
|
93
73
|
function buildProgressStalenessSignature(
|
|
94
74
|
otaStatus: OtaStatus | null,
|
|
@@ -149,6 +129,7 @@ function hasRecoveringOtaReply(otaStatus: OtaStatus | null, otaProgress: OtaProg
|
|
|
149
129
|
export interface OtaInstallSnapshot {
|
|
150
130
|
displayState: DisplayState
|
|
151
131
|
errorMsg: string
|
|
132
|
+
errorCopyKey?: string | null
|
|
152
133
|
continueButtonDisabled: boolean
|
|
153
134
|
connected: boolean
|
|
154
135
|
otaStatus: OtaStatus | null
|
|
@@ -220,6 +201,7 @@ class OtaInstallCoordinator {
|
|
|
220
201
|
|
|
221
202
|
// Genuinely session-local state (was component state/refs).
|
|
222
203
|
private errorMsg = ""
|
|
204
|
+
private errorCopyKey: string | null = null
|
|
223
205
|
private reportAttemptId = `${Date.now()}-0`
|
|
224
206
|
private reportAttemptSequence = 0
|
|
225
207
|
private finishCompletion: {attemptId: string; deviceRevision: number; promise: Promise<void>} | null = null
|
|
@@ -598,6 +580,7 @@ class OtaInstallCoordinator {
|
|
|
598
580
|
versionChangeSession: this.versionChangeSession,
|
|
599
581
|
}),
|
|
600
582
|
errorMsg: this.errorMsg,
|
|
583
|
+
errorCopyKey: this.errorCopyKey,
|
|
601
584
|
// Keep Continue disabled while the expected BES reboot is still in flight.
|
|
602
585
|
continueButtonDisabled: this.continueButtonDisabled || this.besRestartRecovery === "awaiting",
|
|
603
586
|
connected,
|
|
@@ -672,6 +655,7 @@ class OtaInstallCoordinator {
|
|
|
672
655
|
private resetSessionState(): void {
|
|
673
656
|
this.finishCompletion = null
|
|
674
657
|
this.errorMsg = ""
|
|
658
|
+
this.errorCopyKey = null
|
|
675
659
|
this.versionChangeReconnected = false
|
|
676
660
|
this.sawReconnectEdge = false
|
|
677
661
|
this.continueButtonDisabled = false
|
|
@@ -736,12 +720,13 @@ class OtaInstallCoordinator {
|
|
|
736
720
|
|
|
737
721
|
// Setters mirror the old setState calls: bail on identical values (React's
|
|
738
722
|
// setState bailout), notify snapshot subscribers, and queue a reaction pass.
|
|
739
|
-
private reportFailure(reason: OtaFailureReason): void {
|
|
723
|
+
private reportFailure(reason: OtaFailureReason, artifactFailure?: OtaArtifactFailure): void {
|
|
740
724
|
const state = useGlassesStore.getState()
|
|
741
725
|
void submitOtaFailureReport({
|
|
742
726
|
attemptId: this.reportAttemptId,
|
|
743
727
|
deviceRevision: otaDeviceSessionRevision(),
|
|
744
728
|
reason,
|
|
729
|
+
artifactFailure,
|
|
745
730
|
transport: this.observedTransport,
|
|
746
731
|
hotspotPhase: this.hotspotPhase,
|
|
747
732
|
connected: isGlassesConnected(state.connection),
|
|
@@ -763,9 +748,10 @@ class OtaInstallCoordinator {
|
|
|
763
748
|
})
|
|
764
749
|
}
|
|
765
750
|
|
|
766
|
-
private setErrorMsg(next: string, failureReason?: OtaFailureReason): void {
|
|
767
|
-
if (this.errorMsg === next) return
|
|
751
|
+
private setErrorMsg(next: string, failureReason?: OtaFailureReason, copyKey: string | null = null): void {
|
|
752
|
+
if (this.errorMsg === next && this.errorCopyKey === copyKey) return
|
|
768
753
|
this.errorMsg = next
|
|
754
|
+
this.errorCopyKey = copyKey
|
|
769
755
|
if (next && failureReason) this.reportFailure(failureReason)
|
|
770
756
|
this.emitInternalChange()
|
|
771
757
|
this.react()
|
|
@@ -1736,10 +1722,13 @@ class OtaInstallCoordinator {
|
|
|
1736
1722
|
if (this.selectedTransport === "hotspot" && !nativeStartAttempted) {
|
|
1737
1723
|
ownership.outcome = "rejected"
|
|
1738
1724
|
this.clearStuckTimeout()
|
|
1739
|
-
this.reportFailure("hotspot_preflight_failed")
|
|
1725
|
+
this.reportFailure("hotspot_preflight_failed", err instanceof OtaArtifactError ? err.failure : undefined)
|
|
1740
1726
|
await this.teardownHotspotTransport()
|
|
1741
1727
|
if (!isCurrentAttempt()) return
|
|
1742
|
-
if (this.attached)
|
|
1728
|
+
if (this.attached) {
|
|
1729
|
+
const copyKey = phoneOtaErrorCopyKey(err)
|
|
1730
|
+
this.setErrorMsg(OTA_ERROR_ENGLISH_COPY[copyKey], undefined, copyKey)
|
|
1731
|
+
}
|
|
1743
1732
|
return
|
|
1744
1733
|
}
|
|
1745
1734
|
|