@glassly/engine 0.3.7 → 0.3.8-beta.1013

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 (229) hide show
  1. package/README.md +4 -0
  2. package/build/engine.d.ts +18 -2
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -4
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/ai.d.ts +3 -0
  7. package/build/facades/ai.d.ts.map +1 -1
  8. package/build/facades/ai.js +3 -0
  9. package/build/facades/ai.js.map +1 -1
  10. package/build/facades/backup.d.ts +14 -0
  11. package/build/facades/backup.d.ts.map +1 -0
  12. package/build/facades/backup.js +22 -0
  13. package/build/facades/backup.js.map +1 -0
  14. package/build/facades/glasses.d.ts +13 -5
  15. package/build/facades/glasses.d.ts.map +1 -1
  16. package/build/facades/glasses.js +17 -4
  17. package/build/facades/glasses.js.map +1 -1
  18. package/build/generated/releaseMetadata.js +4 -4
  19. package/build/generated/releaseMetadata.js.map +1 -1
  20. package/build/index.d.ts +3 -1
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js.map +1 -1
  23. package/build/services/AppRegistry.d.ts +2 -1
  24. package/build/services/AppRegistry.d.ts.map +1 -1
  25. package/build/services/AppRegistry.js +38 -8
  26. package/build/services/AppRegistry.js.map +1 -1
  27. package/build/services/AppStateBackup.d.ts +98 -0
  28. package/build/services/AppStateBackup.d.ts.map +1 -0
  29. package/build/services/AppStateBackup.js +224 -0
  30. package/build/services/AppStateBackup.js.map +1 -0
  31. package/build/services/BlobStore.d.ts.map +1 -1
  32. package/build/services/BlobStore.js +5 -12
  33. package/build/services/BlobStore.js.map +1 -1
  34. package/build/services/CloudClientService.d.ts +3 -1
  35. package/build/services/CloudClientService.d.ts.map +1 -1
  36. package/build/services/CloudClientService.js +42 -2
  37. package/build/services/CloudClientService.js.map +1 -1
  38. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  39. package/build/services/DeviceEventRouter.js +1 -0
  40. package/build/services/DeviceEventRouter.js.map +1 -1
  41. package/build/services/DisplayDiagnostics.d.ts.map +1 -1
  42. package/build/services/DisplayDiagnostics.js +4 -2
  43. package/build/services/DisplayDiagnostics.js.map +1 -1
  44. package/build/services/DisplayProcessor.d.ts +2 -0
  45. package/build/services/DisplayProcessor.d.ts.map +1 -1
  46. package/build/services/DisplayProcessor.js +15 -1
  47. package/build/services/DisplayProcessor.js.map +1 -1
  48. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  49. package/build/services/GlassesTouchInput.js +4 -2
  50. package/build/services/GlassesTouchInput.js.map +1 -1
  51. package/build/services/JSCrashloopReportService.d.ts.map +1 -1
  52. package/build/services/JSCrashloopReportService.js +5 -0
  53. package/build/services/JSCrashloopReportService.js.map +1 -1
  54. package/build/services/JSRouter.d.ts +11 -3
  55. package/build/services/JSRouter.d.ts.map +1 -1
  56. package/build/services/JSRouter.js +36 -6
  57. package/build/services/JSRouter.js.map +1 -1
  58. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  59. package/build/services/LocalDisplayManager.js +5 -2
  60. package/build/services/LocalDisplayManager.js.map +1 -1
  61. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  62. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  63. package/build/services/LocalMiniappRuntime.js +61 -22
  64. package/build/services/LocalMiniappRuntime.js.map +1 -1
  65. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  66. package/build/services/LocalSttFallbackCoordinator.js +2 -0
  67. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  68. package/build/services/LocalTranslationService.d.ts +2 -0
  69. package/build/services/LocalTranslationService.d.ts.map +1 -1
  70. package/build/services/LocalTranslationService.js +22 -0
  71. package/build/services/LocalTranslationService.js.map +1 -1
  72. package/build/services/MiniappEngine.d.ts +9 -0
  73. package/build/services/MiniappEngine.d.ts.map +1 -1
  74. package/build/services/MiniappEngine.js +60 -27
  75. package/build/services/MiniappEngine.js.map +1 -1
  76. package/build/services/MiniappLauncher.d.ts +2 -0
  77. package/build/services/MiniappLauncher.d.ts.map +1 -1
  78. package/build/services/MiniappLauncher.js +6 -0
  79. package/build/services/MiniappLauncher.js.map +1 -1
  80. package/build/services/NavigationService.d.ts +1 -0
  81. package/build/services/NavigationService.d.ts.map +1 -1
  82. package/build/services/NavigationService.js +20 -6
  83. package/build/services/NavigationService.js.map +1 -1
  84. package/build/services/OfflineActivity.d.ts +54 -0
  85. package/build/services/OfflineActivity.d.ts.map +1 -0
  86. package/build/services/OfflineActivity.js +86 -0
  87. package/build/services/OfflineActivity.js.map +1 -0
  88. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  89. package/build/services/PhoneCalendarService.js +8 -0
  90. package/build/services/PhoneCalendarService.js.map +1 -1
  91. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  92. package/build/services/PhoneNotificationsSync.js +7 -3
  93. package/build/services/PhoneNotificationsSync.js.map +1 -1
  94. package/build/services/STTModelManager.d.ts +1 -0
  95. package/build/services/STTModelManager.d.ts.map +1 -1
  96. package/build/services/STTModelManager.js +6 -0
  97. package/build/services/STTModelManager.js.map +1 -1
  98. package/build/services/SceneRenderer.js +1 -1
  99. package/build/services/SceneRenderer.js.map +1 -1
  100. package/build/services/appStateBackupFile.d.ts +32 -0
  101. package/build/services/appStateBackupFile.d.ts.map +1 -0
  102. package/build/services/appStateBackupFile.js +135 -0
  103. package/build/services/appStateBackupFile.js.map +1 -0
  104. package/build/services/controllers.d.ts.map +1 -1
  105. package/build/services/controllers.js +13 -3
  106. package/build/services/controllers.js.map +1 -1
  107. package/build/services/effectiveCapabilities.js +6 -1
  108. package/build/services/effectiveCapabilities.js.map +1 -1
  109. package/build/services/neuralBand/NeuralBandInput.d.ts +12 -1
  110. package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -1
  111. package/build/services/neuralBand/NeuralBandInput.js +27 -0
  112. package/build/services/neuralBand/NeuralBandInput.js.map +1 -1
  113. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -1
  114. package/build/services/neuralBand/NeuralBandSettingsSync.js +6 -0
  115. package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -1
  116. package/build/services/pages/pageEligibility.d.ts.map +1 -1
  117. package/build/services/pages/pageEligibility.js +8 -1
  118. package/build/services/pages/pageEligibility.js.map +1 -1
  119. package/build/services/settingsBridge.d.ts.map +1 -1
  120. package/build/services/settingsBridge.js +1 -0
  121. package/build/services/settingsBridge.js.map +1 -1
  122. package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
  123. package/build/services/watch/WatchLink.d.ts.map +1 -0
  124. package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
  125. package/build/services/watch/WatchLink.js.map +1 -0
  126. package/build/stores/appVisibility.d.ts.map +1 -1
  127. package/build/stores/appVisibility.js +0 -1
  128. package/build/stores/appVisibility.js.map +1 -1
  129. package/build/stores/apps.d.ts.map +1 -1
  130. package/build/stores/apps.js +22 -10
  131. package/build/stores/apps.js.map +1 -1
  132. package/build/stores/settings.d.ts.map +1 -1
  133. package/build/stores/settings.js +71 -6
  134. package/build/stores/settings.js.map +1 -1
  135. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  136. package/build/types/capabilities/even-realities-g1.js +3 -0
  137. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  138. package/build/types/capabilities/memomind-one.d.ts +3 -0
  139. package/build/types/capabilities/memomind-one.d.ts.map +1 -0
  140. package/build/types/capabilities/memomind-one.js +42 -0
  141. package/build/types/capabilities/memomind-one.js.map +1 -0
  142. package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
  143. package/build/types/capabilities/rayneo-io.js +3 -2
  144. package/build/types/capabilities/rayneo-io.js.map +1 -1
  145. package/build/types/enums.d.ts +8 -2
  146. package/build/types/enums.d.ts.map +1 -1
  147. package/build/types/enums.js +6 -0
  148. package/build/types/enums.js.map +1 -1
  149. package/build/types/hardware.d.ts +3 -1
  150. package/build/types/hardware.d.ts.map +1 -1
  151. package/build/types/hardware.js +3 -1
  152. package/build/types/hardware.js.map +1 -1
  153. package/build/utils/display/index.d.ts +2 -1
  154. package/build/utils/display/index.d.ts.map +1 -1
  155. package/build/utils/display/index.js +3 -2
  156. package/build/utils/display/index.js.map +1 -1
  157. package/build/utils/display/profiles/index.d.ts +2 -1
  158. package/build/utils/display/profiles/index.d.ts.map +1 -1
  159. package/build/utils/display/profiles/index.js +3 -2
  160. package/build/utils/display/profiles/index.js.map +1 -1
  161. package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
  162. package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
  163. package/build/utils/display/profiles/rayneo-io.js +1 -1
  164. package/build/utils/display/profiles/rayneo-io.js.map +1 -1
  165. package/build/utils/display/scene/index.d.ts +1 -1
  166. package/build/utils/display/scene/index.d.ts.map +1 -1
  167. package/build/utils/display/scene/index.js +1 -1
  168. package/build/utils/display/scene/index.js.map +1 -1
  169. package/build/utils/display/scene/process.d.ts +6 -0
  170. package/build/utils/display/scene/process.d.ts.map +1 -1
  171. package/build/utils/display/scene/process.js +22 -7
  172. package/build/utils/display/scene/process.js.map +1 -1
  173. package/build/utils/miniappGlobals.d.ts +13 -17
  174. package/build/utils/miniappGlobals.d.ts.map +1 -1
  175. package/build/utils/miniappGlobals.js +24 -56
  176. package/build/utils/miniappGlobals.js.map +1 -1
  177. package/package.json +7 -7
  178. package/src/engine.ts +7 -4
  179. package/src/facades/ai.ts +4 -0
  180. package/src/facades/backup.ts +28 -0
  181. package/src/facades/glasses.ts +22 -5
  182. package/src/generated/releaseMetadata.ts +4 -4
  183. package/src/index.ts +10 -0
  184. package/src/services/AppRegistry.ts +36 -9
  185. package/src/services/AppStateBackup.ts +283 -0
  186. package/src/services/BlobStore.ts +5 -11
  187. package/src/services/CloudClientService.ts +43 -4
  188. package/src/services/DeviceEventRouter.ts +1 -0
  189. package/src/services/DisplayDiagnostics.ts +4 -2
  190. package/src/services/DisplayProcessor.ts +20 -8
  191. package/src/services/GlassesTouchInput.ts +6 -2
  192. package/src/services/JSCrashloopReportService.ts +5 -0
  193. package/src/services/JSRouter.ts +37 -6
  194. package/src/services/LocalDisplayManager.ts +5 -2
  195. package/src/services/LocalMiniappRuntime.ts +61 -22
  196. package/src/services/LocalSttFallbackCoordinator.ts +4 -0
  197. package/src/services/LocalTranslationService.ts +20 -0
  198. package/src/services/MiniappEngine.ts +62 -27
  199. package/src/services/MiniappLauncher.ts +7 -0
  200. package/src/services/NavigationService.ts +28 -8
  201. package/src/services/OfflineActivity.ts +131 -0
  202. package/src/services/PhoneCalendarService.ts +7 -0
  203. package/src/services/PhoneNotificationsSync.ts +8 -3
  204. package/src/services/STTModelManager.ts +6 -0
  205. package/src/services/SceneRenderer.ts +1 -1
  206. package/src/services/appStateBackupFile.ts +171 -0
  207. package/src/services/controllers.ts +14 -3
  208. package/src/services/effectiveCapabilities.ts +6 -1
  209. package/src/services/neuralBand/NeuralBandInput.ts +37 -1
  210. package/src/services/neuralBand/NeuralBandSettingsSync.ts +12 -0
  211. package/src/services/pages/pageEligibility.ts +9 -1
  212. package/src/services/settingsBridge.ts +1 -0
  213. package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
  214. package/src/stores/appVisibility.ts +0 -1
  215. package/src/stores/apps.ts +22 -11
  216. package/src/stores/settings.ts +78 -10
  217. package/src/types/capabilities/even-realities-g1.ts +4 -0
  218. package/src/types/capabilities/memomind-one.ts +43 -0
  219. package/src/types/capabilities/rayneo-io.ts +3 -2
  220. package/src/types/enums.ts +6 -0
  221. package/src/types/hardware.ts +14 -1
  222. package/src/utils/display/index.ts +4 -2
  223. package/src/utils/display/profiles/index.ts +9 -2
  224. package/src/utils/display/profiles/rayneo-io.ts +6 -1
  225. package/src/utils/display/scene/index.ts +1 -1
  226. package/src/utils/display/scene/process.ts +22 -7
  227. package/src/utils/miniappGlobals.ts +32 -64
  228. package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
  229. package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
