@mentra/engine 3.2.0-dev.116 → 3.2.0-dev.121

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 (84) hide show
  1. package/build/engine.d.ts.map +1 -1
  2. package/build/engine.js +7 -2
  3. package/build/engine.js.map +1 -1
  4. package/build/generated/releaseMetadata.js +5 -5
  5. package/build/generated/releaseMetadata.js.map +1 -1
  6. package/build/index.d.ts +1 -0
  7. package/build/index.d.ts.map +1 -1
  8. package/build/index.js +1 -0
  9. package/build/index.js.map +1 -1
  10. package/build/runtime/streamConfig.d.ts +7 -0
  11. package/build/runtime/streamConfig.d.ts.map +1 -1
  12. package/build/runtime/streamConfig.js +7 -0
  13. package/build/runtime/streamConfig.js.map +1 -1
  14. package/build/services/AcsMeetingService.d.ts +154 -0
  15. package/build/services/AcsMeetingService.d.ts.map +1 -0
  16. package/build/services/AcsMeetingService.js +477 -0
  17. package/build/services/AcsMeetingService.js.map +1 -0
  18. package/build/services/AppRegistry.d.ts +9 -0
  19. package/build/services/AppRegistry.d.ts.map +1 -1
  20. package/build/services/AppRegistry.js +22 -0
  21. package/build/services/AppRegistry.js.map +1 -1
  22. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  23. package/build/services/DeviceEventRouter.js +12 -3
  24. package/build/services/DeviceEventRouter.js.map +1 -1
  25. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  26. package/build/services/GlassesSettingsSync.js +11 -1
  27. package/build/services/GlassesSettingsSync.js.map +1 -1
  28. package/build/services/GlassesStatusProjection.d.ts +12 -0
  29. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  30. package/build/services/GlassesStatusProjection.js +23 -0
  31. package/build/services/GlassesStatusProjection.js.map +1 -1
  32. package/build/services/LocalMiniappRuntime.d.ts +12 -0
  33. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  34. package/build/services/LocalMiniappRuntime.js +184 -2
  35. package/build/services/LocalMiniappRuntime.js.map +1 -1
  36. package/build/services/MentraJSRouter.d.ts.map +1 -1
  37. package/build/services/MentraJSRouter.js +8 -1
  38. package/build/services/MentraJSRouter.js.map +1 -1
  39. package/build/services/MiniappLauncher.d.ts +3 -1
  40. package/build/services/MiniappLauncher.d.ts.map +1 -1
  41. package/build/services/MiniappLauncher.js +66 -50
  42. package/build/services/MiniappLauncher.js.map +1 -1
  43. package/build/services/PhoneCameraFovCoordinator.d.ts +18 -0
  44. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
  45. package/build/services/PhoneCameraFovCoordinator.js +36 -0
  46. package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
  47. package/build/services/PhoneStreamCoordinator.d.ts +67 -1
  48. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  49. package/build/services/PhoneStreamCoordinator.js +204 -15
  50. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  51. package/build/services/StreamLifecycleController.d.ts +23 -5
  52. package/build/services/StreamLifecycleController.d.ts.map +1 -1
  53. package/build/services/StreamLifecycleController.js +31 -10
  54. package/build/services/StreamLifecycleController.js.map +1 -1
  55. package/build/services/acsAudioSource.d.ts +24 -0
  56. package/build/services/acsAudioSource.d.ts.map +1 -0
  57. package/build/services/acsAudioSource.js +37 -0
  58. package/build/services/acsAudioSource.js.map +1 -0
  59. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  60. package/build/utils/devMiniappLaunch.js +4 -0
  61. package/build/utils/devMiniappLaunch.js.map +1 -1
  62. package/build/utils/devMiniappSnapshot.d.ts +46 -0
  63. package/build/utils/devMiniappSnapshot.d.ts.map +1 -0
  64. package/build/utils/devMiniappSnapshot.js +94 -0
  65. package/build/utils/devMiniappSnapshot.js.map +1 -0
  66. package/package.json +7 -6
  67. package/src/engine.ts +10 -2
  68. package/src/generated/releaseMetadata.ts +5 -5
  69. package/src/index.ts +4 -0
  70. package/src/runtime/streamConfig.ts +13 -0
  71. package/src/services/AcsMeetingService.ts +570 -0
  72. package/src/services/AppRegistry.ts +23 -0
  73. package/src/services/DeviceEventRouter.ts +12 -3
  74. package/src/services/GlassesSettingsSync.ts +13 -3
  75. package/src/services/GlassesStatusProjection.ts +28 -0
  76. package/src/services/LocalMiniappRuntime.ts +214 -2
  77. package/src/services/MentraJSRouter.ts +7 -1
  78. package/src/services/MiniappLauncher.ts +73 -50
  79. package/src/services/PhoneCameraFovCoordinator.ts +34 -0
  80. package/src/services/PhoneStreamCoordinator.ts +232 -19
  81. package/src/services/StreamLifecycleController.ts +42 -12
  82. package/src/services/acsAudioSource.ts +53 -0
  83. package/src/utils/devMiniappLaunch.ts +4 -0
  84. package/src/utils/devMiniappSnapshot.ts +119 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Single switch for Mentra Call ACS uplink.
