@glassly/engine 0.1.0-dev.1 → 0.1.0-dev.27

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 (277) hide show
  1. package/README.md +53 -19
  2. package/build/bluetooth-sdk/debug.d.ts +2 -0
  3. package/build/bluetooth-sdk/debug.d.ts.map +1 -0
  4. package/build/bluetooth-sdk/debug.js +2 -0
  5. package/build/bluetooth-sdk/debug.js.map +1 -0
  6. package/build/bluetooth-sdk/index.d.ts +3 -0
  7. package/build/bluetooth-sdk/index.d.ts.map +1 -0
  8. package/build/bluetooth-sdk/index.js +3 -0
  9. package/build/bluetooth-sdk/index.js.map +1 -0
  10. package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
  11. package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
  12. package/build/bluetooth-sdk/ota-transport.js +2 -0
  13. package/build/bluetooth-sdk/ota-transport.js.map +1 -0
  14. package/build/bluetooth-sdk/photo-receiver.d.ts +3 -0
  15. package/build/bluetooth-sdk/photo-receiver.d.ts.map +1 -0
  16. package/build/bluetooth-sdk/photo-receiver.js +3 -0
  17. package/build/bluetooth-sdk/photo-receiver.js.map +1 -0
  18. package/build/bluetooth-sdk/react.d.ts +2 -0
  19. package/build/bluetooth-sdk/react.d.ts.map +1 -0
  20. package/build/bluetooth-sdk/react.js +2 -0
  21. package/build/bluetooth-sdk/react.js.map +1 -0
  22. package/build/bluetooth-sdk/types.d.ts +2 -0
  23. package/build/bluetooth-sdk/types.d.ts.map +1 -0
  24. package/build/bluetooth-sdk/types.js +2 -0
  25. package/build/bluetooth-sdk/types.js.map +1 -0
  26. package/build/engine.d.ts +23 -16
  27. package/build/engine.d.ts.map +1 -1
  28. package/build/engine.js +9 -3
  29. package/build/engine.js.map +1 -1
  30. package/build/facades/dev.d.ts +1 -1
  31. package/build/facades/display.d.ts +1 -1
  32. package/build/facades/display.d.ts.map +1 -1
  33. package/build/facades/display.js +1 -1
  34. package/build/facades/display.js.map +1 -1
  35. package/build/facades/glasses.d.ts +5 -13
  36. package/build/facades/glasses.d.ts.map +1 -1
  37. package/build/facades/glasses.js +5 -3
  38. package/build/facades/glasses.js.map +1 -1
  39. package/build/facades/ota.d.ts +19 -21
  40. package/build/facades/ota.d.ts.map +1 -1
  41. package/build/facades/ota.js +23 -9
  42. package/build/facades/ota.js.map +1 -1
  43. package/build/facades/pairing.d.ts +11 -5
  44. package/build/facades/pairing.d.ts.map +1 -1
  45. package/build/facades/pairing.js +93 -17
  46. package/build/facades/pairing.js.map +1 -1
  47. package/build/facades/speakers.d.ts +1 -1
  48. package/build/facades/speakers.d.ts.map +1 -1
  49. package/build/facades/speakers.js.map +1 -1
  50. package/build/facades/speech.d.ts +7 -6
  51. package/build/facades/speech.d.ts.map +1 -1
  52. package/build/facades/speech.js +1 -1
  53. package/build/facades/speech.js.map +1 -1
  54. package/build/generated/releaseMetadata.d.ts +12 -0
  55. package/build/generated/releaseMetadata.d.ts.map +1 -0
  56. package/build/generated/releaseMetadata.js +10 -0
  57. package/build/generated/releaseMetadata.js.map +1 -0
  58. package/build/index.d.ts +7 -4
  59. package/build/index.d.ts.map +1 -1
  60. package/build/index.js +5 -3
  61. package/build/index.js.map +1 -1
  62. package/build/react/MentraLiveOtaFlow.d.ts +39 -0
  63. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  64. package/build/react/MentraLiveOtaFlow.js +200 -0
  65. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  66. package/build/react/index.d.ts +3 -0
  67. package/build/react/index.d.ts.map +1 -0
  68. package/build/react/index.js +3 -0
  69. package/build/react/index.js.map +1 -0
  70. package/build/react/useEngineSnapshot.d.ts +2 -0
  71. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  72. package/build/react/useEngineSnapshot.js +14 -0
  73. package/build/react/useEngineSnapshot.js.map +1 -0
  74. package/build/react/useMentraLiveOta.d.ts +80 -0
  75. package/build/react/useMentraLiveOta.d.ts.map +1 -0
  76. package/build/react/useMentraLiveOta.js +550 -0
  77. package/build/react/useMentraLiveOta.js.map +1 -0
  78. package/build/services/AppRegistry.d.ts +8 -0
  79. package/build/services/AppRegistry.d.ts.map +1 -1
  80. package/build/services/AppRegistry.js +15 -0
  81. package/build/services/AppRegistry.js.map +1 -1
  82. package/build/services/BlobStore.d.ts +2 -0
  83. package/build/services/BlobStore.d.ts.map +1 -1
  84. package/build/services/BlobStore.js +63 -10
  85. package/build/services/BlobStore.js.map +1 -1
  86. package/build/services/CustomDashboardModel.d.ts +48 -0
  87. package/build/services/CustomDashboardModel.d.ts.map +1 -0
  88. package/build/services/CustomDashboardModel.js +73 -0
  89. package/build/services/CustomDashboardModel.js.map +1 -0
  90. package/build/services/CustomDashboardService.d.ts +61 -0
  91. package/build/services/CustomDashboardService.d.ts.map +1 -0
  92. package/build/services/CustomDashboardService.js +552 -0
  93. package/build/services/CustomDashboardService.js.map +1 -0
  94. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  95. package/build/services/DeviceEventRouter.js +44 -3
  96. package/build/services/DeviceEventRouter.js.map +1 -1
  97. package/build/services/GestureDotsService.d.ts +35 -0
  98. package/build/services/GestureDotsService.d.ts.map +1 -0
  99. package/build/services/GestureDotsService.js +149 -0
  100. package/build/services/GestureDotsService.js.map +1 -0
  101. package/build/services/GlassesStatusProjection.d.ts +15 -1
  102. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  103. package/build/services/GlassesStatusProjection.js +18 -11
  104. package/build/services/GlassesStatusProjection.js.map +1 -1
  105. package/build/services/HotspotOtaTransport.d.ts.map +1 -1
  106. package/build/services/HotspotOtaTransport.js +8 -7
  107. package/build/services/HotspotOtaTransport.js.map +1 -1
  108. package/build/services/LocalDisplayManager.d.ts +43 -1
  109. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  110. package/build/services/LocalDisplayManager.js +128 -2
  111. package/build/services/LocalDisplayManager.js.map +1 -1
  112. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  113. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  114. package/build/services/LocalMiniappRuntime.js +32 -0
  115. package/build/services/LocalMiniappRuntime.js.map +1 -1
  116. package/build/services/MiniappExitGesture.d.ts +33 -0
  117. package/build/services/MiniappExitGesture.d.ts.map +1 -0
  118. package/build/services/MiniappExitGesture.js +54 -0
  119. package/build/services/MiniappExitGesture.js.map +1 -0
  120. package/build/services/OtaAutoChain.d.ts +41 -0
  121. package/build/services/OtaAutoChain.d.ts.map +1 -0
  122. package/build/services/OtaAutoChain.js +91 -0
  123. package/build/services/OtaAutoChain.js.map +1 -0
  124. package/build/services/OtaErrorMapping.d.ts +18 -0
  125. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  126. package/build/services/OtaErrorMapping.js +63 -0
  127. package/build/services/OtaErrorMapping.js.map +1 -0
  128. package/build/services/OtaInstallCoordinator.d.ts +24 -3
  129. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  130. package/build/services/OtaInstallCoordinator.js +17 -12
  131. package/build/services/OtaInstallCoordinator.js.map +1 -1
  132. package/build/services/OtaService.d.ts.map +1 -1
  133. package/build/services/OtaService.js +1 -1
  134. package/build/services/OtaService.js.map +1 -1
  135. package/build/services/OtaUpdateCheckService.d.ts +4 -1
  136. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  137. package/build/services/OtaUpdateCheckService.js +28 -16
  138. package/build/services/OtaUpdateCheckService.js.map +1 -1
  139. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  140. package/build/services/asg/localNetworkTransport.js +13 -12
  141. package/build/services/asg/localNetworkTransport.js.map +1 -1
  142. package/build/services/gestureClaims.d.ts +18 -0
  143. package/build/services/gestureClaims.d.ts.map +1 -0
  144. package/build/services/gestureClaims.js +24 -0
  145. package/build/services/gestureClaims.js.map +1 -0
  146. package/build/services/gestureDotGlyphs.d.ts +8 -0
  147. package/build/services/gestureDotGlyphs.d.ts.map +1 -0
  148. package/build/services/gestureDotGlyphs.js +12 -0
  149. package/build/services/gestureDotGlyphs.js.map +1 -0
  150. package/build/services/glassesClockSync.d.ts.map +1 -1
  151. package/build/services/glassesClockSync.js +5 -1
  152. package/build/services/glassesClockSync.js.map +1 -1
  153. package/build/services/otaDisplayState.d.ts +1 -1
  154. package/build/services/otaDisplayState.d.ts.map +1 -1
  155. package/build/services/otaDisplayState.js.map +1 -1
  156. package/build/services/otaInstallPolicy.d.ts +1 -1
  157. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  158. package/build/services/otaInstallPolicy.js.map +1 -1
  159. package/build/services/otaLegacyMapping.d.ts +1 -1
  160. package/build/services/otaLegacyMapping.d.ts.map +1 -1
  161. package/build/services/otaLegacyMapping.js.map +1 -1
  162. package/build/services/otaManifestPolicy.d.ts +14 -0
  163. package/build/services/otaManifestPolicy.d.ts.map +1 -0
  164. package/build/services/otaManifestPolicy.js +20 -0
  165. package/build/services/otaManifestPolicy.js.map +1 -0
  166. package/build/services/otaManifestUrl.d.ts +2 -1
  167. package/build/services/otaManifestUrl.d.ts.map +1 -1
  168. package/build/services/otaManifestUrl.js +24 -32
  169. package/build/services/otaManifestUrl.js.map +1 -1
  170. package/build/services/otaReleaseVersion.d.ts +10 -0
  171. package/build/services/otaReleaseVersion.d.ts.map +1 -0
  172. package/build/services/otaReleaseVersion.js +14 -0
  173. package/build/services/otaReleaseVersion.js.map +1 -0
  174. package/build/stores/appVisibility.d.ts +3 -0
  175. package/build/stores/appVisibility.d.ts.map +1 -0
  176. package/build/stores/appVisibility.js +3 -0
  177. package/build/stores/appVisibility.js.map +1 -0
  178. package/build/stores/apps.d.ts.map +1 -1
  179. package/build/stores/apps.js +7 -1
  180. package/build/stores/apps.js.map +1 -1
  181. package/build/stores/core.d.ts +3 -3
  182. package/build/stores/core.d.ts.map +1 -1
  183. package/build/stores/core.js.map +1 -1
  184. package/build/stores/glasses.d.ts +10 -4
  185. package/build/stores/glasses.d.ts.map +1 -1
  186. package/build/stores/glasses.js.map +1 -1
  187. package/build/stores/settings.d.ts.map +1 -1
  188. package/build/stores/settings.js +87 -3
  189. package/build/stores/settings.js.map +1 -1
  190. package/build/types/applet.d.ts +2 -0
  191. package/build/types/applet.d.ts.map +1 -1
  192. package/build/types/applet.js +0 -3
  193. package/build/types/applet.js.map +1 -1
  194. package/build/types/enums.d.ts +1 -5
  195. package/build/types/enums.d.ts.map +1 -1
  196. package/build/types/enums.js +1 -8
  197. package/build/types/enums.js.map +1 -1
  198. package/build/types/hardware.d.ts +1 -1
  199. package/build/types/hardware.d.ts.map +1 -1
  200. package/build/types/hardware.js.map +1 -1
  201. package/build/types/index.d.ts +2 -6
  202. package/build/types/index.d.ts.map +1 -1
  203. package/build/types/index.js +2 -6
  204. package/build/types/index.js.map +1 -1
  205. package/build/utils/display/scene/process.d.ts.map +1 -1
  206. package/build/utils/display/scene/process.js +4 -3
  207. package/build/utils/display/scene/process.js.map +1 -1
  208. package/build/utils/display/scene/types.d.ts +6 -2
  209. package/build/utils/display/scene/types.d.ts.map +1 -1
  210. package/build/utils/display/scene/types.js.map +1 -1
  211. package/package.json +65 -33
  212. package/src/bluetooth-sdk/debug.ts +1 -0
  213. package/src/bluetooth-sdk/index.ts +2 -0
  214. package/src/bluetooth-sdk/ota-transport.ts +1 -0
  215. package/src/bluetooth-sdk/photo-receiver.ts +2 -0
  216. package/src/bluetooth-sdk/react.ts +1 -0
  217. package/src/bluetooth-sdk/types.ts +1 -0
  218. package/src/engine.ts +9 -3
  219. package/src/facades/display.ts +1 -1
  220. package/src/facades/glasses.ts +6 -4
  221. package/src/facades/ota.ts +33 -10
  222. package/src/facades/pairing.ts +103 -23
  223. package/src/facades/speakers.ts +1 -1
  224. package/src/facades/speech.ts +21 -7
  225. package/src/generated/releaseMetadata.ts +20 -0
  226. package/src/index.ts +29 -4
  227. package/src/react/MentraLiveOtaFlow.tsx +569 -0
  228. package/src/react/index.ts +21 -0
  229. package/src/react/useEngineSnapshot.ts +16 -0
  230. package/src/react/useMentraLiveOta.ts +707 -0
  231. package/src/services/AppRegistry.ts +16 -0
  232. package/src/services/BlobStore.ts +57 -10
  233. package/src/services/CustomDashboardModel.ts +83 -0
  234. package/src/services/CustomDashboardService.ts +575 -0
  235. package/src/services/DeviceEventRouter.ts +47 -5
  236. package/src/services/GestureDotsService.ts +147 -0
  237. package/src/services/GlassesStatusProjection.ts +20 -11
  238. package/src/services/HotspotOtaTransport.ts +8 -7
  239. package/src/services/LocalDisplayManager.ts +130 -2
  240. package/src/services/LocalMiniappRuntime.ts +37 -0
  241. package/src/services/MiniappExitGesture.ts +76 -0
  242. package/src/services/OtaAutoChain.ts +130 -0
  243. package/src/services/OtaErrorMapping.ts +80 -0
  244. package/src/services/OtaInstallCoordinator.ts +19 -13
  245. package/src/services/OtaService.ts +1 -2
  246. package/src/services/OtaUpdateCheckService.ts +33 -19
  247. package/src/services/asg/localNetworkTransport.ts +20 -19
  248. package/src/services/gestureClaims.ts +28 -0
  249. package/src/services/gestureDotGlyphs.ts +14 -0
  250. package/src/services/glassesClockSync.ts +5 -1
  251. package/src/services/otaDisplayState.ts +1 -1
  252. package/src/services/otaInstallPolicy.ts +1 -1
  253. package/src/services/otaLegacyMapping.ts +1 -1
  254. package/src/services/otaManifestPolicy.ts +43 -0
  255. package/src/services/otaManifestUrl.ts +27 -36
  256. package/src/services/otaReleaseVersion.ts +22 -0
  257. package/src/stores/appVisibility.ts +3 -0
  258. package/src/stores/apps.ts +7 -1
  259. package/src/stores/core.ts +4 -4
  260. package/src/stores/glasses.ts +13 -6
  261. package/src/stores/settings.ts +87 -3
  262. package/src/types/applet.ts +2 -0
  263. package/src/types/enums.ts +1 -9
  264. package/src/types/hardware.ts +1 -1
  265. package/src/types/index.ts +2 -6
  266. package/src/utils/display/scene/process.ts +4 -3
  267. package/src/utils/display/scene/types.ts +0 -0
  268. package/build/devtools.d.ts +0 -11
  269. package/build/devtools.d.ts.map +0 -1
  270. package/build/devtools.js +0 -13
  271. package/build/devtools.js.map +0 -1
  272. package/build/internal.d.ts +0 -67
  273. package/build/internal.d.ts.map +0 -1
  274. package/build/internal.js +0 -90
  275. package/build/internal.js.map +0 -1
  276. package/src/devtools.ts +0 -13
  277. package/src/internal.ts +0 -155
