@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.
- 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 +54 -6
- package/build/react/MentraLiveOtaFlow.js.map +1 -1
- package/build/react/useMentraLiveOta.d.ts +9 -0
- package/build/react/useMentraLiveOta.d.ts.map +1 -1
- package/build/react/useMentraLiveOta.js +4 -0
- package/build/react/useMentraLiveOta.js.map +1 -1
- package/build/services/BlobStore.d.ts +1 -0
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +28 -2
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/OtaArtifactDownloader.d.ts.map +1 -1
- package/build/services/OtaArtifactDownloader.js +10 -0
- package/build/services/OtaArtifactDownloader.js.map +1 -1
- package/build/services/OtaInstallCoordinator.d.ts +3 -0
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +12 -4
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +1 -1
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +5 -1
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/utils/display/profiles/g2.d.ts +1 -1
- package/build/utils/display/profiles/g2.d.ts.map +1 -1
- package/build/utils/display/profiles/g2.js +90 -7
- package/build/utils/display/profiles/g2.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/react/MentraLiveOtaFlow.tsx +66 -6
- package/src/react/useMentraLiveOta.ts +9 -0
- package/src/services/BlobStore.ts +25 -2
- package/src/services/OtaArtifactDownloader.ts +10 -0
- package/src/services/OtaInstallCoordinator.ts +13 -3
- package/src/services/OtaUpdateCheckService.ts +5 -1
- 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 =
|
|
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
|
-
|
|
143
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
1
|
+
import {G1_PROFILE} from "./g1"
|
|
2
|
+
import type {DisplayProfile} from "./types"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Even Realities G2
|
|
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
|
-
|
|
13
|
-
|
|
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
|
/**
|