@mentra/engine 0.1.0-dev.1 → 3.1.0-beta.100

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 (263) hide show
  1. package/README.md +43 -29
  2. package/build/bluetooth-sdk/debug.d.ts +2 -0
  3. package/build/bluetooth-sdk/debug.d.ts.map +1 -0
  4. package/build/bluetooth-sdk/debug.js +2 -0
  5. package/build/bluetooth-sdk/debug.js.map +1 -0
  6. package/build/bluetooth-sdk/index.d.ts +3 -0
  7. package/build/bluetooth-sdk/index.d.ts.map +1 -0
  8. package/build/bluetooth-sdk/index.js +3 -0
  9. package/build/bluetooth-sdk/index.js.map +1 -0
  10. package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
  11. package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
  12. package/build/bluetooth-sdk/ota-transport.js +2 -0
  13. package/build/bluetooth-sdk/ota-transport.js.map +1 -0
  14. package/build/bluetooth-sdk/photo-receiver.d.ts +3 -0
  15. package/build/bluetooth-sdk/photo-receiver.d.ts.map +1 -0
  16. package/build/bluetooth-sdk/photo-receiver.js +3 -0
  17. package/build/bluetooth-sdk/photo-receiver.js.map +1 -0
  18. package/build/bluetooth-sdk/react.d.ts +2 -0
  19. package/build/bluetooth-sdk/react.d.ts.map +1 -0
  20. package/build/bluetooth-sdk/react.js +2 -0
  21. package/build/bluetooth-sdk/react.js.map +1 -0
  22. package/build/bluetooth-sdk/types.d.ts +2 -0
  23. package/build/bluetooth-sdk/types.d.ts.map +1 -0
  24. package/build/bluetooth-sdk/types.js +2 -0
  25. package/build/bluetooth-sdk/types.js.map +1 -0
  26. package/build/engine.d.ts +18 -15
  27. package/build/engine.d.ts.map +1 -1
  28. package/build/engine.js +2 -2
  29. package/build/engine.js.map +1 -1
  30. package/build/facades/display.d.ts +1 -1
  31. package/build/facades/display.d.ts.map +1 -1
  32. package/build/facades/display.js +1 -1
  33. package/build/facades/display.js.map +1 -1
  34. package/build/facades/glasses.d.ts +3 -13
  35. package/build/facades/glasses.d.ts.map +1 -1
  36. package/build/facades/glasses.js +3 -3
  37. package/build/facades/glasses.js.map +1 -1
  38. package/build/facades/ota.d.ts +16 -22
  39. package/build/facades/ota.d.ts.map +1 -1
  40. package/build/facades/ota.js +13 -9
  41. package/build/facades/ota.js.map +1 -1
  42. package/build/facades/pairing.d.ts +8 -5
  43. package/build/facades/pairing.d.ts.map +1 -1
  44. package/build/facades/pairing.js +83 -9
  45. package/build/facades/pairing.js.map +1 -1
  46. package/build/facades/speech.d.ts +7 -6
  47. package/build/facades/speech.d.ts.map +1 -1
  48. package/build/facades/speech.js +1 -1
  49. package/build/facades/speech.js.map +1 -1
  50. package/build/generated/releaseMetadata.d.ts +12 -0
  51. package/build/generated/releaseMetadata.d.ts.map +1 -0
  52. package/build/generated/releaseMetadata.js +10 -0
  53. package/build/generated/releaseMetadata.js.map +1 -0
  54. package/build/index.d.ts +7 -8
  55. package/build/index.d.ts.map +1 -1
  56. package/build/index.js +4 -5
  57. package/build/index.js.map +1 -1
  58. package/build/react/MentraLiveOtaFlow.d.ts +8 -2
  59. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  60. package/build/react/MentraLiveOtaFlow.js +256 -354
  61. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  62. package/build/react/index.d.ts +1 -0
  63. package/build/react/index.d.ts.map +1 -1
  64. package/build/react/index.js +1 -0
  65. package/build/react/index.js.map +1 -1
  66. package/build/react/useMentraLiveOta.d.ts +84 -0
  67. package/build/react/useMentraLiveOta.d.ts.map +1 -0
  68. package/build/react/useMentraLiveOta.js +627 -0
  69. package/build/react/useMentraLiveOta.js.map +1 -0
  70. package/build/services/AppRegistry.d.ts +8 -0
  71. package/build/services/AppRegistry.d.ts.map +1 -1
  72. package/build/services/AppRegistry.js +15 -0
  73. package/build/services/AppRegistry.js.map +1 -1
  74. package/build/services/BlobStore.d.ts +2 -0
  75. package/build/services/BlobStore.d.ts.map +1 -1
  76. package/build/services/BlobStore.js +63 -10
  77. package/build/services/BlobStore.js.map +1 -1
  78. package/build/services/DisplayProcessor.d.ts +3 -3
  79. package/build/services/DisplayProcessor.js +3 -3
  80. package/build/services/DisplayProcessor.js.map +1 -1
  81. package/build/services/GlassesStatusProjection.d.ts +15 -2
  82. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  83. package/build/services/GlassesStatusProjection.js +3 -3
  84. package/build/services/GlassesStatusProjection.js.map +1 -1
  85. package/build/services/HotspotOtaTransport.d.ts.map +1 -1
  86. package/build/services/HotspotOtaTransport.js +8 -7
  87. package/build/services/HotspotOtaTransport.js.map +1 -1
  88. package/build/services/LocalDisplayManager.d.ts +2 -3
  89. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  90. package/build/services/LocalDisplayManager.js +2 -3
  91. package/build/services/LocalDisplayManager.js.map +1 -1
  92. package/build/services/OtaAutoChain.d.ts +9 -2
  93. package/build/services/OtaAutoChain.d.ts.map +1 -1
  94. package/build/services/OtaAutoChain.js +10 -2
  95. package/build/services/OtaAutoChain.js.map +1 -1
  96. package/build/services/OtaInstallCoordinator.d.ts +26 -3
  97. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  98. package/build/services/OtaInstallCoordinator.js +38 -13
  99. package/build/services/OtaInstallCoordinator.js.map +1 -1
  100. package/build/services/OtaService.d.ts.map +1 -1
  101. package/build/services/OtaService.js +1 -1
  102. package/build/services/OtaService.js.map +1 -1
  103. package/build/services/OtaUpdateCheckService.d.ts +4 -1
  104. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  105. package/build/services/OtaUpdateCheckService.js +33 -21
  106. package/build/services/OtaUpdateCheckService.js.map +1 -1
  107. package/build/services/PhonePhotoCoordinator.js +1 -1
  108. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  109. package/build/services/StreamLifecycleController.d.ts +3 -3
  110. package/build/services/StreamLifecycleController.js +3 -3
  111. package/build/services/StreamLifecycleController.js.map +1 -1
  112. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  113. package/build/services/asg/localNetworkTransport.js +13 -12
  114. package/build/services/asg/localNetworkTransport.js.map +1 -1
  115. package/build/services/glassesClockSync.d.ts.map +1 -1
  116. package/build/services/glassesClockSync.js +5 -1
  117. package/build/services/glassesClockSync.js.map +1 -1
  118. package/build/services/otaDisplayState.d.ts +1 -1
  119. package/build/services/otaDisplayState.d.ts.map +1 -1
  120. package/build/services/otaDisplayState.js.map +1 -1
  121. package/build/services/otaInstallPolicy.d.ts +5 -1
  122. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  123. package/build/services/otaInstallPolicy.js +4 -0
  124. package/build/services/otaInstallPolicy.js.map +1 -1
  125. package/build/services/otaLegacyMapping.d.ts +1 -1
  126. package/build/services/otaLegacyMapping.d.ts.map +1 -1
  127. package/build/services/otaLegacyMapping.js.map +1 -1
  128. package/build/services/otaManifestPolicy.d.ts +14 -0
  129. package/build/services/otaManifestPolicy.d.ts.map +1 -0
  130. package/build/services/otaManifestPolicy.js +19 -0
  131. package/build/services/otaManifestPolicy.js.map +1 -0
  132. package/build/services/otaManifestUrl.d.ts +2 -1
  133. package/build/services/otaManifestUrl.d.ts.map +1 -1
  134. package/build/services/otaManifestUrl.js +24 -32
  135. package/build/services/otaManifestUrl.js.map +1 -1
  136. package/build/services/otaReleaseVersion.d.ts +10 -0
  137. package/build/services/otaReleaseVersion.d.ts.map +1 -0
  138. package/build/services/otaReleaseVersion.js +14 -0
  139. package/build/services/otaReleaseVersion.js.map +1 -0
  140. package/build/stores/appVisibility.d.ts +3 -0
  141. package/build/stores/appVisibility.d.ts.map +1 -0
  142. package/build/stores/appVisibility.js +3 -0
  143. package/build/stores/appVisibility.js.map +1 -0
  144. package/build/stores/apps.d.ts.map +1 -1
  145. package/build/stores/apps.js +7 -1
  146. package/build/stores/apps.js.map +1 -1
  147. package/build/stores/core.d.ts +3 -3
  148. package/build/stores/core.d.ts.map +1 -1
  149. package/build/stores/core.js.map +1 -1
  150. package/build/stores/glasses.d.ts +10 -4
  151. package/build/stores/glasses.d.ts.map +1 -1
  152. package/build/stores/glasses.js.map +1 -1
  153. package/build/stores/settings.js +3 -3
  154. package/build/stores/settings.js.map +1 -1
  155. package/build/types/applet.d.ts +1 -1
  156. package/build/types/applet.js +1 -1
  157. package/build/types/applet.js.map +1 -1
  158. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  159. package/build/types/capabilities/even-realities-g1.js +2 -0
  160. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  161. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  162. package/build/types/capabilities/even-realities-g2.js +2 -0
  163. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  164. package/build/types/capabilities/mentra-display.d.ts.map +1 -1
  165. package/build/types/capabilities/mentra-display.js +2 -0
  166. package/build/types/capabilities/mentra-display.js.map +1 -1
  167. package/build/types/capabilities/mentra-live.d.ts.map +1 -1
  168. package/build/types/capabilities/mentra-live.js +2 -0
  169. package/build/types/capabilities/mentra-live.js.map +1 -1
  170. package/build/types/capabilities/nimo.d.ts +9 -0
  171. package/build/types/capabilities/nimo.d.ts.map +1 -0
  172. package/build/types/capabilities/nimo.js +57 -0
  173. package/build/types/capabilities/nimo.js.map +1 -0
  174. package/build/types/capabilities/none.d.ts.map +1 -1
  175. package/build/types/capabilities/none.js +2 -0
  176. package/build/types/capabilities/none.js.map +1 -1
  177. package/build/types/capabilities/simulated-glasses.d.ts.map +1 -1
  178. package/build/types/capabilities/simulated-glasses.js +2 -0
  179. package/build/types/capabilities/simulated-glasses.js.map +1 -1
  180. package/build/types/capabilities/vuzix-z100.d.ts.map +1 -1
  181. package/build/types/capabilities/vuzix-z100.js +2 -0
  182. package/build/types/capabilities/vuzix-z100.js.map +1 -1
  183. package/build/types/enums.d.ts +3 -26
  184. package/build/types/enums.d.ts.map +1 -1
  185. package/build/types/enums.js +2 -27
  186. package/build/types/enums.js.map +1 -1
  187. package/build/types/hardware.d.ts +6 -15
  188. package/build/types/hardware.d.ts.map +1 -1
  189. package/build/types/hardware.js +4 -4
  190. package/build/types/hardware.js.map +1 -1
  191. package/build/types/index.d.ts +3 -7
  192. package/build/types/index.d.ts.map +1 -1
  193. package/build/types/index.js +3 -7
  194. package/build/types/index.js.map +1 -1
  195. package/build/utils/display/composer/ColumnComposer.d.ts +1 -1
  196. package/build/utils/display/composer/ColumnComposer.js +1 -1
  197. package/build/utils/display/composer/ColumnComposer.js.map +1 -1
  198. package/package.json +47 -22
  199. package/src/bluetooth-sdk/debug.ts +1 -0
  200. package/src/bluetooth-sdk/index.ts +2 -0
  201. package/src/bluetooth-sdk/ota-transport.ts +1 -0
  202. package/src/bluetooth-sdk/photo-receiver.ts +2 -0
  203. package/src/bluetooth-sdk/react.ts +1 -0
  204. package/src/bluetooth-sdk/types.ts +1 -0
  205. package/src/engine.ts +2 -2
  206. package/src/facades/display.ts +1 -1
  207. package/src/facades/glasses.ts +4 -4
  208. package/src/facades/ota.ts +23 -10
  209. package/src/facades/pairing.ts +92 -16
  210. package/src/facades/speech.ts +21 -7
  211. package/src/generated/releaseMetadata.ts +20 -0
  212. package/src/index.ts +16 -11
  213. package/src/react/MentraLiveOtaFlow.tsx +378 -480
  214. package/src/react/index.ts +14 -0
  215. package/src/react/useMentraLiveOta.ts +793 -0
  216. package/src/services/AppRegistry.ts +16 -0
  217. package/src/services/BlobStore.ts +57 -10
  218. package/src/services/DisplayProcessor.ts +3 -3
  219. package/src/services/GlassesStatusProjection.ts +7 -6
  220. package/src/services/HotspotOtaTransport.ts +8 -7
  221. package/src/services/LocalDisplayManager.ts +2 -3
  222. package/src/services/OtaAutoChain.ts +26 -2
  223. package/src/services/OtaInstallCoordinator.ts +40 -13
  224. package/src/services/OtaService.ts +1 -2
  225. package/src/services/OtaUpdateCheckService.ts +45 -27
  226. package/src/services/PhonePhotoCoordinator.ts +1 -1
  227. package/src/services/StreamLifecycleController.ts +3 -3
  228. package/src/services/asg/localNetworkTransport.ts +20 -19
  229. package/src/services/glassesClockSync.ts +5 -1
  230. package/src/services/otaDisplayState.ts +1 -1
  231. package/src/services/otaInstallPolicy.ts +5 -1
  232. package/src/services/otaLegacyMapping.ts +1 -1
  233. package/src/services/otaManifestPolicy.ts +42 -0
  234. package/src/services/otaManifestUrl.ts +27 -36
  235. package/src/services/otaReleaseVersion.ts +22 -0
  236. package/src/stores/appVisibility.ts +3 -0
  237. package/src/stores/apps.ts +7 -1
  238. package/src/stores/core.ts +4 -4
  239. package/src/stores/glasses.ts +13 -6
  240. package/src/stores/settings.ts +3 -3
  241. package/src/types/applet.ts +1 -1
  242. package/src/types/capabilities/even-realities-g1.ts +3 -0
  243. package/src/types/capabilities/even-realities-g2.ts +3 -0
  244. package/src/types/capabilities/mentra-display.ts +3 -0
  245. package/src/types/capabilities/mentra-live.ts +3 -0
  246. package/src/types/capabilities/nimo.ts +59 -0
  247. package/src/types/capabilities/none.ts +3 -0
  248. package/src/types/capabilities/simulated-glasses.ts +3 -0
  249. package/src/types/capabilities/vuzix-z100.ts +3 -0
  250. package/src/types/enums.ts +2 -27
  251. package/src/types/hardware.ts +7 -15
  252. package/src/types/index.ts +3 -6
  253. package/src/utils/display/composer/ColumnComposer.ts +1 -1
  254. package/build/devtools.d.ts +0 -11
  255. package/build/devtools.d.ts.map +0 -1
  256. package/build/devtools.js +0 -13
  257. package/build/devtools.js.map +0 -1
  258. package/build/internal.d.ts +0 -67
  259. package/build/internal.d.ts.map +0 -1
  260. package/build/internal.js +0 -90
  261. package/build/internal.js.map +0 -1
  262. package/src/devtools.ts +0 -13
  263. package/src/internal.ts +0 -160
