@mentra/engine 3.2.0-dev.217 → 3.2.0-dev.221

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/engine",
3
- "version": "3.2.0-dev.217",
3
+ "version": "3.2.0-dev.221",
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,12 +97,12 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@mentra/acs-meeting": "3.2.0-dev.217",
101
- "@mentra/bluetooth-sdk": "3.2.0-dev.217",
102
- "@mentra/cloud-client": "3.2.0-dev.217",
103
- "@mentra/cloud-protocol": "3.2.0-dev.217",
104
- "@mentra/crust": "3.2.0-dev.217",
105
- "@mentra/miniapp": "3.2.0-dev.217",
100
+ "@mentra/acs-meeting": "3.2.0-dev.221",
101
+ "@mentra/bluetooth-sdk": "3.2.0-dev.221",
102
+ "@mentra/cloud-client": "3.2.0-dev.221",
103
+ "@mentra/cloud-protocol": "3.2.0-dev.221",
104
+ "@mentra/crust": "3.2.0-dev.221",
105
+ "@mentra/miniapp": "3.2.0-dev.221",
106
106
  "buffer": "^6.0.3",
107
107
  "events": "^3.3.0",
108
108
  "react-native-marked": "8.1.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.2.0",
15
- "releaseIdentity": "3.2.0-dev.217",
16
- "releaseSetId": "mentra-3.2.0-dev.217",
17
- "sourceCommit": "acd4336bcf67c5e2c7ddfa17ea1a2600dc7fb1d9",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.217.json",
19
- "otaManifestSha256": "4ec6bf57ab08ef2749a58232737dface3b0ddb0d8269b61e134b7cb5986efa4a"
15
+ "releaseIdentity": "3.2.0-dev.221",
16
+ "releaseSetId": "mentra-3.2.0-dev.221",
17
+ "sourceCommit": "3837a3055e7a69e18553ce51c3a97579d2ad3b83",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.221.json",
19
+ "otaManifestSha256": "657e9616ad0c464ee57d827c05b53393e45559992c13b15adbb06e0c903eb870"
20
20
  })