@@ -1,15 +1,15 @@
1
1
  /**
2
- * The Apple Watch remote's phone half: the watch app's status context (glasses, the miniapp in
2
+ * The watch remote's phone half, for the Apple Watch (iOS, WatchConnectivity) and a Wear OS
3
+ * watch (Android, Wearable Data Layer) alike: the watch app's status (glasses, the miniapp in
3
4
  * front, launchable miniapps, whether Assist takes questions, the mirror setting), mirror frames
4
5
  * while the setting is on, and the commands the watch sends back. Native adds whether the active
5
6
  * watch is paired and keeps the newest frame for when the watch app opens. Gestures ride
6
7
  * `touch_event` like any controller's.
7
8
  */
8
- import type {AppleWatchCommandEvent} from "@glassly/bluetooth-sdk"
9
+ import type {WatchCommandEvent} from "@glassly/bluetooth-sdk"
9
10
  import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
10
11
  import type {MirrorSnapshot} from "@glassly/miniapp"
11
12
  import {MiniappStreamType} from "@glassly/miniapp"
12
- import {Platform} from "react-native"
13
13
 
14
14
  import {useAppStatusStore} from "../../stores/apps"
15
15
  import {useGlassesStore} from "../../stores/glasses"
@@ -23,7 +23,7 @@ import {keyEventsForTextChange} from "../neuralBand/NeuralBandInput"
23
23
 
