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

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 (69) hide show
  1. package/build/generated/releaseMetadata.js +5 -5
  2. package/build/generated/releaseMetadata.js.map +1 -1
  3. package/build/index.d.ts +2 -0
  4. package/build/index.d.ts.map +1 -1
  5. package/build/index.js +1 -0
  6. package/build/index.js.map +1 -1
  7. package/build/services/AcsMeetingService.d.ts +360 -10
  8. package/build/services/AcsMeetingService.d.ts.map +1 -1
  9. package/build/services/AcsMeetingService.js +836 -19
  10. package/build/services/AcsMeetingService.js.map +1 -1
  11. package/build/services/AudioPlaybackService.d.ts +6 -0
  12. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  13. package/build/services/AudioPlaybackService.js +4 -3
  14. package/build/services/AudioPlaybackService.js.map +1 -1
  15. package/build/services/GlassesMicProbe.d.ts +78 -0
  16. package/build/services/GlassesMicProbe.d.ts.map +1 -0
  17. package/build/services/GlassesMicProbe.js +258 -0
  18. package/build/services/GlassesMicProbe.js.map +1 -0
  19. package/build/services/LocalMiniappRuntime.d.ts +83 -0
  20. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  21. package/build/services/LocalMiniappRuntime.js +714 -39
  22. package/build/services/LocalMiniappRuntime.js.map +1 -1
  23. package/build/services/MentraJSLogPipeline.d.ts +39 -0
  24. package/build/services/MentraJSLogPipeline.d.ts.map +1 -1
  25. package/build/services/MentraJSLogPipeline.js +59 -3
  26. package/build/services/MentraJSLogPipeline.js.map +1 -1
  27. package/build/services/MentraJSRouter.d.ts +1 -1
  28. package/build/services/MentraJSRouter.d.ts.map +1 -1
  29. package/build/services/MentraJSRouter.js +2 -2
  30. package/build/services/MentraJSRouter.js.map +1 -1
  31. package/build/services/MicStateCoordinator.d.ts +22 -0
  32. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  33. package/build/services/MicStateCoordinator.js +34 -3
  34. package/build/services/MicStateCoordinator.js.map +1 -1
  35. package/build/services/PhoneStreamCoordinator.d.ts +8 -0
  36. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  37. package/build/services/PhoneStreamCoordinator.js +2 -0
  38. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  39. package/build/services/SoftapCallTransport.d.ts +365 -0
  40. package/build/services/SoftapCallTransport.d.ts.map +1 -0
  41. package/build/services/SoftapCallTransport.js +722 -0
  42. package/build/services/SoftapCallTransport.js.map +1 -0
  43. package/build/services/SoftapCleanupBarrier.d.ts +45 -0
  44. package/build/services/SoftapCleanupBarrier.d.ts.map +1 -0
  45. package/build/services/SoftapCleanupBarrier.js +51 -0
  46. package/build/services/SoftapCleanupBarrier.js.map +1 -0
  47. package/build/utils/pcm16.d.ts +35 -0
  48. package/build/utils/pcm16.d.ts.map +1 -0
  49. package/build/utils/pcm16.js +85 -0
  50. package/build/utils/pcm16.js.map +1 -0
  51. package/build/utils/softapTrace.d.ts +39 -0
  52. package/build/utils/softapTrace.d.ts.map +1 -0
  53. package/build/utils/softapTrace.js +124 -0
  54. package/build/utils/softapTrace.js.map +1 -0
  55. package/package.json +7 -7
  56. package/src/generated/releaseMetadata.ts +5 -5
  57. package/src/index.ts +2 -0
  58. package/src/services/AcsMeetingService.ts +956 -22
  59. package/src/services/AudioPlaybackService.ts +12 -3
  60. package/src/services/GlassesMicProbe.ts +300 -0
  61. package/src/services/LocalMiniappRuntime.ts +775 -42
  62. package/src/services/MentraJSLogPipeline.ts +70 -3
  63. package/src/services/MentraJSRouter.ts +2 -2
  64. package/src/services/MicStateCoordinator.ts +35 -3
  65. package/src/services/PhoneStreamCoordinator.ts +10 -0
  66. package/src/services/SoftapCallTransport.ts +928 -0
  67. package/src/services/SoftapCleanupBarrier.ts +72 -0
  68. package/src/utils/pcm16.ts +93 -0
  69. package/src/utils/softapTrace.ts +133 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The wait a new SoftAP call does on the previous one's cleanup.
