@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
@@ -15,11 +15,7 @@ import type {OtaStatus} from "@mentra/bluetooth-sdk/internal"
15
15
  import GlobalEventEmitter from "../utils/GlobalEventEmitter"
16
16
  import {useGlassesStore} from "../stores/glasses"
17
17
  import {handleOtaClockSkewFromGlasses} from "./glassesClockSync"
18
- import {
19
- legacyOtaProgressFromOtaStatusEvent,
20
- normalizeOtaStatusEvent,
21
- otaStatusFromNormalized,
22
- } from "./otaLegacyMapping"
18
+ import {legacyOtaProgressFromOtaStatusEvent, normalizeOtaStatusEvent, otaStatusFromNormalized} from "./otaLegacyMapping"
23
19
 
24
20
  let subs: Array<{remove: () => void}> = []
25
21
 
@@ -40,6 +36,16 @@ export function startOtaService(): void {
40
36
  }),
41
37
  )
42
38
 
39
+ // The glasses process restarted under a live BLE link (sid changed in
40
+ // glasses_ready/version_info_1). The physical connection never dropped, so this is
41
+ // the ONLY reconnect-edge signal the OTA coordinator gets after an APK install.
42
+ subs.push(
43
+ BluetoothSdk.addListener("glasses_session_changed", (event) => {
44
+ console.log(`[OTA] glasses_session_changed ${event.previous_sid || "<none>"} -> ${event.sid}`)
45
+ GlobalEventEmitter.emit("glasses_session_changed", {previousSid: event.previous_sid, sid: event.sid})
46
+ }),
47
+ )
48
+
43
49
  // Install progress / lifecycle. Updates both the new (otaStatus) and the legacy
44
50
  // (otaProgress) store shapes; auto-fixes clock skew on the relevant failures.