24
24
  export const ASSIST_PACKAGE = "com.glassly.assistant"
25
25
  export const ASSIST_ACTION = "ask"
26
- /** WatchConnectivity messages top out near 64 KB; frames past this are skipped, not truncated. */
26
+ /** WatchConnectivity messages top out near 64 KB (Data Layer messages at 100 KB); frames past this are skipped, not truncated. */
27
27
  const MAX_FRAME_BYTES = 48_000
28
28
  const MAX_IMAGE_CHARS = 16_000
29
29
  const MAX_APPS = 24
@@ -142,7 +142,7 @@ export function mirrorFrame(snapshot: MirrorSnapshot): Record<string, unknown> |
142
142
  return JSON.stringify(frame).length > MAX_FRAME_BYTES ? null : frame
143
143
  }
144
144
 
145
- export interface AppleWatchCommandDeps {
145
+ export interface WatchCommandDeps {
146
146
  apps: () => readonly ClientApp[]
147
147
  start: (app: ClientApp) => Promise<unknown>
148
148
  stop: (packageName: string) => Promise<unknown>
@@ -151,7 +151,7 @@ export interface AppleWatchCommandDeps {
151
151
  now: () => number
152
152
  }
153
153
 
154
- const defaultDeps: AppleWatchCommandDeps = {
154
+ const defaultDeps: WatchCommandDeps = {
155
155
  apps: () => useAppStatusStore.getState().apps,
156
156
  start: (app) => useAppStatusStore.getState().start(app),
157
157
  stop: (packageName) => useAppStatusStore.getState().stop(packageName),
@@ -160,9 +160,9 @@ const defaultDeps: AppleWatchCommandDeps = {
160
160
  now: () => Date.now(),
161
161
  }
162
162
 
163
- export async function handleAppleWatchCommand(
164
- event: AppleWatchCommandEvent,
165
- deps: AppleWatchCommandDeps = defaultDeps,
163
+ export async function handleWatchCommand(
164
+ event: WatchCommandEvent,
165
+ deps: WatchCommandDeps = defaultDeps,
166
166
  ): Promise<void> {
167
167
  switch (event.command) {
168
168
  case "launch": {
@@ -170,7 +170,7 @@ export async function handleAppleWatchCommand(
170
170
  ? deps.apps().find((entry) => entry.packageName === event.packageName)
171
171
  : undefined
172
172
  if (!app) {
173
- console.warn(`AppleWatch: launch of unknown miniapp ${event.packageName}`)
173
+ console.warn(`Watch: launch of unknown miniapp ${event.packageName}`)
174
174
  return
175
175
  }
176
176
  await deps.start(app)
@@ -206,7 +206,7 @@ function currentState(): Record<string, unknown> {
206
206
  },
207
207
  apps: useAppStatusStore.getState().apps,
208
208
  assist: localMiniappRuntime.hostActionAvailable(ASSIST_PACKAGE, ASSIST_ACTION),
209
- mirror: useSettingsStore.getState().getSetting(SETTINGS.apple_watch_mirror.key) === true,
209
+ mirror: useSettingsStore.getState().getSetting(SETTINGS.watch_mirror.key) === true,
210
210
  })
211
211
  }
212
212
 
@@ -226,7 +226,7 @@ function syncMirror(enabled: boolean): void {
226
226
  const send = (snapshot: MirrorSnapshot) => {
227
227
  lastSentAt = Date.now()
228
228
  void BluetoothSdk.setWatchMirror(mirrorFrame(snapshot)).catch((err) =>
229
- console.warn("AppleWatch: mirror push failed:", err),
229
+ console.warn("Watch: mirror push failed:", err),
230
230
  )
231
231
  }
232
232
  const unsubscribe = mirrorService.subscribe((snapshot) => {
@@ -252,8 +252,8 @@ function syncMirror(enabled: boolean): void {
252
252
  }
253
253
  }
254
254
 
255
- export function startAppleWatchLink(): void {
256
- if (Platform.OS !== "ios" || stops.length > 0) return
255
+ export function startWatchLink(): void {
256
+ if (stops.length > 0) return
257
257
  let lastKey = ""
258
258
  let timer: ReturnType<typeof BgTimer.setTimeout> | null = null
259
259
  const push = () => {
@@ -262,21 +262,21 @@ export function startAppleWatchLink(): void {
262
262
  const key = JSON.stringify(state)
263
263
  if (key === lastKey) return
264
264
  lastKey = key
265
- void BluetoothSdk.setWatchState(state).catch((err) => console.warn("AppleWatch: state push failed:", err))
265
+ void BluetoothSdk.setWatchState(state).catch((err) => console.warn("Watch: state push failed:", err))
266
266
  }
267
267
  const schedule = () => {
268
268
  timer ??= BgTimer.setTimeout(push, STATE_DEBOUNCE_MS)
269
269
  }
270
- const mirrorEnabled = () => useSettingsStore.getState().getSetting(SETTINGS.apple_watch_mirror.key) === true
271
- const commands = BluetoothSdk.addListener("apple_watch_command", (event) => {
272
- handleAppleWatchCommand(event).catch((err) => console.warn(`AppleWatch: ${event.command} failed:`, err))
270
+ const mirrorEnabled = () => useSettingsStore.getState().getSetting(SETTINGS.watch_mirror.key) === true
271
+ const commands = BluetoothSdk.addListener("watch_command", (event) => {
272
+ handleWatchCommand(event).catch((err) => console.warn(`Watch: ${event.command} failed:`, err))
273
273
  })
274
274
  stops = [
275
275
  () => commands.remove(),
276
276
  useGlassesStore.subscribe(schedule),
277
277
  useAppStatusStore.subscribe(schedule),
278
278
  useSettingsStore.subscribe(
279
- (s) => s.getSetting(SETTINGS.apple_watch_mirror.key),
279
+ (s) => s.getSetting(SETTINGS.watch_mirror.key),
280
280
  () => {
281
281
  syncMirror(mirrorEnabled())
282
282
  schedule()
@@ -291,7 +291,7 @@ export function startAppleWatchLink(): void {
291
291
  schedule()
292
292
  }
293
293
 
294
- export function stopAppleWatchLink(): void {
294
+ export function stopWatchLink(): void {
295
295
  stops.splice(0).forEach((stop) => stop())
296
296
  syncMirror(false)
297
297
  }
@@ -28,7 +28,6 @@ export const DEV_TOOLS_PACKAGES: ReadonlySet<string> = new Set([
28
28
  "com.glassly.example",
29
29
  "com.glassly.tetris",
30
30
  "com.glassly.compass",
31
- "com.glassly.showcase",
32
31
  ])
33
32
 
34
33
  /** True when the app is dev-only and developer tools are off. */
@@ -217,6 +217,7 @@ function projectApps(previousState: AppStatusState, localApps: ClientApp[]): Cli
217
217
  DeviceTypes.NONE
218
218
  const capabilities = getModelCapabilities(defaultWearable)
219
219
  const devToolsEnabled = useSettingsStore.getState().getSetting(SETTINGS.dev_tools.key) === true
220
+ const persistScreenshots = useSettingsStore.getState().getSetting(SETTINGS.persist_miniapp_screenshots.key) !== false
220
221
  const hasPairedGlasses = useSettingsStore.getState().getSetting(SETTINGS.has_paired_glasses.key) === true
221
222
 
222
223
  // Single pass: dedupe → screenshot carry-over → compat → hidden, all into
@@ -246,7 +247,10 @@ function projectApps(previousState: AppStatusState, localApps: ClientApp[]): Cli
246
247
  .map((app) => {
247
248
  const next: ClientApp = {
248
249
  ...app,
249
- screenshot: previousByPackage.get(app.packageName)?.screenshot ?? app.screenshot,
250
+ // The registry's screenshot is the one restored from disk; only an
251
+ // in-memory capture carries over when persistence is off.
252
+ screenshot:
253
+ previousByPackage.get(app.packageName)?.screenshot ?? (persistScreenshots ? app.screenshot : undefined),
250
254
  compatibility: HardwareCompatibility.checkCompatibility(app.hardwareRequirements, capabilities),
251
255
  // A registry source can force an app off the primary home screen.
252
256
  // Compose that gate with the user's persisted home-screen layout instead
@@ -384,15 +388,6 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
384
388
  // owns the settings store).
385
389
  useSettingsStore.getState().setSetting(SETTINGS.has_ever_activated_app.key, true)
386
390
 
387
- // If the tapped app is already running, this is a re-open: the nav/foreground
388
- // above already drove the animation, and there's nothing to spawn. Short-
389
- // circuit before the spawn pipeline. A promoted app keeps its context but
390
- // still takes the foreground below.
391
- if (app.running && !app.widgetOnly) {
392
- saveLastOpenTime(packageName)
393
- return true
394
- }
395
-
396
391
  // Only-one rules: one standard app in the foreground, one dashboard
397
392
  // drawing the overlay. Starting another stops the one running. Widget-only
398
393
  // peers hold no foreground, so they stay.
@@ -405,6 +400,17 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
405
400
  }
406
401
  }
407
402
 
403
+ // If the tapped app is already running, this is a re-open: the nav/foreground
404
+ // above already drove the animation, and there's nothing to spawn. Short-
405
+ // circuit before the spawn pipeline. A promoted app keeps its context but
406
+ // still takes the foreground below.
407
+ if (app.running && !app.widgetOnly) {
408
+ saveLastOpenTime(packageName)
409
+ // The running background only knows the source of its spawn.
410
+ if (app.local) miniappLauncher.notifyLaunch(packageName, opts?.launchSource ?? "phone")
411
+ return true
412
+ }
413
+
408
414
  set((s) => ({
409
415
  apps: s.apps.map((a) => (a.packageName === packageName ? {...a, running: true, loading: false} : a)),
410
416
  }))
@@ -543,7 +549,12 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
543
549
  },
544
550
 
545
551
  saveScreenshot: async (packageName: string, screenshot: string) => {
546
- storage.save(`${packageName}_screenshot`, screenshot)
552
+ const key = `${packageName}_screenshot`
553
+ if (useSettingsStore.getState().getSetting(SETTINGS.persist_miniapp_screenshots.key) !== false) {
554
+ storage.save(key, screenshot)
555
+ } else {
556
+ storage.remove(key)
557
+ }
547
558
  set((s) => ({
548
559
  apps: s.apps.map((a) => (a.packageName === packageName ? {...a, screenshot} : a)),
549
560
  }))
@@ -132,6 +132,13 @@ export const SETTINGS: Record<string, Setting> = {
132
132
  saveOnServer: true,
133
133
  persist: true,
134
134
  },
135
+ memomind_accessory_picker: {
136
+ key: "memomind_accessory_picker",
137
+ defaultValue: () => false,
138
+ writable: true,
139
+ saveOnServer: false,
140
+ persist: true,
141
+ },
135
142
  debug_console: {
136
143
  key: "debug_console",
137
144
  defaultValue: () => false,
@@ -803,8 +810,8 @@ export const SETTINGS: Record<string, Setting> = {
803
810
  // OFF by default: they crowd out everything else in the ten-minute buffer.
804
811
  // Synced to the Bluetooth SDK, which keeps counting either way and only
805
812
  // logs while this is on. Device-local debug knob, so not saved on the server.
806
- g2_perf_logs: {
807
- key: "g2_perf_logs",
813
+ perf_logs: {
814
+ key: "perf_logs",
808
815
  defaultValue: () => false,
809
816
  writable: true,
810
817
  saveOnServer: false,
@@ -841,12 +848,29 @@ export const SETTINGS: Record<string, Setting> = {
841
848
  saveOnServer: false,
842
849
  persist: true,
843
850
  },
844
- // Apple Watch: draw the glasses' current frame behind the watch trackpad. Frames
845
- // cross WatchConnectivity while the watch app is up, so it stays off unless asked for.
851
+ // The band's own buzz on each gesture (its haptics pattern 1; pattern 2 is silent).
852
+ neural_band_vibration: {
853
+ key: "neural_band_vibration",
854
+ defaultValue: () => true,
855
+ writable: true,
856
+ saveOnServer: false,
857
+ persist: true,
858
+ },
859
+ // Watch (Apple Watch, Wear OS): draw the glasses' current frame behind the watch
860
+ // trackpad. Frames cross the watch link while the watch app is up, so it stays off
861
+ // unless asked for.
862
+ watch_mirror: {
863
+ key: "watch_mirror",
864
+ defaultValue: () => false,
865
+ writable: true,
866
+ saveOnServer: false,
867
+ persist: true,
868
+ },
869
+ // Deprecated: the setting above under its Apple-only name; read once to migrate.
846
870
  apple_watch_mirror: {
847
871
  key: "apple_watch_mirror",
848
872
  defaultValue: () => false,
849
- writable: true,
873
+ writable: false,
850
874
  saveOnServer: false,
851
875
  persist: true,
852
876
  },
@@ -904,6 +928,23 @@ export const SETTINGS: Record<string, Setting> = {
904
928
  saveOnServer: false,
905
929
  persist: true,
906
930
  },
931
+ // Debug: keep each miniapp's switcher-card screenshot on disk so the card
932
+ // comes back after an app restart. Off keeps captures in memory only, for
933
+ // checking the capture path itself. Device-local, default on.
934
+ persist_miniapp_screenshots: {
935
+ key: "persist_miniapp_screenshots",
936
+ defaultValue: () => true,
937
+ writable: true,
938
+ saveOnServer: false,
939
+ persist: true,
940
+ },
941
+ home_offline_card: {
942
+ key: "home_offline_card",
943
+ defaultValue: () => true,
944
+ writable: true,
945
+ saveOnServer: false,
946
+ persist: true,
947
+ },
907
948
  // Gesture-dots debug indicator: flash a glyph on the glasses when a touch
908
949
  // gesture is received while the display is empty. Works on any
909
950
  // scene-capable device, not only CFW. Device-local debug knob, default off.
@@ -1128,6 +1169,13 @@ export const SETTINGS: Record<string, Setting> = {
1128
1169
  saveOnServer: true,
1129
1170
  persist: true,
1130
1171
  },
1172
+ notifications_batch_seconds: {
1173
+ key: "notifications_batch_seconds",
1174
+ defaultValue: () => 0,
1175
+ writable: true,
1176
+ saveOnServer: true,
1177
+ persist: true,
1178
+ },
1131
1179
  // Hold popups while another glasses feature (navigation, a miniapp) is on screen.
1132
1180
  notifications_hold_during_apps: {
1133
1181
  key: "notifications_hold_during_apps",
@@ -1205,7 +1253,8 @@ export const BLUETOOTH_SETTING_KEYS: string[] = [
1205
1253
  SETTINGS.calendar_events.key,
1206
1254
  SETTINGS.cfw_scene_renderer.key,
1207
1255
  SETTINGS.cfw_fast_ble.key,
1208
- SETTINGS.g2_perf_logs.key,
1256
+ SETTINGS.perf_logs.key,
1257
+ SETTINGS.memomind_accessory_picker.key,
1209
1258
  SETTINGS.ring_via_glasses.key,
1210
1259
  SETTINGS.display_max_fps.key,
1211
1260
  SETTINGS.twelve_hour_time.key,
@@ -1276,10 +1325,13 @@ export interface SettingsState {
1276
1325
  }
1277
1326
 
1278
1327
  const getDefaultSettings = () =>
1279
- Object.keys(SETTINGS).reduce((acc, key) => {
1280
- acc[key] = SETTINGS[key].defaultValue()
1281
- return acc
1282
- }, {} as Record<string, any>)
1328
+ Object.keys(SETTINGS).reduce(
1329
+ (acc, key) => {
1330
+ acc[key] = SETTINGS[key].defaultValue()
1331
+ return acc
1332
+ },
1333
+ {} as Record<string, any>,
1334
+ )
1283
1335
 
1284
1336
  // Single-flight for loadAllSettings: the host fires it at module load and
1285
1337
  // engine.start()'s device-store hydration awaits it — without the memo the
@@ -1619,6 +1671,22 @@ export const useSettingsStore = create<SettingsState>()(
1619
1671
  }
1620
1672
  if (menuMigrated) storage.save(MENU_SETTINGS_MIGRATION_KEY, true)
1621
1673
  }
1674
+
1675
+ // The watch mirror switch lost its Apple-only name when Wear OS joined: carry a
1676
+ // turned-on `apple_watch_mirror` over to `watch_mirror` once.
1677
+ const WATCH_MIRROR_MIGRATION_KEY = "migration:watch_mirror_v1"
1678
+ const watchMirrorMigrationDone = storage.load<boolean>(WATCH_MIRROR_MIGRATION_KEY)
1679
+ if (watchMirrorMigrationDone.is_error() || !watchMirrorMigrationDone.value) {
1680
+ let watchMirrorMigrated = true
1681
+ if (get().getSetting(SETTINGS.apple_watch_mirror.key) === true) {
1682
+ const result = await get().setSetting(SETTINGS.watch_mirror.key, true, false)
1683
+ if (result.is_error()) {
1684
+ console.log("SETTINGS: watch_mirror migration failed:", result.error)
1685
+ watchMirrorMigrated = false
1686
+ }
1687
+ }
1688
+ if (watchMirrorMigrated) storage.save(WATCH_MIRROR_MIGRATION_KEY, true)
1689
+ }
1622
1690
  })
1623
1691
  loadAllSettingsInFlight = inFlight
1624
1692
  void Promise.resolve(inFlight).then((result) => {
@@ -68,4 +68,8 @@ export const evenRealitiesG1: Capabilities = {
68
68
 
69
69
  // OTA capabilities - G1 firmware updates are not driven by the ASG OTA flow
70
70
  hasOta: false,
71
+
72
+ // The app flashes the bundled Glassly CFW over MCUmgr SMP (see
73
+ // bluetooth-sdk/ios/Source/Resources/G1Firmware/README.md).
74
+ hasCustomFirmware: true,
71
75
  }
@@ -0,0 +1,43 @@
1
+ import type {Capabilities} from "../hardware"
2
+
3
+ export const memoMindOne: Capabilities = {
4
+ modelName: "MemoMind One",
5
+ hasCamera: false,
6
+ camera: null,
7
+ hasDisplay: true,
8
+ display: {
9
+ // One panel: the vector canvas offers no depth on it.
10
+ count: 1,
11
+ isColor: false,
12
+ canDisplayBitmap: true,
13
+ resolution: {width: 600, height: 350},
14
+ width: 600,
15
+ height: 350,
16
+ canPosition: true,
17
+ intensityLevels: 16,
18
+ partialUpdate: true,
19
+ adjustBrightness: true,
20
+ positionRange: {height: {min: 0, max: 8}, depth: {min: 0, max: 8}},
21
+ },
22
+ hasMicrophone: true,
23
+ microphone: {count: 1, hasVAD: false},
24
+ hasSpeaker: false,
25
+ speaker: null,
26
+ hasIMU: false,
27
+ imu: null,
28
+ hasButton: true,
29
+ button: {
30
+ count: 1,
31
+ buttons: [{type: "press", events: ["TAP", "DOUBLE_TAP", "PRESS_HOLD", "RELEASE"], isCapacitive: false}],
32
+ },
33
+ hasLight: false,
34
+ light: null,
35
+ power: {hasExternalBattery: false},
36
+ hasWifi: false,
37
+ hasOta: false,
38
+ hasCustomFirmware: true,
39
+ hasNativeDashboard: false,
40
+ hasNativeNavigation: false,
41
+ hasNativeNotifications: false,
42
+ hasNotificationRelay: true,
43
+ }
@@ -19,10 +19,11 @@ export const rayneoIO: Capabilities = {
19
19
  hasCamera: false,
20
20
  camera: null,
21
21
 
22
- // Display capabilities - single monochrome (1-bit) panel
22
+ // Display capabilities - a monochrome panel per eye; the Glassly CFW keeps
23
+ // a scene per lens, so element depth is a real disparity here
23
24
  hasDisplay: true,
24
25
  display: {
25
- count: 1,
26
+ count: 2,
26
27
  isColor: false,
27
28
  color: "green",
28
29
  // Bitmaps and positioned elements need the Glassly CFW; stock firmware
@@ -13,10 +13,16 @@ export enum DeviceTypes {
13
13
  NIMO = "NIMO",
14
14
  FRAME = "Brilliant Frame",
15
15
  RAYNEO_IO = "RayNeo iO",
16
+ MEMOMIND_ONE = "MemoMind One",
16
17
  }
17
18
 
18
19
  export enum ControllerTypes {
19
20
  R1 = "Even Realities R1",
20
21
  NEURAL_BAND = "Meta Neural Band",
21
22
  APPLE_WATCH = "Apple Watch",
23
+ WEAR_OS = "Wear OS Watch",
24
+ GAMEBRICK = "IINE Gamebrick",
25
+ IINE_ANANKE = "IINE Ananke Gen 2",
26
+ ZERO2 = "8BitDo Zero 2",
27
+ MICRO = "8BitDo Micro",
22
28
  }
@@ -9,6 +9,7 @@ import {mentraLive} from "./capabilities/mentra-live"
9
9
  import {simulatedGlasses} from "./capabilities/simulated-glasses"
10
10
  import {nimo} from "./capabilities/nimo"
11
11
  import {rayneoIO} from "./capabilities/rayneo-io"
12
+ import {memoMindOne} from "./capabilities/memomind-one"
12
13
  import {vuzixZ100} from "./capabilities/vuzix-z100"
13
14
  import {none} from "./capabilities/none"
14
15
  import {DeviceTypes} from "./enums"
@@ -217,6 +218,7 @@ export interface Capabilities {
217
218
  // (reading the phone's stream, or an upload from the phone) is the driver's
218
219
  // business; the engine only decides who presents.
219
220
  hasNativeNotifications?: boolean
221
+ hasNotificationRelay?: boolean
220
222
  // Popup durations the firmware accepts, in seconds (0 = firmware default).
221
223
  // Drivers snap other values; the settings UI offers exactly these.
222
224
  nativeNotificationPopupSeconds?: number[]
@@ -236,6 +238,7 @@ export const HARDWARE_CAPABILITIES: Record<string, Capabilities> = {
236
238
  [vuzixZ100.modelName]: vuzixZ100,
237
239
  [nimo.modelName]: nimo,
238
240
  [rayneoIO.modelName]: rayneoIO,
241
+ [memoMindOne.modelName]: memoMindOne,
239
242
  [DeviceTypes.MACH1]: vuzixZ100, // Mach1 uses same Vuzix Ultralite hardware as Z100
240
243
  [DeviceTypes.AR99]: {
241
244
  ...evenRealitiesG1,
@@ -265,4 +268,14 @@ export const getModelCapabilities = (deviceType: DeviceTypes): Capabilities => {
265
268
  }
266
269
 
267
270
  // export * from "./capabilities"
268
- export {simulatedGlasses, evenRealitiesG1, evenRealitiesG2, mentraLive, vuzixZ100, mentraDisplay, nimo, rayneoIO}
271
+ export {
272
+ simulatedGlasses,
273
+ evenRealitiesG1,
274
+ evenRealitiesG2,
275
+ mentraLive,
276
+ vuzixZ100,
277
+ mentraDisplay,
278
+ nimo,
279
+ rayneoIO,
280
+ memoMindOne,
281
+ }
@@ -70,8 +70,8 @@ export {Z100_PROFILE, Z100_HYPHEN_WIDTH_PX, Z100_SPACE_WIDTH_PX} from "./profile
70
70
  // Mentra Nex Profile (a.k.a. Mentra Display)
71
71
  export {NEX_PROFILE, NEX_HYPHEN_WIDTH_PX, NEX_SPACE_WIDTH_PX} from "./profiles"
72
72
 
73
- // RayNeo iO Profile (stock subtitle page)
74
- export {RAYNEO_IO_PROFILE, RAYNEO_IO_HYPHEN_WIDTH_PX, RAYNEO_IO_SPACE_WIDTH_PX} from "./profiles"
73
+ // RayNeo iO Profiles (stock subtitle page, Glassly CFW scene page)
74
+ export {RAYNEO_IO_PROFILE, RAYNEO_IO_CFW_PROFILE, RAYNEO_IO_HYPHEN_WIDTH_PX, RAYNEO_IO_SPACE_WIDTH_PX} from "./profiles"
75
75
 
76
76
  // =============================================================================
77
77
  // Measurer - Pixel-accurate text measurement
@@ -274,3 +274,5 @@ export function createNexToolkit(): {
274
274
  breakMode: "character-no-hyphen",
275
275
  })
276
276
  }
277
+
278
+ export {MEMOMIND_ONE_PROFILE} from "./profiles"
@@ -27,5 +27,12 @@ export {G2_PROFILE, G2_HYPHEN_WIDTH_PX, G2_SPACE_WIDTH_PX} from "./g2"
27
27
  // Mentra Nex Profile (a.k.a. Mentra Display)
28
28
  export {NEX_PROFILE, NEX_HYPHEN_WIDTH_PX, NEX_SPACE_WIDTH_PX} from "./nex"
29
29
 
30
- // RayNeo iO Profile (stock subtitle page)
31
- export {RAYNEO_IO_PROFILE, RAYNEO_IO_HYPHEN_WIDTH_PX, RAYNEO_IO_SPACE_WIDTH_PX} from "./rayneo-io"
30
+ // RayNeo iO Profiles (stock subtitle page, Glassly CFW scene page)
31
+ export {
32
+ RAYNEO_IO_PROFILE,
33
+ RAYNEO_IO_CFW_PROFILE,
34
+ RAYNEO_IO_HYPHEN_WIDTH_PX,
35
+ RAYNEO_IO_SPACE_WIDTH_PX,
36
+ } from "./rayneo-io"
37
+
38
+ export {MEMOMIND_ONE_PROFILE} from "@glassly/miniapp/display"
@@ -1,2 +1,7 @@
1
1
  // Moved into the SDK (@glassly/miniapp/display) so miniapps lay text out the way the host measures it; this path re-exports it for the engine.
2
- export {RAYNEO_IO_PROFILE, RAYNEO_IO_HYPHEN_WIDTH_PX, RAYNEO_IO_SPACE_WIDTH_PX} from "@glassly/miniapp/display"
2
+ export {
3
+ RAYNEO_IO_PROFILE,
4
+ RAYNEO_IO_CFW_PROFILE,
5
+ RAYNEO_IO_HYPHEN_WIDTH_PX,
6
+ RAYNEO_IO_SPACE_WIDTH_PX,
7
+ } from "@glassly/miniapp/display"
@@ -24,7 +24,7 @@ export {resolveAnchors} from "./anchor"
24
24
  export type {DiffResult, DiffableElement} from "./differ"
25
25
  export {diffScene} from "./differ"
26
26
  export type {ProcessedScene} from "./process"
27
- export {processScene, profileLineHeightPx} from "./process"
27
+ export {processScene, profileLineHeightPx, wrapsOnDevice} from "./process"
28
28
  export type {DegradedScene} from "./degrade"
29
29
  export {degradeScene} from "./degrade"
30
30
  export {SceneStore} from "./store"
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import {compileSvgPath} from "./svgPath"
12
+ import {isUnsupportedScript} from "../measurer/script-detection"
12
13
  import {TextMeasurer} from "../measurer/TextMeasurer"
13
14
  import {TextWrapper} from "../wrapper/TextWrapper"
14
15
  import type {DisplayProfile} from "../profiles/types"
@@ -47,6 +48,18 @@ function clampBox(box: SceneBox, width: number, height: number): SceneBox | null
47
48
 
48
49
  const isFinite = (n: unknown): n is number => typeof n === "number" && Number.isFinite(n)
49
50
 
51
+ /**
52
+ * Text the host cannot wrap: a script the measurer has no widths or break
53
+ * rules for (Thai has no spaces between words, for one). It is passed through
54
+ * unwrapped and the device's text engine lays it out in the box.
55
+ */
56
+ export function wrapsOnDevice(text: string): boolean {
57
+ for (const char of text) {
58
+ if (isUnsupportedScript(char)) return true
59
+ }
60
+ return false
61
+ }
62
+
50
63
  /** Integer points, or null when the count/values are invalid for the type. */
51
64
  function validPoints(el: SceneElementInput): ScenePoint[] | null {
52
65
  const raw = (el as {points?: unknown}).points
@@ -372,10 +385,11 @@ export function processScene(
372
385
  // Height clipping only applies with a CALIBRATED line height — otherwise
373
386
  // the firmware clips vertically in-box (legacy behavior).
374
387
  const style: SceneTextStyle = el.style ?? {}
375
- if (style.font === "mono" || style.font === "builtin") {
376
- // Grid text is laid out by the app (cols × rows) and builtin text by the
377
- // glasses' own font engine; the host's proportional wrap metrics don't
378
- // apply to either. Pass lines through and let the device clip.
388
+ if (style.font === "mono" || style.font === "builtin" || wrapsOnDevice(el.text ?? "")) {
389
+ // Grid text is laid out by the app (cols × rows), builtin text by the
390
+ // glasses' own font engine and unsupported scripts by the device's text
391
+ // engine; the host's proportional wrap metrics don't apply to any of
392
+ // them. Pass lines through and let the device wrap and clip.
379
393
  const monoText = el.text ?? ""
380
394
  out.push({
381
395
  id: el.id,
@@ -390,8 +404,9 @@ export function processScene(
390
404
  continue
391
405
  }
392
406
  const maxLines = lineHeight ? Math.max(1, Math.floor(clamped.h / lineHeight)) : profile.maxLines
407
+ const maxWidthPx = Math.max(1, clamped.w - 2 * (profile.textInsetPx ?? 0))
393
408
  const result = wrapper.wrap(el.text ?? "", {
394
- maxWidthPx: clamped.w,
409
+ maxWidthPx,
395
410
  maxLines,
396
411
  ...(style.breakMode ? {breakMode: style.breakMode} : {}),
397
412
  })
@@ -402,12 +417,12 @@ export function processScene(
402
417
  // Trim the last line until it fits WITH the ellipsis appended —
403
418
  // otherwise the extra glyph can overflow the clamped box width.
404
419
  let last = lines[lines.length - 1]
405
- while (last.length > 0 && !measurer.fitsInWidth(`${last}…`, clamped.w)) {
420
+ while (last.length > 0 && !measurer.fitsInWidth(`${last}…`, maxWidthPx)) {
406
421
  last = last.slice(0, -1)
407
422
  }
408
423
  // A box narrower than the ellipsis glyph itself gets an empty line —
409
424
  // never emit a line wider than the clamped box.
410
- const ellipsized = last.length > 0 || measurer.fitsInWidth("…", clamped.w) ? `${last}…` : ""
425
+ const ellipsized = last.length > 0 || measurer.fitsInWidth("…", maxWidthPx) ? `${last}…` : ""
411
426
  lines = [...lines.slice(0, -1), ellipsized]
412
427
  }
413
428
  }