@@ -871,6 +871,22 @@ class AppRegistry {
871
871
  this.notify()
872
872
  }
873
873
 
874
+ /**
875
+ * Update whether a registered offline app is projected into launcher UIs.
876
+ *
877
+ * Offline apps are registered once at host startup, so settings-backed
878
+ * visibility changes need to update the projection without reinstalling or
879
+ * duplicating the app. No-op when the app is missing or already matches.
880
+ */
881
+ public setOfflineAppHidden(packageName: string, hidden: boolean): void {
882
+ const app = this.offlineApps.find((candidate) => candidate.packageName === packageName)
883
+ if (!app || app.hidden === hidden) return
884
+
885
+ app.hidden = hidden
886
+ this.refreshNeeded = true
887
+ this.notify()
888
+ }
889
+
874
890
  /**
875
891
  * Registered as needing the on-device STT model before it can start (the
876
892
  * host declares this per offline app; the apps store gates start() on it).
@@ -40,8 +40,15 @@ const LOG_TAG = "LocalMiniappRuntime"
40
40
  /** Per-app blob quota: 1 GB. */
41
41
  export const BLOB_QUOTA_BYTES = 1024 * 1024 * 1024
42
42
  const ROOT_DIR_NAME = "mentra_blobs"
43
- /** Cache dir root holding short-lived, properly-named copies handed to the OS share sheet (one unique subdir per share). */
43
+ /** Cache dir root holding short-lived, properly-named copies handed to the OS share sheet (one subdir per stored blob). */
44
44
  const SHARE_DIR_NAME = "mentra_blob_share"
