@mentra/engine 0.1.0-beta.0 → 0.1.0-dev.1

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 (355) hide show
  1. package/README.md +42 -1
  2. package/build/engine.d.ts +11 -3
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -2
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/dev.d.ts.map +1 -1
  7. package/build/facades/dev.js +9 -5
  8. package/build/facades/dev.js.map +1 -1
  9. package/build/facades/gallery.js +1 -1
  10. package/build/facades/gallery.js.map +1 -1
  11. package/build/facades/glasses.d.ts +1 -0
  12. package/build/facades/glasses.d.ts.map +1 -1
  13. package/build/facades/glasses.js +19 -4
  14. package/build/facades/glasses.js.map +1 -1
  15. package/build/facades/glassesSettings.d.ts.map +1 -1
  16. package/build/facades/glassesSettings.js +1 -0
  17. package/build/facades/glassesSettings.js.map +1 -1
  18. package/build/facades/miniapps.d.ts +4 -0
  19. package/build/facades/miniapps.d.ts.map +1 -1
  20. package/build/facades/miniapps.js +5 -0
  21. package/build/facades/miniapps.js.map +1 -1
  22. package/build/facades/ota.d.ts +11 -2
  23. package/build/facades/ota.d.ts.map +1 -1
  24. package/build/facades/ota.js +19 -4
  25. package/build/facades/ota.js.map +1 -1
  26. package/build/facades/pairing.d.ts +4 -0
  27. package/build/facades/pairing.d.ts.map +1 -1
  28. package/build/facades/pairing.js +10 -8
  29. package/build/facades/pairing.js.map +1 -1
  30. package/build/facades/phoneNotifications.d.ts +0 -4
  31. package/build/facades/phoneNotifications.d.ts.map +1 -1
  32. package/build/facades/phoneNotifications.js +4 -14
  33. package/build/facades/phoneNotifications.js.map +1 -1
  34. package/build/index.d.ts +6 -3
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +4 -2
  37. package/build/index.js.map +1 -1
  38. package/build/internal.d.ts +2 -2
  39. package/build/internal.d.ts.map +1 -1
  40. package/build/internal.js +1 -1
  41. package/build/internal.js.map +1 -1
  42. package/build/react/MentraLiveOtaFlow.d.ts +38 -0
  43. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  44. package/build/react/MentraLiveOtaFlow.js +528 -0
  45. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  46. package/build/react/index.d.ts +2 -0
  47. package/build/react/index.d.ts.map +1 -0
  48. package/build/react/index.js +2 -0
  49. package/build/react/index.js.map +1 -0
  50. package/build/react/useEngineSnapshot.d.ts +2 -0
  51. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  52. package/build/react/useEngineSnapshot.js +14 -0
  53. package/build/react/useEngineSnapshot.js.map +1 -0
  54. package/build/runtime/bootstrap.d.ts +24 -1
  55. package/build/runtime/bootstrap.d.ts.map +1 -1
  56. package/build/runtime/bootstrap.js +17 -0
  57. package/build/runtime/bootstrap.js.map +1 -1
  58. package/build/runtime/scanQrSeam.d.ts +7 -0
  59. package/build/runtime/scanQrSeam.d.ts.map +1 -0
  60. package/build/runtime/scanQrSeam.js +16 -0
  61. package/build/runtime/scanQrSeam.js.map +1 -0
  62. package/build/services/AppRegistry.d.ts +18 -8
  63. package/build/services/AppRegistry.d.ts.map +1 -1
  64. package/build/services/AppRegistry.js +44 -45
  65. package/build/services/AppRegistry.js.map +1 -1
  66. package/build/services/AudioCloudUplink.d.ts +9 -3
  67. package/build/services/AudioCloudUplink.d.ts.map +1 -1
  68. package/build/services/AudioCloudUplink.js +9 -3
  69. package/build/services/AudioCloudUplink.js.map +1 -1
  70. package/build/services/AudioPlaybackService.d.ts +9 -1
  71. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  72. package/build/services/AudioPlaybackService.js +77 -32
  73. package/build/services/AudioPlaybackService.js.map +1 -1
  74. package/build/services/BlobStore.d.ts +2 -1
  75. package/build/services/BlobStore.d.ts.map +1 -1
  76. package/build/services/BlobStore.js +17 -9
  77. package/build/services/BlobStore.js.map +1 -1
  78. package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
  79. package/build/services/CloudAudioSubscriptionSync.js +13 -1
  80. package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
  81. package/build/services/CloudClientService.d.ts +15 -0
  82. package/build/services/CloudClientService.d.ts.map +1 -1
  83. package/build/services/CloudClientService.js +75 -0
  84. package/build/services/CloudClientService.js.map +1 -1
  85. package/build/services/ForegroundLocationPermission.d.ts +13 -0
  86. package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
  87. package/build/services/ForegroundLocationPermission.js +12 -0
  88. package/build/services/ForegroundLocationPermission.js.map +1 -0
  89. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  90. package/build/services/GlassesSettingsSync.js +9 -3
  91. package/build/services/GlassesSettingsSync.js.map +1 -1
  92. package/build/services/GlassesStatusProjection.d.ts +2 -1
  93. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  94. package/build/services/GlassesStatusProjection.js +15 -8
  95. package/build/services/GlassesStatusProjection.js.map +1 -1
  96. package/build/services/HotspotOtaTransport.d.ts +29 -0
  97. package/build/services/HotspotOtaTransport.d.ts.map +1 -0
  98. package/build/services/HotspotOtaTransport.js +146 -0
  99. package/build/services/HotspotOtaTransport.js.map +1 -0
  100. package/build/services/HotspotShutdown.d.ts +17 -0
  101. package/build/services/HotspotShutdown.d.ts.map +1 -0
  102. package/build/services/HotspotShutdown.js +29 -0
  103. package/build/services/HotspotShutdown.js.map +1 -0
  104. package/build/services/LocalDisplayManager.d.ts +15 -0
  105. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  106. package/build/services/LocalDisplayManager.js +106 -20
  107. package/build/services/LocalDisplayManager.js.map +1 -1
  108. package/build/services/LocalMiniappRuntime.d.ts +67 -1
  109. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  110. package/build/services/LocalMiniappRuntime.js +486 -126
  111. package/build/services/LocalMiniappRuntime.js.map +1 -1
  112. package/build/services/LocalMiniappStorage.d.ts +32 -0
  113. package/build/services/LocalMiniappStorage.d.ts.map +1 -0
  114. package/build/services/LocalMiniappStorage.js +66 -0
  115. package/build/services/LocalMiniappStorage.js.map +1 -0
  116. package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
  117. package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
  118. package/build/services/LocalMiniappUserIdentity.js +43 -0
  119. package/build/services/LocalMiniappUserIdentity.js.map +1 -0
  120. package/build/services/MicStateCoordinator.d.ts +42 -1
  121. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  122. package/build/services/MicStateCoordinator.js +141 -3
  123. package/build/services/MicStateCoordinator.js.map +1 -1
  124. package/build/services/MiniappLauncher.d.ts.map +1 -1
  125. package/build/services/MiniappLauncher.js +23 -3
  126. package/build/services/MiniappLauncher.js.map +1 -1
  127. package/build/services/MiniappLiveness.d.ts +12 -0
  128. package/build/services/MiniappLiveness.d.ts.map +1 -0
  129. package/build/services/MiniappLiveness.js +16 -0
  130. package/build/services/MiniappLiveness.js.map +1 -0
  131. package/build/services/OtaArtifactDownloader.d.ts +65 -0
  132. package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
  133. package/build/services/OtaArtifactDownloader.js +226 -0
  134. package/build/services/OtaArtifactDownloader.js.map +1 -0
  135. package/build/services/OtaAutoChain.d.ts +34 -0
  136. package/build/services/OtaAutoChain.d.ts.map +1 -0
  137. package/build/services/OtaAutoChain.js +83 -0
  138. package/build/services/OtaAutoChain.js.map +1 -0
  139. package/build/services/OtaErrorMapping.d.ts +18 -0
  140. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  141. package/build/services/OtaErrorMapping.js +63 -0
  142. package/build/services/OtaErrorMapping.js.map +1 -0
  143. package/build/services/OtaInstallCoordinator.d.ts +94 -14
  144. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  145. package/build/services/OtaInstallCoordinator.js +574 -130
  146. package/build/services/OtaInstallCoordinator.js.map +1 -1
  147. package/build/services/OtaService.d.ts.map +1 -1
  148. package/build/services/OtaService.js +8 -1
  149. package/build/services/OtaService.js.map +1 -1
  150. package/build/services/OtaUpdateCheckService.d.ts +38 -3
  151. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  152. package/build/services/OtaUpdateCheckService.js +142 -22
  153. package/build/services/OtaUpdateCheckService.js.map +1 -1
  154. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  155. package/build/services/PhoneCalendarService.js +22 -1
  156. package/build/services/PhoneCalendarService.js.map +1 -1
  157. package/build/services/PhoneCameraFovCoordinator.d.ts +2 -0
  158. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
  159. package/build/services/PhoneCameraFovCoordinator.js +14 -0
  160. package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
  161. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  162. package/build/services/PhoneNotificationsSync.js +12 -10
  163. package/build/services/PhoneNotificationsSync.js.map +1 -1
  164. package/build/services/PhonePhotoCoordinator.d.ts +10 -3
  165. package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
  166. package/build/services/PhonePhotoCoordinator.js +21 -3
  167. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  168. package/build/services/PhoneStreamCoordinator.d.ts +8 -1
  169. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  170. package/build/services/PhoneStreamCoordinator.js +125 -13
  171. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  172. package/build/services/PhoneVideoCoordinator.d.ts +2 -0
  173. package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
  174. package/build/services/PhoneVideoCoordinator.js +10 -0
  175. package/build/services/PhoneVideoCoordinator.js.map +1 -1
  176. package/build/services/SceneRenderer.d.ts.map +1 -1
  177. package/build/services/SceneRenderer.js +15 -0
  178. package/build/services/SceneRenderer.js.map +1 -1
  179. package/build/services/SupportProfileSync.d.ts +22 -0
  180. package/build/services/SupportProfileSync.d.ts.map +1 -0
  181. package/build/services/SupportProfileSync.js +213 -0
  182. package/build/services/SupportProfileSync.js.map +1 -0
  183. package/build/services/TtsTextSanitizer.d.ts +5 -0
  184. package/build/services/TtsTextSanitizer.d.ts.map +1 -0
  185. package/build/services/TtsTextSanitizer.js +61 -0
  186. package/build/services/TtsTextSanitizer.js.map +1 -0
  187. package/build/services/WifiAdbRequest.d.ts +17 -0
  188. package/build/services/WifiAdbRequest.d.ts.map +1 -0
  189. package/build/services/WifiAdbRequest.js +35 -0
  190. package/build/services/WifiAdbRequest.js.map +1 -0
  191. package/build/services/asg/asgCameraApi.d.ts +3 -0
  192. package/build/services/asg/asgCameraApi.d.ts.map +1 -1
  193. package/build/services/asg/asgCameraApi.js +72 -2
  194. package/build/services/asg/asgCameraApi.js.map +1 -1
  195. package/build/services/asg/gallerySyncService.d.ts.map +1 -1
  196. package/build/services/asg/gallerySyncService.js +26 -14
  197. package/build/services/asg/gallerySyncService.js.map +1 -1
  198. package/build/services/asg/localNetworkTransport.d.ts +2 -2
  199. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  200. package/build/services/asg/localNetworkTransport.js +6 -6
  201. package/build/services/asg/localNetworkTransport.js.map +1 -1
  202. package/build/services/audioPlaybackAssets.d.ts +8 -0
  203. package/build/services/audioPlaybackAssets.d.ts.map +1 -0
  204. package/build/services/audioPlaybackAssets.js +7 -0
  205. package/build/services/audioPlaybackAssets.js.map +1 -0
  206. package/build/services/glassesClockSync.js +2 -2
  207. package/build/services/glassesClockSync.js.map +1 -1
  208. package/build/services/manifestActions.d.ts +7 -0
  209. package/build/services/manifestActions.d.ts.map +1 -0
  210. package/build/services/manifestActions.js +25 -0
  211. package/build/services/manifestActions.js.map +1 -0
  212. package/build/services/otaDisplayState.d.ts +22 -11
  213. package/build/services/otaDisplayState.d.ts.map +1 -1
  214. package/build/services/otaDisplayState.js +36 -14
  215. package/build/services/otaDisplayState.js.map +1 -1
  216. package/build/services/otaInstallPolicy.d.ts +15 -3
  217. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  218. package/build/services/otaInstallPolicy.js +24 -3
  219. package/build/services/otaInstallPolicy.js.map +1 -1
  220. package/build/services/otaManifestUrl.d.ts +2 -0
  221. package/build/services/otaManifestUrl.d.ts.map +1 -0
  222. package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
  223. package/build/services/otaManifestUrl.js.map +1 -0
  224. package/build/services/slimStreamStatus.d.ts +11 -0
  225. package/build/services/slimStreamStatus.d.ts.map +1 -1
  226. package/build/services/slimStreamStatus.js +14 -4
  227. package/build/services/slimStreamStatus.js.map +1 -1
  228. package/build/stores/apps.d.ts.map +1 -1
  229. package/build/stores/apps.js +11 -0
  230. package/build/stores/apps.js.map +1 -1
  231. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  232. package/build/stores/bluetoothSettingKeys.js +2 -0
  233. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  234. package/build/stores/gallerySync.d.ts +2 -0
  235. package/build/stores/gallerySync.d.ts.map +1 -1
  236. package/build/stores/gallerySync.js +2 -0
  237. package/build/stores/gallerySync.js.map +1 -1
  238. package/build/stores/glasses.d.ts.map +1 -1
  239. package/build/stores/glasses.js +3 -0
  240. package/build/stores/glasses.js.map +1 -1
  241. package/build/stores/settings.d.ts +17 -0
  242. package/build/stores/settings.d.ts.map +1 -1
  243. package/build/stores/settings.js +152 -19
  244. package/build/stores/settings.js.map +1 -1
  245. package/build/types/applet.d.ts +2 -0
  246. package/build/types/applet.d.ts.map +1 -1
  247. package/build/types/applet.js.map +1 -1
  248. package/build/types/enums.d.ts +1 -0
  249. package/build/types/enums.d.ts.map +1 -1
  250. package/build/types/enums.js +1 -0
  251. package/build/types/enums.js.map +1 -1
  252. package/build/types/hardware.d.ts +1 -0
  253. package/build/types/hardware.d.ts.map +1 -1
  254. package/build/types/hardware.js +14 -0
  255. package/build/types/hardware.js.map +1 -1
  256. package/build/utils/configuredDevHost.d.ts +6 -0
  257. package/build/utils/configuredDevHost.d.ts.map +1 -0
  258. package/build/utils/configuredDevHost.js +19 -0
  259. package/build/utils/configuredDevHost.js.map +1 -0
  260. package/build/utils/devMiniappLaunch.d.ts +19 -8
  261. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  262. package/build/utils/devMiniappLaunch.js +144 -21
  263. package/build/utils/devMiniappLaunch.js.map +1 -1
  264. package/build/utils/diagnosticContext.d.ts.map +1 -1
  265. package/build/utils/diagnosticContext.js +15 -17
  266. package/build/utils/diagnosticContext.js.map +1 -1
  267. package/build/utils/hardware/hardware.d.ts.map +1 -1
  268. package/build/utils/hardware/hardware.js +7 -3
  269. package/build/utils/hardware/hardware.js.map +1 -1
  270. package/build/utils/miniappDiagnostics.d.ts +64 -0
  271. package/build/utils/miniappDiagnostics.d.ts.map +1 -0
  272. package/build/utils/miniappDiagnostics.js +173 -0
  273. package/build/utils/miniappDiagnostics.js.map +1 -0
  274. package/build/utils/timers/timers.d.ts.map +1 -1
  275. package/build/utils/timers/timers.js +55 -18
  276. package/build/utils/timers/timers.js.map +1 -1
  277. package/package.json +27 -18
  278. package/src/assets/silence.wav +0 -0
  279. package/src/engine.ts +7 -2
  280. package/src/facades/dev.ts +9 -5
  281. package/src/facades/gallery.ts +1 -1
  282. package/src/facades/glasses.ts +17 -4
  283. package/src/facades/glassesSettings.ts +5 -0
  284. package/src/facades/miniapps.ts +7 -0
  285. package/src/facades/ota.ts +19 -4
  286. package/src/facades/pairing.ts +11 -7
  287. package/src/facades/phoneNotifications.ts +4 -13
  288. package/src/index.ts +26 -2
  289. package/src/internal.ts +2 -1
  290. package/src/react/MentraLiveOtaFlow.tsx +856 -0
  291. package/src/react/index.ts +7 -0
  292. package/src/react/useEngineSnapshot.ts +16 -0
  293. package/src/runtime/bootstrap.ts +33 -1
  294. package/src/runtime/scanQrSeam.ts +20 -0
  295. package/src/services/AppRegistry.ts +72 -48
  296. package/src/services/AudioCloudUplink.ts +9 -3
  297. package/src/services/AudioPlaybackService.ts +94 -34
  298. package/src/services/BlobStore.ts +22 -9
  299. package/src/services/CloudAudioSubscriptionSync.ts +13 -1
  300. package/src/services/CloudClientService.ts +74 -0
  301. package/src/services/ForegroundLocationPermission.ts +24 -0
  302. package/src/services/GlassesSettingsSync.ts +9 -3
  303. package/src/services/GlassesStatusProjection.ts +16 -8
  304. package/src/services/HotspotOtaTransport.ts +184 -0
  305. package/src/services/HotspotShutdown.ts +41 -0
  306. package/src/services/LocalDisplayManager.ts +112 -21
  307. package/src/services/LocalMiniappRuntime.ts +576 -130
  308. package/src/services/LocalMiniappStorage.ts +82 -0
  309. package/src/services/LocalMiniappUserIdentity.ts +46 -0
  310. package/src/services/MicStateCoordinator.ts +169 -4
  311. package/src/services/MiniappLauncher.ts +30 -3
  312. package/src/services/MiniappLiveness.ts +24 -0
  313. package/src/services/OtaArtifactDownloader.ts +312 -0
  314. package/src/services/OtaAutoChain.ts +106 -0
  315. package/src/services/OtaErrorMapping.ts +80 -0
  316. package/src/services/OtaInstallCoordinator.ts +645 -143
  317. package/src/services/OtaService.ts +11 -5
  318. package/src/services/OtaUpdateCheckService.ts +180 -20
  319. package/src/services/PhoneCalendarService.ts +27 -4
  320. package/src/services/PhoneCameraFovCoordinator.ts +15 -0
  321. package/src/services/PhoneNotificationsSync.ts +12 -10
  322. package/src/services/PhonePhotoCoordinator.ts +33 -7
  323. package/src/services/PhoneStreamCoordinator.ts +139 -25
  324. package/src/services/PhoneVideoCoordinator.ts +11 -0
  325. package/src/services/SceneRenderer.ts +16 -0
  326. package/src/services/SupportProfileSync.ts +218 -0
  327. package/src/services/TtsTextSanitizer.ts +66 -0
  328. package/src/services/WifiAdbRequest.ts +53 -0
  329. package/src/services/asg/asgCameraApi.ts +99 -8
  330. package/src/services/asg/gallerySyncService.ts +28 -19
  331. package/src/services/asg/localNetworkTransport.ts +7 -11
  332. package/src/services/audioPlaybackAssets.ts +8 -0
  333. package/src/services/glassesClockSync.ts +2 -2
  334. package/src/services/manifestActions.ts +30 -0
  335. package/src/services/otaDisplayState.ts +57 -16
  336. package/src/services/otaInstallPolicy.ts +29 -3
  337. package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
  338. package/src/services/slimStreamStatus.ts +16 -5
  339. package/src/stores/apps.ts +12 -0
  340. package/src/stores/bluetoothSettingKeys.ts +2 -0
  341. package/src/stores/gallerySync.ts +4 -0
  342. package/src/stores/glasses.ts +3 -0
  343. package/src/stores/settings.ts +174 -21
  344. package/src/types/applet.ts +2 -0
  345. package/src/types/enums.ts +2 -1
  346. package/src/types/hardware.ts +17 -0
  347. package/src/utils/configuredDevHost.ts +17 -0
  348. package/src/utils/devMiniappLaunch.ts +151 -23
  349. package/src/utils/diagnosticContext.ts +17 -17
  350. package/src/utils/hardware/hardware.ts +7 -9
  351. package/src/utils/miniappDiagnostics.ts +226 -0
  352. package/src/utils/timers/timers.ts +57 -21
  353. package/build/services/asgOtaVersionUrl.d.ts +0 -2
  354. package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
  355. package/build/services/asgOtaVersionUrl.js.map +0 -1
