@mentra/engine 3.2.0-dev.174 → 3.2.0-dev.180

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 (37) hide show
  1. package/build/generated/releaseMetadata.js +5 -5
  2. package/build/generated/releaseMetadata.js.map +1 -1
  3. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  4. package/build/react/MentraLiveOtaFlow.js +54 -6
  5. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  6. package/build/react/useMentraLiveOta.d.ts +9 -0
  7. package/build/react/useMentraLiveOta.d.ts.map +1 -1
  8. package/build/react/useMentraLiveOta.js +4 -0
  9. package/build/react/useMentraLiveOta.js.map +1 -1
  10. package/build/services/BlobStore.d.ts +1 -0
  11. package/build/services/BlobStore.d.ts.map +1 -1
  12. package/build/services/BlobStore.js +28 -2
  13. package/build/services/BlobStore.js.map +1 -1
  14. package/build/services/OtaArtifactDownloader.d.ts.map +1 -1
  15. package/build/services/OtaArtifactDownloader.js +10 -0
  16. package/build/services/OtaArtifactDownloader.js.map +1 -1
  17. package/build/services/OtaInstallCoordinator.d.ts +3 -0
  18. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  19. package/build/services/OtaInstallCoordinator.js +12 -4
  20. package/build/services/OtaInstallCoordinator.js.map +1 -1
  21. package/build/services/OtaUpdateCheckService.d.ts +1 -1
  22. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  23. package/build/services/OtaUpdateCheckService.js +5 -1
  24. package/build/services/OtaUpdateCheckService.js.map +1 -1
  25. package/build/utils/display/profiles/g2.d.ts +1 -1
  26. package/build/utils/display/profiles/g2.d.ts.map +1 -1
  27. package/build/utils/display/profiles/g2.js +90 -7
  28. package/build/utils/display/profiles/g2.js.map +1 -1
  29. package/package.json +7 -7
  30. package/src/generated/releaseMetadata.ts +5 -5
  31. package/src/react/MentraLiveOtaFlow.tsx +66 -6
  32. package/src/react/useMentraLiveOta.ts +9 -0
  33. package/src/services/BlobStore.ts +25 -2
  34. package/src/services/OtaArtifactDownloader.ts +10 -0
  35. package/src/services/OtaInstallCoordinator.ts +13 -3
  36. package/src/services/OtaUpdateCheckService.ts +5 -1
  37. package/src/utils/display/profiles/g2.ts +94 -9
@@ -49,6 +49,7 @@ const SHARE_DIR_NAME = "mentra_blob_share"
49
49
  * files sooner under storage pressure.
50
50
  */
51
51
  const SHARE_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000
52
+ const SHARE_LAST_USED_FILE = ".last-shared"
52
53
  const META_KEY_ROOT = "mentraos_blobmeta_"
53
54
  /** Cap md5 computation so we don't block the JS thread hashing a huge file. */
54
55
  const MD5_MAX_BYTES = 50 * 1024 * 1024