45
+ /**
46
+ * Keep prepared share handoffs alive long enough for recipient apps to read
47
+ * them asynchronously (for example, an email composer attaching the file).
48
+ * Stale copies are collected before the next share; the OS may evict cache
49
+ * files sooner under storage pressure.
50
+ */
51
+ const SHARE_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000
45
52
  const META_KEY_ROOT = "mentraos_blobmeta_"
46
53
  /** Cap md5 computation so we don't block the JS thread hashing a huge file. */
47
54
  const MD5_MAX_BYTES = 50 * 1024 * 1024
@@ -598,6 +605,32 @@ export class BlobStore {
598
605
 
599
606
  // ---- share ---------------------------------------------------------------
600
607
 
608
+ /** Best-effort GC for prepared share handoffs retained in the OS cache. */
609
+ private cleanupShareCache(now = Date.now()): void {
610
+ try {
611
+ const root = new Directory(Paths.cache, SHARE_DIR_NAME)
612
+ if (!root.exists) return
613
+ const cutoff = now - SHARE_CACHE_MAX_AGE_MS
614
+ for (const entry of root.list()) {
615
+ if (!(entry instanceof Directory)) continue
616
+ try {
617
+ const info = entry.info()
618
+ // modificationTime is available on both platforms; creationTime can
619
+ // be absent on Android versions before API 26. The directory id
620
+ // begins with Date.now().toString(36), which is a final fallback.
621
+ const encoded = entry.name.split("-", 1)[0]
622
+ const encodedTime = /^[0-9a-z]+$/.test(encoded) ? Number.parseInt(encoded, 36) : Number.NaN
623
+ const timestamp = info.modificationTime ?? info.creationTime ?? encodedTime
624
+ if (Number.isFinite(timestamp) && timestamp < cutoff) entry.delete()
625
+ } catch {
626
+ // Keep scanning if one cache entry disappears or is unreadable.
627
+ }
628
+ }
629
+ } catch {
630
+ // Cache cleanup must never prevent a share.
631
+ }
632
+ }
633
+
601
634
  async handleShare(packageName: string, payload: Record<string, unknown>, requestId?: string): Promise<void> {
602
635
  const key = payload.key as string
603
636
  const meta = this.readMeta(packageName, key)
@@ -615,23 +648,33 @@ export class BlobStore {
615
648
  // applies to base64/data payloads, not file:// URLs), so sharing `file.uri`
616
649
  // directly would hand the recipient a machine-named file. Copy to a temp
617
650
  // file that carries the real display name + extension, share that, then
618
- // clean it up.
651
+ // clean it up after the recipient has had time to consume it.
619
652
  const shareName = shareFileName(meta)
620
- // Copy into a per-share unique subdir so two concurrent shares that resolve
621
- // to the same display name can't clobber each other's temp file while
622
- // Share.open still references it. The file keeps `shareName` as its basename
623
- // so the OS share sheet shows the right name + extension.
653
+ // Key the cache directory by the immutable stored filename. Repeated or
654
+ // concurrent shares of the same blob can safely reference the same bytes,
655
+ // avoiding an extra full-sized cache copy for every share. Different blobs
656
+ // still get isolated directories even when they use the same display name.
657
+ // The file keeps `shareName` as its basename so the OS share sheet shows the
658
+ // right name + extension.
624
659
  let tempDir: Directory | null = null
660
+ let cacheReady = false
625
661
  try {
626
- tempDir = new Directory(Paths.cache, SHARE_DIR_NAME, this.makeId())
627
- tempDir.create({intermediates: true})
662
+ this.cleanupShareCache()
663
+ tempDir = new Directory(Paths.cache, SHARE_DIR_NAME, sanitizeSegment(meta.fileName))
664
+ if (!tempDir.exists) tempDir.create({intermediates: true})
628
665
  const temp = new File(tempDir, shareName)
629
- file.copy(temp)
666
+ if (!temp.exists || temp.size !== meta.bytes) {
667
+ if (temp.exists) temp.delete()
668
+ file.copy(temp)
669
+ }
670
+ cacheReady = true
630
671
  await Share.open({
631
672
  url: temp.uri,
632
673
  type: meta.mimeType || OCTET,
633
674
  filename: shareName,
634
675
  })
676
+ // Share.open resolves once the OS accepts the handoff. Recipient apps may
677
+ // still read the URI asynchronously, so retain this cache copy for GC.
635
678
  this.hooks.sendResult(packageName, requestId, true, {success: true})
636
679
  } catch (error: any) {
637
680
  if (error?.message?.includes("User did not share")) {
@@ -641,7 +684,11 @@ export class BlobStore {
641
684
  this.hooks.sendResult(packageName, requestId, true, {success: false})
642
685
  }
643
686
  } finally {
644
- if (tempDir) {
687
+ // Once a reusable cache entry is ready, retain it even if this particular
688
+ // sheet is cancelled or fails: another concurrent/recent share may still
689
+ // be reading the same file. Only remove an entry whose preparation did
690
+ // not finish; ready entries are collected after the retention window.
691
+ if (tempDir && !cacheReady) {
645
692
  try {
646
693
  if (tempDir.exists) tempDir.delete()
647
694
  } catch {
@@ -2,15 +2,15 @@
2
2
  * DisplayProcessor
3
3
  *
4
4
  * Processes display events with pixel-accurate text wrapping using the same
5
- * display-utils library as the cloud. This ensures the mobile preview matches
6
- * exactly what is shown on the glasses.
5
+ * Engine display utilities. This ensures the mobile preview matches exactly
6
+ * what is shown on the glasses.
7
7
  *
8
8
  * Key responsibilities:
9
9
  * 1. Intercept display events before sending to native SGC
10
10
  * 2. Wrap text using the correct device profile (G1, etc.)
11
11
  * 3. Ensure GlassesDisplayMirror shows exactly what glasses will show
12
12
  *
13
- * @see cloud/issues/026-mobile-display-processor for design docs
13
+ * @see notes/miniapp-display-api-redesign.md for the current display design
14
14
  */
15
15
 
16
16
  import {
@@ -11,17 +11,18 @@
11
11
  *
12
12
  * Started by `engine.start()`. Idempotent.
13
13
  */
14
- import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
15
- import type {GlassesStatus} from "@mentra/bluetooth-sdk/internal"
14
+ import BluetoothSdk, {type PublicGlassesStatus} from "@mentra/bluetooth-sdk"
16
15
  import {useCoreStore} from "../stores/core"
17
16
  import {useGlassesStore} from "../stores/glasses"
18
17
 
19
18
  let unsubs: Array<() => void> = []
20
19
  let projectionRunId = 0
21
- let glassesStatusForwarder: ((status: Partial<GlassesStatus>) => void) | null = null
20
+ let glassesStatusForwarder: ((status: Partial<PublicGlassesStatus>) => void) | null = null
22
21
  let hydrationPromise: Promise<void> | null = null
23
22
 
24
- export function startGlassesStatusProjection(forwarder?: (status: Partial<GlassesStatus>) => void): Promise<void> {
23
+ export function startGlassesStatusProjection(
24
+ forwarder?: (status: Partial<PublicGlassesStatus>) => void,
25
+ ): Promise<void> {
25
26
  if (forwarder) glassesStatusForwarder = forwarder
26
27
  if (unsubs.length) return hydrationPromise ?? Promise.resolve()
27
28
 
@@ -49,7 +50,7 @@ export function startGlassesStatusProjection(forwarder?: (status: Partial<Glasse
49
50
 
50
51
  // Bluetooth-adapter status -> core store.
51
52
  unsubs.push(
52
- BluetoothSdk.onBluetoothStatus((changed) => {
53
+ BluetoothSdk.subscribeBluetoothStatus((changed) => {
53
54
  bluetoothEventSeen = true
54
55
  useCoreStore.getState().setCoreInfo(changed)
55
56
  }),
@@ -58,7 +59,7 @@ export function startGlassesStatusProjection(forwarder?: (status: Partial<Glasse
58
59
  // Glasses status -> glasses store (+ optional full-runtime forwarding to local
59
60
  // miniapps; clear any stale OTA-available flag on disconnect).
60
61
  unsubs.push(
61
- BluetoothSdk.onGlassesStatus((changed) => {
62
+ BluetoothSdk.subscribeGlassesStatus((changed) => {
62
63
  glassesEventSeen = true
63
64
  useGlassesStore.getState().setGlassesInfo(changed)
64
65
  glassesStatusForwarder?.(changed)
@@ -1,4 +1,5 @@
1
- import BluetoothSdk, {isEnabledHotspotStatus, MentraOtaServer} from "@mentra/bluetooth-sdk/internal"
1
+ import BluetoothSdk, {isEnabledHotspotStatus} from "@mentra/bluetooth-sdk"
2
+ import {otaServer} from "@mentra/bluetooth-sdk/ota-transport"
2
3
  // One coordinator selects the native Android or iOS staging implementation at runtime.
3
4
  // eslint-disable-next-line react-native/split-platform-components
4
5
  import {PermissionsAndroid, Platform} from "react-native"
@@ -83,7 +84,7 @@ class HotspotOtaTransport {
83
84
  let localAddress = scopedAddress
84
85
  if (Platform.OS === "ios") {
85
86
  try {
86
- localAddress = await MentraOtaServer.waitForWifiAddress(hotspot.localIp, 15_000)
87
+ localAddress = await otaServer.waitForWifiAddress(hotspot.localIp, 15_000)
87
88
  } catch (error) {
88
89
  throw new HotspotOtaTransportError(
89
90
  "hotspot_join_failed",
@@ -94,10 +95,10 @@ class HotspotOtaTransport {
94
95
  const artifactPaths = Object.fromEntries(this.prepared.map((artifact) => [artifact.sha256, artifact.filePath]))
95
96
  // Start exactly one native listener to learn its selected port, then atomically replace
96
97
  // the placeholder with the immutable rewritten manifest before ota_start is sent.
97
- const server = await MentraOtaServer.startOtaServer("{}", artifactPaths, localAddress)
98
+ const server = await otaServer.start("{}", artifactPaths, localAddress)
98
99
  this.serverStarted = true
99
100
  const manifest = rewriteManifestForLocalServer(checkResult.manifestBody, this.prepared, server.baseUrl)
100
- const published = await MentraOtaServer.startOtaServer(manifest, artifactPaths, server.host)
101
+ const published = await otaServer.start(manifest, artifactPaths, server.host)
101
102
  if (published.manifestUrl !== server.manifestUrl) {
102
103
  throw new Error("Local OTA server endpoint changed while publishing the manifest")
103
104
  }
@@ -141,11 +142,11 @@ class HotspotOtaTransport {
141
142
  destination: string,
142
143
  onProgress?: (bytesWritten: number, contentLength: number) => void,
143
144
  ): Promise<{statusCode: number}> => {
144
- const subscription = MentraOtaServer.addListener("artifactDownloadProgress", (event) => {
145
+ const subscription = otaServer.onArtifactDownloadProgress((event) => {
145
146
  if (event.destination === destination) onProgress?.(event.bytesWritten, event.contentLength)
146
147
  })
147
148
  try {
148
- return await MentraOtaServer.downloadArtifact(entry.url, destination)
149
+ return await otaServer.downloadArtifact(entry.url, destination)
149
150
  } finally {
150
151
  subscription.remove()
151
152
  }
@@ -164,7 +165,7 @@ class HotspotOtaTransport {
164
165
  console.warn("[OTA_PROGRESS] glasses hotspot shutdown was not confirmed after bounded retries")
165
166
  }
166
167
  }
167
- if (this.serverStarted) await MentraOtaServer.stopOtaServer().catch(() => {})
168
+ if (this.serverStarted) await otaServer.stop().catch(() => {})
168
169
  if (this.localNetworkConnected) await localNetworkTransport.disconnect().catch(() => {})
169
170
  await cleanupArtifacts().catch(() => {})
170
171
  this.prepared = []
@@ -2,9 +2,8 @@
2
2
  * LocalDisplayManager
3
3
  *
4
4
  * Phone-side arbiter for display requests originating from LOCAL miniapps
5
- * (the @mentra/miniapp SDK). Mirrors the user-facing behavior of the cloud
6
- * DisplayManager (cloud/packages/cloud/src/services/layout/DisplayManager6.1.ts)
7
- * for the features that matter on the phone:
5
+ * (the @mentra/miniapp SDK). Preserves the retired cloud display manager's
6
+ * user-facing behavior for the features that matter on the phone:
8
7
  *
9
8
  * - boot message ("Starting <AppName>…") with a bounded window
10
9
  * - durationMs auto-clear
@@ -6,10 +6,18 @@ export const OTA_AUTO_CHAIN_RECONNECT_TIMEOUT_MS = 120_000
6
6
  type OtaAutoChainSession = {
7
7
  approvedDowngrade: boolean
8
8
  passCount: number
9
+ releaseRange: OtaAutoChainReleaseRange
9
10
  reconnectDeadline: number | null
10
11
  seenFingerprints: Set<string>
11
12
  }
12
13
 
14
+ export type OtaAutoChainReleaseRange = {
15
+ fromVersion: string | null
16
+ toVersion: string | null
17
+ /** Exact coordinated release identity for the OTA pin, absent for legacy manifests. */
18
+ releaseVersion?: string | null
19
+ }
20
+
13
21
  export type OtaAutoChainAdvanceResult =
14
22
  | {advance: true; passCount: number}
15
23
  | {advance: false; reason: "inactive" | "duplicate" | "max_passes" | "downgrade_not_approved"}
@@ -42,10 +50,15 @@ export function otaAutoChainFingerprint(result: OtaCheckCurrentGlassesResult): s
42
50
  }
43
51
 
44
52
  /** Start a chain after the user explicitly approves its first update pass. */
45
- export function beginOtaAutoChain(initialFingerprint: string, approvedDowngrade: boolean): void {
53
+ export function beginOtaAutoChain(
54
+ initialFingerprint: string,
55
+ approvedDowngrade: boolean,
56
+ releaseRange: OtaAutoChainReleaseRange,
57
+ ): void {
46
58
  session = {
47
59
  approvedDowngrade,
48
60
  passCount: 1,
61
+ releaseRange: {...releaseRange},
49
62
  reconnectDeadline: null,
50
63
  seenFingerprints: new Set([initialFingerprint]),
51
64
  }
@@ -55,6 +68,10 @@ export function isOtaAutoChainActive(): boolean {
55
68
  return session !== null
56
69
  }
57
70
 
71
+ export function otaAutoChainReleaseRange(): OtaAutoChainReleaseRange | null {
72
+ return session ? {...session.releaseRange} : null
73
+ }
74
+
58
75
  /** End the current chain. Safe to call when no chain is active. */
59
76
  export function stopOtaAutoChain(): void {
60
77
  session = null
@@ -80,7 +97,12 @@ export function clearOtaAutoChainReconnectWait(): void {
80
97
  * Admit the next pass of an active chain. Repeated offers, unexpectedly
81
98
  * destructive downgrades, and runaway chains fail closed and end automation.
82
99
  */
83
- export function tryAdvanceOtaAutoChain(fingerprint: string, isDowngrade: boolean): OtaAutoChainAdvanceResult {
100
+ export function tryAdvanceOtaAutoChain(
101
+ fingerprint: string,
102
+ isDowngrade: boolean,
103
+ targetVersion: string | null,
104
+ releaseVersion: string | null = null,
105
+ ): OtaAutoChainAdvanceResult {
84
106
  if (!session) {
85
107
  return {advance: false, reason: "inactive"}
86
108
  }
@@ -102,5 +124,7 @@ export function tryAdvanceOtaAutoChain(fingerprint: string, isDowngrade: boolean
102
124
 
103
125
  session.seenFingerprints.add(fingerprint)
104
126
  session.passCount += 1
127
+ if (targetVersion) session.releaseRange.toVersion = targetVersion
128
+ if (releaseVersion) session.releaseRange.releaseVersion = releaseVersion
105
129
  return {advance: true, passCount: session.passCount}
106
130
  }
@@ -17,8 +17,7 @@
17
17
  * state written mid-pass queues a follow-up pass — the same way a setState
18
18
  * during React effects scheduled a re-render after the current effect batch.
19
19
  */
20
- import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
21
- import type {OtaProgress, OtaStatus} from "@mentra/bluetooth-sdk/internal"
20
+ import BluetoothSdk, {type OtaProgress, type OtaStatus} from "@mentra/bluetooth-sdk"
22
21
  import GlobalEventEmitter from "../utils/GlobalEventEmitter"
23
22
  import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
24
23
  import {resolveOtaManifestUrl} from "./otaManifestUrl"
@@ -27,10 +26,12 @@ import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
27
26
  import {deriveDisplayState, type DisplayState} from "./otaDisplayState"
28
27
  import {
29
28
  BES_CONTINUE_LOCKOUT_MS,
29
+ BES_RESTART_TIMEOUT_MS,
30
30
  DOWNLOAD_STUCK_TIMEOUT_MS,
31
31
  GLOBAL_OTA_TIMEOUT_MS,
32
32
  LEGACY_APK_COMPLETION_SETTLE_MS,
33
33
  LEGACY_BES_CONTINUE_LOCKOUT_MS,
34
+ LEGACY_BES_RESTART_TIMEOUT_MS,
34
35
  LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS,
35
36
  LEGACY_GLOBAL_OTA_TIMEOUT_MS,
36
37
  LEGACY_MTK_INSTALL_TIMEOUT_MS,
@@ -170,6 +171,8 @@ export interface OtaInstallSnapshot {
170
171
  /** Pre-ota_start phone staging/join state for a hotspot attempt. */
171
172
  hotspotPhase: HotspotOtaPhase
172
173
  hotspotArtifactPercent: number | null
174
+ /** Transport selected from the checked glasses capabilities and Wi-Fi state. */
175
+ transport: "wifi" | "hotspot"
173
176
  }
174
177
 
175
178
  type OtaStartOutcome = "pending" | "acknowledged" | "rejected"
@@ -244,6 +247,7 @@ class OtaInstallCoordinator {
244
247
  private pingInterval: ReturnType<typeof setInterval> | null = null
245
248
  private queryReplyTimeout: ReturnType<typeof setTimeout> | null = null
246
249
  private continueLockoutTimer: ReturnType<typeof setTimeout> | null = null
250
+ private restartRecoveryTimeout: ReturnType<typeof setTimeout> | null = null
247
251
  private legacyApkSettleTimer: ReturnType<typeof setTimeout> | null = null
248
252
  private mtkStallDetectTimer: ReturnType<typeof setTimeout> | null = null
249
253
  private mtkSimTickTimer: ReturnType<typeof setInterval> | null = null
@@ -407,7 +411,7 @@ class OtaInstallCoordinator {
407
411
  if (this.otaStartOwnership?.outcome === "pending") {
408
412
  void this.sendOtaStartWithWatchdogs()
409
413
  } else {
410
- void BluetoothSdk.sendOtaQueryStatus().catch(() => {})
414
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
411
415
  this.armQueryReplyFallback("retry")
412
416
  }
413
417
  }
@@ -449,16 +453,17 @@ class OtaInstallCoordinator {
449
453
  * stays in the screen): clear the selected update, and after an APK step
450
454
  * clear the stale build number so the next check re-reads version_info.
451
455
  */
452
- finish(): void {
456
+ async finish(): Promise<void> {
453
457
  if (this.otaStartOwnership?.outcome !== "pending") {
454
458
  this.otaStartOwnership = null
455
459
  }
456
460
  useGlassesStore.getState().setOtaUpdateAvailable(null)
457
461
  if (this.apkStepSeen) {
458
- BluetoothSdk.updateGlasses({buildNumber: ""})
459
462
  useGlassesStore.getState().setGlassesInfo({buildNumber: ""})
460
463
  }
461
- void this.teardownHotspotTransport()
464
+ // The post-install check needs internet again. Do not let callers leave the
465
+ // terminal screen while Android still owns the no-internet hotspot route.
466
+ await this.teardownHotspotTransport()
462
467
  }
463
468
 
464
469
  /**
@@ -467,8 +472,8 @@ class OtaInstallCoordinator {
467
472
  * deriveDisplayState doesn't resurrect the stale install when the host
468
473
  * navigates away and back through /ota routes.
469
474
  */
470
- discard(): void {
471
- this.finish()
475
+ async discard(): Promise<void> {
476
+ await this.finish()
472
477
  const store = useGlassesStore.getState()
473
478
  store.setOtaStatus(null)
474
479
  store.setOtaProgress(null)
@@ -505,6 +510,7 @@ class OtaInstallCoordinator {
505
510
  versionChangePhase: this.deriveVersionChangePhase(connected),
506
511
  hotspotPhase: this.hotspotPhase,
507
512
  hotspotArtifactPercent: this.hotspotArtifactPercent,
513
+ transport: this.selectedTransport,
508
514
  }
509
515
  }
510
516
 
@@ -997,12 +1003,22 @@ class OtaInstallCoordinator {
997
1003
  // (15s unified, 35s for legacy-shaped sessions — WP 8C-f).
998
1004
  if (displayStateChanged && displayState === "restarting") {
999
1005
  this.clearContinueLockoutTimer()
1006
+ this.clearRestartRecoveryTimeout()
1000
1007
  const lockoutMs = legacySession ? LEGACY_BES_CONTINUE_LOCKOUT_MS : BES_CONTINUE_LOCKOUT_MS
1008
+ const restartTimeoutMs = legacySession ? LEGACY_BES_RESTART_TIMEOUT_MS : BES_RESTART_TIMEOUT_MS
1001
1009
  this.setContinueButtonDisabled(true)
1002
1010
  this.continueLockoutTimer = setTimeout(() => {
1003
1011
  this.continueLockoutTimer = null
1004
1012
  this.setContinueButtonDisabled(false)
1005
1013
  }, lockoutMs)
1014
+ this.restartRecoveryTimeout = setTimeout(() => {
1015
+ this.restartRecoveryTimeout = null
1016
+ if (this.computeDisplayStateNow() !== "restarting") return
1017
+ console.log(`[OTA_PROGRESS] watchdog: glasses did not finish restarting in ${restartTimeoutMs}ms`)
1018
+ this.setErrorMsg(OtaProgressMessages.restartTimeout)
1019
+ }, restartTimeoutMs)
1020
+ } else if (displayStateChanged) {
1021
+ this.clearRestartRecoveryTimeout()
1006
1022
  }
1007
1023
 
1008
1024
  // Legacy MTK install stall simulation (WP 8C-e): display-only. The simulation
@@ -1065,7 +1081,7 @@ class OtaInstallCoordinator {
1065
1081
  if (this.otaStartOwnership?.outcome === "pending") {
1066
1082
  void this.sendOtaStartWithWatchdogs()
1067
1083
  }
1068
- void BluetoothSdk.sendOtaQueryStatus()
1084
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1069
1085
  this.armQueryReplyFallback("reconnect")
1070
1086
  return true
1071
1087
  }
@@ -1109,7 +1125,7 @@ class OtaInstallCoordinator {
1109
1125
  if (noSessionYet) {
1110
1126
  if (!isIdleStatus && this.otaStartOwnership?.outcome === "acknowledged") {
1111
1127
  console.log("[OTA_PROGRESS] initial mount, acknowledged session has no cached status — reconciling")
1112
- void BluetoothSdk.sendOtaQueryStatus().catch(() => {})
1128
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1113
1129
  this.armQueryReplyFallback("initial-mount")
1114
1130
  return
1115
1131
  }
@@ -1125,7 +1141,7 @@ class OtaInstallCoordinator {
1125
1141
  void this.sendOtaStartWithWatchdogs()
1126
1142
  } else {
1127
1143
  console.log("[OTA_PROGRESS] initial mount, session exists, sending ota_query_status")
1128
- void BluetoothSdk.sendOtaQueryStatus()
1144
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1129
1145
  this.armQueryReplyFallback("initial-mount")
1130
1146
  }
1131
1147
  }
@@ -1154,7 +1170,7 @@ class OtaInstallCoordinator {
1154
1170
  this.clearProgressTimeout()
1155
1171
  this.onFirstActivity()
1156
1172
  this.onFirstNonZeroProgress()
1157
- void BluetoothSdk.sendOtaQueryStatus()
1173
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1158
1174
  useGlassesStore.getState().setMtkUpdatedThisSession(true)
1159
1175
  }
1160
1176
 
@@ -1345,7 +1361,7 @@ class OtaInstallCoordinator {
1345
1361
  const s = useGlassesStore.getState().otaStatus
1346
1362
  if (s?.stepType === "apk" && s.phase === "install" && s.status === "in_progress") {
1347
1363
  this.apkInstallPollInFlight = true
1348
- void BluetoothSdk.sendOtaQueryStatus()
1364
+ void BluetoothSdk.queryOtaStatus()
1349
1365
  .catch(() => {})
1350
1366
  .finally(() => {
1351
1367
  this.apkInstallPollInFlight = false
@@ -1492,6 +1508,9 @@ class OtaInstallCoordinator {
1492
1508
  otaVersionUrl = this.hotspotManifestUrl
1493
1509
  this.maybeArmStuckWatchdog()
1494
1510
  }
1511
+ if (!otaVersionUrl) {
1512
+ throw new Error("OTA is disabled because this build has no immutable manifest pin")
1513
+ }
1495
1514
  console.log(`[OTA_PROGRESS] sending ota_start with ${this.selectedTransport} manifest URL: ${otaVersionUrl}`)
1496
1515
  nativeStartAttempted = true
1497
1516
  await BluetoothSdk.startOtaUpdate(otaVersionUrl)
@@ -1662,6 +1681,13 @@ class OtaInstallCoordinator {
1662
1681
  }
1663
1682
  }
1664
1683
 
1684
+ private clearRestartRecoveryTimeout(): void {
1685
+ if (this.restartRecoveryTimeout) {
1686
+ clearTimeout(this.restartRecoveryTimeout)
1687
+ this.restartRecoveryTimeout = null
1688
+ }
1689
+ }
1690
+
1665
1691
  private clearLegacyApkSettleTimer(): void {
1666
1692
  if (this.legacyApkSettleTimer) {
1667
1693
  clearTimeout(this.legacyApkSettleTimer)
@@ -1700,6 +1726,7 @@ class OtaInstallCoordinator {
1700
1726
  this.clearPingInterval()
1701
1727
  this.clearLegacyApkSettleTimer()
1702
1728
  this.clearMtkSimulationTimers()
1729
+ this.clearRestartRecoveryTimeout()
1703
1730
  }
1704
1731
  }
1705
1732
 
@@ -10,8 +10,7 @@
10
10
  *
11
11
  * Started by `engine.start()`. Idempotent.
12
12
  */
13
- import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
14
- import type {OtaStatus} from "@mentra/bluetooth-sdk/internal"
13
+ import BluetoothSdk, {type OtaStatus} from "@mentra/bluetooth-sdk"
15
14
  import GlobalEventEmitter from "../utils/GlobalEventEmitter"
16
15
  import {useGlassesStore} from "../stores/glasses"
17
16
  import {handleOtaClockSkewFromGlasses} from "./glassesClockSync"