@@ -0,0 +1,53 @@
1
+ export interface WifiAdbRequestDependencies {
2
+ isSystemPackage: (packageName: string) => boolean
3
+ setWifiAdbState: (enabled: boolean) => Promise<void>
4
+ }
5
+
6
+ export type WifiAdbRequestErrorCode = "NOT_PERMITTED" | "INVALID_ARGUMENT" | "INTERNAL"
7
+
8
+ export type WifiAdbRequestResult =
9
+ | {ok: true}
10
+ | {
11
+ ok: false
12
+ error: {code: WifiAdbRequestErrorCode; message: string}
13
+ }
14
+
15
+ /** Apply the SYSTEM-only Wi-Fi ADB policy before calling the native SDK. */
16
+ export async function handleWifiAdbRequest(
17
+ packageName: string,
18
+ payload: Record<string, unknown>,
19
+ dependencies: WifiAdbRequestDependencies,
20
+ ): Promise<WifiAdbRequestResult> {
21
+ if (!dependencies.isSystemPackage(packageName)) {
22
+ return {
23
+ ok: false,
24
+ error: {
25
+ code: "NOT_PERMITTED",
26
+ message: "setWifiAdbState is restricted to system apps",
27
+ },
28
+ }
29
+ }
30
+
31
+ if (typeof payload.enabled !== "boolean") {
32
+ return {
33
+ ok: false,
34
+ error: {
35
+ code: "INVALID_ARGUMENT",
36
+ message: "enabled must be a boolean",
37
+ },
38
+ }
39
+ }
40
+
41
+ try {
42
+ await dependencies.setWifiAdbState(payload.enabled)
43
+ return {ok: true}
44
+ } catch (error) {
45
+ return {
46
+ ok: false,
47
+ error: {
48
+ code: "INTERNAL",
49
+ message: error instanceof Error ? error.message : "Wi-Fi ADB error",
50
+ },
51
+ }
52
+ }
53
+ }
@@ -1071,7 +1071,13 @@ export class AsgCameraApiClient {
1071
1071
  capture: CaptureGroup,
1072
1072
  onProgress?: (bytesDownloaded: number, totalBytes: number) => void,
1073
1073
  abortSignal?: AbortSignal,
1074
- ): Promise<{captureDir: string; primaryPath: string; bracketPaths: string[]; sidecarPath?: string}> {
1074
+ ): Promise<{
1075
+ captureDir: string
1076
+ primaryPath: string
1077
+ bracketPaths: string[]
1078
+ sidecarPath?: string
1079
+ thumbnailPath?: string
1080
+ }> {
1075
1081
  if (capture.files.some((file) => !file.size || file.size <= 0)) {
1076
1082
  // Preserve support for malformed manifests from pre-v2 development firmware.
1077
1083
  return this.downloadCaptureLegacyUnsafe(capture, onProgress, abortSignal)
@@ -1136,9 +1142,14 @@ export class AsgCameraApiClient {
1136
1142
  const leafName = sortedFiles[0].name.includes("/") ? sortedFiles[0].name.split("/").pop()! : sortedFiles[0].name
1137
1143
  primaryPath = `${captureDir}/${leafName}`
1138
1144
  }
1145
+ const primaryFile = sortedFiles.find((file) => file.role === "primary") || sortedFiles[0]
1146
+ const thumbnailPath =
1147
+ capture.type === "video" && primaryFile
1148
+ ? await this.downloadVideoThumbnail(primaryFile.name, `${captureDir}/.thumb.jpg`, abortSignal)
1149
+ : undefined
1139
1150
  galleryTransferLedger.transition(capture.capture_id, "VERIFIED")
1140
1151
  onProgress?.(capture.total_size, capture.total_size)
1141
- return {captureDir, primaryPath, bracketPaths, sidecarPath}
1152
+ return {captureDir, primaryPath, bracketPaths, sidecarPath, thumbnailPath}
1142
1153
  } catch (error) {
1143
1154
  galleryTransferLedger.recordFailure(capture.capture_id, error)
1144
1155
  throw error
@@ -1150,7 +1161,13 @@ export class AsgCameraApiClient {
1150
1161
  capture: CaptureGroup,
1151
1162
  onProgress?: (bytesDownloaded: number, totalBytes: number) => void,
1152
1163
  abortSignal?: AbortSignal,
1153
- ): Promise<{captureDir: string; primaryPath: string; bracketPaths: string[]; sidecarPath?: string}> {
1164
+ ): Promise<{
1165
+ captureDir: string
1166
+ primaryPath: string
1167
+ bracketPaths: string[]
1168
+ sidecarPath?: string
1169
+ thumbnailPath?: string
1170
+ }> {
1154
1171
  const captureDir = localStorageService.getPhotoFilePath(capture.capture_id)
1155
1172
  console.log(
1156
1173
  `[ASG Camera API] downloadCapture: ${capture.capture_id} (${capture.files.length} files) -> ${captureDir}`,
@@ -1300,7 +1317,76 @@ export class AsgCameraApiClient {
1300
1317
  onProgress(totalBytes, totalBytes)
1301
1318
  }
1302
1319
 
1303
- return {captureDir, primaryPath, bracketPaths, sidecarPath}
1320
+ const primaryFile = sortedFiles.find((file) => file.role === "primary") || sortedFiles[0]
1321
+ const thumbnailPath =
1322
+ capture.type === "video" && primaryFile
1323
+ ? await this.downloadVideoThumbnail(primaryFile.name, `${captureDir}/.thumb.jpg`, abortSignal)
1324
+ : undefined
1325
+
1326
+ return {captureDir, primaryPath, bracketPaths, sidecarPath, thumbnailPath}
1327
+ }
1328
+
1329
+ /** Download an on-demand video thumbnail while the source capture is still on the glasses. */
1330
+ private async downloadVideoThumbnail(
1331
+ sourceFileName: string,
1332
+ thumbnailPath: string,
1333
+ abortSignal?: AbortSignal,
1334
+ ): Promise<string | undefined> {
1335
+ const partialPath = `${thumbnailPath}.partial`
1336
+ const parentDir = thumbnailPath.substring(0, thumbnailPath.lastIndexOf("/"))
1337
+
1338
+ try {
1339
+ if (parentDir && !(await RNFS.exists(parentDir))) {
1340
+ await RNFS.mkdir(parentDir)
1341
+ }
1342
+ await RNFS.unlink(partialPath).catch(() => {})
1343
+ const {jobId, promise} = localNetworkTransport.downloadFile({
1344
+ fromUrl: `${this.baseUrl}/api/photo?file=${encodeURIComponent(sourceFileName)}`,
1345
+ toFile: partialPath,
1346
+ headers: {
1347
+ "Accept": "image/jpeg",
1348
+ "User-Agent": "MentraOS-Mobile/1.0",
1349
+ },
1350
+ connectionTimeout: 60000,
1351
+ readTimeout: 60000,
1352
+ progressDivider: 10,
1353
+ })
1354
+
1355
+ let abortPollTimer: number | undefined
1356
+ if (abortSignal) {
1357
+ if (abortSignal.aborted) {
1358
+ localNetworkTransport.stopDownload(jobId)
1359
+ throw new Error("Sync cancelled")
1360
+ }
1361
+ abortPollTimer = BgTimer.setInterval(() => {
1362
+ if (abortSignal.aborted) localNetworkTransport.stopDownload(jobId)
1363
+ }, 500)
1364
+ }
1365
+
1366
+ let result
1367
+ try {
1368
+ result = await promise
1369
+ } finally {
1370
+ if (abortPollTimer !== undefined) BgTimer.clearInterval(abortPollTimer)
1371
+ }
1372
+
1373
+ if (abortSignal?.aborted) throw new Error("Sync cancelled")
1374
+ if (result.statusCode !== 200 || result.bytesWritten <= 0) {
1375
+ throw new Error(`HTTP ${result.statusCode}, ${result.bytesWritten} bytes`)
1376
+ }
1377
+
1378
+ await RNFS.unlink(thumbnailPath).catch(() => {})
1379
+ await RNFS.moveFile(partialPath, thumbnailPath)
1380
+ console.log(`[ASG Camera API] Downloaded video thumbnail for ${sourceFileName}`)
1381
+ return thumbnailPath
1382
+ } catch (error) {
1383
+ await RNFS.unlink(partialPath).catch(() => {})
1384
+ if (abortSignal?.aborted || (error instanceof Error && error.message === "Sync cancelled")) {
1385
+ throw new Error("Sync cancelled")
1386
+ }
1387
+ console.warn(`[ASG Camera API] Failed to download video thumbnail for ${sourceFileName}:`, error)
1388
+ return undefined
1389
+ }
1304
1390
  }
1305
1391
 
1306
1392
  /**
@@ -1421,15 +1507,20 @@ export class AsgCameraApiClient {
1421
1507
  mediaKind: isVideo ? "video" : "photo",
1422
1508
  })
1423
1509
  galleryTransferLedger.transition(captureId, "VERIFIED")
1510
+ const thumbnailPath =
1511
+ includeThumbnail && isVideo
1512
+ ? await this.downloadVideoThumbnail(filename, localStorageService.getThumbnailFilePath(filename), abortSignal)
1513
+ : undefined
1424
1514
  return {
1425
1515
  filePath: localFilePath,
1516
+ thumbnailPath,
1426
1517
  mime_type: isVideo
1427
1518
  ? "video/mp4"
1428
1519
  : lowerName.endsWith(".png")
1429
- ? "image/png"
1430
- : lowerName.endsWith(".avif")
1431
- ? "image/avif"
1432
- : "image/jpeg",
1520
+ ? "image/png"
1521
+ : lowerName.endsWith(".avif")
1522
+ ? "image/avif"
1523
+ : "image/jpeg",
1433
1524
  }
1434
1525
  } catch (error) {
1435
1526
  galleryTransferLedger.recordFailure(captureId, error)
@@ -156,6 +156,10 @@ class GallerySyncService {
156
156
  }
157
157
 
158
158
  this.syncStartPromise = null
159
+ useGallerySyncStore.getState().setSyncStarting(false)
160
+ this.waitingForWifiRetry = false
161
+ this.wifiSettingsOpenedAt = null
162
+ this.startAborted = false
159
163
  this.isInitialized = false
160
164
  console.log("[GallerySyncService] Cleaned up")
161
165
  }
@@ -228,14 +232,23 @@ class GallerySyncService {
228
232
  if (!glassesConnected) {
229
233
  console.log("[GallerySyncService] Glasses disconnected - not retrying sync")
230
234
  this.waitingForWifiRetry = false
235
+ this.wifiSettingsOpenedAt = null
231
236
  return
232
237
  }
233
238
 
239
+ // The user has returned from the WiFi settings prompt, so the previous
240
+ // "Sync failed" state is no longer actionable. Make the button available
241
+ // immediately while the native WiFi state catches up; a successful check
242
+ // below will start the sync automatically.
243
+ if (store.syncState === "error" && store.lastError?.startsWith("WiFi disabled")) {
244
+ store.setSyncState("idle")
245
+ }
246
+
234
247
  // Check if WiFi is now enabled (Android only)
235
248
  // Use retry logic because WiFi status takes time to propagate after user enables it
236
249
  if (Platform.OS === "android") {
237
250
  const MAX_RETRIES = 5
238
- const RETRY_DELAY_MS = 1000 // Wait 500ms between checks
251
+ const RETRY_DELAY_MS = 1000
239
252
 
240
253
  console.log("[GallerySyncService] Waiting for WiFi to initialize (may take a moment after enabling)...")
241
254
 
@@ -243,35 +256,28 @@ class GallerySyncService {
243
256
  try {
244
257
  await new Promise<void>((resolve) => BgTimer.setTimeout(() => resolve(), RETRY_DELAY_MS))
245
258
 
246
- const netState = await NetInfo.fetch()
247
- console.log(
248
- `[GallerySyncService] WiFi check attempt ${attempt}/${MAX_RETRIES}: enabled=${netState.isWifiEnabled}`,
249
- )
259
+ // NetInfo can remain stale after Android's WiFi settings close. Use
260
+ // the same native source as the normal sync preflight.
261
+ const wifiEnabled = await WifiManager.isEnabled()
262
+ console.log(`[GallerySyncService] WiFi check attempt ${attempt}/${MAX_RETRIES}: enabled=${wifiEnabled}`)
250
263
 
251
- if (netState.isWifiEnabled === true) {
264
+ if (wifiEnabled) {
252
265
  console.log("[GallerySyncService] ✅ WiFi is now enabled - auto-retrying sync")
253
266
  this.waitingForWifiRetry = false
254
267
  this.wifiSettingsOpenedAt = null // Clear cooldown timestamp
255
- // Clear previous error state
256
- store.setSyncState("idle")
257
268
  // Auto-retry sync
258
269
  await this.startSync()
259
270
  return
260
271
  }
261
-
262
- // If this was the last attempt, log and give up
263
- if (attempt === MAX_RETRIES) {
264
- console.log(
265
- "[GallerySyncService] ❌ WiFi still disabled after all retries - user may need to tap sync manually",
266
- )
267
- this.waitingForWifiRetry = false
268
- this.wifiSettingsOpenedAt = null // Clear cooldown timestamp
269
- }
270
272
  } catch (error) {
271
273
  console.warn(`[GallerySyncService] Failed to check WiFi status on attempt ${attempt}:`, error)
272
274
  // Continue to next retry
273
275
  }
274
276
  }
277
+
278
+ console.log("[GallerySyncService] WiFi not detected after retries - leaving sync available for manual retry")
279
+ this.waitingForWifiRetry = false
280
+ this.wifiSettingsOpenedAt = null
275
281
  }
276
282
  }
277
283
 
@@ -378,8 +384,10 @@ class GallerySyncService {
378
384
  return this.syncStartPromise
379
385
  }
380
386
 
387
+ useGallerySyncStore.getState().setSyncStarting(true)
381
388
  this.syncStartPromise = this.runStartSync().finally(() => {
382
389
  this.syncStartPromise = null
390
+ useGallerySyncStore.getState().setSyncStarting(false)
383
391
  })
384
392
  return this.syncStartPromise
385
393
  }
@@ -1321,7 +1329,7 @@ class GallerySyncService {
1321
1329
  }
1322
1330
  }
1323
1331
  // Old firmware remains on the exact v2 endpoint/schema. Avoid expensive inline thumbnails;
1324
- // the UI can request them lazily after the primary transfer is safe.
1332
+ // video thumbnails are fetched as small side downloads with their primary media.
1325
1333
  const syncResponse = await asgCameraApi.syncWithServer(clientId, lastSyncTime, false)
1326
1334
  const data = (syncResponse.data || syncResponse) as SyncManifestData
1327
1335
  if (data.captures) {
@@ -1915,6 +1923,7 @@ class GallerySyncService {
1915
1923
  bracketPaths: result.bracketPaths,
1916
1924
  sidecarPath: result.sidecarPath,
1917
1925
  thumbnailData: capture.thumbnail_data,
1926
+ thumbnailPath: result.thumbnailPath,
1918
1927
  captureDir: result.captureDir,
1919
1928
  timestamp: capture.timestamp,
1920
1929
  totalSize: capture.total_size,
@@ -2251,7 +2260,7 @@ class GallerySyncService {
2251
2260
 
2252
2261
  /** True while pre-flight startSync work is in flight (before sync state transitions). */
2253
2262
  isSyncStarting(): boolean {
2254
- return this.syncStartPromise !== null
2263
+ return useGallerySyncStore.getState().syncStarting
2255
2264
  }
2256
2265
 
2257
2266
  /**
@@ -12,17 +12,12 @@ let nextJobId = 1_000_000
12
12
  const nativeJobs = new Map<number, string>()
13
13
  const cancelledNativeJobs = new Set<number>()
14
14
 
15
- export function shouldUseScopedLocalNetwork(
16
- os: string,
17
- platformVersion: number | string,
18
- moduleAvailable: boolean,
19
- ): boolean {
20
- const level = typeof platformVersion === "number" ? platformVersion : Number.parseInt(platformVersion, 10)
21
- return os === "android" && level >= 29 && moduleAvailable
15
+ export function shouldUseScopedLocalNetwork(os: string, moduleAvailable: boolean): boolean {
16
+ return os === "android" && moduleAvailable
22
17
  }
23
18
 
24
19
  function supportsScopedConnection(): boolean {
25
- return shouldUseScopedLocalNetwork(Platform.OS, Platform.Version, MentraLocalNetwork != null)
20
+ return shouldUseScopedLocalNetwork(Platform.OS, MentraLocalNetwork != null)
26
21
  }
27
22
 
28
23
  function normalizeHeaders(headers?: HeadersInit): Record<string, string> {
@@ -46,15 +41,16 @@ export const localNetworkTransport = {
46
41
  supportsScopedConnection: (): boolean => supportsScopedConnection(),
47
42
  isScopedConnectionActive: (): boolean => scopedConnectionActive,
48
43
 
49
- async connect(ssid: string, password: string): Promise<void> {
44
+ async connect(ssid: string, password: string): Promise<string | undefined> {
50
45
  scopedConnectionActive = false
51
46
  nativeJobs.clear()
52
47
  if (!supportsScopedConnection()) {
53
48
  await WifiManager.connectToProtectedSSID(ssid, password, false, false)
54
- return
49
+ return undefined
55
50
  }
56
- await MentraLocalNetwork!.connect(ssid, password)
51
+ const result = await MentraLocalNetwork!.connect(ssid, password)
57
52
  scopedConnectionActive = true
53
+ return result.localAddress
58
54
  },
59
55
 
60
56
  async disconnect(): Promise<void> {
@@ -0,0 +1,8 @@
1
+ import type {AudioSource} from "expo-audio"
2
+
3
+ /**
4
+ * A tiny bundled source used to detach completed or interrupted audio without
5
+ * destroying the reusable native player. If a media control later sends Play,
6
+ * only this silence can be resumed instead of the previous miniapp audio.
7
+ */
8
+ export const SILENT_AUDIO_SOURCE: AudioSource = require("../assets/silence.wav")
@@ -6,7 +6,7 @@ import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
6
6
  import {BgTimer} from "../utils/timers"
7
7
  import {useGlassesStore} from "../stores/glasses"
8
8
 
9
- import {getAsgOtaVersionUrl} from "./asgOtaVersionUrl"
9
+ import {resolveOtaManifestUrl} from "./otaManifestUrl"
10
10
  import {detectClockSkew} from "./gallerySyncClock"
11
11
 
12
12
  export const CLOCK_SETTLE_MS = 500
@@ -86,7 +86,7 @@ export async function handleOtaClockSkewFromGlasses(
86
86
 
87
87
  lastOtaClockFixAt = Date.now()
88
88
  const {buildNumber, otaVersionUrl} = useGlassesStore.getState()
89
- const manifestUrl = getAsgOtaVersionUrl(otaVersionUrl, buildNumber)
89
+ const manifestUrl = resolveOtaManifestUrl(otaVersionUrl, buildNumber)
90
90
  console.log("[GlassesClockSync] ⏰ Restarting OTA with ota_start after clock fix")
91
91
  await BluetoothSdk.startOtaUpdate(manifestUrl)
92
92
  return true
@@ -0,0 +1,30 @@
1
+ import type {DeclaredAction} from "../types/applet"
2
+
3
+ /**
4
+ * Normalize manifest actions defensively for installed and dev-sideloaded
5
+ * bundles. Input and output schemas are passed through as opaque JSON Schema.
6
+ */
7
+ export function normalizeManifestActions(raw: unknown): DeclaredAction[] {
8
+ if (!Array.isArray(raw)) return []
9
+ const out: DeclaredAction[] = []
10
+ for (const a of raw as Array<{
11
+ id?: unknown
12
+ description?: unknown
13
+ parameters?: unknown
14
+ outputSchema?: unknown
15
+ }>) {
16
+ if (a && typeof a.id === "string" && typeof a.description === "string") {
17
+ out.push({
18
+ id: a.id,
19
+ description: a.description,
20
+ ...(a.parameters && typeof a.parameters === "object" && !Array.isArray(a.parameters)
21
+ ? {parameters: a.parameters as Record<string, unknown>}
22
+ : {}),
23
+ ...(a.outputSchema && typeof a.outputSchema === "object" && !Array.isArray(a.outputSchema)
24
+ ? {outputSchema: a.outputSchema as Record<string, unknown>}
25
+ : {}),
26
+ })
27
+ }
28
+ }
29
+ return out
30
+ }
@@ -18,38 +18,80 @@ export type DisplayState = "starting" | "updating" | "complete" | "failed" | "di
18
18
  *
19
19
  * Priority-ordered rules (first match wins):
20
20
  * 1. errorMsg !== "" -> "failed"
21
- * 2. BES terminal + connected + sawReconnectEdge -> "complete"
22
- * 3. BES terminal (any connection state) -> "restarting"
21
+ * 2. BES reboot recovery complete -> "complete"
22
+ * 3. BES reboot recovery awaiting reconnect -> "restarting"
23
+ * 4. BES terminal (any connection state) -> "restarting"
23
24
  * (legacy-shaped BES "complete" counts only in install phase)
24
- * 4. apkCompletedViaBuildIncrease (legacy build-number port) -> "complete"
25
- * 5. otaStatus.status === "failed" -> "failed"
26
- * 6. otaStatus.status === "complete" (non-BES) -> "complete"
25
+ * 6. apkCompletedViaBuildIncrease (legacy build-number port) -> "complete"
26
+ * 7. otaStatus.status === "failed" -> "failed"
27
+ * 8. otaStatus.status === "complete" (non-BES) -> "complete"
27
28
  * (unless legacy-shaped download-complete or held apk settle -> in flight)
28
- * 7. !connected + legacy in-flight complete (held apk) -> "updating"
29
- * 8. !connected + APK step_complete with totalSteps > 1 -> "starting" (expected reboot)
30
- * 9. !connected + BES in flight -> "restarting" (defensive)
31
- * 10. !connected -> "disconnected"
32
- * 11. connected + in_progress | step_complete | legacy in-flight complete -> "updating"
33
- * 12. fallback -> "starting"
29
+ * 9. !connected + legacy in-flight complete (held apk) -> "updating"
30
+ * 10. !connected + APK step_complete with totalSteps > 1 -> "starting" (expected reboot)
31
+ * 11. !connected + BES in flight -> "restarting" (defensive)
32
+ * 12. !connected -> "disconnected"
33
+ * 13. connected + in_progress | step_complete | legacy in-flight complete -> "updating"
34
+ * 14. fallback -> "starting"
34
35
  */
35
36
  export function deriveDisplayState(args: {
36
37
  otaStatus: OtaStatus | null
37
38
  otaProgress: OtaProgress | null
38
39
  connected: boolean
39
40
  errorMsg: string
41
+ /**
42
+ * Retained for host API compatibility. A session-wide reconnect edge is not
43
+ * sufficient for BES completion because it may belong to an earlier APK step.
44
+ */
40
45
  sawReconnectEdge: boolean
41
46
  /** Coordinator is holding a legacy apk install "complete" for the settle window (WP 8C). */
42
47
  legacyApkSettleHold?: boolean
43
48
  /** Legacy APK completion detected by build-number increase (WP 8C). */
44
49
  apkCompletedViaBuildIncrease?: boolean
50
+ /** Phone-observed BES disconnect/reconnect recovery, independent of stale OTA progress. */
51
+ besRestartRecovery?: "awaiting" | "complete" | null
52
+ /** Downgrade detour: the reconnected glasses reported exactly the pinned target version. */
53
+ versionChangeConverged?: boolean
54
+ /** True for the whole downgrade (version-change) session. */
55
+ versionChangeSession?: boolean
45
56
  }): DisplayState {
46
- const {otaStatus, otaProgress, connected, errorMsg, sawReconnectEdge, legacyApkSettleHold, apkCompletedViaBuildIncrease} =
47
- args
57
+ const {
58
+ otaStatus,
59
+ otaProgress,
60
+ connected,
61
+ errorMsg,
62
+ legacyApkSettleHold,
63
+ apkCompletedViaBuildIncrease,
64
+ besRestartRecovery,
65
+ versionChangeConverged,
66
+ versionChangeSession,
67
+ } = args
48
68
 
49
69
  if (errorMsg) return "failed"
50
70
 
71
+ // Downgrade detour completion: exact-version convergence after reconnect is the
72
+ // only completion signal (the wipe destroys ASG's ota_session, and the target is
73
+ // a LOWER build so no build-number increase fires). Outranks the stale install
74
+ // status still sitting in the store from before the handoff.
75
+ if (versionChangeConverged) return "complete"
76
+
77
+ // A BES reboot destroys or strands the glasses-side OTA session on older
78
+ // clients. Once the phone observed that reboot, stale pre-reboot progress must
79
+ // not move the UI from restarting back to updating.
80
+ if (besRestartRecovery === "complete") return "complete"
81
+ if (besRestartRecovery === "awaiting") return "restarting"
82
+
83
+ // In a version-change session, convergence (above) is the ONLY completion for the ASG
84
+ // downgrade. A bare non-firmware "complete" before convergence — the glasses refusing
85
+ // the downgrade (floor/gate) with no install, or a stale pre-handoff status — must never
86
+ // present as success. Firmware-step completions (mtk/bes) DO pass through: a combined
87
+ // downgrade+firmware manifest applies firmware first on its own pass, which must finish
88
+ // so the phone re-checks and re-offers the now-firmware-free downgrade.
89
+ const isFirmwareComplete = otaStatus?.stepType === "mtk" || otaStatus?.stepType === "bes"
90
+ if (versionChangeSession && otaStatus?.status === "complete" && !isFirmwareComplete) {
91
+ return connected ? "updating" : "disconnected"
92
+ }
93
+
51
94
  const besTerminal = isBesTerminal(otaStatus, otaProgress)
52
- if (besTerminal && connected && sawReconnectEdge) return "complete"
53
95
  if (besTerminal) return "restarting"
54
96
 
55
97
  // Legacy build-number APK completion: the coordinator clears errorMsg when it fires,
@@ -85,8 +127,7 @@ export function deriveDisplayState(args: {
85
127
  // has not started); the legacy screen treated that drop as a plain disconnect.
86
128
  const besStatusInFlight =
87
129
  otaStatus?.stepType === "bes" && !(isLegacyShapedOtaStatus(otaStatus) && otaStatus.phase === "download")
88
- const besInFlight =
89
- besStatusInFlight || (otaProgress?.currentUpdate === "bes" && otaProgress?.stage === "install")
130
+ const besInFlight = besStatusInFlight || (otaProgress?.currentUpdate === "bes" && otaProgress?.stage === "install")
90
131
  if (besInFlight) return "restarting"
91
132
 
92
133
  return "disconnected"
@@ -10,6 +10,7 @@ import type {OtaProgress, OtaStatus} from "@mentra/bluetooth-sdk/internal"
10
10
  export const MINIMUM_OTA_STATUS_BUILD = 37
11
11
 
12
12
  export const MAX_RETRIES = 3
13
+ /** Wait between ended native ota_start requests; never overlaps a pending request. */
13
14
  export const RETRY_INTERVAL_MS = 5000
14
15
  /** APK/BES and general install when progress events are expected */
15
16
  export const PROGRESS_TIMEOUT_MS = 120_000
@@ -19,8 +20,6 @@ export const DOWNLOAD_STUCK_TIMEOUT_MS = 70_000
19
20
  export const MTK_INSTALL_TIMEOUT_MS = 300_000
20
21
  /** Whole multi-step session cap */
21
22
  export const GLOBAL_OTA_TIMEOUT_MS = 20 * 60 * 1000
22
- /** After APK reboot, ASG OTA service needs time before next ota_start */
23
- export const POST_APK_OTA_START_DELAY_MS = 6000
24
23
  /** BLE keepalive during OTA */
25
24
  export const PING_INTERVAL_MS = 10_000
26
25
  /**
@@ -71,6 +70,8 @@ export const LEGACY_MTK_STALL_ZONE_MAX_PERCENT = 55
71
70
  export const LEGACY_MTK_SIM_FLOOR_PERCENT = 51
72
71
  export const LEGACY_MTK_SIM_CAP_PERCENT = 60
73
72
 
73
+ export type OtaProtocolProfile = "legacy" | "unified"
74
+
74
75
  /**
75
76
  * Legacy-shaped ota_status: old (< 37) ASG builds send `ota_progress`, which the SDK
76
77
  * (Android MentraLive.kt / iOS MentraLive.swift) maps to a unified ota_status with
@@ -98,9 +99,34 @@ export function isLegacyShapedOtaSession(
98
99
  return Number.isFinite(build) && build > 0 && build < MINIMUM_OTA_STATUS_BUILD
99
100
  }
100
101
 
102
+ /**
103
+ * Select the compatibility profile once at install-screen attach. The old split
104
+ * routes made this decision from the pre-update build and kept it for the whole
105
+ * mounted flow, even when an APK step restarted into a newer ASG build. Returning
106
+ * null leaves the coordinator undecided until the first meaningful OTA event.
107
+ */
108
+ export function selectOtaProtocolProfile(
109
+ otaStatus: OtaStatus | null,
110
+ otaProgress: OtaProgress | null,
111
+ buildNumber: string | null | undefined,
112
+ ): OtaProtocolProfile | null {
113
+ const build = Number.parseInt(buildNumber ?? "", 10)
114
+ if (Number.isFinite(build) && build > 0) {
115
+ return build < MINIMUM_OTA_STATUS_BUILD ? "legacy" : "unified"
116
+ }
117
+ if (otaStatus) return isLegacyShapedOtaStatus(otaStatus) ? "legacy" : "unified"
118
+ if (otaProgress) return "legacy"
119
+ return null
120
+ }
121
+
101
122
  export const OtaProgressMessages = {
102
- noAckResponse: "Unable to start update. Glasses did not respond.",
103
123
  stalledOrStuck: "Update may have failed. Ensure glasses have internet access and try again.",
104
124
  globalTimeout: "Update took too long. Please try again.",
105
125
  sendOtaStartFailed: "Failed to communicate with glasses.",
126
+ hotspotArtifactDownloadFailed: "Could not download the update to your phone. Check phone internet and try again.",
127
+ hotspotArtifactVerifyFailed: "The update package could not be verified. Please try again later.",
128
+ hotspotWifiPermissionDenied: "Allow nearby devices access so the phone can connect to the glasses hotspot.",
129
+ hotspotStartFailed: "Could not start the glasses hotspot. Please try again.",
130
+ hotspotJoinFailed: "Could not connect the phone to the glasses hotspot. Please try again.",
131
+ hotspotServerFailed: "Could not make the update available to the glasses. Please try again.",
106
132
  } as const
@@ -1,6 +1,13 @@
1
+ import {sdkPinnedOtaManifestUrl} from "@mentra/bluetooth-sdk/internal"
2
+
1
3
  import {SETTINGS, useSettingsStore} from "../stores/settings"
2
4
 
3
- const OTA_VERSION_URL_PROD = "https://ota.mentraglass.com/prod_live_version.json"
5
+ // Legacy production manifest — pre-39 glasses install from this (they ignore ota_start's URL).
6
+ const OTA_VERSION_URL_LEGACY_PROD = "https://ota.mentraglass.com/prod_live_version.json"
7
+ // Modern production fleet manifest (v2). Only a last-resort fallback when the SDK-derived pin
8
+ // URL is somehow unavailable; modern glasses honor ota_start's URL, so this must be the v2
9
+ // artifact, not the legacy v1 rescue manifest.
10
+ const OTA_VERSION_URL_PROD = "https://ota.mentraglass.com/prod_live_version_v2.json"
4
11
 
5
12
  function isLegacyAsgOtaStartBuild(glassesBuildNumber?: string | null): boolean {
6
13
  const buildNumber = Number.parseInt(glassesBuildNumber ?? "", 10)
@@ -20,12 +27,12 @@ function getOtaVersionUrlDevOverride(): string | null {
20
27
  return trimmed || null
21
28
  }
22
29
 
23
- export function getAsgOtaVersionUrl(glassesUrl?: string | null, glassesBuildNumber?: string | null): string {
30
+ export function resolveOtaManifestUrl(glassesUrl?: string | null, glassesBuildNumber?: string | null): string {
24
31
  const deviceUrl = glassesUrl?.trim()
25
32
  if (isLegacyAsgOtaStartBuild(glassesBuildNumber)) {
26
33
  // Legacy glasses ignore ota_start.ota_version_url and install from their
27
34
  // compiled default, so the developer override does not apply to them either.
28
- return deviceUrl || OTA_VERSION_URL_PROD
35
+ return deviceUrl || OTA_VERSION_URL_LEGACY_PROD
29
36
  }
30
37
 
31
38
  const devOverrideUrl = getOtaVersionUrlDevOverride()
@@ -37,5 +44,10 @@ export function getAsgOtaVersionUrl(glassesUrl?: string | null, glassesBuildNumb
37
44
  if (envUrl) {
38
45
  return envUrl
39
46
  }
40
- return deviceUrl || OTA_VERSION_URL_PROD
47
+
48
+ // The phone owns manifest selection: modern glasses are driven with the manifest pinned to
49
+ // this app's Bluetooth SDK version. Modern ASG builds report no manifest URL of their own;
50
+ // a glasses-reported URL (older modern builds advertising their baked fleet default) is only
51
+ // a last resort when the SDK version is somehow unavailable.
52
+ return sdkPinnedOtaManifestUrl() || deviceUrl || OTA_VERSION_URL_PROD
41
53
  }