@@ -0,0 +1,130 @@
1
+ import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
2
+
3
+ export const MAX_OTA_AUTO_CHAIN_PASSES = 8
4
+ export const OTA_AUTO_CHAIN_RECONNECT_TIMEOUT_MS = 120_000
5
+
6
+ type OtaAutoChainSession = {
7
+ approvedDowngrade: boolean
8
+ passCount: number
9
+ releaseRange: OtaAutoChainReleaseRange
10
+ reconnectDeadline: number | null
11
+ seenFingerprints: Set<string>
12
+ }
13
+
14
+ export type OtaAutoChainReleaseRange = {
15
+ fromVersion: string | null
16
+ toVersion: string | null
17
+ /** Exact coordinated release identity for the OTA pin, absent for legacy manifests. */
18
+ releaseVersion?: string | null
19
+ }
20
+
21
+ export type OtaAutoChainAdvanceResult =
22
+ | {advance: true; passCount: number}
23
+ | {advance: false; reason: "inactive" | "duplicate" | "max_passes" | "downgrade_not_approved"}
24
+
25
+ let session: OtaAutoChainSession | null = null
26
+
27
+ /**
28
+ * Identify the exact update offer that produced one OTA pass. Including the
29
+ * observed versions and manifest body lets a changed manifest advance while a
30
+ * stale post-install version_info response is rejected as a duplicate.
31
+ */
32
+ export function otaAutoChainFingerprint(result: OtaCheckCurrentGlassesResult): string {
33
+ return JSON.stringify({
34
+ manifestUrl: result.manifestUrl ?? "",
35
+ manifestBody: result.manifestBody ?? "",
36
+ buildNumber: result.buildNumber ?? "",
37
+ mtkFirmwareVersion: result.mtkFirmwareVersion ?? "",
38
+ besFirmwareVersion: result.besFirmwareVersion ?? "",
39
+ updates: result.updates,
40
+ targetBuildNumber: result.updateInfo?.versionCode ?? result.latestVersionInfo?.versionCode ?? 0,
41
+ targetBesVersion: result.besVersion ?? "",
42
+ mtkPatch: result.mtkPatch
43
+ ? {
44
+ startFirmware: result.mtkPatch.start_firmware,
45
+ endFirmware: result.mtkPatch.end_firmware,
46
+ url: result.mtkPatch.url,
47
+ }
48
+ : null,
49
+ })
50
+ }
51
+
52
+ /** Start a chain after the user explicitly approves its first update pass. */
53
+ export function beginOtaAutoChain(
54
+ initialFingerprint: string,
55
+ approvedDowngrade: boolean,
56
+ releaseRange: OtaAutoChainReleaseRange,
57
+ ): void {
58
+ session = {
59
+ approvedDowngrade,
60
+ passCount: 1,
61
+ releaseRange: {...releaseRange},
62
+ reconnectDeadline: null,
63
+ seenFingerprints: new Set([initialFingerprint]),
64
+ }
65
+ }
66
+
67
+ export function isOtaAutoChainActive(): boolean {
68
+ return session !== null
69
+ }
70
+
71
+ export function otaAutoChainReleaseRange(): OtaAutoChainReleaseRange | null {
72
+ return session ? {...session.releaseRange} : null
73
+ }
74
+
75
+ /** End the current chain. Safe to call when no chain is active. */
76
+ export function stopOtaAutoChain(): void {
77
+ session = null
78
+ }
79
+
80
+ /**
81
+ * Start (or continue) the bounded wait for glasses that are rebooting between
82
+ * passes. Re-renders must not extend the original deadline indefinitely.
83
+ */
84
+ export function otaAutoChainReconnectWaitRemaining(now = performance.now()): number | null {
85
+ if (!session) return null
86
+
87
+ session.reconnectDeadline ??= now + OTA_AUTO_CHAIN_RECONNECT_TIMEOUT_MS
88
+ return Math.max(0, session.reconnectDeadline - now)
89
+ }
90
+
91
+ /** A successful reconnect allows a future pass to establish a fresh wait. */
92
+ export function clearOtaAutoChainReconnectWait(): void {
93
+ if (session) session.reconnectDeadline = null
94
+ }
95
+
96
+ /**
97
+ * Admit the next pass of an active chain. Repeated offers, unexpectedly
98
+ * destructive downgrades, and runaway chains fail closed and end automation.
99
+ */
100
+ export function tryAdvanceOtaAutoChain(
101
+ fingerprint: string,
102
+ isDowngrade: boolean,
103
+ targetVersion: string | null,
104
+ releaseVersion: string | null = null,
105
+ ): OtaAutoChainAdvanceResult {
106
+ if (!session) {
107
+ return {advance: false, reason: "inactive"}
108
+ }
109
+
110
+ if (isDowngrade && !session.approvedDowngrade) {
111
+ stopOtaAutoChain()
112
+ return {advance: false, reason: "downgrade_not_approved"}
113
+ }
114
+
115
+ if (session.seenFingerprints.has(fingerprint)) {
116
+ stopOtaAutoChain()
117
+ return {advance: false, reason: "duplicate"}
118
+ }
119
+
120
+ if (session.passCount >= MAX_OTA_AUTO_CHAIN_PASSES) {
121
+ stopOtaAutoChain()
122
+ return {advance: false, reason: "max_passes"}
123
+ }
124
+
125
+ session.seenFingerprints.add(fingerprint)
126
+ session.passCount += 1
127
+ if (targetVersion) session.releaseRange.toVersion = targetVersion
128
+ if (releaseVersion) session.releaseRange.releaseVersion = releaseVersion
129
+ return {advance: true, passCount: session.passCount}
130
+ }
@@ -0,0 +1,80 @@
1
+ import type {OtaProgress, OtaStatus} from "../facades/ota"
2
+
3
+ export const BES_INSTALL_RESTART_MESSAGE =
4
+ "Restart your glasses to safely exit firmware update mode before trying again"
5
+
6
+ function isDownloadPhaseSnapshot(
7
+ otaStatus: OtaStatus | null | undefined,
8
+ otaProgress: OtaProgress | null | undefined,
9
+ ): boolean {
10
+ if (otaStatus?.phase === "download") {
11
+ return true
12
+ }
13
+ if (otaProgress?.stage === "download") {
14
+ return true
15
+ }
16
+ return false
17
+ }
18
+
19
+ /**
20
+ * Offer Change WiFi whenever the OTA flow failed while the glasses were in the
21
+ * download step (any download failure — network, SSL, size cap, verify, etc.).
22
+ *
23
+ * Also covers phone-side watchdog failures that fire while the store still shows
24
+ * an active download phase (stall / global timeout mid-download).
25
+ */
26
+ export function shouldShowChangeWifiForOtaDownloadFailure(
27
+ otaStatus: OtaStatus | null | undefined,
28
+ otaProgress: OtaProgress | null | undefined,
29
+ localErrorMessage: string,
30
+ ): boolean {
31
+ if (otaStatus?.status === "failed" && otaStatus.phase === "download") {
32
+ return true
33
+ }
34
+ if (otaProgress?.status === "FAILED" && otaProgress.stage === "download") {
35
+ return true
36
+ }
37
+ if (localErrorMessage && isDownloadPhaseSnapshot(otaStatus, otaProgress)) {
38
+ return true
39
+ }
40
+ return false
41
+ }
42
+
43
+ export function getOtaErrorMessage(error?: string): string {
44
+ switch (error) {
45
+ case "no_internet":
46
+ return "Glasses WiFi has no internet connection"
47
+ case "clock_skew":
48
+ return "Glasses clock is wrong — syncing time from your phone, then retrying update check"
49
+ case "ssl_error":
50
+ return "Secure connection failed — try a different WiFi network"
51
+ case "download_failed":
52
+ return "Download failed — check glasses WiFi connection"
53
+ case "firmware_too_large":
54
+ return "Firmware file is unexpectedly large — please contact support"
55
+ case "firmware_verify_failed":
56
+ return "Firmware verification failed — please try again or contact support"
57
+ case "apk_verify_failed":
58
+ return "Update verification failed — please try again or contact support"
59
+ case "install_failed":
60
+ return "Install failed — please try again"
61
+ default:
62
+ return error || "Update failed"
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Once a BES install has started, any failure conservatively requires a glasses restart. This
68
+ * avoids adding a BES-specific error code to the wire protocol and, more importantly, never offers
69
+ * an unsafe retry when a generic failure or silence may mean BES entered its raw OTA parser.
70
+ */
71
+ export function shouldRequireGlassesRebootForBesFailure(
72
+ otaStatus: OtaStatus | null | undefined,
73
+ _otaProgress: OtaProgress | null | undefined,
74
+ localErrorMessage: string,
75
+ ): boolean {
76
+ if (otaStatus?.stepType !== "bes" || otaStatus.phase !== "install") {
77
+ return false
78
+ }
79
+ return otaStatus.status === "failed" || Boolean(localErrorMessage)
80
+ }
@@ -17,8 +17,7 @@
17
17
  * state written mid-pass queues a follow-up pass — the same way a setState
18
18
  * during React effects scheduled a re-render after the current effect batch.
19
19
  */
20
- import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
21
- import type {OtaProgress, OtaStatus} from "@glassly/bluetooth-sdk/internal"
20
+ import BluetoothSdk, {type OtaProgress, type OtaStatus} from "@glassly/bluetooth-sdk"
22
21
  import GlobalEventEmitter from "../utils/GlobalEventEmitter"
23
22
  import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
24
23
  import {resolveOtaManifestUrl} from "./otaManifestUrl"
@@ -170,6 +169,8 @@ export interface OtaInstallSnapshot {
170
169
  /** Pre-ota_start phone staging/join state for a hotspot attempt. */
171
170
  hotspotPhase: HotspotOtaPhase
172
171
  hotspotArtifactPercent: number | null
172
+ /** Transport selected from the checked glasses capabilities and Wi-Fi state. */
173
+ transport: "wifi" | "hotspot"
173
174
  }
174
175
 
175
176
  type OtaStartOutcome = "pending" | "acknowledged" | "rejected"
@@ -407,7 +408,7 @@ class OtaInstallCoordinator {
407
408
  if (this.otaStartOwnership?.outcome === "pending") {
408
409
  void this.sendOtaStartWithWatchdogs()
409
410
  } else {
410
- void BluetoothSdk.sendOtaQueryStatus().catch(() => {})
411
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
411
412
  this.armQueryReplyFallback("retry")
412
413
  }
413
414
  }
@@ -449,16 +450,17 @@ class OtaInstallCoordinator {
449
450
  * stays in the screen): clear the selected update, and after an APK step
450
451
  * clear the stale build number so the next check re-reads version_info.
451
452
  */
452
- finish(): void {
453
+ async finish(): Promise<void> {
453
454
  if (this.otaStartOwnership?.outcome !== "pending") {
454
455
  this.otaStartOwnership = null
455
456
  }
456
457
  useGlassesStore.getState().setOtaUpdateAvailable(null)
457
458
  if (this.apkStepSeen) {
458
- BluetoothSdk.updateGlasses({buildNumber: ""})
459
459
  useGlassesStore.getState().setGlassesInfo({buildNumber: ""})
460
460
  }
461
- void this.teardownHotspotTransport()
461
+ // The post-install check needs internet again. Do not let callers leave the
462
+ // terminal screen while Android still owns the no-internet hotspot route.
463
+ await this.teardownHotspotTransport()
462
464
  }
463
465
 
464
466
  /**
@@ -467,8 +469,8 @@ class OtaInstallCoordinator {
467
469
  * deriveDisplayState doesn't resurrect the stale install when the host
468
470
  * navigates away and back through /ota routes.
469
471
  */
470
- discard(): void {
471
- this.finish()
472
+ async discard(): Promise<void> {
473
+ await this.finish()
472
474
  const store = useGlassesStore.getState()
473
475
  store.setOtaStatus(null)
474
476
  store.setOtaProgress(null)
@@ -505,6 +507,7 @@ class OtaInstallCoordinator {
505
507
  versionChangePhase: this.deriveVersionChangePhase(connected),
506
508
  hotspotPhase: this.hotspotPhase,
507
509
  hotspotArtifactPercent: this.hotspotArtifactPercent,
510
+ transport: this.selectedTransport,
508
511
  }
509
512
  }
510
513
 
@@ -1065,7 +1068,7 @@ class OtaInstallCoordinator {
1065
1068
  if (this.otaStartOwnership?.outcome === "pending") {
1066
1069
  void this.sendOtaStartWithWatchdogs()
1067
1070
  }
1068
- void BluetoothSdk.sendOtaQueryStatus()
1071
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1069
1072
  this.armQueryReplyFallback("reconnect")
1070
1073
  return true
1071
1074
  }
@@ -1109,7 +1112,7 @@ class OtaInstallCoordinator {
1109
1112
  if (noSessionYet) {
1110
1113
  if (!isIdleStatus && this.otaStartOwnership?.outcome === "acknowledged") {
1111
1114
  console.log("[OTA_PROGRESS] initial mount, acknowledged session has no cached status — reconciling")
1112
- void BluetoothSdk.sendOtaQueryStatus().catch(() => {})
1115
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1113
1116
  this.armQueryReplyFallback("initial-mount")
1114
1117
  return
1115
1118
  }
@@ -1125,7 +1128,7 @@ class OtaInstallCoordinator {
1125
1128
  void this.sendOtaStartWithWatchdogs()
1126
1129
  } else {
1127
1130
  console.log("[OTA_PROGRESS] initial mount, session exists, sending ota_query_status")
1128
- void BluetoothSdk.sendOtaQueryStatus()
1131
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1129
1132
  this.armQueryReplyFallback("initial-mount")
1130
1133
  }
1131
1134
  }
@@ -1154,7 +1157,7 @@ class OtaInstallCoordinator {
1154
1157
  this.clearProgressTimeout()
1155
1158
  this.onFirstActivity()
1156
1159
  this.onFirstNonZeroProgress()
1157
- void BluetoothSdk.sendOtaQueryStatus()
1160
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1158
1161
  useGlassesStore.getState().setMtkUpdatedThisSession(true)
1159
1162
  }
1160
1163
 
@@ -1345,7 +1348,7 @@ class OtaInstallCoordinator {
1345
1348
  const s = useGlassesStore.getState().otaStatus
1346
1349
  if (s?.stepType === "apk" && s.phase === "install" && s.status === "in_progress") {
1347
1350
  this.apkInstallPollInFlight = true
1348
- void BluetoothSdk.sendOtaQueryStatus()
1351
+ void BluetoothSdk.queryOtaStatus()
1349
1352
  .catch(() => {})
1350
1353
  .finally(() => {
1351
1354
  this.apkInstallPollInFlight = false
@@ -1492,6 +1495,9 @@ class OtaInstallCoordinator {
1492
1495
  otaVersionUrl = this.hotspotManifestUrl
1493
1496
  this.maybeArmStuckWatchdog()
1494
1497
  }
1498
+ if (!otaVersionUrl) {
1499
+ throw new Error("OTA is disabled because this build has no immutable manifest pin")
1500
+ }
1495
1501
  console.log(`[OTA_PROGRESS] sending ota_start with ${this.selectedTransport} manifest URL: ${otaVersionUrl}`)
1496
1502
  nativeStartAttempted = true
1497
1503
  await BluetoothSdk.startOtaUpdate(otaVersionUrl)
@@ -10,8 +10,7 @@
10
10
  *
11
11
  * Started by `engine.start()`. Idempotent.
12
12
  */
13
- import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
14
- import type {OtaStatus} from "@glassly/bluetooth-sdk/internal"
13
+ import BluetoothSdk, {type OtaStatus} from "@glassly/bluetooth-sdk"
15
14
  import GlobalEventEmitter from "../utils/GlobalEventEmitter"
16
15
  import {useGlassesStore} from "../stores/glasses"
17
16
  import {handleOtaClockSkewFromGlasses} from "./glassesClockSync"
@@ -1,9 +1,9 @@
1
- import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
2
- import type {OtaUpdateInfo} from "@glassly/bluetooth-sdk/internal"
1
+ import BluetoothSdk, {type OtaUpdateInfo} from "@glassly/bluetooth-sdk"
2
+ import {ENGINE_RELEASE_METADATA} from "../generated/releaseMetadata"
3
3
  import {getGlassesSystemTimeMs, isGlassesConnected, useGlassesStore, waitForGlassesState} from "../stores/glasses"
4
- import {SETTINGS, useSettingsStore} from "../stores/settings"
5
4
  import {maybeFixGlassesClockFromVersionInfo} from "./glassesClockSync"
6
- import {resolveOtaManifestUrl} from "./otaManifestUrl"
5
+ import {hasConfiguredModernOtaManifestPin, resolveOtaManifestUrl} from "./otaManifestUrl"
6
+ import {resolveOtaReleaseVersion} from "./otaReleaseVersion"
7
7
 
8
8
  /**
9
9
  * Phone-side mirror of ASG's `DOWNGRADE_FLOOR_VERSION_CODE`. Set to the versionCode of the first
@@ -39,6 +39,7 @@ export interface BesFirmware {
39
39
  }
40
40
 
41
41
  export interface VersionJson {
42
+ releaseVersion?: string
42
43
  apps?: {
43
44
  [packageName: string]: VersionInfo
44
45
  }
@@ -67,6 +68,8 @@ export interface OtaCheckResult {
67
68
  * instead of racing it with a second fetch of the same URL.
68
69
  */
69
70
  manifestBody: string | null
71
+ /** Coordinated release identity for the selected OTA pin. */
72
+ releaseVersion: string | null
70
73
  /**
71
74
  * Why the check failed, when it did. "network" is retryable (connection/server
72
75
  * trouble); "pin_unavailable" is permanent for this app build (manifest 404/gone,
@@ -107,6 +110,7 @@ function emptyCheckResult(skippedReason?: OtaCheckSkippedReason): OtaCheckCurren
107
110
  besVersion: null,
108
111
  isApkDowngrade: false,
109
112
  manifestBody: null,
113
+ releaseVersion: null,
110
114
  updateInfo: null,
111
115
  isRequired: true,
112
116
  skippedReason,
@@ -123,7 +127,7 @@ async function fetchVersionInfoDetailed(url: string): Promise<ManifestFetchResul
123
127
  try {
124
128
  const response = await fetch(url)
125
129
  if (!response.ok) {
126
- console.error("Failed to fetch version info:", response.status)
130
+ console.warn("Failed to fetch version info:", response.status)
127
131
  // 4xx: the manifest does not exist (or is gone) — permanent for this app
128
132
  // build, retrying cannot help. Everything else is transient server/network
129
133
  // trouble and retryable.
@@ -135,11 +139,11 @@ async function fetchVersionInfoDetailed(url: string): Promise<ManifestFetchResul
135
139
  try {
136
140
  return {json: await response.json()}
137
141
  } catch (parseError) {
138
- console.error("OTA: manifest is not valid JSON:", parseError)
142
+ console.warn("OTA: manifest is not valid JSON:", parseError)
139
143
  return {json: null, failureReason: "pin_unavailable"}
140
144
  }
141
145
  } catch (error) {
142
- console.error("OTA: Error fetching version info:", error)
146
+ console.warn("OTA: Error fetching version info:", error)
143
147
  return {json: null, failureReason: "network"}
144
148
  }
145
149
  }
@@ -299,6 +303,7 @@ export async function checkForOtaUpdate(
299
303
  besVersion: null,
300
304
  isApkDowngrade: false,
301
305
  manifestBody: null,
306
+ releaseVersion: null,
302
307
  checkFailureReason: fetched.failureReason ?? "network",
303
308
  }
304
309
  }
@@ -330,6 +335,7 @@ export async function checkForOtaUpdate(
330
335
  besVersion: null,
331
336
  isApkDowngrade: false,
332
337
  manifestBody: null,
338
+ releaseVersion: null,
333
339
  checkFailureReason: "pin_unavailable",
334
340
  }
335
341
  }
@@ -368,6 +374,12 @@ export async function checkForOtaUpdate(
368
374
  besVersion: versionJson?.bes_firmware?.version || null,
369
375
  isApkDowngrade: apkDirection === "downgrade",
370
376
  manifestBody: versionJson ? JSON.stringify(versionJson) : null,
377
+ releaseVersion: resolveOtaReleaseVersion({
378
+ manifestReleaseVersion: versionJson.releaseVersion,
379
+ manifestUrl: otaVersionUrl,
380
+ packagedManifestUrl: ENGINE_RELEASE_METADATA.otaManifestUrl,
381
+ packagedReleaseIdentity: ENGINE_RELEASE_METADATA.releaseIdentity,
382
+ }),
371
383
  }
372
384
  } catch (error) {
373
385
  console.error("Error checking for OTA update:", error)
@@ -380,6 +392,7 @@ export async function checkForOtaUpdate(
380
392
  besVersion: null,
381
393
  isApkDowngrade: false,
382
394
  manifestBody: null,
395
+ releaseVersion: null,
383
396
  checkFailureReason: "network",
384
397
  }
385
398
  }
@@ -401,22 +414,19 @@ export async function checkCurrentGlassesForUpdate(
401
414
  return emptyCheckResult("disconnected")
402
415
  }
403
416
 
404
- const embeddedManifestUrl = process.env.EXPO_PUBLIC_ASG_OTA_VERSION_URL?.trim()
405
- const superMode = useSettingsStore.getState().getSetting(SETTINGS.super_mode.key)
406
- const overrideUrl = useSettingsStore.getState().getSetting(SETTINGS.ota_version_url.key)
407
- const overrideActive = superMode && typeof overrideUrl === "string" && overrideUrl.trim() !== ""
408
- if (!embeddedManifestUrl && !overrideActive) {
409
- console.log("OTA: check skipped - mobile app has no embedded OTA manifest pin")
417
+ if (!hasConfiguredModernOtaManifestPin()) {
418
+ console.log("OTA: check skipped - this app or Engine package has no embedded OTA manifest pin")
410
419
  return emptyCheckResult("dev_build")
411
420
  }
412
- if (!embeddedManifestUrl) {
413
- console.log("OTA: no embedded manifest pin, but super-mode manifest override is active - checking anyway")
414
- }
415
421
 
416
422
  if (refreshVersionInfo) {
417
- void BluetoothSdk.requestVersionInfo().catch((error) => {
418
- console.warn("OTA: Failed to request version_info from glasses:", error)
419
- })
423
+ void BluetoothSdk.requestVersionInfo()
424
+ .then((versionInfo) => {
425
+ useGlassesStore.getState().setGlassesInfo(versionInfo)
426
+ })
427
+ .catch((error) => {
428
+ console.warn("OTA: Failed to request version_info from glasses:", error)
429
+ })
420
430
  }
421
431
 
422
432
  let buildNumber = useGlassesStore.getState().buildNumber
@@ -470,6 +480,10 @@ export async function checkCurrentGlassesForUpdate(
470
480
  }
471
481
 
472
482
  const manifestUrl = resolveOtaManifestUrl(useGlassesStore.getState().otaVersionUrl, buildNumber)
483
+ if (!manifestUrl) {
484
+ console.log("OTA: check skipped - no OTA manifest pin resolved")
485
+ return emptyCheckResult("dev_build")
486
+ }
473
487
  const result = await checkForOtaUpdate(
474
488
  manifestUrl,
475
489
  buildNumber,
@@ -1,5 +1,5 @@
1
1
  import * as RNFS from "@dr.pogodin/react-native-fs"
2
- import {LocalNetwork, type LocalNetworkDownloadProgress} from "@glassly/bluetooth-sdk/internal"
2
+ import {otaLocalNetwork, type OtaLocalNetworkDownloadProgress} from "@glassly/bluetooth-sdk/ota-transport"
3
3
  import {Buffer} from "buffer"
4
4
  import {Platform} from "react-native"
5
5
  import WifiManager from "react-native-wifi-reborn"
@@ -17,7 +17,7 @@ export function shouldUseScopedLocalNetwork(os: string, moduleAvailable: boolean
17
17
  }
18
18
 
19
19
  function supportsScopedConnection(): boolean {
20
- return shouldUseScopedLocalNetwork(Platform.OS, LocalNetwork != null)
20
+ return shouldUseScopedLocalNetwork(Platform.OS, otaLocalNetwork.isAvailable())
21
21
  }
22
22
 
23
23
  function normalizeHeaders(headers?: HeadersInit): Record<string, string> {
@@ -48,25 +48,25 @@ export const localNetworkTransport = {
48
48
  await WifiManager.connectToProtectedSSID(ssid, password, false, false)
49
49
  return undefined
50
50
  }
51
- const result = await LocalNetwork!.connect(ssid, password)
51
+ const result = await otaLocalNetwork.connect(ssid, password)
52
52
  scopedConnectionActive = true
53
53
  return result.localAddress
54
54
  },
55
55
 
56
56
  async disconnect(): Promise<void> {
57
57
  if (supportsScopedConnection()) {
58
- await LocalNetwork!.disconnect().catch(() => {})
58
+ await otaLocalNetwork.disconnect().catch(() => {})
59
59
  }
60
60
  scopedConnectionActive = false
61
61
  nativeJobs.clear()
62
62
  },
63
63
 
64
64
  async fetch(url: string | URL | Request, init?: RequestInit, timeoutMs = 30_000): Promise<Response> {
65
- if (!scopedConnectionActive || !LocalNetwork || typeof url !== "string") {
65
+ if (!scopedConnectionActive || !otaLocalNetwork.isAvailable() || typeof url !== "string") {
66
66
  return globalThis.fetch(url, init)
67
67
  }
68
68
 
69
- const nativeModule = LocalNetwork
69
+ const nativeModule = otaLocalNetwork
70
70
  const id = requestId("request")
71
71
  const signal = init?.signal
72
72
  const cancel = () => void nativeModule.cancel(id)
@@ -92,13 +92,13 @@ export const localNetworkTransport = {
92
92
  },
93
93
 
94
94
  downloadFile(options: DownloadOptions): DownloadHandle {
95
- if (!scopedConnectionActive || !LocalNetwork) return RNFS.downloadFile(options)
95
+ if (!scopedConnectionActive || !otaLocalNetwork.isAvailable()) return RNFS.downloadFile(options)
96
96
 
97
97
  const jobId = nextJobId++
98
98
  const id = requestId("download")
99
99
  let began = false
100
100
  nativeJobs.set(jobId, id)
101
- const subscription = LocalNetwork.addListener("downloadProgress", (event: LocalNetworkDownloadProgress) => {
101
+ const subscription = otaLocalNetwork.onDownloadProgress((event: OtaLocalNetworkDownloadProgress) => {
102
102
  if (event.requestId !== id) return
103
103
  if (!began) {
104
104
  began = true
@@ -116,14 +116,15 @@ export const localNetworkTransport = {
116
116
  })
117
117
  })
118
118
 
119
- const promise = LocalNetwork.download(
120
- id,
121
- options.fromUrl,
122
- options.toFile,
123
- options.headers || {},
124
- options.connectionTimeout || 30_000,
125
- options.readTimeout || 30_000,
126
- )
119
+ const promise = otaLocalNetwork
120
+ .download(
121
+ id,
122
+ options.fromUrl,
123
+ options.toFile,
124
+ options.headers || {},
125
+ options.connectionTimeout || 30_000,
126
+ options.readTimeout || 30_000,
127
+ )
127
128
  .then((result) => {
128
129
  if (!began) {
129
130
  options.begin?.({
@@ -154,16 +155,16 @@ export const localNetworkTransport = {
154
155
 
155
156
  stopDownload(jobId: number): void {
156
157
  const nativeId = nativeJobs.get(jobId)
157
- if (nativeId && LocalNetwork) {
158
+ if (nativeId && otaLocalNetwork.isAvailable()) {
158
159
  cancelledNativeJobs.add(jobId)
159
- void LocalNetwork.cancel(nativeId)
160
+ void otaLocalNetwork.cancel(nativeId)
160
161
  return
161
162
  }
162
163
  RNFS.stopDownload(jobId)
163
164
  },
164
165
  }
165
166
 
166
- LocalNetwork?.addListener("networkLost", () => {
167
+ otaLocalNetwork.onNetworkLost(() => {
167
168
  scopedConnectionActive = false
168
169
  nativeJobs.clear()
169
170
  })
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Gesture-claim arbitration (CLAIM_GESTURES), split out of
3
+ * LocalMiniappRuntime so it is unit-testable — the runtime module itself
4
+ * pulls a native import surface bun can't parse.
5
+ */
6
+
7
+ /**
8
+ * True iff the miniapp in front of the user has claimed `gesture`.
9
+ *
10
+ * "In front" spans both screens. Most claiming apps are glasses-side: the
11
+ * terminal runs as a background miniapp with the phone pocketed, so gating on
12
+ * the phone's Compositor foreground alone left its double_tap claim inert and
13
+ * the system dashboard kept opening mid-session. Glasses ownership is the
14
+ * primary signal; the phone foreground is still honored for apps driven from
15
+ * the handset. A claimer that owns neither screen is background and never
16
+ * hijacks a system gesture.
17
+ */
18
+ export function claimsGesture(
19
+ claims: ReadonlyMap<string, ReadonlySet<string>>,
20
+ gesture: string,
21
+ glassesOwner: string | null,
22
+ phoneForeground: string | null,
23
+ ): boolean {
24
+ for (const pkg of [glassesOwner, phoneForeground]) {
25
+ if (pkg && claims.get(pkg)?.has(gesture) === true) return true
26
+ }
27
+ return false
28
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * GENERATED by scripts/gen-gesture-dot-glyphs.mjs — do not edit by hand.
3
+ * 24×24 8-bit grayscale PNGs (white on black) for the gesture-dots indicator.
4
+ */
5
+ export const GESTURE_DOT_GLYPH_SIZE = 24
6
+
7
+ export type GestureDotGlyph = "dot" | "up" | "down" | "blank"
8
+
9
+ export const GESTURE_DOT_GLYPHS: Record<GestureDotGlyph, string> = {
10
+ dot: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAALklEQVR4nM2QsQ0AAAjC/P/pmrhqZ+xIF6DqBQyWbwO3gduAmKjQuj7QL/ETEzQmMptl7Q9NBAAAAABJRU5ErkJggg==",
11
+ up: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAMElEQVR4nGNgoCv4////f1zi2GT+/4ciTHE4RhdHIlDFUUhkcTQKQx9WBw8PCVoBAIcbOcf3xeeBAAAAAElFTkSuQmCC",
12
+ down: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAALUlEQVR4nGNgoC/4/3+4SkBFUCkkNjKJIoMg0GRgGEMGgrC4CQSwuxanR2gEAO1/Ocfgp8uvAAAAAElFTkSuQmCC",
13
+ blank: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAD0lEQVR4nGNgGAWjgPoAAAJYAAGgxYwDAAAAAElFTkSuQmCC",
14
+ }
@@ -2,7 +2,7 @@
2
2
  * Push phone time to glasses only when clock skew is detected (gallery sync, OTA, etc.).
3
3
  */
4
4
 
5
- import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
5
+ import BluetoothSdk from "@glassly/bluetooth-sdk"
6
6
  import {BgTimer} from "../utils/timers"
7
7
  import {useGlassesStore} from "../stores/glasses"
8
8
 
@@ -87,6 +87,10 @@ export async function handleOtaClockSkewFromGlasses(
87
87
  lastOtaClockFixAt = Date.now()
88
88
  const {buildNumber, otaVersionUrl} = useGlassesStore.getState()
89
89
  const manifestUrl = resolveOtaManifestUrl(otaVersionUrl, buildNumber)
90
+ if (!manifestUrl) {
91
+ console.warn("[GlassesClockSync] OTA restart skipped because this build has no manifest pin")
92
+ return false
93
+ }
90
94
  console.log("[GlassesClockSync] ⏰ Restarting OTA with ota_start after clock fix")
91
95
  await BluetoothSdk.startOtaUpdate(manifestUrl)
92
96
  return true
@@ -1,4 +1,4 @@
1
- import type {OtaProgress, OtaStatus} from "@glassly/bluetooth-sdk/internal"
1
+ import type {OtaProgress, OtaStatus} from "@glassly/bluetooth-sdk"
2
2
 
3
3
  import {isLegacyShapedOtaStatus} from "./otaInstallPolicy"
4
4
 
@@ -1,5 +1,5 @@
1
1
  /** Shared OTA progress watchdog timings (see progress-legacy.tsx / progress.tsx). */
2
- import type {OtaProgress, OtaStatus} from "@glassly/bluetooth-sdk/internal"
2
+ import type {OtaProgress, OtaStatus} from "@glassly/bluetooth-sdk"
3
3
 
4
4
  /**
5
5
  * First glasses `build_number` (ASG `versionCode`) that uses `progress.tsx` with unified
@@ -1,4 +1,4 @@
1
- import type {OtaProgress, OtaProgressStatus, OtaStatus} from "@glassly/bluetooth-sdk/internal"
1
+ import type {OtaProgress, OtaProgressStatus, OtaStatus} from "@glassly/bluetooth-sdk"
2
2
 
3
3
  /** Normalized ota_status fields (snake_case) after bridging Core/expo (snake or camel). */
4
4
  export type NormalizedOtaStatusEvent = {