@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.
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/services/AcsMeetingService.d.ts +360 -10
- package/build/services/AcsMeetingService.d.ts.map +1 -1
- package/build/services/AcsMeetingService.js +836 -19
- package/build/services/AcsMeetingService.js.map +1 -1
- package/build/services/AudioPlaybackService.d.ts +6 -0
- package/build/services/AudioPlaybackService.d.ts.map +1 -1
- package/build/services/AudioPlaybackService.js +4 -3
- package/build/services/AudioPlaybackService.js.map +1 -1
- package/build/services/GlassesMicProbe.d.ts +78 -0
- package/build/services/GlassesMicProbe.d.ts.map +1 -0
- package/build/services/GlassesMicProbe.js +258 -0
- package/build/services/GlassesMicProbe.js.map +1 -0
- package/build/services/LocalMiniappRuntime.d.ts +83 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +714 -39
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MentraJSLogPipeline.d.ts +39 -0
- package/build/services/MentraJSLogPipeline.d.ts.map +1 -1
- package/build/services/MentraJSLogPipeline.js +59 -3
- package/build/services/MentraJSLogPipeline.js.map +1 -1
- package/build/services/MentraJSRouter.d.ts +1 -1
- package/build/services/MentraJSRouter.d.ts.map +1 -1
- package/build/services/MentraJSRouter.js +2 -2
- package/build/services/MentraJSRouter.js.map +1 -1
- package/build/services/MicStateCoordinator.d.ts +22 -0
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +34 -3
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +8 -0
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +2 -0
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/SoftapCallTransport.d.ts +365 -0
- package/build/services/SoftapCallTransport.d.ts.map +1 -0
- package/build/services/SoftapCallTransport.js +722 -0
- package/build/services/SoftapCallTransport.js.map +1 -0
- package/build/services/SoftapCleanupBarrier.d.ts +45 -0
- package/build/services/SoftapCleanupBarrier.d.ts.map +1 -0
- package/build/services/SoftapCleanupBarrier.js +51 -0
- package/build/services/SoftapCleanupBarrier.js.map +1 -0
- package/build/utils/pcm16.d.ts +35 -0
- package/build/utils/pcm16.d.ts.map +1 -0
- package/build/utils/pcm16.js +85 -0
- package/build/utils/pcm16.js.map +1 -0
- package/build/utils/softapTrace.d.ts +39 -0
- package/build/utils/softapTrace.d.ts.map +1 -0
- package/build/utils/softapTrace.js +124 -0
- package/build/utils/softapTrace.js.map +1 -0
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +2 -0
- package/src/services/AcsMeetingService.ts +956 -22
- package/src/services/AudioPlaybackService.ts +12 -3
- package/src/services/GlassesMicProbe.ts +300 -0
- package/src/services/LocalMiniappRuntime.ts +775 -42
- package/src/services/MentraJSLogPipeline.ts +70 -3
- package/src/services/MentraJSRouter.ts +2 -2
- package/src/services/MicStateCoordinator.ts +35 -3
- package/src/services/PhoneStreamCoordinator.ts +10 -0
- package/src/services/SoftapCallTransport.ts +928 -0
- package/src/services/SoftapCleanupBarrier.ts +72 -0
- package/src/utils/pcm16.ts +93 -0
- 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
|
+
}
|