package/src/index.ts CHANGED
@@ -119,7 +119,13 @@ export {
119
119
  export type {OtaAutoChainAdvanceResult} from "./services/OtaAutoChain"
120
120
  export {
121
121
  BES_INSTALL_RESTART_MESSAGE,
122
+ OTA_ERROR_BES_RESTART_REQUIRED_COPY_KEY,
123
+ OTA_ERROR_ENGLISH_COPY,
124
+ OTA_ERROR_GENERIC_COPY_KEY,
125
+ OTA_ERROR_UNKNOWN_GLASSES_COPY_KEY,
126
+ OTA_GLASSES_ERROR_COPY_KEYS,
122
127
  getOtaErrorMessage,
128
+ otaErrorCopyKey,
123
129
  shouldRequireGlassesRebootForBesFailure,
124
130
  shouldShowChangeWifiForOtaDownloadFailure,
125
131
  } from "./services/OtaErrorMapping"
@@ -14,10 +14,12 @@ import {useMarkdown, type MarkedStyles, type useMarkdownHookOptions} from "react
14
14
  import {SafeAreaView} from "react-native-safe-area-context"
15
15
  import Svg, {Path, Rect} from "react-native-svg"
16
16
 
17
+ import {OTA_ERROR_ENGLISH_COPY} from "../services/OtaErrorMapping"
17
18
  import {
18
19
  MINIMUM_OTA_BATTERY_LEVEL,
19
20
  useMentraLiveOta,
20
21
  type MentraLiveOtaController,
22
+ type MentraLiveOtaError,
21
23
  type MentraLiveOtaFlowPage,
22
24
  } from "./useMentraLiveOta"
23
25
 
@@ -142,6 +144,8 @@ const ENGLISH_COPY: Record<string, string> = {
142
144
  "ota:versionChangeFirmwarePassComplete": "Firmware updated",
143
145
  "ota:versionChangeFirmwarePassCompleteMessage":
144
146
  "Your glasses restarted with new firmware. One more step: they'll now continue to the required version.",
147
+ "ota:updateFailed": "Update Failed",
148
+ ...OTA_ERROR_ENGLISH_COPY,
145
149
  }
146
150
 
147
151
  const componentCopyKey = {
@@ -150,8 +154,17 @@ const componentCopyKey = {
150
154
  bes: "ota:componentBes",
151
155
  } as const
152
156
 
157
+ /**
158
+ * Copy for the failed screen: the translated copy key when the failure maps to one,
159
+ * otherwise the engine's English message (phone-side watchdog and preflight text).
160
+ */
161
+ function failureMessage(error: MentraLiveOtaError | null, translate: MentraLiveOtaFlowTranslate): string {
162
+ if (!error) return translate("ota:errorGeneric")
163
+ return error.copyKey ? translate(error.copyKey) : error.message
164
+ }
165
+
153
166
  function defaultTranslate(key: string, options?: Record<string, string>): string {
154
- let value = ENGLISH_COPY[key] ?? key
167
+ let value = Object.prototype.hasOwnProperty.call(ENGLISH_COPY, key) ? ENGLISH_COPY[key] : key
155
168
  for (const [name, replacement] of Object.entries(options ?? {})) {
156
169
  value = value.replaceAll(`{{${name}}}`, replacement)
157
170
  }
@@ -559,8 +572,13 @@ function OtaFlowContent({
559
572
  }
560
573
  colors={colors}
561
574
  icon="alert"
562
- title="Update Failed">
563
- <BodyText colors={colors}>{state.error?.message}</BodyText>
575
+ title={translate("ota:updateFailed")}>
576
+ <BodyText colors={colors}>{failureMessage(state.error, translate)}</BodyText>
577
+ {state.error?.glassesCode ? (
578
+ <Text style={[styles.errorCode, {color: colors.textDim}]} testID="ota-error-code">
579
+ {translate("ota:errorCode", {code: state.error.glassesCode})}
580
+ </Text>
581
+ ) : null}
564
582
  </FlowPage>
565
583
  )
566
584
  }
@@ -810,6 +828,7 @@ const styles = StyleSheet.create({
810
828
  icon: {fontSize: 64, fontWeight: "500", lineHeight: 72, textAlign: "center"},
811
829
  title: {fontSize: 20, fontWeight: "600", textAlign: "center"},
812
830
  body: {fontSize: 14, lineHeight: 20, maxWidth: 420, textAlign: "center"},
831
+ errorCode: {fontSize: 12, fontVariant: ["tabular-nums"], lineHeight: 16, opacity: 0.7, textAlign: "center"},
813
832
  percent: {fontSize: 30, fontVariant: ["tabular-nums"], fontWeight: "700"},
814
833
  progressTrack: {borderRadius: 4, height: 8, maxWidth: 420, overflow: "hidden", width: "100%"},
815
834
  progressFill: {borderRadius: 4, height: 8},
@@ -14,7 +14,9 @@ import {
14
14
  } from "../services/OtaAutoChain"
15
15
  import {
16
16
  BES_INSTALL_RESTART_MESSAGE,
17
+ OTA_ERROR_BES_RESTART_REQUIRED_COPY_KEY,
17
18
  getOtaErrorMessage,
19
+ otaErrorCopyKey,
18
20
  shouldRequireGlassesRebootForBesFailure,
19
21
  shouldShowChangeWifiForOtaDownloadFailure,
20
22
  } from "../services/OtaErrorMapping"
@@ -54,7 +56,19 @@ export type MentraLiveOtaErrorCode =
54
56
 
55
57
  export type MentraLiveOtaError = {
56
58
  code: MentraLiveOtaErrorCode
59
+ /** English copy for hosts without localization. */
57
60
  message: string
61
+ /**
62
+ * Copy key for `message` when the failure maps to known copy, so localized hosts can
63
+ * translate it. Null for phone-side watchdog and preflight messages, which are English-only.
64
+ * Optional so host code that builds this type by hand keeps compiling; the hook always sets it.
65
+ */
66
+ copyKey?: string | null
67
+ /**
68
+ * Raw failure code reported by the glasses (`ota_status.error`), for support. Null when the
69
+ * failure originated on the phone. Optional for the same source-compatibility reason.
70
+ */
71
+ glassesCode?: string | null
58
72
  }
59
73
 
60
74
  export type MentraLiveOtaTransport = "wifi" | "hotspot"
@@ -680,9 +694,16 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
680
694
  error = {
681
695
  code: "check_failed",
682
696
  message: "Couldn't check for updates. Please check your connection and try again.",
697
+ copyKey: "ota:checkFailedMessage",
698
+ glassesCode: null,
683
699
  }
684
700
  } else if (screen === "update_info_unavailable") {
685
- error = {code: "update_info_unavailable", message: "Update information for this app version is unavailable."}
701
+ error = {
702
+ code: "update_info_unavailable",
703
+ message: "Update information for this app version is unavailable.",
704
+ copyKey: "ota:updateInfoUnavailableMessage",
705
+ glassesCode: null,
706
+ }
686
707
  }
687
708
  return {
688
709
  screen,
@@ -745,16 +766,33 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
745
766
  installSnapshot.otaProgress,
746
767
  installSnapshot.errorMsg,
747
768
  )
748
- const displayedError = requiresGlassesReboot
749
- ? BES_INSTALL_RESTART_MESSAGE
750
- : installSnapshot.errorMsg || getOtaErrorMessage(installSnapshot.otaStatus?.error)
769
+ // The raw code the glasses attached to their failure report, kept for support even when
770
+ // phone-side copy outranks it. Only a failed ota_status carries a current code.
771
+ const glassesCode = installSnapshot.otaStatus?.status === "failed" ? installSnapshot.otaStatus.error || null : null
772
+ // Precedence mirrors the legacy screen: the BES restart instruction, then a phone-side
773
+ // watchdog/preflight message (English-only, no copy key), then the glasses code mapped to
774
+ // copy (unknown codes get the generic glasses-error copy rather than the raw code).
775
+ let displayedError: string
776
+ let copyKey: string | null
777
+ if (requiresGlassesReboot) {
778
+ displayedError = BES_INSTALL_RESTART_MESSAGE
779
+ copyKey = OTA_ERROR_BES_RESTART_REQUIRED_COPY_KEY
780
+ } else if (installSnapshot.errorMsg) {
781
+ displayedError = installSnapshot.errorMsg
782
+ copyKey = null
783
+ } else {
784
+ displayedError = getOtaErrorMessage(glassesCode)
785
+ copyKey = otaErrorCopyKey(glassesCode)
786
+ }
751
787
  const progressState = progressScreen(installSnapshot)
752
788
  const screen = progressState === "complete" && autoChainActive ? "finishing" : progressState
753
- const error =
789
+ const error: MentraLiveOtaError | null =
754
790
  screen === "failed"
755
791
  ? {
756
- code: requiresGlassesReboot ? ("bes_restart_required" as const) : ("install_failed" as const),
792
+ code: requiresGlassesReboot ? "bes_restart_required" : "install_failed",
757
793
  message: displayedError,
794
+ copyKey,
795
+ glassesCode,
758
796
  }
759
797
  : null
760
798
  const totalSteps = installSnapshot.otaStatus?.totalSteps ?? null
@@ -1,7 +1,79 @@
1
1
  import type {OtaProgress, OtaStatus} from "../facades/ota"
2
2
 
3
- export const BES_INSTALL_RESTART_MESSAGE =
4
- "Restart your glasses to safely exit firmware update mode before trying again"
3
+ /**
4
+ * Copy keys for OTA failures the glasses report by code (`ota_status.error`), plus the
5
+ * phone-side BES restart instruction. Hosts translate these keys; the engine's own
6
+ * English copy for each key lives in `OTA_ERROR_ENGLISH_COPY`.
7
+ *
8
+ * The glasses-side producers are `sendProgressToPhone(..., "FAILED", errorCode)` in the
9
+ * ASG client's OtaHelper (download classification, install failures, and the downgrade
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` and the raw code is shown in a
12
+ * secondary line so support can still identify it.
13
+ */
14
+ export const OTA_GLASSES_ERROR_COPY_KEYS: Readonly<Record<string, string>> = {
15
+ no_internet: "ota:errorNoInternet",
16
+ clock_skew: "ota:errorClockSkew",
17
+ ssl_error: "ota:errorSslError",
18
+ download_failed: "ota:errorDownloadFailed",
19
+ insufficient_storage: "ota:errorInsufficientStorage",
20
+ firmware_too_large: "ota:errorFirmwareTooLarge",
21
+ firmware_verify_failed: "ota:errorFirmwareVerifyFailed",
22
+ apk_verify_failed: "ota:errorApkVerifyFailed",
23
+ install_failed: "ota:errorInstallFailed",
24
+ apk_restart_guard_not_persisted: "ota:errorApkRestartGuardNotPersisted",
25
+ downgrade_handoff_refused: "ota:errorDowngradeHandoffRefused",
26
+ downgrade_handoff_failed: "ota:errorDowngradeHandoffFailed",
27
+ downgrade_transaction_stalled: "ota:errorDowngradeTransactionStalled",
28
+ }
29
+
30
+ export const OTA_ERROR_UNKNOWN_GLASSES_COPY_KEY = "ota:errorGlassesUnknown"
31
+ export const OTA_ERROR_GENERIC_COPY_KEY = "ota:errorGeneric"
32
+ export const OTA_ERROR_BES_RESTART_REQUIRED_COPY_KEY = "ota:errorBesRestartRequired"
33
+
34
+ /** English copy for every OTA error key above. Mirrored in the Mentra App's `ota` i18n namespace. */
35
+ export const OTA_ERROR_ENGLISH_COPY: Readonly<Record<string, string>> = {
36
+ "ota:errorNoInternet": "Glasses WiFi has no internet connection",
37
+ "ota:errorClockSkew": "Glasses clock is wrong — syncing time from your phone, then retrying update check",
38
+ "ota:errorSslError": "Secure connection failed — try a different WiFi network",
39
+ "ota:errorDownloadFailed": "Download failed — check glasses WiFi connection",
40
+ "ota:errorInsufficientStorage": "Not enough storage on your glasses — free up space before trying again",
41
+ "ota:errorFirmwareTooLarge": "Firmware file is unexpectedly large — please contact support",
42
+ "ota:errorFirmwareVerifyFailed": "Firmware verification failed — please try again or contact support",
43
+ "ota:errorApkVerifyFailed": "Update verification failed — please try again or contact support",
44
+ "ota:errorInstallFailed": "Install failed — please try again",
45
+ "ota:errorApkRestartGuardNotPersisted":
46
+ "Your glasses could not save the update before restarting. Restart your glasses and try again.",
47
+ "ota:errorDowngradeHandoffRefused":
48
+ "Your glasses could not start the version change. Restart your glasses and try again.",
49
+ "ota:errorDowngradeHandoffFailed":
50
+ "The recovery service on your glasses did not respond. Restart your glasses and try again.",
51
+ "ota:errorDowngradeTransactionStalled":
52
+ "The version change on your glasses did not finish. Restart your glasses and try again.",
53
+ "ota:errorGlassesUnknown": "Your glasses reported an unexpected error. Restart your glasses and try again.",
54
+ "ota:errorGeneric": "Update failed",
55
+ "ota:errorBesRestartRequired": "Restart your glasses to safely exit firmware update mode before trying again",
56
+ "ota:errorCode": "Error code: {{code}}",
57
+ }
58
+
59
+ export const BES_INSTALL_RESTART_MESSAGE = OTA_ERROR_ENGLISH_COPY[OTA_ERROR_BES_RESTART_REQUIRED_COPY_KEY]
60
+
61
+ /**
62
+ * Copy key for a glasses-reported failure code. Unknown codes get the generic
63
+ * glasses-error copy; a missing code gets the plain generic copy.
64
+ */
65
+ export function otaErrorCopyKey(error?: string | null): string {
66
+ if (!error) return OTA_ERROR_GENERIC_COPY_KEY
67
+ // Own-property lookup: a code that happens to name an inherited Object member
68
+ // ("constructor", "toString", ...) is unknown, not a mapped key.
69
+ return hasOwn(OTA_GLASSES_ERROR_COPY_KEYS, error)
70
+ ? OTA_GLASSES_ERROR_COPY_KEYS[error]
71
+ : OTA_ERROR_UNKNOWN_GLASSES_COPY_KEY
72
+ }
73
+
74
+ function hasOwn(table: Readonly<Record<string, string>>, key: string): boolean {
75
+ return Object.prototype.hasOwnProperty.call(table, key)
76
+ }
5
77
 
6
78
  function isDownloadPhaseSnapshot(
7
79
  otaStatus: OtaStatus | null | undefined,
@@ -43,29 +115,9 @@ export function shouldShowChangeWifiForOtaDownloadFailure(
43
115
  return false
44
116
  }
45
117
 
46
- export function getOtaErrorMessage(error?: string): string {
47
- switch (error) {
48
- case "no_internet":
49
- return "Glasses WiFi has no internet connection"
50
- case "clock_skew":
51
- return "Glasses clock is wrong — syncing time from your phone, then retrying update check"
52
- case "ssl_error":
53
- return "Secure connection failed — try a different WiFi network"
54
- case "download_failed":
55
- return "Download failed — check glasses WiFi connection"
56
- case "insufficient_storage":
57
- return "Not enough storage on your glasses — free up space before trying again"
58
- case "firmware_too_large":
59
- return "Firmware file is unexpectedly large — please contact support"
60
- case "firmware_verify_failed":
61
- return "Firmware verification failed — please try again or contact support"
62
- case "apk_verify_failed":
63
- return "Update verification failed — please try again or contact support"
64
- case "install_failed":
65
- return "Install failed — please try again"
66
- default:
67
- return error || "Update failed"
68
- }
118
+ /** English message for a glasses-reported failure code. Never echoes the raw code. */
119
+ export function getOtaErrorMessage(error?: string | null): string {
120
+ return OTA_ERROR_ENGLISH_COPY[otaErrorCopyKey(error)]
69
121
  }
70
122
 
71
123
  /**
@@ -853,7 +853,12 @@ class OtaInstallCoordinator {
853
853
  // suppress ota_start and hang the fresh downgrade until the global timeout).
854
854
  this.otaStartOwnership?.outcome === "acknowledged" &&
855
855
  otaStatus?.stepType === "apk" &&
856
- otaStatus.phase === "install"
856
+ otaStatus.phase === "install" &&
857
+ // Only a live install arms the latch. A terminal failed status keeps stepType/phase
858
+ // apk/install and stays in the store, so without this gate every later pass (any
859
+ // unrelated store change) would re-arm the latch and the non-ownership block below
860
+ // would release it again, flooding the log with paired enter/release lines.
861
+ otaStatus.status === "in_progress"
857
862
  ) {
858
863
  console.log("[OTA_PROGRESS] version-change: apk install started — entering detour wait")
859
864
  this.versionChangeInstallStarted = true