3
+ * `"phone"` = AudioRecord / iOS input tap → RawOutgoingAudioStream (no Cloudflare audio).
4
+ * `"glasses"` = today's glasses WHIP → Cloudflare → WHEP PCM → ACS path.
5
+ * preferred_mic does not govern this call.
6
+ */
7
+ export const ACS_CALL_MIC = "glasses";
8
+ function mapMic(value) {
9
+ if (value === "phone" || value === "bluetooth" || value === "bluetoothClassic")
10
+ return "phone";
11
+ if (value === "glasses")
12
+ return "glasses";
13
+ return null;
14
+ }
15
+ /**
16
+ * Pure preferred_mic → ACS source mapping. Store reads stay in the thin wrapper
17
+ * so this table can be unit tested without zustand.
18
+ */
19
+ export function resolveAudioSource(input) {
20
+ const preferred = input.preferred || "auto";
21
+ if (preferred === "phone" || preferred === "bluetooth") {
22
+ return { source: "phone", reason: "explicit" };
23
+ }
24
+ if (preferred === "glasses") {
25
+ return { source: "glasses", reason: "explicit" };
26
+ }
27
+ const fromCurrent = mapMic(input.currentMic);
28
+ if (fromCurrent)
29
+ return { source: fromCurrent, reason: "current-mic" };
30
+ const fromRank = mapMic(input.micRanking[0]);
31
+ if (fromRank)
32
+ return { source: fromRank, reason: "ranking" };
33
+ return input.glassesConnected
34
+ ? { source: "glasses", reason: "fallback-glasses-connected" }
35
+ : { source: "phone", reason: "fallback-no-glasses" };
36
+ }
37
+ //# sourceMappingURL=acsAudioSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acsAudioSource.js","sourceRoot":"","sources":["../../src/services/acsAudioSource.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAmB,SAAS,CAAA;AAOrD,SAAS,MAAM,CAAC,KAAgC;IAC9C,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,kBAAkB;QAAE,OAAO,OAAO,CAAA;IAC9F,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACzC,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAKlC;IACC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,MAAM,CAAA;IAC3C,IAAI,SAAS,KAAK,OAAO,IAAI,SAAS,KAAK,WAAW,EAAE,CAAC;QACvD,OAAO,EAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAC,CAAA;IAC9C,CAAC;IACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,EAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAC,CAAA;IAChD,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IAC5C,IAAI,WAAW;QAAE,OAAO,EAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAC,CAAA;IACpE,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5C,IAAI,QAAQ;QAAE,OAAO,EAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAC,CAAA;IAC1D,OAAO,KAAK,CAAC,gBAAgB;QAC3B,CAAC,CAAC,EAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,4BAA4B,EAAC;QAC3D,CAAC,CAAC,EAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,qBAAqB,EAAC,CAAA;AACtD,CAAC","sourcesContent":["export type SourceReason =\n | \"explicit\"\n | \"current-mic\"\n | \"ranking\"\n | \"fallback-glasses-connected\"\n | \"fallback-no-glasses\"\n\nexport type AcsAudioSource = \"glasses\" | \"phone\"\n\n/**\n * Single switch for Mentra Call ACS uplink.\n * `\"phone\"` = AudioRecord / iOS input tap → RawOutgoingAudioStream (no Cloudflare audio).\n * `\"glasses\"` = today's glasses WHIP → Cloudflare → WHEP PCM → ACS path.\n * preferred_mic does not govern this call.\n */\nexport const ACS_CALL_MIC: AcsAudioSource = \"glasses\"\n\nexport type ResolvedAudioSource = {\n source: AcsAudioSource\n reason: SourceReason\n}\n\nfunction mapMic(value: string | null | undefined): AcsAudioSource | null {\n if (value === \"phone\" || value === \"bluetooth\" || value === \"bluetoothClassic\") return \"phone\"\n if (value === \"glasses\") return \"glasses\"\n return null\n}\n\n/**\n * Pure preferred_mic → ACS source mapping. Store reads stay in the thin wrapper\n * so this table can be unit tested without zustand.\n */\nexport function resolveAudioSource(input: {\n preferred: string\n currentMic: string | null\n micRanking: string[]\n glassesConnected: boolean\n}): ResolvedAudioSource {\n const preferred = input.preferred || \"auto\"\n if (preferred === \"phone\" || preferred === \"bluetooth\") {\n return {source: \"phone\", reason: \"explicit\"}\n }\n if (preferred === \"glasses\") {\n return {source: \"glasses\", reason: \"explicit\"}\n }\n const fromCurrent = mapMic(input.currentMic)\n if (fromCurrent) return {source: fromCurrent, reason: \"current-mic\"}\n const fromRank = mapMic(input.micRanking[0])\n if (fromRank) return {source: fromRank, reason: \"ranking\"}\n return input.glassesConnected\n ? {source: \"glasses\", reason: \"fallback-glasses-connected\"}\n : {source: \"phone\", reason: \"fallback-no-glasses\"}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"devMiniappLaunch.d.ts","sourceRoot":"","sources":["../../src/utils/devMiniappLaunch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAgBH,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,eAAe,GACvB;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,GAC9D;IAAC,QAAQ,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,SAAS,CAAA;CAAC,CAAA;AAElE,MAAM,MAAM,sBAAsB,GAAG;IACnC,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAC1B,CAAA;AA4FD;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAoC1B"}
1
+ {"version":3,"file":"devMiniappLaunch.d.ts","sourceRoot":"","sources":["../../src/utils/devMiniappLaunch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAgBH,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,eAAe,GACvB;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,GAC9D;IAAC,QAAQ,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,SAAS,CAAA;CAAC,CAAA;AAElE,MAAM,MAAM,sBAAsB,GAAG;IACnC,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAC1B,CAAA;AAgGD;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAoC1B"}
@@ -102,6 +102,10 @@ function collectAlternateHosts(packageName, extra) {
102
102
  }
103
103
  for (const h of extra ?? [])
104
104
  push(h);
105
+ // USB `adb reverse` publishes the laptop on the phone's loopback. Mentra
106
+ // Call's QR still advertises the Wi-Fi IP, which AP isolation drops. Try
107
+ // loopback last so LAN/mDNS keep winning when they work.
108
+ push("127.0.0.1");
105
109
  return hosts;
106
110
  }