45
51
  subs.push(
@@ -1,8 +1,19 @@
1
1
  import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
2
2
  import type {OtaUpdateInfo} from "@mentra/bluetooth-sdk/internal"
3
3
  import {getGlassesSystemTimeMs, isGlassesConnected, useGlassesStore, waitForGlassesState} from "../stores/glasses"
4
+ import {SETTINGS, useSettingsStore} from "../stores/settings"
4
5
  import {maybeFixGlassesClockFromVersionInfo} from "./glassesClockSync"
5
- import {getAsgOtaVersionUrl} from "./asgOtaVersionUrl"
6
+ import {resolveOtaManifestUrl} from "./otaManifestUrl"
7
+
8
+ /**
9
+ * Phone-side mirror of ASG's `DOWNGRADE_FLOOR_VERSION_CODE`. Set to the versionCode of the first
10
+ * release shipping the shared media root (the first downgrade-safe build). A non-positive value
11
+ * means downgrades are disabled, so the phone must NOT offer them — offering a downgrade the
12
+ * glasses' `DowngradeGate` will refuse leaves the version-change session with no install and the
13
+ * glasses reporting a bare "complete", which would otherwise present as false success. Keep this
14
+ * in lockstep with the ASG/recovery constant at release time.
15
+ */
16
+ export const DOWNGRADE_FLOOR_VERSION_CODE = 0
6
17
 
7
18
  export interface VersionInfo {
8
19
  versionCode: number
@@ -18,11 +29,13 @@ export interface MtkPatch {
18
29
  start_firmware: string
19
30
  end_firmware: string
20
31
  url: string
32
+ sha256?: string
21
33
  }
22
34
 
23
35
  export interface BesFirmware {
24
36
  version: string
25
37
  url: string
38
+ sha256?: string
26
39
  }
27
40
 
28
41
  export interface VersionJson {
@@ -46,9 +59,23 @@ export interface OtaCheckResult {
46
59
  updates: string[]
47
60
  mtkPatch: MtkPatch | null
48
61
  besVersion: string | null
62
+ /** True when the pending APK step installs an OLDER build than the glasses currently run. */
63
+ isApkDowngrade: boolean
64
+ /**
65
+ * Raw manifest JSON (stringified) this check actually fetched, or null when the
66
+ * fetch failed. Lets change-watchers baseline on exactly what the check saw,
67
+ * instead of racing it with a second fetch of the same URL.
68
+ */
69
+ manifestBody: string | null
70
+ /**
71
+ * Why the check failed, when it did. "network" is retryable (connection/server
72
+ * trouble); "pin_unavailable" is permanent for this app build (manifest 404/gone,
73
+ * unparseable, or carrying no valid ASG pin) — the remedy is updating the app.
74
+ */
75
+ checkFailureReason?: "network" | "pin_unavailable"
49
76
  }
50
77
 
51
- export type OtaCheckSkippedReason = "disconnected" | "missing_build"
78
+ export type OtaCheckSkippedReason = "disconnected" | "missing_build" | "dev_build"
52
79
 
53
80
  export interface OtaCheckCurrentGlassesResult extends OtaCheckResult {
54
81
  updateInfo: OtaUpdateInfo | null
@@ -66,6 +93,8 @@ export interface OtaCheckCurrentGlassesOptions {
66
93
  waitForMtkVersionMs?: number
67
94
  refreshVersionInfo?: boolean
68
95
  fixClockBeforeCheck?: boolean
96
+ /** Downgrade floor override (defaults to DOWNGRADE_FLOOR_VERSION_CODE); for tests. */
97
+ floorVersionCode?: number
69
98
  }
70
99
 
71
100
  function emptyCheckResult(skippedReason?: OtaCheckSkippedReason): OtaCheckCurrentGlassesResult {
@@ -76,6 +105,8 @@ function emptyCheckResult(skippedReason?: OtaCheckSkippedReason): OtaCheckCurren
76
105
  updates: [],
77
106
  mtkPatch: null,
78
107
  besVersion: null,
108
+ isApkDowngrade: false,
109
+ manifestBody: null,
79
110
  updateInfo: null,
80
111
  isRequired: true,
81
112
  skippedReason,
@@ -86,46 +117,92 @@ function glassesConnectedNow(): boolean {
86
117
  return isGlassesConnected(useGlassesStore.getState().connection)
87
118
  }
88
119
 
89
- export async function fetchVersionInfo(url: string): Promise<VersionJson | null> {
120
+ type ManifestFetchResult = {json: VersionJson | null; failureReason?: "network" | "pin_unavailable"}
121
+
122
+ async function fetchVersionInfoDetailed(url: string): Promise<ManifestFetchResult> {
90
123
  try {
91
124
  const response = await fetch(url)
92
125
  if (!response.ok) {
93
126
  console.error("Failed to fetch version info:", response.status)
94
- return null
127
+ // 4xx: the manifest does not exist (or is gone) — permanent for this app
128
+ // build, retrying cannot help. Everything else is transient server/network
129
+ // trouble and retryable.
130
+ return {json: null, failureReason: response.status >= 400 && response.status < 500 ? "pin_unavailable" : "network"}
131
+ }
132
+ try {
133
+ return {json: await response.json()}
134
+ } catch (parseError) {
135
+ console.error("OTA: manifest is not valid JSON:", parseError)
136
+ return {json: null, failureReason: "pin_unavailable"}
95
137
  }
96
- return await response.json()
97
138
  } catch (error) {
98
139
  console.error("OTA: Error fetching version info:", error)
99
- return null
140
+ return {json: null, failureReason: "network"}
100
141
  }
101
142
  }
102
143
 
144
+ export async function fetchVersionInfo(url: string): Promise<VersionJson | null> {
145
+ return (await fetchVersionInfoDetailed(url)).json
146
+ }
147
+
103
148
  export function checkVersionUpdateAvailable(
104
149
  currentBuildNumber: string | undefined,
105
150
  versionJson: VersionJson | null,
151
+ floorVersionCode: number = DOWNGRADE_FLOOR_VERSION_CODE,
106
152
  ): boolean {
153
+ return getApkUpdateDirection(currentBuildNumber, versionJson, floorVersionCode) !== null
154
+ }
155
+
156
+ /**
157
+ * Direction of the pending APK change, or null when the glasses already match the manifest.
158
+ * Every apps-shaped manifest the phone drives is an exact pin, so both directions are
159
+ * actionable (mirroring the glasses-side `DowngradeGate`); the ancient top-level manifest
160
+ * shape stays upgrade-only, and zeroed rescue pins are never actionable.
161
+ */
162
+ export function getApkUpdateDirection(
163
+ currentBuildNumber: string | undefined,
164
+ versionJson: VersionJson | null,
165
+ floorVersionCode: number = DOWNGRADE_FLOOR_VERSION_CODE,
166
+ ): "upgrade" | "downgrade" | null {
107
167
  if (!currentBuildNumber || !versionJson) {
108
- return false
168
+ return null
109
169
  }
110
170
 
111
171
  const currentVersion = parseInt(currentBuildNumber, 10)
112
172
  if (isNaN(currentVersion)) {
113
- return false
173
+ return null
114
174
  }
115
175
 
116
176
  let serverVersion: number | undefined
177
+ let exactPin = false
117
178
 
118
- if (versionJson.apps?.["com.mentra.asg_client"]) {
119
- serverVersion = versionJson.apps["com.mentra.asg_client"].versionCode
179
+ const appEntry = versionJson.apps?.["com.mentra.asg_client"]
180
+ if (appEntry) {
181
+ serverVersion = appEntry.versionCode
182
+ exactPin = true
120
183
  } else if (versionJson.versionCode) {
121
184
  serverVersion = versionJson.versionCode
122
185
  }
123
186
 
124
187
  if (!serverVersion || isNaN(serverVersion)) {
125
- return false
188
+ return null
126
189
  }
127
190
 
128
- return serverVersion > currentVersion
191
+ if (serverVersion > currentVersion) {
192
+ return "upgrade"
193
+ }
194
+ // Downgrades require an exact pin AND a target at/above the enabled floor. A non-positive floor
195
+ // disables downgrades entirely, matching ASG's fail-closed DowngradeGate — so the phone never
196
+ // offers what the glasses would refuse.
197
+ if (
198
+ serverVersion < currentVersion &&
199
+ exactPin &&
200
+ floorVersionCode > 0 &&
201
+ serverVersion >= floorVersionCode
202
+ ) {
203
+ return "downgrade"
204
+ }
205
+ return null
129
206
  }
130
207
 
131
208
  export function getLatestVersionInfo(versionJson: VersionJson | null): VersionInfo | null {
@@ -184,7 +261,7 @@ export function checkBesUpdate(besFirmware: BesFirmware | undefined, currentVers
184
261
  return compareVersions(besFirmware.version, currentVersion) > 0
185
262
  }
186
263
 
187
- function compareVersions(version1: string, version2: string): number {
264
+ export function compareVersions(version1: string, version2: string): number {
188
265
  if (version1.includes(".") && version2.includes(".")) {
189
266
  const parts1 = version1.split(".")
190
267
  const parts2 = version2.split(".")
@@ -208,14 +285,62 @@ export async function checkForOtaUpdate(
208
285
  currentBuildNumber: string,
209
286
  currentMtkVersion?: string,
210
287
  currentBesVersion?: string,
288
+ floorVersionCode: number = DOWNGRADE_FLOOR_VERSION_CODE,
211
289
  ): Promise<OtaCheckResult> {
212
290
  try {
213
291
  console.log("OTA: Checking for OTA update - URL: " + otaVersionUrl + ", current build: " + currentBuildNumber)
214
- const versionJson = await fetchVersionInfo(otaVersionUrl)
292
+ const fetched = await fetchVersionInfoDetailed(otaVersionUrl)
293
+ const versionJson = fetched.json
294
+ if (!versionJson) {
295
+ return {
296
+ hasCheckCompleted: false,
297
+ updateAvailable: false,
298
+ latestVersionInfo: null,
299
+ updates: [],
300
+ mtkPatch: null,
301
+ besVersion: null,
302
+ isApkDowngrade: false,
303
+ manifestBody: null,
304
+ checkFailureReason: fetched.failureReason ?? "network",
305
+ }
306
+ }
215
307
  const latestVersionInfo = getLatestVersionInfo(versionJson)
216
308
 
217
- const apkUpdateAvailable = checkVersionUpdateAvailable(currentBuildNumber, versionJson)
218
- console.log(`OTA: APK update available: ${apkUpdateAvailable} (current: ${currentBuildNumber})`)
309
+ // A manifest whose ASG pin is missing/zeroed cannot verify anything for modern
310
+ // glasses (zeroed pins are only legitimate in the frozen legacy rescue manifests
311
+ // that pre-39 glasses are checked against). Report the check as FAILED rather
312
+ // than silently completing as "up to date" — an unverifiable state must never
313
+ // present as a verified one.
314
+ const currentVersionNumber = parseInt(currentBuildNumber, 10)
315
+ const pinnedEntry = versionJson?.apps?.["com.mentra.asg_client"]
316
+ const pinnedVersion = pinnedEntry?.versionCode ?? versionJson?.versionCode
317
+ if (
318
+ versionJson &&
319
+ Number.isFinite(currentVersionNumber) &&
320
+ currentVersionNumber >= 39 &&
321
+ !(typeof pinnedVersion === "number" && pinnedVersion > 0)
322
+ ) {
323
+ console.error(
324
+ `OTA: manifest at ${otaVersionUrl} has no valid ASG pin (versionCode=${String(pinnedVersion)}) - treating check as failed`,
325
+ )
326
+ return {
327
+ hasCheckCompleted: false,
328
+ updateAvailable: false,
329
+ latestVersionInfo: null,
330
+ updates: [],
331
+ mtkPatch: null,
332
+ besVersion: null,
333
+ isApkDowngrade: false,
334
+ manifestBody: null,
335
+ checkFailureReason: "pin_unavailable",
336
+ }
337
+ }
338
+
339
+ const apkDirection = getApkUpdateDirection(currentBuildNumber, versionJson, floorVersionCode)
340
+ const apkUpdateAvailable = apkDirection !== null
341
+ console.log(
342
+ `OTA: APK update available: ${apkUpdateAvailable} (current: ${currentBuildNumber}, direction: ${apkDirection ?? "none"})`,
343
+ )
219
344
 
220
345
  const mtkPatch = findMatchingMtkPatch(versionJson?.mtk_patches, currentMtkVersion)
221
346
  const mtkUpdateAvailable = mtkPatch !== null
@@ -243,6 +368,8 @@ export async function checkForOtaUpdate(
243
368
  updates,
244
369
  mtkPatch,
245
370
  besVersion: versionJson?.bes_firmware?.version || null,
371
+ isApkDowngrade: apkDirection === "downgrade",
372
+ manifestBody: versionJson ? JSON.stringify(versionJson) : null,
246
373
  }
247
374
  } catch (error) {
248
375
  console.error("Error checking for OTA update:", error)
@@ -253,6 +380,9 @@ export async function checkForOtaUpdate(
253
380
  updates: [],
254
381
  mtkPatch: null,
255
382
  besVersion: null,
383
+ isApkDowngrade: false,
384
+ manifestBody: null,
385
+ checkFailureReason: "network",
256
386
  }
257
387
  }
258
388
  }
@@ -266,12 +396,25 @@ export async function checkCurrentGlassesForUpdate(
266
396
  waitForMtkVersionMs = 2000,
267
397
  refreshVersionInfo = true,
268
398
  fixClockBeforeCheck = true,
399
+ floorVersionCode = DOWNGRADE_FLOOR_VERSION_CODE,
269
400
  } = options
270
401
 
271
402
  if (!glassesConnectedNow()) {
272
403
  return emptyCheckResult("disconnected")
273
404
  }
274
405
 
406
+ const embeddedManifestUrl = process.env.EXPO_PUBLIC_ASG_OTA_VERSION_URL?.trim()
407
+ const superMode = useSettingsStore.getState().getSetting(SETTINGS.super_mode.key)
408
+ const overrideUrl = useSettingsStore.getState().getSetting(SETTINGS.ota_version_url.key)
409
+ const overrideActive = superMode && typeof overrideUrl === "string" && overrideUrl.trim() !== ""
410
+ if (!embeddedManifestUrl && !overrideActive) {
411
+ console.log("OTA: check skipped - mobile app has no embedded OTA manifest pin")
412
+ return emptyCheckResult("dev_build")
413
+ }
414
+ if (!embeddedManifestUrl) {
415
+ console.log("OTA: no embedded manifest pin, but super-mode manifest override is active - checking anyway")
416
+ }
417
+
275
418
  if (refreshVersionInfo) {
276
419
  void BluetoothSdk.requestVersionInfo().catch((error) => {
277
420
  console.warn("OTA: Failed to request version_info from glasses:", error)
@@ -284,7 +427,10 @@ export async function checkCurrentGlassesForUpdate(
284
427
  buildNumber = useGlassesStore.getState().buildNumber
285
428
  }
286
429
 
287
- if (!buildNumber) {
430
+ if (!buildNumber || !(parseInt(buildNumber, 10) > 0)) {
431
+ // Covers absent, non-numeric, and zero build numbers: a glasses version we
432
+ // cannot parse means nothing is verifiable, so skip rather than ghost an
433
+ // "up to date" result from a comparison that never really ran.
288
434
  return emptyCheckResult("missing_build")
289
435
  }
290
436
 
@@ -325,8 +471,8 @@ export async function checkCurrentGlassesForUpdate(
325
471
  })
326
472
  }
327
473
 
328
- const manifestUrl = getAsgOtaVersionUrl(useGlassesStore.getState().otaVersionUrl, buildNumber)
329
- const result = await checkForOtaUpdate(manifestUrl, buildNumber, mtkFirmwareVersion, besFirmwareVersion)
474
+ const manifestUrl = resolveOtaManifestUrl(useGlassesStore.getState().otaVersionUrl, buildNumber)
475
+ const result = await checkForOtaUpdate(manifestUrl, buildNumber, mtkFirmwareVersion, besFirmwareVersion, floorVersionCode)
330
476
 
331
477
  if (!result.hasCheckCompleted) {
332
478
  return {
@@ -360,6 +506,7 @@ export async function checkCurrentGlassesForUpdate(
360
506
  )
361
507
  }
362
508
  const updateAvailable = filteredUpdates.length > 0 && !!result.latestVersionInfo
509
+ const isApkDowngrade = result.isApkDowngrade && filteredUpdates.includes("apk")
363
510
  const updateInfo = updateAvailable
364
511
  ? {
365
512
  available: true,
@@ -367,6 +514,8 @@ export async function checkCurrentGlassesForUpdate(
367
514
  versionName: result.latestVersionInfo?.versionName || "",
368
515
  updates: filteredUpdates,
369
516
  totalSize: 0,
517
+ ...(filteredUpdates.includes("bes") && result.besVersion ? {besVersion: result.besVersion} : {}),
518
+ isDowngrade: isApkDowngrade,
370
519
  }
371
520
  : null
372
521
 
@@ -376,8 +525,19 @@ export async function checkCurrentGlassesForUpdate(
376
525
  ...result,
377
526
  updateAvailable,
378
527
  updates: filteredUpdates,
528
+ isApkDowngrade,
379
529
  updateInfo,
380
- isRequired: result.latestVersionInfo?.isRequired !== false,
530
+ // Required-update policy: an EXPLICIT isRequired in the manifest is the
531
+ // release-engineering escape hatch and wins in both directions. When absent,
532
+ // upgrades keep the historical forced default, but downgrades are always
533
+ // skippable — a version change that resets glasses settings must be
534
+ // declinable unless someone explicitly forced it.
535
+ isRequired:
536
+ result.latestVersionInfo?.isRequired === true
537
+ ? true
538
+ : result.latestVersionInfo?.isRequired === false
539
+ ? false
540
+ : !isApkDowngrade,
381
541
  manifestUrl,
382
542
  buildNumber,
383
543
  mtkFirmwareVersion,
@@ -1,4 +1,6 @@
1
1
  import * as Calendar from "expo-calendar"
2
+ import {Platform} from "react-native"
3
+ import {check, PERMISSIONS, RESULTS} from "react-native-permissions"
2
4
 
3
5
  import {MiniappErrorCode, type CalendarListResult} from "@mentra/miniapp"
4
6
 
@@ -12,14 +14,35 @@ import {
12
14
 
13
15
  export {PhoneCalendarError} from "./PhoneCalendarHelpers"
14
16
 
17
+ type CalendarPermissionStatus = Awaited<
18
+ ReturnType<typeof Calendar.getCalendarPermissionsAsync>
19
+ >["status"]
20
+
21
+ /**
22
+ * expo-calendar reports anything below EventKit full access as not granted,
23
+ * so distinguish the states the user can actually fix. "Reopen the miniapp"
24
+ * is only true while the permission is undetermined — once iOS has resolved
25
+ * it (denied or write-only/limited), only Settings can change it.
26
+ */
27
+ async function calendarPermissionMessage(status: CalendarPermissionStatus): Promise<string> {
28
+ if (status === "undetermined") {
29
+ return "Calendar permission has not been granted yet. Reopen the miniapp and allow calendar access."
30
+ }
31
+ if (Platform.OS === "ios") {
32
+ const writeOnly = await check(PERMISSIONS.IOS.CALENDARS_WRITE_ONLY).catch(() => null)
33
+ if (writeOnly === RESULTS.GRANTED || writeOnly === RESULTS.LIMITED) {
34
+ return 'Calendar access is limited to write-only. Enable "Full Access" for Mentra in Settings › Privacy & Security › Calendars.'
35
+ }
36
+ return "Calendar access is denied. Enable it for Mentra in Settings › Privacy & Security › Calendars."
37
+ }
38
+ return "Calendar access is denied. Enable the Calendar permission for Mentra in your phone's app settings."
39
+ }
40
+
15
41
  export async function listPhoneCalendarEvents(request: CalendarListRequest): Promise<CalendarListResult> {
16
42
  const {startsAt, endsAt, limit} = validateCalendarListRequest(request)
17
43
  const permission = await Calendar.getCalendarPermissionsAsync()
18
44
  if (permission.status !== "granted") {
19
- throw new PhoneCalendarError(
20
- MiniappErrorCode.PERMISSION_DENIED,
21
- "Calendar permission is not granted. Reopen the miniapp and grant calendar access.",
22
- )
45
+ throw new PhoneCalendarError(MiniappErrorCode.PERMISSION_DENIED, await calendarPermissionMessage(permission.status))
23
46
  }
24
47
 
25
48
  const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.EVENT)
@@ -41,6 +41,21 @@ export class PhoneCameraFovCoordinator {
41
41
 
42
42
  constructor(private readonly legacyRestartSettleMs = LEGACY_CAMERA_RESTART_SETTLE_MS) {}
43
43
 
44
+ /** Report-safe FOV ownership snapshot for incident diagnostics. */
45
+ getDiagnosticSnapshot(): Record<string, unknown> {
46
+ return {
47
+ owners: [...this.overrides.entries()]
48
+ .sort(([, a], [, b]) => b.order - a.order)
49
+ .map(([packageName, entry]) => ({
50
+ packageName,
51
+ effective: entry.leaseId === this.effectiveLeaseId,
52
+ fov: entry.fov,
53
+ roiPosition: entry.roiPosition,
54
+ })),
55
+ legacyMode: this.legacyMode,
56
+ }
57
+ }
58
+
44
59
  setOverride(packageName: string, request: CameraFovRequest): Promise<CameraFovResult> {
45
60
  return this.enqueue(async () => {
46
61
  const previous = this.overrides.get(packageName)
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * Phone-notifications sync — engine-owned. Pushes the notification-forwarding
3
- * config (`notifications_enabled` + `notifications_blocklist`) to the native
3
+ * operational kill switch + config (`android_notification_listener_enabled`
4
+ * and `notifications_blocklist`) to the native
4
5
  * NotificationListener via `CrustModule.setNotificationConfig`, so
5
- * `engine.phoneNotifications.setEnabled()/setBlocklist()` actually reach the
6
- * listener for ANY host not just the Mentra app, where this used to live in
7
- * MantleManager. Android-only; a no-op elsewhere. Started by `engine.start()`.
6
+ * the blocklist reaches the listener for ANY host — not just the Mentra App,
7
+ * where this used to live in MantleManager. The Android service is desired by
8
+ * default now that its cold start is isolated from React Native. Native code
9
+ * keeps the component discoverable in Android's notification-access Settings,
10
+ * but only binds the service after access is granted. The debug setting remains
11
+ * a hidden emergency kill switch; Notify's running state gates presentation,
12
+ * not capture. Android-only; a no-op elsewhere. Started by `engine.start()`.
8
13
  */
9
14
  import {shallow} from "zustand/shallow"
10
15
  import CrustModule from "@mentra/crust"
@@ -15,12 +20,9 @@ let unsubscribe: (() => void) | null = null
15
20
 
16
21
  function pushConfig(): void {
17
22
  const s = useSettingsStore.getState()
18
- const enabled = Boolean(s.getSetting(SETTINGS.notifications_enabled.key))
23
+ const listenerEnabled = Boolean(s.getSetting(SETTINGS.android_notification_listener_enabled.key))
19
24
  const blocklist = s.getSetting(SETTINGS.notifications_blocklist.key)
20
- // Unconditional (matches the prior MantleManager sync) the listener is
21
- // Android-only, but CrustModule.setNotificationConfig is a safe no-op elsewhere;
22
- // the Android-only gating lives on the facade's setEnabled/setBlocklist.
23
- CrustModule.setNotificationConfig(enabled, Array.isArray(blocklist) ? blocklist : []).catch((err: unknown) =>
25
+ CrustModule.setNotificationConfig(listenerEnabled, Array.isArray(blocklist) ? blocklist : []).catch((err: unknown) =>
24
26
  console.warn(`PhoneNotificationsSync: setNotificationConfig failed: ${(err as Error)?.message ?? err}`),
25
27
  )
26
28
  }
@@ -30,7 +32,7 @@ export function startPhoneNotificationsSync(): void {
30
32
  pushConfig() // initial sync so the listener has the current config
31
33
  unsubscribe = useSettingsStore.subscribe(
32
34
  (state) => ({
33
- enabled: state.getSetting(SETTINGS.notifications_enabled.key),
35
+ listenerEnabled: state.getSetting(SETTINGS.android_notification_listener_enabled.key),
34
36
  blocklist: state.getSetting(SETTINGS.notifications_blocklist.key),
35
37
  }),
36
38
  () => pushConfig(),
@@ -19,7 +19,7 @@
19
19
  */
20
20
 
21
21
  import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
22
- import type {PhotoSize} from "@mentra/bluetooth-sdk/internal"
22
+ import type {PhotoSize, PhotoTransferMethod} from "@mentra/bluetooth-sdk/internal"
23
23
  import {cloudClientService} from "./CloudClientService"
24
24
  import {isGlassesConnected} from "./GlassesReadiness"
25
25
  import {useGlassesStore} from "../stores/glasses"
@@ -49,8 +49,8 @@ export interface PhotoOpts {
49
49
  /** Legacy cloud size names are normalized before the native take_photo command. */
50
50
  size?: "low" | "medium" | "high" | "max" | "small" | "large" | "full"
51
51
  mode?: "photo" | "text"
52
- /** Force phone-relayed BLE delivery, or leave native on its auto Wi-Fi/BLE policy. */
53
- transferMethod?: "auto" | "ble"
52
+ /** Select direct-only, phone-relayed BLE, or the default Wi-Fi/BLE fallback policy. */
53
+ transferMethod?: PhotoTransferMethod
54
54
  compress?: "none" | "low" | "medium" | "high"
55
55
  sound?: boolean
56
56
  saveToGallery?: boolean
@@ -60,8 +60,10 @@ export interface PhotoOpts {
60
60
  isoCap?: number
61
61
  noiseReduction?: boolean
62
62
  edgeEnhancement?: boolean
63
- mfnr?: boolean
63
+ /** ZSL preview buffering. */
64
64
  zsl?: boolean
65
+ /** MFNR still capture. */
66
+ mfnr?: boolean
65
67
  ispDigitalGain?: number
66
68
  ispAnalogGain?: string
67
69
  }
@@ -91,6 +93,15 @@ export class PhotoError extends Error {
91
93
  }
92
94
  }
93
95
 
96
+ function parsePhotoTransferMethod(value: unknown): PhotoTransferMethod | undefined {
97
+ if (value === undefined) return undefined
98
+ if (value === "auto" || value === "direct" || value === "ble") return value
99
+ throw new PhotoError(
100
+ "INVALID_ARGUMENT",
101
+ `Invalid transferMethod ${JSON.stringify(value)}. Expected "auto", "direct", or "ble".`,
102
+ )
103
+ }
104
+
94
105
  interface ActiveRequest {
95
106
  packageName: string
96
107
  abort: AbortController
@@ -138,7 +149,18 @@ export class PhonePhotoCoordinator {
138
149
  {requestId: string; durationMs: number; expiryTimer?: ReturnType<typeof setTimeout>}
139
150
  >()
140
151
 
152
+ /** Report-safe camera ownership snapshot for incident diagnostics. */
153
+ getDiagnosticSnapshot(): Record<string, unknown> {
154
+ return {
155
+ captureOwners: [...new Set([...this.activeRequests.values()].map((request) => request.packageName))].sort(),
156
+ warmUpOwners: [...this.activeWarmUps.keys()].sort(),
157
+ activeCaptureCount: this.activeRequests.size,
158
+ }
159
+ }
160
+
141
161
  async takePhoto(packageName: string, opts: PhotoOpts): Promise<PhotoTaken> {
162
+ const transferMethod = parsePhotoTransferMethod(opts.transferMethod)
163
+
142
164
  // Pre-check: if glasses aren't even connected, the BLE photo command
143
165
  // would be sent into the void and we'd wait 30s for the cloud long-poll
144
166
  // to time out. Fail fast with a typed error.
@@ -241,7 +263,7 @@ export class PhonePhotoCoordinator {
241
263
  mode: opts.mode ?? "photo",
242
264
  webhookUrl: uploadUrl,
243
265
  authToken: null,
244
- ...(isLoopbackUpload || opts.transferMethod === "ble" ? {transferMethod: "ble" as const} : {}),
266
+ ...(isLoopbackUpload ? {transferMethod: "ble" as const} : transferMethod ? {transferMethod} : {}),
245
267
  compress: toNativeCompression(opts.compress),
246
268
  save: opts.saveToGallery ?? false,
247
269
  sound: opts.sound ?? true,
@@ -251,8 +273,8 @@ export class PhonePhotoCoordinator {
251
273
  isoCap: opts.isoCap,
252
274
  noiseReduction: opts.noiseReduction,
253
275
  edgeEnhancement: opts.edgeEnhancement,
254
- mfnr: opts.mfnr,
255
- zsl: opts.zsl,
276
+ ...(opts.zsl != null ? {zsl: opts.zsl} : {}),
277
+ ...(opts.mfnr != null ? {mfnr: opts.mfnr} : {}),
256
278
  ispDigitalGain: opts.ispDigitalGain,
257
279
  ispAnalogGain: opts.ispAnalogGain,
258
280
  }).catch((err) => {
@@ -322,6 +344,8 @@ export class PhonePhotoCoordinator {
322
344
  mode?: "photo" | "text"
323
345
  exposureTimeNs?: number
324
346
  durationMs?: number
347
+ zsl?: boolean
348
+ mfnr?: boolean
325
349
  },
326
350
  ): Promise<void> {
327
351
  // Pre-check: if glasses aren't connected, the BLE warm-up command would be
@@ -347,6 +371,8 @@ export class PhonePhotoCoordinator {
347
371
  mode: opts.mode ?? "photo",
348
372
  exposureTimeNs: opts.exposureTimeNs ?? null,
349
373
  durationMs,
374
+ ...(opts.zsl != null ? {zsl: opts.zsl} : {}),
375
+ ...(opts.mfnr != null ? {mfnr: opts.mfnr} : {}),
350
376
  })
351
377
  if (this.activeWarmUps.get(packageName) === lease) {
352
378
  lease.expiryTimer = setTimeout(() => {