@mentra/engine 3.1.0-dev.98 → 3.1.1-beta.230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/build/generated/releaseMetadata.js +6 -6
  3. package/build/generated/releaseMetadata.js.map +1 -1
  4. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  5. package/build/react/MentraLiveOtaFlow.js +65 -15
  6. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  7. package/build/react/useMentraLiveOta.d.ts +9 -0
  8. package/build/react/useMentraLiveOta.d.ts.map +1 -1
  9. package/build/react/useMentraLiveOta.js +4 -0
  10. package/build/react/useMentraLiveOta.js.map +1 -1
  11. package/build/services/OtaArtifactDownloader.d.ts.map +1 -1
  12. package/build/services/OtaArtifactDownloader.js +10 -0
  13. package/build/services/OtaArtifactDownloader.js.map +1 -1
  14. package/build/services/OtaInstallCoordinator.d.ts +3 -0
  15. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  16. package/build/services/OtaInstallCoordinator.js +12 -4
  17. package/build/services/OtaInstallCoordinator.js.map +1 -1
  18. package/build/services/OtaUpdateCheckService.d.ts +1 -1
  19. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  20. package/build/services/OtaUpdateCheckService.js +5 -1
  21. package/build/services/OtaUpdateCheckService.js.map +1 -1
  22. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  23. package/build/services/PhoneStreamCoordinator.js +9 -6
  24. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  25. package/build/stores/settings.d.ts.map +1 -1
  26. package/build/stores/settings.js +17 -2
  27. package/build/stores/settings.js.map +1 -1
  28. package/build/utils/devLogging.d.ts.map +1 -1
  29. package/build/utils/devLogging.js +0 -1
  30. package/build/utils/devLogging.js.map +1 -1
  31. package/package.json +7 -7
  32. package/src/generated/releaseMetadata.ts +6 -6
  33. package/src/react/MentraLiveOtaFlow.tsx +82 -14
  34. package/src/react/useMentraLiveOta.ts +9 -0
  35. package/src/services/OtaArtifactDownloader.ts +10 -0
  36. package/src/services/OtaInstallCoordinator.ts +13 -3
  37. package/src/services/OtaUpdateCheckService.ts +5 -1
  38. package/src/services/PhoneStreamCoordinator.ts +10 -6
  39. package/src/stores/settings.ts +17 -2
  40. package/src/utils/devLogging.ts +0 -1
@@ -70,6 +70,20 @@ const DEFAULT_THEME: MentraLiveOtaFlowTheme = {
70
70
  }
71
71
 