107
111
  async function probeUrl(devUrl) {
@@ -1 +1 @@
1
- {"version":3,"file":"devMiniappLaunch.js","sourceRoot":"","sources":["../../src/utils/devMiniappLaunch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * decideDevLaunchRoute — pre-flight reachability + manifest fetch for dev\n * miniapp launches.\n *\n * Every entry point that wants to launch a dev miniapp (home tile, QR\n * scan, URL screen, dev-offline \"Try again\" button) calls this BEFORE\n * navigating, so:\n *\n * 1. We land on the right destination in a single transition\n * (live mount vs offline takeover) — no flash through /applet/local.\n * 2. Callers that need the manifest (permission gate, name/icon read)\n * get it in the same round trip — no second fetch.\n *\n * Pre-flighting at the call site keeps /applet/local a pure mount route.\n *\n * When the laptop's Wi-Fi IP changes, the URL stored from the last QR scan\n * goes stale. We then retry alternate hosts (Metro / EXPO_PUBLIC_LOCAL_MINIAPP_HOST,\n * plus an mDNS name captured at scan time) and persist the working URL so\n * the next launch doesn't need a re-scan.\n */\n\nimport {configuredDevHost} from \"./configuredDevHost\"\nimport {storage} from \"./storage/storage\"\n\nconst REACHABILITY_TIMEOUT_MS = 1500\n\n// A single dropped DNS lookup or momentary Wi-Fi blip (AP roam, DHCP renewal,\n// phone hopping to cellular for a beat) looks identical to a dead dev server —\n// both throw out of fetch(). Retrying once absorbs that transient case instead\n// of bouncing straight to the offline screen. We do NOT retry a real HTTP\n// response (res.ok === false): that's the server telling us something\n// definitive, not a network-layer hiccup.\nconst REACHABILITY_MAX_ATTEMPTS = 2\nconst REACHABILITY_RETRY_DELAY_MS = 400\n\nexport type DevManifest = {\n packageName?: string\n name?: string\n /** First-found of `icon` / `iconUrl` / `logoUrl` (relative or absolute). */\n icon?: string\n permissions?: unknown\n hardwareRequirements?: unknown\n [key: string]: unknown\n}\n\nexport type DevLaunchResult =\n | {decision: \"live\"; manifest: DevManifest; resolvedUrl: string}\n | {decision: \"offline\"; manifest: null; resolvedUrl?: undefined}\n\nexport type DecideDevLaunchOptions = {\n /** Extra hostnames / IPs to try if `devUrl` is unreachable (no scheme/port). */\n alternateHosts?: string[]\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/** Single GET <devUrl>/miniapp.json attempt with a hard timeout. */\nasync function fetchManifestOnce(devUrl: string): Promise<DevManifest | null> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), REACHABILITY_TIMEOUT_MS)\n try {\n const res = await fetch(`${devUrl.replace(/\\/$/, \"\")}/miniapp.json`, {\n method: \"GET\",\n signal: controller.signal,\n })\n if (!res.ok) return null\n return (await res.json()) as DevManifest\n } finally {\n clearTimeout(timer)\n }\n}\n\nfunction rewriteHost(devUrl: string, host: string): string | null {\n const trimmed = host.trim()\n if (!trimmed) return null\n try {\n const url = new URL(devUrl)\n if (url.hostname === trimmed) return null\n url.hostname = trimmed\n return url.toString().replace(/\\/$/, \"\")\n } catch {\n return null\n }\n}\n\n/**\n * Point an http(s) icon at the host that actually answered. Cached dev icons are\n * local `file://` paths and third-party icons live on unrelated hosts, so only\n * icons still served by the stale dev host are rewritten.\n */\nfunction rewriteIconHost(iconUrl: string, staleDevUrl: string, resolvedDevUrl: string): string | null {\n try {\n const icon = new URL(iconUrl)\n if (icon.protocol !== \"http:\" && icon.protocol !== \"https:\") return null\n const stale = new URL(staleDevUrl)\n const resolved = new URL(resolvedDevUrl)\n // Compare host (hostname+port) so an icon on the same hostname but a\n // different service port is left alone.\n if (icon.host !== stale.host) return null\n icon.host = resolved.host\n return icon.toString()\n } catch {\n return null\n }\n}\n\nfunction collectAlternateHosts(packageName: string, extra?: string[]): string[] {\n const hosts: string[] = []\n const push = (h: string | undefined | null) => {\n const v = h?.trim()\n if (v && !hosts.includes(v)) hosts.push(v)\n }\n\n push(configuredDevHost())\n\n if (packageName) {\n const mdns = storage.load<string>(`${packageName}_dev_mdns`)\n if (mdns.is_ok()) push(mdns.value)\n }\n\n for (const h of extra ?? []) push(h)\n return hosts\n}\n\nasync function probeUrl(devUrl: string): Promise<DevManifest | null | \"network-error\"> {\n for (let attempt = 1; attempt <= REACHABILITY_MAX_ATTEMPTS; attempt++) {\n try {\n const manifest = await fetchManifestOnce(devUrl)\n // Definitive HTTP failure — do not retry this URL further.\n if (!manifest) return null\n return manifest\n } catch {\n if (attempt < REACHABILITY_MAX_ATTEMPTS) {\n await delay(REACHABILITY_RETRY_DELAY_MS)\n continue\n }\n return \"network-error\"\n }\n }\n return \"network-error\"\n}\n\n/**\n * GET <devUrl>/miniapp.json with a hard timeout, retrying once on a\n * network-layer failure (DNS/connection/abort) before trying alternate\n * hosts (Metro laptop IP, mDNS name from the QR). Returns the parsed\n * manifest on success (\"live\") or null (\"offline\").\n *\n * Side effects on success:\n * - writes <packageName>_dev_last_reachable\n * - if an alternate host worked, rewrites <packageName>_dev_url so the\n * next launch skips the stale IP\n */\nexport async function decideDevLaunchRoute(\n packageName: string,\n devUrl: string,\n options?: DecideDevLaunchOptions,\n): Promise<DevLaunchResult> {\n const candidates: string[] = [devUrl]\n for (const host of collectAlternateHosts(packageName, options?.alternateHosts)) {\n const rewritten = rewriteHost(devUrl, host)\n if (rewritten && !candidates.includes(rewritten)) candidates.push(rewritten)\n }\n\n for (const candidate of candidates) {\n const result = await probeUrl(candidate)\n if (result === \"network-error\" || result === null) continue\n\n if (packageName) {\n storage.save(`${packageName}_dev_last_reachable`, Date.now())\n if (candidate !== devUrl) {\n storage.save(`${packageName}_dev_url`, candidate)\n // Keep _dev_meta.devUrl in sync when present so home-tile reads match.\n const metaRes = storage.load<string>(`${packageName}_dev_meta`)\n if (metaRes.is_ok()) {\n try {\n const meta = JSON.parse(metaRes.value) as {devUrl?: string; iconUrl?: string}\n meta.devUrl = candidate\n if (typeof meta.iconUrl === \"string\") {\n const rewritten = rewriteIconHost(meta.iconUrl, devUrl, candidate)\n if (rewritten) meta.iconUrl = rewritten\n }\n storage.save(`${packageName}_dev_meta`, JSON.stringify(meta))\n } catch {\n /* ignore malformed meta */\n }\n }\n }\n }\n return {decision: \"live\", manifest: result, resolvedUrl: candidate}\n }\n\n return {decision: \"offline\", manifest: null}\n}\n"]}
1
+ {"version":3,"file":"devMiniappLaunch.js","sourceRoot":"","sources":["../../src/utils/devMiniappLaunch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * decideDevLaunchRoute — pre-flight reachability + manifest fetch for dev\n * miniapp launches.\n *\n * Every entry point that wants to launch a dev miniapp (home tile, QR\n * scan, URL screen, dev-offline \"Try again\" button) calls this BEFORE\n * navigating, so:\n *\n * 1. We land on the right destination in a single transition\n * (live mount vs offline takeover) — no flash through /applet/local.\n * 2. Callers that need the manifest (permission gate, name/icon read)\n * get it in the same round trip — no second fetch.\n *\n * Pre-flighting at the call site keeps /applet/local a pure mount route.\n *\n * When the laptop's Wi-Fi IP changes, the URL stored from the last QR scan\n * goes stale. We then retry alternate hosts (Metro / EXPO_PUBLIC_LOCAL_MINIAPP_HOST,\n * plus an mDNS name captured at scan time) and persist the working URL so\n * the next launch doesn't need a re-scan.\n */\n\nimport {configuredDevHost} from \"./configuredDevHost\"\nimport {storage} from \"./storage/storage\"\n\nconst REACHABILITY_TIMEOUT_MS = 1500\n\n// A single dropped DNS lookup or momentary Wi-Fi blip (AP roam, DHCP renewal,\n// phone hopping to cellular for a beat) looks identical to a dead dev server —\n// both throw out of fetch(). Retrying once absorbs that transient case instead\n// of bouncing straight to the offline screen. We do NOT retry a real HTTP\n// response (res.ok === false): that's the server telling us something\n// definitive, not a network-layer hiccup.\nconst REACHABILITY_MAX_ATTEMPTS = 2\nconst REACHABILITY_RETRY_DELAY_MS = 400\n\nexport type DevManifest = {\n packageName?: string\n name?: string\n /** First-found of `icon` / `iconUrl` / `logoUrl` (relative or absolute). */\n icon?: string\n permissions?: unknown\n hardwareRequirements?: unknown\n [key: string]: unknown\n}\n\nexport type DevLaunchResult =\n | {decision: \"live\"; manifest: DevManifest; resolvedUrl: string}\n | {decision: \"offline\"; manifest: null; resolvedUrl?: undefined}\n\nexport type DecideDevLaunchOptions = {\n /** Extra hostnames / IPs to try if `devUrl` is unreachable (no scheme/port). */\n alternateHosts?: string[]\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/** Single GET <devUrl>/miniapp.json attempt with a hard timeout. */\nasync function fetchManifestOnce(devUrl: string): Promise<DevManifest | null> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), REACHABILITY_TIMEOUT_MS)\n try {\n const res = await fetch(`${devUrl.replace(/\\/$/, \"\")}/miniapp.json`, {\n method: \"GET\",\n signal: controller.signal,\n })\n if (!res.ok) return null\n return (await res.json()) as DevManifest\n } finally {\n clearTimeout(timer)\n }\n}\n\nfunction rewriteHost(devUrl: string, host: string): string | null {\n const trimmed = host.trim()\n if (!trimmed) return null\n try {\n const url = new URL(devUrl)\n if (url.hostname === trimmed) return null\n url.hostname = trimmed\n return url.toString().replace(/\\/$/, \"\")\n } catch {\n return null\n }\n}\n\n/**\n * Point an http(s) icon at the host that actually answered. Cached dev icons are\n * local `file://` paths and third-party icons live on unrelated hosts, so only\n * icons still served by the stale dev host are rewritten.\n */\nfunction rewriteIconHost(iconUrl: string, staleDevUrl: string, resolvedDevUrl: string): string | null {\n try {\n const icon = new URL(iconUrl)\n if (icon.protocol !== \"http:\" && icon.protocol !== \"https:\") return null\n const stale = new URL(staleDevUrl)\n const resolved = new URL(resolvedDevUrl)\n // Compare host (hostname+port) so an icon on the same hostname but a\n // different service port is left alone.\n if (icon.host !== stale.host) return null\n icon.host = resolved.host\n return icon.toString()\n } catch {\n return null\n }\n}\n\nfunction collectAlternateHosts(packageName: string, extra?: string[]): string[] {\n const hosts: string[] = []\n const push = (h: string | undefined | null) => {\n const v = h?.trim()\n if (v && !hosts.includes(v)) hosts.push(v)\n }\n\n push(configuredDevHost())\n\n if (packageName) {\n const mdns = storage.load<string>(`${packageName}_dev_mdns`)\n if (mdns.is_ok()) push(mdns.value)\n }\n\n for (const h of extra ?? []) push(h)\n // USB `adb reverse` publishes the laptop on the phone's loopback. Mentra\n // Call's QR still advertises the Wi-Fi IP, which AP isolation drops. Try\n // loopback last so LAN/mDNS keep winning when they work.\n push(\"127.0.0.1\")\n return hosts\n}\n\nasync function probeUrl(devUrl: string): Promise<DevManifest | null | \"network-error\"> {\n for (let attempt = 1; attempt <= REACHABILITY_MAX_ATTEMPTS; attempt++) {\n try {\n const manifest = await fetchManifestOnce(devUrl)\n // Definitive HTTP failure — do not retry this URL further.\n if (!manifest) return null\n return manifest\n } catch {\n if (attempt < REACHABILITY_MAX_ATTEMPTS) {\n await delay(REACHABILITY_RETRY_DELAY_MS)\n continue\n }\n return \"network-error\"\n }\n }\n return \"network-error\"\n}\n\n/**\n * GET <devUrl>/miniapp.json with a hard timeout, retrying once on a\n * network-layer failure (DNS/connection/abort) before trying alternate\n * hosts (Metro laptop IP, mDNS name from the QR). Returns the parsed\n * manifest on success (\"live\") or null (\"offline\").\n *\n * Side effects on success:\n * - writes <packageName>_dev_last_reachable\n * - if an alternate host worked, rewrites <packageName>_dev_url so the\n * next launch skips the stale IP\n */\nexport async function decideDevLaunchRoute(\n packageName: string,\n devUrl: string,\n options?: DecideDevLaunchOptions,\n): Promise<DevLaunchResult> {\n const candidates: string[] = [devUrl]\n for (const host of collectAlternateHosts(packageName, options?.alternateHosts)) {\n const rewritten = rewriteHost(devUrl, host)\n if (rewritten && !candidates.includes(rewritten)) candidates.push(rewritten)\n }\n\n for (const candidate of candidates) {\n const result = await probeUrl(candidate)\n if (result === \"network-error\" || result === null) continue\n\n if (packageName) {\n storage.save(`${packageName}_dev_last_reachable`, Date.now())\n if (candidate !== devUrl) {\n storage.save(`${packageName}_dev_url`, candidate)\n // Keep _dev_meta.devUrl in sync when present so home-tile reads match.\n const metaRes = storage.load<string>(`${packageName}_dev_meta`)\n if (metaRes.is_ok()) {\n try {\n const meta = JSON.parse(metaRes.value) as {devUrl?: string; iconUrl?: string}\n meta.devUrl = candidate\n if (typeof meta.iconUrl === \"string\") {\n const rewritten = rewriteIconHost(meta.iconUrl, devUrl, candidate)\n if (rewritten) meta.iconUrl = rewritten\n }\n storage.save(`${packageName}_dev_meta`, JSON.stringify(meta))\n } catch {\n /* ignore malformed meta */\n }\n }\n }\n }\n return {decision: \"live\", manifest: result, resolvedUrl: candidate}\n }\n\n return {decision: \"offline\", manifest: null}\n}\n"]}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Dev miniapp snapshot — keep a local copy of the last live-dev bundle so
3
+ * walking away from the laptop still opens the miniapp while Mentra App is
4
+ * running (and after, as long as the on-disk `dev-*` snapshot remains).
5
+ *
6
+ * Live launches load HTTP off `mentra-miniapp dev` for hot reload. After a
7
+ * successful reachability probe we fire-and-forget `${devUrl}/bundle.zip`
8
+ * into `lmas/<pkg>/dev-<ms>/`. When the laptop is unreachable, launch
9
+ * routes and MiniappLauncher fall back to that snapshot instead of the
10
+ * "Dev server offline" dead-end.
11
+ */
12
+ import { type DecideDevLaunchOptions, type DevManifest } from "./devMiniappLaunch";
13
+ export type DevOpenDecision = {
14
+ decision: "live";
15
+ manifest: DevManifest;
16
+ resolvedUrl: string;
17
+ } | {
18
+ decision: "cached";
19
+ } | {
20
+ decision: "offline";
21
+ };
22
+ export type DevBundleSource = {
23
+ kind: "live";
24
+ resolvedUrl: string;
25
+ manifest: DevManifest;
26
+ } | {
27
+ kind: "snapshot";
28
+ version: string;
29
+ } | {
30
+ kind: "none";
31
+ };
32
+ /** Candidate zip URLs: same-origin `/bundle.zip`, then the sidecar path. */
33
+ export declare function snapshotCandidateUrls(baseUrl: string, sidecarPort?: number | null): string[];
34
+ /**
35
+ * Download the current live-dev zip into `lmas/<pkg>/dev-<ms>/` and keep
36
+ * only the newest snapshot. Coalesces concurrent calls per package.
37
+ */
38
+ export declare function queueDevSnapshot(packageName: string, baseUrl: string, sidecarPort?: number | null): void;
39
+ /** Reachability first; if the laptop is down, open the last local snapshot. */
40
+ export declare function decideDevOpenRoute(packageName: string, devUrl: string, options?: DecideDevLaunchOptions): Promise<DevOpenDecision>;
41
+ /**
42
+ * Same live-vs-snapshot choice MiniappLauncher uses to resolve a bundle.
43
+ * Live also kicks a background snapshot so the next offline open has a copy.
44
+ */
45
+ export declare function resolveDevBundleSource(packageName: string, devUrl: string, options?: DecideDevLaunchOptions): Promise<DevBundleSource>;
46
+ //# sourceMappingURL=devMiniappSnapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devMiniappSnapshot.d.ts","sourceRoot":"","sources":["../../src/utils/devMiniappSnapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAEL,KAAK,sBAAsB,EAE3B,KAAK,WAAW,EACjB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,MAAM,eAAe,GACvB;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,GAC9D;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAC,GACpB;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAC,CAAA;AAEzB,MAAM,MAAM,eAAe,GACvB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAC,GAC1D;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACnC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAA;AAIlB,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAc5F;AAOD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAOxG;AAkBD,+EAA+E;AAC/E,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAU1B;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAS1B"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Dev miniapp snapshot — keep a local copy of the last live-dev bundle so
3
+ * walking away from the laptop still opens the miniapp while Mentra App is
4
+ * running (and after, as long as the on-disk `dev-*` snapshot remains).
5
+ *
6
+ * Live launches load HTTP off `mentra-miniapp dev` for hot reload. After a
7
+ * successful reachability probe we fire-and-forget `${devUrl}/bundle.zip`
8
+ * into `lmas/<pkg>/dev-<ms>/`. When the laptop is unreachable, launch
9
+ * routes and MiniappLauncher fall back to that snapshot instead of the
10
+ * "Dev server offline" dead-end.
11
+ */
12
+ import appRegistry from "../services/AppRegistry";
13
+ import { storage } from "./storage/storage";
14
+ import { decideDevLaunchRoute, } from "./devMiniappLaunch";
15
+ const snapshotInFlight = new Map();
16
+ /** Candidate zip URLs: same-origin `/bundle.zip`, then the sidecar path. */
17
+ export function snapshotCandidateUrls(baseUrl, sidecarPort) {
18
+ const trimmed = baseUrl.replace(/\/$/, "");
19
+ const urls = [`${trimmed}/bundle.zip`];
20
+ try {
21
+ const url = new URL(trimmed);
22
+ const userPort = Number(url.port) || (url.protocol === "https:" ? 443 : 80);
23
+ const sidecar = sidecarPort && sidecarPort > 0 ? sidecarPort : userPort + 1;
24
+ url.port = String(sidecar);
25
+ const sidecarZip = `${url.origin}/__mentra_dev/bundle.zip`;
26
+ if (!urls.includes(sidecarZip))
27
+ urls.push(sidecarZip);
28
+ }
29
+ catch {
30
+ /* ignore malformed base */
31
+ }
32
+ return urls;
33
+ }
34
+ function storedSidecarPort(packageName) {
35
+ const stored = storage.load(`${packageName}_dev_port`);
36
+ return stored.is_ok() && Number.isFinite(stored.value) ? stored.value : undefined;
37
+ }
38
+ /**
39
+ * Download the current live-dev zip into `lmas/<pkg>/dev-<ms>/` and keep
40
+ * only the newest snapshot. Coalesces concurrent calls per package.
41
+ */
42
+ export function queueDevSnapshot(packageName, baseUrl, sidecarPort) {
43
+ if (!packageName || !baseUrl)
44
+ return;
45
+ if (snapshotInFlight.has(packageName))
46
+ return;
47
+ const promise = installDevSnapshot(packageName, baseUrl, sidecarPort ?? storedSidecarPort(packageName)).finally(() => {
48
+ snapshotInFlight.delete(packageName);
49
+ });
50
+ snapshotInFlight.set(packageName, promise);
51
+ }
52
+ async function installDevSnapshot(packageName, baseUrl, sidecarPort) {
53
+ let lastError;
54
+ for (const url of snapshotCandidateUrls(baseUrl, sidecarPort)) {
55
+ const res = await appRegistry.installFromUrl(url, {
56
+ versionOverride: `dev-${Date.now()}`,
57
+ releaseIdentity: { source: "dev_snapshot" },
58
+ });
59
+ if (res.is_ok()) {
60
+ appRegistry.gcDevVersions(packageName, 1);
61
+ return;
62
+ }
63
+ lastError = res.error;
64
+ }
65
+ console.warn(`Dev snapshot failed for ${packageName}:`, lastError);
66
+ }
67
+ /** Reachability first; if the laptop is down, open the last local snapshot. */
68
+ export async function decideDevOpenRoute(packageName, devUrl, options) {
69
+ const route = await decideDevLaunchRoute(packageName, devUrl, options);
70
+ if (route.decision === "live") {
71
+ queueDevSnapshot(packageName, route.resolvedUrl);
72
+ return route;
73
+ }
74
+ if (packageName && appRegistry.hasDevSnapshot(packageName)) {
75
+ return { decision: "cached" };
76
+ }
77
+ return { decision: "offline" };
78
+ }
79
+ /**
80
+ * Same live-vs-snapshot choice MiniappLauncher uses to resolve a bundle.
81
+ * Live also kicks a background snapshot so the next offline open has a copy.
82
+ */
83
+ export async function resolveDevBundleSource(packageName, devUrl, options) {
84
+ const route = await decideDevLaunchRoute(packageName, devUrl, options);
85
+ if (route.decision === "live" && route.manifest) {
86
+ queueDevSnapshot(packageName, route.resolvedUrl);
87
+ return { kind: "live", resolvedUrl: route.resolvedUrl, manifest: route.manifest };
88
+ }
89
+ const version = appRegistry.getLatestDevSnapshotVersion(packageName);
90
+ if (version)
91
+ return { kind: "snapshot", version };
92
+ return { kind: "none" };
93
+ }
94
+ //# sourceMappingURL=devMiniappSnapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devMiniappSnapshot.js","sourceRoot":"","sources":["../../src/utils/devMiniappSnapshot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dev miniapp snapshot — keep a local copy of the last live-dev bundle so\n * walking away from the laptop still opens the miniapp while Mentra App is\n * running (and after, as long as the on-disk `dev-*` snapshot remains).\n *\n * Live launches load HTTP off `mentra-miniapp dev` for hot reload. After a\n * successful reachability probe we fire-and-forget `${devUrl}/bundle.zip`\n * into `lmas/<pkg>/dev-<ms>/`. When the laptop is unreachable, launch\n * routes and MiniappLauncher fall back to that snapshot instead of the\n * \"Dev server offline\" dead-end.\n */\n\nimport appRegistry from \"../services/AppRegistry\"\nimport {storage} from \"./storage/storage\"\nimport {\n decideDevLaunchRoute,\n type DecideDevLaunchOptions,\n type DevLaunchResult,\n type DevManifest,\n} from \"./devMiniappLaunch\"\n\nexport type DevOpenDecision =\n | {decision: \"live\"; manifest: DevManifest; resolvedUrl: string}\n | {decision: \"cached\"}\n | {decision: \"offline\"}\n\nexport type DevBundleSource =\n | {kind: \"live\"; resolvedUrl: string; manifest: DevManifest}\n | {kind: \"snapshot\"; version: string}\n | {kind: \"none\"}\n\nconst snapshotInFlight = new Map<string, Promise<void>>()\n\n/** Candidate zip URLs: same-origin `/bundle.zip`, then the sidecar path. */\nexport function snapshotCandidateUrls(baseUrl: string, sidecarPort?: number | null): string[] {\n const trimmed = baseUrl.replace(/\\/$/, \"\")\n const urls = [`${trimmed}/bundle.zip`]\n try {\n const url = new URL(trimmed)\n const userPort = Number(url.port) || (url.protocol === \"https:\" ? 443 : 80)\n const sidecar = sidecarPort && sidecarPort > 0 ? sidecarPort : userPort + 1\n url.port = String(sidecar)\n const sidecarZip = `${url.origin}/__mentra_dev/bundle.zip`\n if (!urls.includes(sidecarZip)) urls.push(sidecarZip)\n } catch {\n /* ignore malformed base */\n }\n return urls\n}\n\nfunction storedSidecarPort(packageName: string): number | undefined {\n const stored = storage.load<number>(`${packageName}_dev_port`)\n return stored.is_ok() && Number.isFinite(stored.value) ? stored.value : undefined\n}\n\n/**\n * Download the current live-dev zip into `lmas/<pkg>/dev-<ms>/` and keep\n * only the newest snapshot. Coalesces concurrent calls per package.\n */\nexport function queueDevSnapshot(packageName: string, baseUrl: string, sidecarPort?: number | null): void {\n if (!packageName || !baseUrl) return\n if (snapshotInFlight.has(packageName)) return\n const promise = installDevSnapshot(packageName, baseUrl, sidecarPort ?? storedSidecarPort(packageName)).finally(() => {\n snapshotInFlight.delete(packageName)\n })\n snapshotInFlight.set(packageName, promise)\n}\n\nasync function installDevSnapshot(packageName: string, baseUrl: string, sidecarPort?: number | null): Promise<void> {\n let lastError: unknown\n for (const url of snapshotCandidateUrls(baseUrl, sidecarPort)) {\n const res = await appRegistry.installFromUrl(url, {\n versionOverride: `dev-${Date.now()}`,\n releaseIdentity: {source: \"dev_snapshot\"},\n })\n if (res.is_ok()) {\n appRegistry.gcDevVersions(packageName, 1)\n return\n }\n lastError = res.error\n }\n console.warn(`Dev snapshot failed for ${packageName}:`, lastError)\n}\n\n/** Reachability first; if the laptop is down, open the last local snapshot. */\nexport async function decideDevOpenRoute(\n packageName: string,\n devUrl: string,\n options?: DecideDevLaunchOptions,\n): Promise<DevOpenDecision> {\n const route = await decideDevLaunchRoute(packageName, devUrl, options)\n if (route.decision === \"live\") {\n queueDevSnapshot(packageName, route.resolvedUrl)\n return route\n }\n if (packageName && appRegistry.hasDevSnapshot(packageName)) {\n return {decision: \"cached\"}\n }\n return {decision: \"offline\"}\n}\n\n/**\n * Same live-vs-snapshot choice MiniappLauncher uses to resolve a bundle.\n * Live also kicks a background snapshot so the next offline open has a copy.\n */\nexport async function resolveDevBundleSource(\n packageName: string,\n devUrl: string,\n options?: DecideDevLaunchOptions,\n): Promise<DevBundleSource> {\n const route: DevLaunchResult = await decideDevLaunchRoute(packageName, devUrl, options)\n if (route.decision === \"live\" && route.manifest) {\n queueDevSnapshot(packageName, route.resolvedUrl)\n return {kind: \"live\", resolvedUrl: route.resolvedUrl, manifest: route.manifest}\n }\n const version = appRegistry.getLatestDevSnapshotVersion(packageName)\n if (version) return {kind: \"snapshot\", version}\n return {kind: \"none\"}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/engine",
3
- "version": "3.2.0-dev.116",
3
+ "version": "3.2.0-dev.121",
4
4
  "description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -97,11 +97,12 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@mentra/bluetooth-sdk": "3.2.0-dev.116",
101
- "@mentra/cloud-client": "3.2.0-dev.116",
102
- "@mentra/cloud-protocol": "3.2.0-dev.116",
103
- "@mentra/crust": "3.2.0-dev.116",
104
- "@mentra/miniapp": "3.2.0-dev.116",
100
+ "@mentra/acs-meeting": "3.2.0-dev.121",
101
+ "@mentra/bluetooth-sdk": "3.2.0-dev.121",
102
+ "@mentra/cloud-client": "3.2.0-dev.121",
103
+ "@mentra/cloud-protocol": "3.2.0-dev.121",
104
+ "@mentra/crust": "3.2.0-dev.121",
105
+ "@mentra/miniapp": "3.2.0-dev.121",
105
106
  "buffer": "^6.0.3",
106
107
  "events": "^3.3.0",
107
108
  "react-native-marked": "8.1.1",
package/src/engine.ts CHANGED
@@ -12,7 +12,11 @@ import {configure, start as bootstrapStart, stop as bootstrapStop, updateUiSeams
12
12
  import {cloudClientService} from "./services/CloudClientService"
13
13
  import {hydrateDeviceStore, demoteOrphanedDefaultWearable} from "./services/DeviceStoreHydration"
14
14
  import {startGlassesSettingsSync, stopGlassesSettingsSync} from "./services/GlassesSettingsSync"
15
- import {startGlassesStatusProjection, stopGlassesStatusProjection} from "./services/GlassesStatusProjection"
15
+ import {
16
+ startGlassesStatusProjection,
17
+ stopGlassesStatusProjection,
18
+ toMiniappConnectionData,
19
+ } from "./services/GlassesStatusProjection"
16
20
  import {startOtaService, stopOtaService} from "./services/OtaService"
17
21
  import {startAudioCloudUplink, stopAudioCloudUplink} from "./services/AudioCloudUplink"
18
22
  import {startSupportProfileSync, stopSupportProfileSync} from "./services/SupportProfileSync"
@@ -62,7 +66,11 @@ export const engine = {
62
66
  // Project native device status -> the engine stores (the inbound feed the rest
63
67
  // of the runtime reads). Established first so the stores are live before the
64
68
  // syncs below react to them.
65
- startGlassesStatusProjection((changed) => localMiniappRuntime.forwardEvent("glasses_connection_state", changed))
69
+ startGlassesStatusProjection((changed) => {
70
+ const connection = toMiniappConnectionData(changed)
71
+ if (!connection) return
72
+ localMiniappRuntime.forwardEvent("glasses_connection_state", connection)
73
+ })
66
74
  // Route the rest of the inbound device events (wifi/hotspot/gallery -> stores+bus,
67
75
  // photo/stream -> coordinators, button/touch/accel/head -> miniapps, save_setting ->
68
76
  // store, miniapp_selected -> launcher) so a bare OEM gets device data, not just the
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.0",
15
- "releaseIdentity": "3.2.0-dev.116",
16
- "releaseSetId": "mentra-3.2.0-dev.116",
17
- "sourceCommit": "d5ccf36f2b9bb3ca8384ac628bf9ef0502e0b379",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.116.json",
19
- "otaManifestSha256": "b6a4b0b6ececa107f8b20691a10b8e31066da83698748cbdbbb8f4d392177040"
15
+ "releaseIdentity": "3.2.0-dev.121",
16
+ "releaseSetId": "mentra-3.2.0-dev.121",
17
+ "sourceCommit": "4b4ff7a0a5c3c73f79ccc99e2464f75c23472cf0",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.121.json",
19
+ "otaManifestSha256": "2e8470ba4809eb5c1ea2d250772ab8cdbb542805526c3900d6d18d906b29bbb2"
20
20
  })
package/src/index.ts CHANGED
@@ -182,6 +182,10 @@ export {
182
182
  type DevLaunchResult,
183
183
  type DevManifest,
184
184
  } from "./utils/devMiniappLaunch"
185
+ export {
186
+ decideDevOpenRoute,
187
+ type DevOpenDecision,
188
+ } from "./utils/devMiniappSnapshot"
185
189
  export {HardwareCompatibility, type CompatibilityResult} from "./utils/hardware"
186
190
  export {BgTimer, throttle, debounce} from "./utils/timers"
187
191
 
@@ -32,3 +32,16 @@ export const normalizeStreamAudioConfig = (value: unknown): StreamAudioConfig |
32
32
  if (typeof value.noiseSuppression === "boolean") config.noiseSuppression = value.noiseSuppression
33
33
  return Object.keys(config).length > 0 ? config : undefined
34
34
  }
35
+
36
+ /** Absent or non-boolean values keep the caller-supplied fallback (default true). */
37
+ export const normalizeCaptureAudio = (value: unknown, fallback = true): boolean =>
38
+ typeof value === "boolean" ? value : fallback
39
+
40
+ /**
41
+ * Glasses capture is locked to the ACS source for the WHIP session.
42
+ * Phone/bluetooth never encode the glasses mic, even if a miniapp asks for it.
43
+ */
44
+ export const resolveCaptureAudio = (
45
+ payload: unknown,
46
+ source: "glasses" | "phone",
47
+ ): boolean => source === "glasses" && normalizeCaptureAudio(payload, true)