3
+ *
4
+ * Starting a call, stopping it before it finishes, and starting it again used to report "Cannot
5
+ * start glasses hotspot" and then fail the scoped join with the SSID missing from the scan. Both
6
+ * symptoms are the same cause: the second attempt raised a hotspot while the first attempt's
7
+ * `setHotspotState(false)` was still in flight, so the teardown of call N landed on call N+1.
8
+ *
9
+ * The rule this encodes is that the barrier is the *only* thing that decides when the next call
10
+ * may proceed, and it opens on evidence rather than on a clock. A timeout that says "long enough,
11
+ * probably safe" reintroduces exactly the race it was added to prevent, so there is deliberately
12
+ * no deadline here: a cleanup that never finishes holds the next call back forever, visibly,
13
+ * which is a bug someone can see and fix. The only thing time is used for is deciding whether the
14
+ * wait is worth mentioning — the common restart clears in a microtask, and a status line that
15
+ * flashed on every Start would teach the wearer to ignore the one time it mattered.
16
+ */
17
+
18
+ import {softapTrace} from "../utils/softapTrace"
19
+
20
+ export interface CleanupBarrierOptions {
21
+ /** Opens when the previous attempt's teardown *and* its in-flight join body have both finished. */
22
+ settled: Promise<void>
23
+ /** Already open: the caller can skip the wait, and the narration, entirely. */
24
+ settledDone: boolean
25
+ /** Told to the wearer only if the wait outlasts {@link narrateAfterMs}. */
26
+ narrate: (detail: string) => void
27
+ narrateAfterMs: number
28
+ /** Injectable for tests; a real timer everywhere else. */
29
+ delay?: (ms: number) => Promise<void>
30
+ /**
31
+ * Which attempt is being held, and which one it is held behind. Diagnostics only, and separate
32
+ * from the trace id because the barrier runs before the transport mints one: a restart that was
33
+ * delayed here is otherwise indistinguishable in the log from one that started slowly.
34
+ */
35
+ attempt?: number
36
+ waitingFor?: string
37
+ }
38
+
39
+ export const CLEANUP_BARRIER_COPY = "Still cleaning up the last call…"
40
+
41
+ /**
42
+ * Resolves once the previous attempt has genuinely finished, narrating if the wait is noticeable.
43
+ *
44
+ * Returns whether the wearer was told, so a caller can assert on it and a log can record that the
45
+ * restart was held rather than instant.
46
+ */
47
+ export async function awaitCleanupBarrier(options: CleanupBarrierOptions): Promise<{narrated: boolean}> {
48
+ const trace = {attempt: options.attempt ?? "unknown", waitingFor: options.waitingFor ?? "unknown"}
49
+ if (options.settledDone) {
50
+ softapTrace("softap_cleanup_barrier_open", {...trace, waitedMs: 0, reason: "already settled"})
51
+ return {narrated: false}
52
+ }
53
+ const startedAt = Date.now()
54
+ // Logged on entry as well as on exit, because the interesting case is the one where the exit
55
+ // line never arrives: a wait that is still open is only visible as an entry with nothing after it.
56
+ softapTrace("softap_cleanup_barrier_wait", trace)
57
+ const delay = options.delay ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))
58
+ const promptly = await Promise.race([
59
+ options.settled.then(() => true),
60
+ delay(options.narrateAfterMs).then(() => false),
61
+ ])
62
+ if (promptly) {
63
+ softapTrace("softap_cleanup_barrier_open", {...trace, waitedMs: Date.now() - startedAt, narrated: false})
64
+ return {narrated: false}
65
+ }
66
+ options.narrate(CLEANUP_BARRIER_COPY)
67
+ // The race decided what to say. It does not decide whether to proceed: that is this line, and
68
+ // it has no escape.
69
+ await options.settled
70
+ softapTrace("softap_cleanup_barrier_open", {...trace, waitedMs: Date.now() - startedAt, narrated: true})
71
+ return {narrated: true}
72
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Level statistics for 16-bit little-endian mono PCM, as delivered by `mic_pcm`.
3
+ *
4
+ * Shared by the call uplink and the dev mic probe so both report the same number: a soak that
5
+ * says `meanAbs=40` in one log and `meanAbs=40` in the other is measuring the same thing.
6
+ */
7
+
8
+ /**
9
+ * Hermes / the Expo bridge delivers `mic_pcm.pcm` as a Uint8Array, not an ArrayBuffer.
10
+ * `new DataView(uint8Array)` throws `buffer must be an ArrayBuffer` — that is the redbox
11
+ * attributed to whatever screen happens to be up (wifi scan, home, …).
12
+ */
13
+ export function pcmDataView(frame: unknown): DataView | null {
14
+ if (frame instanceof ArrayBuffer) {
15
+ return new DataView(frame)
16
+ }
17
+ if (ArrayBuffer.isView(frame)) {
18
+ try {
19
+ return new DataView(frame.buffer, frame.byteOffset, frame.byteLength)
20
+ } catch {
21
+ const copy = Uint8Array.from(frame as unknown as ArrayLike<number>)
22
+ return new DataView(copy.buffer)
23
+ }
24
+ }
25
+ if (Array.isArray(frame)) {
26
+ const copy = Uint8Array.from(frame)
27
+ return new DataView(copy.buffer)
28
+ }
29
+ return null
30
+ }
31
+
32
+ export type Pcm16Level = {
33
+ /** Mean absolute sample value (16-bit scale). ~30–60 is a quiet room on Mentra Live LC3. */
34
+ meanAbs: number
35
+ /** Largest absolute sample. */
36
+ peak: number
37
+ /** Samples counted. */
38
+ samples: number
39
+ }
40
+
41
+ /** Level statistics over a batch of PCM16 frames. Unreadable frames are skipped, never thrown on. */
42
+ export function summarizePcm16(frames: unknown[]): Pcm16Level {
43
+ let sum = 0
44
+ let peak = 0
45
+ let samples = 0
46
+ for (const frame of frames) {
47
+ const view = pcmDataView(frame)
48
+ if (!view) continue
49
+ const n = Math.floor(view.byteLength / 2)
50
+ for (let i = 0; i < n; i++) {
51
+ const v = Math.abs(view.getInt16(i * 2, true))
52
+ sum += v
53
+ if (v > peak) peak = v
54
+ }
55
+ samples += n
56
+ }
57
+ return {meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples}
58
+ }
59
+
60
+ /**
61
+ * Incremental accumulator for the same statistics, for hot paths that see one frame at a time
62
+ * and report once a window (the call uplink logs every 5 s at 20 frames/s).
63
+ */
64
+ export class Pcm16LevelMeter {
65
+ private sum = 0
66
+ private peakValue = 0
67
+ private count = 0
68
+
69
+ add(frame: unknown): void {
70
+ const view = pcmDataView(frame)
71
+ if (!view) return
72
+ const n = Math.floor(view.byteLength / 2)
73
+ for (let i = 0; i < n; i++) {
74
+ const v = Math.abs(view.getInt16(i * 2, true))
75
+ this.sum += v
76
+ if (v > this.peakValue) this.peakValue = v
77
+ }
78
+ this.count += n
79
+ }
80
+
81
+ /** Read the window and start a new one. */
82
+ take(): Pcm16Level {
83
+ const level = {
84
+ meanAbs: this.count ? Math.round(this.sum / this.count) : 0,
85
+ peak: this.peakValue,
86
+ samples: this.count,
87
+ }
88
+ this.sum = 0
89
+ this.peakValue = 0
90
+ this.count = 0
91
+ return level
92
+ }
93
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @fileoverview Correlated stage logging for the SoftAP calling pipeline (host side).
3
+ *
4
+ * TEMPORARY DIAGNOSTIC. Every line carries the literal `SOFTAP_TRACE` marker so a single
5
+ * `rg -n 'SOFTAP_TRACE'` finds all of them at cleanup time. Set `SOFTAP_TRACE_ENABLED` to
6
+ * false to mute the layer for a release without deleting call sites.
7
+ *
8
+ * The `phase=` shape matches the `[AcsMeeting] phase=...` convention already used by
9
+ * AcsMeetingService, so existing log tooling keeps parsing it.
10
+ *
11
+ * `traceId` is minted here when the hotspot request starts and travels to the glasses in
12
+ * `start_stream`. Both devices stamp the same id on every line, which is the only way to
13
+ * correlate two logs whose clocks were never synchronised.
14
+ */
15
+
16
+ /** Master switch. Flip to false to mute the trace without removing call sites. */
17
+ export const SOFTAP_TRACE_ENABLED = true
18
+
19
+ /** Grep marker. Never build this by concatenation — a single grep must be exhaustive. */
20
+ export const SOFTAP_TRACE_MARKER = "SOFTAP_TRACE"
21
+
22
+ const REDACTED = "<redacted>"
23
+
24
+ /** Keys whose values must never reach the log. Matched case-insensitively as substrings. */
25
+ const SENSITIVE_KEYS = [
26
+ "password",
27
+ "passwd",
28
+ "passphrase",
29
+ "psk",
30
+ "token",
31
+ "secret",
32
+ "credential",
33
+ "authorization",
34
+ "bearer",
35
+ "meetingurl",
36
+ ]
37
+
38
+ let traceId = ""
39
+ let originMs = 0
40
+ let lastStage = ""
41
+
42
+ /** Mint a trace id and reset the elapsed-time origin. Called when the hotspot request starts. */
43
+ export function beginSoftapTrace(id: string = newSoftapTraceId()): string {
44
+ traceId = id
45
+ originMs = Date.now()
46
+ lastStage = ""
47
+ return id
48
+ }
49
+
50
+ export function resetSoftapTrace(): void {
51
+ traceId = ""
52
+ originMs = 0
53
+ lastStage = ""
54
+ }
55
+
56
+ export function softapTraceId(): string {
57
+ return traceId
58
+ }
59
+
60
+ /** Last stage logged; use as failure context when reporting an error. */
61
+ export function softapLastStage(): string {
62
+ return lastStage
63
+ }
64
+
65
+ export function newSoftapTraceId(): string {
66
+ return Math.floor(Math.random() * 0xffffffff).toString(16)
67
+ }
68
+
69
+ function isSensitive(key: string): boolean {
70
+ const lower = key.toLowerCase()
71
+ return SENSITIVE_KEYS.some((sensitive) => lower.includes(sensitive))
72
+ }
73
+
74
+ /** Drop `?query`, `#fragment`, and `user:pass@` from anything URL-shaped. */
75
+ function stripUrlSecrets(text: string): string {
76
+ const schemeIndex = text.indexOf("://")
77
+ if (schemeIndex < 0) return text
78
+
79
+ let result = text
80
+ const query = result.indexOf("?")
81
+ if (query >= 0) result = `${result.slice(0, query)}?${REDACTED}`
82
+ const fragment = result.indexOf("#")
83
+ if (fragment >= 0) result = result.slice(0, fragment)
84
+
85
+ const at = result.indexOf("@", schemeIndex + 3)
86
+ if (at >= 0) {
87
+ result = `${result.slice(0, schemeIndex + 3)}${REDACTED}@${result.slice(at + 1)}`
88
+ }
89
+ return result
90
+ }
91
+
92
+ /**
93
+ * Redact secrets outright and strip query strings from URLs. The local WHIP URL is genuinely
94
+ * useful in a trace, but a URL carrying a watch token is not, so query and userinfo go.
95
+ */
96
+ export function sanitizeSoftapField(key: string, value: unknown): unknown {
97
+ if (isSensitive(key)) return REDACTED
98
+ if (typeof value === "string") return stripUrlSecrets(value)
99
+ return value
100
+ }
101
+
102
+ /** Apply {@link sanitizeSoftapField} across an object, leaving non-string values alone. */
103
+ export function sanitizeSoftapData(data: Record<string, unknown>): Record<string, unknown> {
104
+ const result: Record<string, unknown> = {}
105
+ for (const [key, value] of Object.entries(data)) {
106
+ result[key] = sanitizeSoftapField(key, value)
107
+ }
108
+ return result
109
+ }
110
+
111
+ /** Build the log head. Exported for tests. */
112
+ export function formatSoftapTrace(id: string, stage: string, elapsedMs: number): string {
113
+ const head = id ? `[${SOFTAP_TRACE_MARKER}] traceId=${id} phase=${stage}` : `[${SOFTAP_TRACE_MARKER}] phase=${stage}`
114
+ return `${head} elapsedMs=${elapsedMs}`
115
+ }
116
+
117
+ /** Log one pipeline stage transition. */
118
+ export function softapTrace(stage: string, data?: Record<string, unknown>): void {
119
+ if (!SOFTAP_TRACE_ENABLED) return
120
+ lastStage = stage
121
+ const elapsedMs = originMs === 0 ? 0 : Date.now() - originMs
122
+ const head = formatSoftapTrace(traceId, stage, elapsedMs)
123
+ if (data) console.log(head, sanitizeSoftapData(data))
124
+ else console.log(head)
125
+ }
126
+
127
+ /** Warn-level variant so genuine failures survive a log level filter. */
128
+ export function softapTraceFailure(stage: string, data?: Record<string, unknown>): void {
129
+ if (!SOFTAP_TRACE_ENABLED) return
130
+ const elapsedMs = originMs === 0 ? 0 : Date.now() - originMs
131
+ const head = formatSoftapTrace(traceId, stage, elapsedMs)
132
+ console.warn(head, sanitizeSoftapData({...(data ?? {}), afterStage: lastStage}))
133
+ }