@@ -103,6 +104,7 @@ interface ActiveReader {
103
104
 
104
105
  export class BlobStore {
105
106
  private readonly uploads = new Map<string, ActiveUpload>() // key: `${pkg} ${key}`
107
+ private readonly activeShareDirs = new Map<string, number>()
106
108
  private readonly readers = new Map<string, ActiveReader>() // key: host handle id
107
109
  /** user + package → committed usage bytes, cached; invalidated on commit/delete/clear. */
108
110
  private readonly usageCache = new Map<string, number>()
@@ -612,15 +614,18 @@ export class BlobStore {
612
614
  if (!root.exists) return
613
615
  const cutoff = now - SHARE_CACHE_MAX_AGE_MS
614
616
  for (const entry of root.list()) {
615
- if (!(entry instanceof Directory)) continue
617
+ if (!(entry instanceof Directory) || this.activeShareDirs.has(entry.uri)) continue
616
618
  try {
619
+ const lastUsed = new File(entry, SHARE_LAST_USED_FILE)
617
620
  const info = entry.info()
618
621
  // modificationTime is available on both platforms; creationTime can
619
622
  // be absent on Android versions before API 26. The directory id
620
623
  // begins with Date.now().toString(36), which is a final fallback.
621
624
  const encoded = entry.name.split("-", 1)[0]
622
625
  const encodedTime = /^[0-9a-z]+$/.test(encoded) ? Number.parseInt(encoded, 36) : Number.NaN
623
- const timestamp = info.modificationTime ?? info.creationTime ?? encodedTime
626
+ const timestamp = lastUsed.exists
627
+ ? Number(lastUsed.textSync())
628
+ : info.modificationTime ?? info.creationTime ?? encodedTime
624
629
  if (Number.isFinite(timestamp) && timestamp < cutoff) entry.delete()
625
630
  } catch {
626
631
  // Keep scanning if one cache entry disappears or is unreadable.
@@ -658,6 +663,7 @@ export class BlobStore {
658
663
  // right name + extension.
659
664
  let tempDir: Directory | null = null
660
665
  let cacheReady = false
666
+ let handoffStarted = false
661
667
  try {
662
668
  this.cleanupShareCache()
663
669
  tempDir = new Directory(Paths.cache, SHARE_DIR_NAME, sanitizeSegment(meta.fileName))
@@ -668,6 +674,11 @@ export class BlobStore {
668
674
  file.copy(temp)
669
675
  }
670
676
  cacheReady = true
677
+ // Persist each handoff's age, even when reusing old bytes. Directory
678
+ // modification time does not change when an existing file is shared.
679
+ new File(tempDir, SHARE_LAST_USED_FILE).write(String(Date.now()))
680
+ this.activeShareDirs.set(tempDir.uri, (this.activeShareDirs.get(tempDir.uri) ?? 0) + 1)
681
+ handoffStarted = true
671
682
  await Share.open({
672
683
  url: temp.uri,
673
684
  type: meta.mimeType || OCTET,
@@ -684,6 +695,18 @@ export class BlobStore {
684
695
  this.hooks.sendResult(packageName, requestId, true, {success: false})
685
696
  }
686
697
  } finally {
698
+ if (tempDir && handoffStarted) {
699
+ // Give async readers a full window after the chooser returns, even
700
+ // if it stayed open longer than the retention period.
701
+ try {
702
+ new File(tempDir, SHARE_LAST_USED_FILE).write(String(Date.now()))
703
+ } catch (error) {
704
+ console.warn(`${LOG_TAG}: could not refresh share retention`, error)
705
+ }
706
+ const remaining = (this.activeShareDirs.get(tempDir.uri) ?? 1) - 1
707
+ if (remaining > 0) this.activeShareDirs.set(tempDir.uri, remaining)
708
+ else this.activeShareDirs.delete(tempDir.uri)
709
+ }
687
710
  // Once a reusable cache entry is ready, retain it even if this particular
688
711
  // sheet is cancelled or fails: another concurrent/recent share may still
689
712
  // be reading the same file. Only remove an entry whose preparation did
@@ -140,6 +140,16 @@ export async function prepareArtifacts(
140
140
  const prepared: PreparedOtaArtifact[] = []
141
141
  for (let index = 0; index < plan.length; index++) {
142
142
  const entry = plan[index]
143
+ // Announce each file before cache verification or native download callbacks,
144
+ // so the previous file's 100% cannot linger under the next file's label.
145
+ onProgress?.({
146
+ kind: entry.kind,
147
+ index,
148
+ totalCount: plan.length,
149
+ artifactPercent: 0,
150
+ bytesWritten: 0,
151
+ contentLength: 0,
152
+ })
143
153
 
144
154
  const cachedPath = `${directory}/${entry.sha256}`
145
155
  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 = ""
@@ -305,6 +310,7 @@ class OtaInstallCoordinator {
305
310
  this.hotspotManifestUrl = null
306
311
  this.hotspotPhase = "idle"
307
312
  this.hotspotArtifactPercent = null
313
+ this.hotspotArtifact = null
308
314
  return this.selectedTransport
309
315
  }
310
316
 
@@ -512,6 +518,7 @@ class OtaInstallCoordinator {
512
518
  versionChangePhase: this.deriveVersionChangePhase(connected),
513
519
  hotspotPhase: this.hotspotPhase,
514
520
  hotspotArtifactPercent: this.hotspotArtifactPercent,
521
+ hotspotArtifact: this.hotspotArtifact ? {...this.hotspotArtifact} : null,
515
522
  transport: this.selectedTransport,
516
523
  }
517
524
  }
@@ -1517,7 +1524,9 @@ class OtaInstallCoordinator {
1517
1524
  }
1518
1525
  this.hotspotManifestUrl = await hotspotOtaTransport.prepare(this.preparedCheckResult, (progress) => {
1519
1526
  this.hotspotPhase = progress.phase
1520
- this.hotspotArtifactPercent = progress.artifact?.artifactPercent ?? null
1527
+ this.hotspotArtifact = progress.artifact ? {...progress.artifact} : null
1528
+ this.hotspotArtifactPercent =
1529
+ progress.artifact && progress.artifact.contentLength > 0 ? progress.artifact.artifactPercent : null
1521
1530
  this.emitInternalChange()
1522
1531
  })
1523
1532
  }
@@ -1597,6 +1606,7 @@ class OtaInstallCoordinator {
1597
1606
  this.hotspotManifestUrl = null
1598
1607
  this.hotspotPhase = "idle"
1599
1608
  this.hotspotArtifactPercent = null
1609
+ this.hotspotArtifact = null
1600
1610
  this.emitInternalChange()
1601
1611
  }
1602
1612
 
@@ -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
@@ -1,16 +1,89 @@
1
- import {DisplayProfile} from "./types"
1
+ import {G1_PROFILE} from "./g1"
2
+ import type {DisplayProfile} from "./types"
2
3
 
3
4
  /**
4
- * Even Realities G2 Smart Glasses Display Profile
5
- *
6
- * G2 uses the same display hardware as G1 (green monochrome, ~640x200).
7
- * Glyph widths and rendering formula are identical to G1.
8
- * The protocol differs (EvenHub protobuf vs G1 binary) but display
9
- * characteristics are the same.
5
+ * Even Realities G2 display profile. Reuse G1's baseline metrics with
6
+ * G2-specific line limits and proportional Russian Cyrillic advances.
10
7
  */
11
8
 
12
- // G2 uses the same glyph widths as G1 (same display hardware/font)
13
- import {G1_PROFILE} from "./g1"
9
+ /**
10
+ * Russian Cyrillic advance widths in rendered pixels. Independently reproduced
11
+ * with @evenrealities/evenhub-simulator 0.9.5: render each glyph 8 and 16 times,
12
+ * subtract the alpha-bounds widths, then divide by 8 to cancel glyph bearings
13
+ * and container padding. Simulator font rendering can differ from hardware.
14
+ * Other Cyrillic characters retain the inherited 18px fallback.
15
+ *
16
+ * Values below are converted to raw glyph units for (g + 1) * 2. Half-unit
17
+ * raw values preserve odd rendered advances exactly.
18
+ */
19
+ const G2_CYRILLIC_RENDERED_PX: Record<string, number> = {
20
+ а: 12,
21
+ б: 12,
22
+ в: 12,
23
+ г: 9,
24
+ д: 12,
25
+ е: 11,
26
+ ё: 11,
27
+ ж: 13,
28
+ з: 11,
29
+ и: 12,
30
+ й: 12,
31
+ к: 10,
32
+ л: 12,
33
+ м: 14,
34
+ н: 12,
35
+ о: 11,
36
+ п: 11,
37
+ р: 12,
38
+ с: 11,
39
+ т: 11,
40
+ у: 11,
41
+ ф: 13,
42
+ х: 11,
43
+ ц: 12,
44
+ ч: 12,
45
+ ш: 14,
46
+ щ: 15,
47
+ ъ: 13,
48
+ ы: 14,
49
+ ь: 12,
50
+ э: 11,
51
+ ю: 14,
52
+ я: 11,
53
+ А: 13,
54
+ Б: 13,
55
+ В: 13,
56
+ Г: 10,
57
+ Д: 15,
58
+ Е: 12,
59
+ Ё: 12,
60
+ Ж: 17,
61
+ З: 12,
62
+ И: 14,
63
+ Й: 14,
64
+ К: 13,
65
+ Л: 14,
66
+ М: 16,
67
+ Н: 13,
68
+ О: 14,
69
+ П: 13,
70
+ Р: 13,
71
+ С: 13,
72
+ Т: 12,
73
+ У: 13,
74
+ Ф: 16,
75
+ Х: 13,
76
+ Ц: 13,
77
+ Ч: 12,
78
+ Ш: 16,
79
+ Щ: 17,
80
+ Ъ: 15,
81
+ Ы: 16,
82
+ Ь: 13,
83
+ Э: 13,
84
+ Ю: 17,
85
+ Я: 13,
86
+ }
14
87
 
15
88
  export const G2_PROFILE: DisplayProfile = {
16
89
  ...G1_PROFILE,
@@ -24,6 +97,18 @@ export const G2_PROFILE: DisplayProfile = {
24
97
  // clean. With this set, the scene pipeline height-clips text so a box is
25
98
  // never handed more lines than fit.
26
99
  lineHeightPx: 40,
100
+ fontMetrics: {
101
+ ...G1_PROFILE.fontMetrics,
102
+ // Glyph map takes priority over uniformScripts in TextMeasurer, so the
103
+ // Cyrillic table overrides the uniform width for G2 only —
104
+ // G1 keeps its verified uniform 18px untouched.
105
+ glyphWidths: new Map<string, number>([
106
+ ...G1_PROFILE.fontMetrics.glyphWidths,
107
+ ...Object.entries(G2_CYRILLIC_RENDERED_PX).map(
108
+ ([char, renderedPx]) => [char, renderedPx / 2 - 1] as [string, number],
109
+ ),
110
+ ]),
111
+ },
27
112
  }
28
113
 
29
114
  /**