72
72
  const ENGLISH_COPY: Record<string, string> = {
73
+ "ota:downloadingToPhone": "Downloading update to phone...",
74
+ "ota:startingGlassesHotspot": "Starting glasses hotspot...",
75
+ "ota:connectingPhoneToGlasses": "Connecting phone to glasses...",
76
+ "ota:startingHotspotUpdate": "Starting update...",
77
+ "ota:transferringToGlasses": "Transferring update to glasses...",
78
+ "ota:installingOnGlasses": "Installing update on glasses...",
79
+ "ota:componentApk": "Glasses software",
80
+ "ota:componentMtk": "System firmware",
81
+ "ota:componentBes": "Bluetooth firmware",
82
+ "ota:updateFile": "File {{current}} of {{total}} · {{component}}",
83
+ "ota:updatePart": "Update {{current}} of {{total}} · {{component}}",
84
+ "ota:phoneFileProgress": "Each file downloads separately. Progress is for the current file.",
85
+ "ota:transferFileProgress": "Progress is for this file’s transfer from your phone.",
86
+
73
87
  "common:continue": "Continue",
74
88
  "common:done": "Done",
75
89
  "ota:checkingForUpdates": "Checking for updates",
@@ -125,6 +139,12 @@ const ENGLISH_COPY: Record<string, string> = {
125
139
  "Your glasses restarted with new firmware. One more step: they'll now continue to the required version.",
126
140
  }
127
141
 
142
+ const componentCopyKey = {
143
+ apk: "ota:componentApk",
144
+ mtk: "ota:componentMtk",
145
+ bes: "ota:componentBes",
146
+ } as const
147
+
128
148
  function defaultTranslate(key: string, options?: Record<string, string>): string {
129
149
  let value = ENGLISH_COPY[key] ?? key
130
150
  for (const [name, replacement] of Object.entries(options ?? {})) {
@@ -374,28 +394,65 @@ function OtaFlowContent({
374
394
  }
375
395
 
376
396
  if (state.screen === "starting" || state.screen === "preparing_hotspot") {
377
- const title =
397
+ const title = translate(
378
398
  state.hotspotPhase === "downloading"
379
- ? "Downloading update to phone..."
399
+ ? "ota:downloadingToPhone"
380
400
  : state.hotspotPhase === "starting_hotspot"
381
- ? "Starting glasses hotspot..."
401
+ ? "ota:startingGlassesHotspot"
382
402
  : state.hotspotPhase === "joining_hotspot"
383
- ? "Connecting phone to glasses..."
384
- : "Starting update..."
403
+ ? "ota:connectingPhoneToGlasses"
404
+ : "ota:startingHotspotUpdate",
405
+ )
406
+ const artifact = state.hotspotPhase === "downloading" ? state.hotspotArtifact : null
385
407
  return (
386
408
  <FlowPage colors={colors} icon="download" title={title}>
409
+ {artifact ? (
410
+ <BodyText colors={colors}>
411
+ {translate("ota:updateFile", {
412
+ current: String(artifact.index + 1),
413
+ total: String(artifact.totalCount),
414
+ component: translate(componentCopyKey[artifact.kind]),
415
+ })}
416
+ </BodyText>
417
+ ) : null}
387
418
  {state.hotspotPhase === "downloading" && state.hotspotArtifactPercent !== null ? (
388
419
  <PercentText colors={colors} percent={state.hotspotArtifactPercent} />
389
420
  ) : null}
390
421
  <ActivityIndicator size="large" color={colors.foreground} />
422
+ {state.hotspotPhase === "downloading" ? (
423
+ <BodyText colors={colors}>{translate("ota:phoneFileProgress")}</BodyText>
424
+ ) : null}
391
425
  <BodyText colors={colors}>Do not disconnect your glasses</BodyText>
392
426
  </FlowPage>
393
427
  )
394
428
  }
395
429
 
396
430
  if (state.screen === "updating") {
431
+ const hotspot = state.transport === "hotspot"
432
+ const title = hotspot
433
+ ? translate(state.phase === "download" ? "ota:transferringToGlasses" : "ota:installingOnGlasses")
434
+ : state.phase === "download"
435
+ ? "Downloading..."
436
+ : "Installing..."
437
+ const component = state.step ? translate(componentCopyKey[state.step]) : null
438
+ const hasStepCount =
439
+ state.currentStep !== null &&
440
+ state.totalSteps !== null &&
441
+ state.currentStep > 0 &&
442
+ state.currentStep <= state.totalSteps
397
443
  return (
398
- <FlowPage colors={colors} icon="download" title={state.phase === "download" ? "Downloading..." : "Installing..."}>
444
+ <FlowPage colors={colors} icon="download" title={title}>
445
+ {hotspot && component ? (
446
+ <BodyText colors={colors}>
447
+ {hasStepCount
448
+ ? translate("ota:updatePart", {
449
+ current: String(state.currentStep),
450
+ total: String(state.totalSteps),
451
+ component,
452
+ })
453
+ : component}
454
+ </BodyText>
455
+ ) : null}
399
456
  {state.installingApkOnly ? (
400
457
  <ActivityIndicator size="large" color={colors.foreground} />
401
458
  ) : (
@@ -409,6 +466,9 @@ function OtaFlowContent({
409
466
  </>
410
467
  )}
411
468
  <BodyText colors={colors}>Do not disconnect your glasses</BodyText>
469
+ {hotspot && state.phase === "download" ? (
470
+ <BodyText colors={colors}>{translate("ota:transferFileProgress")}</BodyText>
471
+ ) : null}
412
472
  {state.versionChange && state.phase === "install" ? (
413
473
  <BodyText colors={colors}>{translate("ota:downgradeDuration")}</BodyText>
414
474
  ) : null}
@@ -512,11 +572,15 @@ type FlowPageProps = {
512
572
  function FlowPage({actions, children, colors, contentAlignment = "center", icon, title}: FlowPageProps) {
513
573
  return (
514
574
  <View style={styles.page} testID="mentra-live-ota-flow">
515
- <View style={[styles.centerContent, contentAlignment === "top" && styles.topContent]}>
575
+ <ScrollView
576
+ contentContainerStyle={[styles.centerContent, contentAlignment === "top" && styles.topContent]}
577
+ nestedScrollEnabled
578
+ style={styles.contentScroll}
579
+ testID="ota-page-scroll">
516
580
  <FlowIcon colors={colors} name={icon} />
517
581
  <Text style={[styles.title, {color: colors.foreground}]}>{title}</Text>
518
582
  {children}
519
- </View>
583
+ </ScrollView>
520
584
  {actions ? <View style={styles.actions}>{actions}</View> : <View style={styles.actionSpacer} />}
521
585
  </View>
522
586
  )
@@ -628,12 +692,14 @@ export function ChangelogList({
628
692
  title: string
629
693
  }) {
630
694
  if (changelogs.length === 0) return null
695
+
631
696
  return (
632
697
  <View style={[styles.changelogCard, {borderColor: colors.border}]} testID="ota-changelog-card">
633
698
  <Text style={[styles.changelogTitle, {color: colors.foreground}]}>{title}</Text>
634
699
  <ScrollView
635
700
  contentContainerStyle={styles.changelogContent}
636
701
  nestedScrollEnabled
702
+ persistentScrollbar
637
703
  showsVerticalScrollIndicator
638
704
  style={styles.changelogList}
639
705
  testID="ota-changelog-scroll">
@@ -715,8 +781,9 @@ const styles = StyleSheet.create({
715
781
  paddingHorizontal: 20,
716
782
  },
717
783
  page: {flex: 1, paddingBottom: 24, paddingHorizontal: 24},
718
- centerContent: {alignItems: "center", flex: 1, gap: 16, justifyContent: "center"},
719
- topContent: {justifyContent: "flex-start", paddingTop: 12},
784
+ contentScroll: {flex: 1},
785
+ centerContent: {alignItems: "center", flexGrow: 1, gap: 16, justifyContent: "center"},
786
+ topContent: {justifyContent: "flex-start", paddingBottom: 16, paddingTop: 12},
720
787
  actionSpacer: {height: 48},
721
788
  actions: {gap: 12},
722
789
  icon: {fontSize: 64, fontWeight: "500", lineHeight: 72, textAlign: "center"},
@@ -728,16 +795,17 @@ const styles = StyleSheet.create({
728
795
  changelogCard: {
729
796
  borderRadius: 16,
730
797
  borderWidth: 1,
731
- flexShrink: 1,
798
+ flexGrow: 1,
732
799
  gap: 12,
733
- maxHeight: 300,
734
800
  maxWidth: 420,
801
+ minHeight: 200,
735
802
  padding: 16,
736
803
  width: "100%",
737
804
  },
738
805
  changelogTitle: {fontSize: 16, fontWeight: "700"},
739
- changelogList: {flexShrink: 1, maxHeight: 232, width: "100%"},
740
- changelogContent: {gap: 20, paddingBottom: 2},
806
+ // Bound the notes themselves so the card can grow for its title, but not for all of the Markdown.
807
+ changelogList: {flexGrow: 1, height: 120, width: "100%"},
808
+ changelogContent: {gap: 20, paddingBottom: 4},
741
809
  changelogEntry: {gap: 8},
742
810
  changelogEntryDivider: {borderTopWidth: StyleSheet.hairlineWidth, paddingTop: 20},
743
811
  changelogVersion: {fontSize: 14, fontWeight: "600"},
@@ -82,6 +82,11 @@ export type MentraLiveOtaState = {
82
82
  hotspotSupported: boolean
83
83
  hotspotPhase: MentraLiveOtaHotspotPhase
84
84
  hotspotArtifactPercent: number | null
85
+ /**
86
+ * Current phone download context. Index is zero-based; percent applies to this file only.
87
+ * Optional for source compatibility; this hook returns null when no file is active.
88
+ */
89
+ hotspotArtifact?: {kind: MentraLiveOtaStep; index: number; totalCount: number} | null
85
90
  phase: MentraLiveOtaInstallPhase | null
86
91
  step: MentraLiveOtaStep | null
87
92
  currentStep: number | null
@@ -624,6 +629,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
624
629
  hotspotSupported,
625
630
  hotspotPhase: "idle",
626
631
  hotspotArtifactPercent: null,
632
+ hotspotArtifact: null,
627
633
  phase: null,
628
634
  step: null,
629
635
  currentStep: null,
@@ -678,6 +684,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
678
684
  hotspotSupported,
679
685
  hotspotPhase: "idle",
680
686
  hotspotArtifactPercent: null,
687
+ hotspotArtifact: null,
681
688
  phase: null,
682
689
  step: null,
683
690
  currentStep: null,
@@ -732,6 +739,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
732
739
  }
733
740
  : null
734
741
  const totalSteps = installSnapshot.otaStatus?.totalSteps ?? null
742
+ const artifact = installSnapshot.hotspotArtifact
735
743
  const changelogs = screen === "complete" ? releaseChangelogsForActiveChain() : []
736
744
  return {
737
745
  screen,
@@ -747,6 +755,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
747
755
  hotspotSupported,
748
756
  hotspotPhase: installSnapshot.hotspotPhase,
749
757
  hotspotArtifactPercent: installSnapshot.hotspotArtifactPercent,
758
+ hotspotArtifact: artifact ? {kind: artifact.kind, index: artifact.index, totalCount: artifact.totalCount} : null,
750
759
  phase: installSnapshot.otaStatus?.phase ?? installSnapshot.otaProgress?.stage ?? null,
751
760
  step: installSnapshot.otaStatus?.stepType ?? null,
752
761
  currentStep: installSnapshot.otaStatus?.currentStep ?? null,
@@ -139,6 +139,16 @@ export async function prepareArtifacts(
139
139
  const prepared: PreparedOtaArtifact[] = []
140
140
  for (let index = 0; index < plan.length; index++) {
141
141
  const entry = plan[index]
142
+ // Announce each file before cache verification or native download callbacks,
143
+ // so the previous file's 100% cannot linger under the next file's label.
144
+ onProgress?.({
145
+ kind: entry.kind,
146
+ index,
147
+ totalCount: plan.length,
148
+ artifactPercent: 0,
149
+ bytesWritten: 0,
150
+ contentLength: 0,
151
+ })
142
152
 
143
153
  const cachedPath = `${directory}/${entry.sha256}`
144
154
  if (await RNFS.exists(cachedPath)) {
@@ -22,6 +22,7 @@ import GlobalEventEmitter from "../utils/GlobalEventEmitter"
22
22
  import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
23
23
  import {resolveOtaManifestUrl} from "./otaManifestUrl"
24
24
  import {hotspotOtaTransport, type HotspotOtaPhase} from "./HotspotOtaTransport"
25
+ import type {OtaArtifactDownloadProgress} from "./OtaArtifactDownloader"
25
26
  import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
26
27
  import {deriveDisplayState, type DisplayState} from "./otaDisplayState"
27
28
  import {
@@ -139,8 +140,10 @@ function latestPercentForStuck(otaStatus: OtaStatus | null, otaProgress: OtaProg
139
140
  * reply that cancels the fallback.
140
141
  */
141
142
  function hasRecoveringOtaReply(otaStatus: OtaStatus | null, otaProgress: OtaProgress | null): boolean {
142
- if (otaProgress) return true
143
- return !!otaStatus && otaStatus.status !== "idle"
143
+ // OtaService projects status into legacy progress too: idle becomes STARTED.
144
+ // Neither that projection nor progress retained across reboot proves activity.
145
+ if (otaStatus) return otaStatus.status !== "idle"
146
+ return !!otaProgress
144
147
  }
145
148
 
146
149
  /** Read model the host progress screen renders from. */
@@ -171,6 +174,7 @@ export interface OtaInstallSnapshot {
171
174
  /** Pre-ota_start phone staging/join state for a hotspot attempt. */
172
175
  hotspotPhase: HotspotOtaPhase
173
176
  hotspotArtifactPercent: number | null
177
+ hotspotArtifact: OtaArtifactDownloadProgress | null
174
178
  /** Transport selected from the checked glasses capabilities and Wi-Fi state. */
175
179
  transport: "wifi" | "hotspot"
176
180
  }
@@ -189,6 +193,7 @@ class OtaInstallCoordinator {
189
193
  private hotspotManifestUrl: string | null = null
190
194
  private hotspotPhase: HotspotOtaPhase = "idle"
191
195
  private hotspotArtifactPercent: number | null = null
196
+ private hotspotArtifact: OtaArtifactDownloadProgress | null = null
192
197
 
193
198
  // Genuinely session-local state (was component state/refs).
194
199
  private errorMsg = ""
@@ -303,6 +308,7 @@ class OtaInstallCoordinator {
303
308
  this.hotspotManifestUrl = null
304
309
  this.hotspotPhase = "idle"
305
310
  this.hotspotArtifactPercent = null
311
+ this.hotspotArtifact = null
306
312
  return this.selectedTransport
307
313
  }
308
314
 
@@ -510,6 +516,7 @@ class OtaInstallCoordinator {
510
516
  versionChangePhase: this.deriveVersionChangePhase(connected),
511
517
  hotspotPhase: this.hotspotPhase,
512
518
  hotspotArtifactPercent: this.hotspotArtifactPercent,
519
+ hotspotArtifact: this.hotspotArtifact ? {...this.hotspotArtifact} : null,
513
520
  transport: this.selectedTransport,
514
521
  }
515
522
  }
@@ -1501,7 +1508,9 @@ class OtaInstallCoordinator {
1501
1508
  }
1502
1509
  this.hotspotManifestUrl = await hotspotOtaTransport.prepare(this.preparedCheckResult, (progress) => {
1503
1510
  this.hotspotPhase = progress.phase
1504
- this.hotspotArtifactPercent = progress.artifact?.artifactPercent ?? null
1511
+ this.hotspotArtifact = progress.artifact ? {...progress.artifact} : null
1512
+ this.hotspotArtifactPercent =
1513
+ progress.artifact && progress.artifact.contentLength > 0 ? progress.artifact.artifactPercent : null
1505
1514
  this.emitInternalChange()
1506
1515
  })
1507
1516
  }
@@ -1581,6 +1590,7 @@ class OtaInstallCoordinator {
1581
1590
  this.hotspotManifestUrl = null
1582
1591
  this.hotspotPhase = "idle"
1583
1592
  this.hotspotArtifactPercent = null
1593
+ this.hotspotArtifact = null
1584
1594
  this.emitInternalChange()
1585
1595
  }
1586
1596
 
@@ -13,7 +13,11 @@ import {resolveOtaReleaseVersion} from "./otaReleaseVersion"
13
13
  * glasses reporting a bare "complete", which would otherwise present as false success. Keep this
14
14
  * in lockstep with the ASG/recovery constant at release time.
15
15
  */
16
- export const DOWNGRADE_FLOOR_VERSION_CODE = 0
16
+ // Mentra 3.0: matches OtaConstants and RecoveryConstants on the glasses.
17
+ // Coordinated releases guarantee higher supported ASG builds can downgrade; no source gate is needed.
18
+ // The shipped floor may increase, never decrease, and must stay aligned across all checkers.
19
+ // See asg_client/docs/mentra-live-spec.md#ota-and-updates.
20
+ export const DOWNGRADE_FLOOR_VERSION_CODE = 51518114
17
21
 
18
22
  export interface VersionInfo {
19
23
  versionCode: number
@@ -813,12 +813,6 @@ export class PhoneStreamCoordinator {
813
813
  for (const reject of entry.hlsReadyRejecters) reject(pendingErr)
814
814
  entry.hlsReadyResolvers = []
815
815
  entry.hlsReadyRejecters = []
816
- // Cloudflare DELETE is fire-and-forget — failing it doesn't undo the
817
- // BLE stop we're about to do, and Cloudflare's idle-input scavenger
818
- // will eventually clean up leaked inputs.
819
- teardownManagedStream(entry.liveInputId).catch((err) => {
820
- console.warn("[STREAM] teardownManagedStream failed:", err)
821
- })
822
816
  }
823
817
 
824
818
  try {
@@ -833,6 +827,16 @@ export class PhoneStreamCoordinator {
833
827
  // Only clear if we're still the active entry (defensive — runExclusive
834
828
  // serializes us, so this should always be true).
835
829
  if (this.current === entry) this.current = null
830
+
831
+ if (entry.kind === "managed") {
832
+ // Start remote cleanup only after the publisher has stopped, but do not
833
+ // hold the local transition lock on an unbounded network request. The
834
+ // runtime's durable cleanup queue owns retries if this request fails or
835
+ // the app exits before Cloudflare finishes finalizing the recording.
836
+ void teardownManagedStream(entry.liveInputId).catch((err) => {
837
+ console.warn("[STREAM] teardownManagedStream failed:", err)
838
+ })
839
+ }
836
840
  }
837
841
  }
838
842
  }
@@ -410,10 +410,10 @@ export const SETTINGS: Record<string, Setting> = {
410
410
  saveOnServer: true,
411
411
  persist: true,
412
412
  },
413
- // Mentra Live center-mic loudness / "Barrier" gate (cs_swit type 10). Default on.
413
+ // Mentra Live center-mic loudness / "Barrier" gate (cs_swit type 10). Opt-in.
414
414
  loudness_gate_enabled: {
415
415
  key: "loudness_gate_enabled",
416
- defaultValue: () => true,
416
+ defaultValue: () => false,
417
417
  writable: true,
418
418
  saveOnServer: true,
419
419
  persist: true,
@@ -1048,6 +1048,21 @@ export const useSettingsStore = create<SettingsState>()(
1048
1048
  }
1049
1049
  }
1050
1050
 
1051
+ // Reset existing installs once; later user/app opt-ins remain available.
1052
+ const LOUDNESS_GATE_MIGRATION_KEY = "migration:loudness_gate_default_off_v1"
1053
+ const loudnessGateMigrationDone = storage.load<boolean>(LOUDNESS_GATE_MIGRATION_KEY)
1054
+ if (loudnessGateMigrationDone.is_error() || !loudnessGateMigrationDone.value) {
1055
+ // updateServer: true, matching the android_blur / camera_fov migrations. The flag is
1056
+ // inert until the Cloud V2 settings sync lands, but this setting is saveOnServer, so
1057
+ // the intent recorded here is the one that should carry over.
1058
+ const result = await get().setSetting(SETTINGS.loudness_gate_enabled.key, false, true)
1059
+ if (result.is_error()) {
1060
+ console.log("SETTINGS: loudness gate migration failed:", result.error)
1061
+ } else {
1062
+ storage.save(LOUDNESS_GATE_MIGRATION_KEY, true)
1063
+ }
1064
+ }
1065
+
1051
1066
  const NOTIFICATION_LISTENER_MIGRATION_KEY = "migration:android_notification_listener_default_on_v1"
1052
1067
  const notificationListenerMigrationDone = storage.load<boolean>(NOTIFICATION_LISTENER_MIGRATION_KEY)
1053
1068
  if (notificationListenerMigrationDone.is_error() || !notificationListenerMigrationDone.value) {
@@ -59,7 +59,6 @@ class LogRingBuffer {
59
59
  /The action 'POP_TO_TOP' was not handled/,
60
60
  /socket-0 binding/,
61
61
  /socket-0 bound to/,
62
- /Error while flushing PostHog/,
63
62
  /backTitleFontFamily prop is not available on Android/,
64
63
  /disableBackButtonMenu prop is not available on Android/,
65
64
  /backTitleVisible prop is not available on Android/,