@glassly/engine 0.3.7 → 0.3.8-dev.1000

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 (212) hide show
  1. package/README.md +4 -0
  2. package/build/engine.d.ts +10 -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 +4 -4
  15. package/build/facades/glasses.d.ts.map +1 -1
  16. package/build/facades/glasses.js +4 -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 +2 -0
  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 +1 -0
  35. package/build/services/CloudClientService.d.ts.map +1 -1
  36. package/build/services/CloudClientService.js +34 -2
  37. package/build/services/CloudClientService.js.map +1 -1
  38. package/build/services/DisplayDiagnostics.d.ts.map +1 -1
  39. package/build/services/DisplayDiagnostics.js +4 -2
  40. package/build/services/DisplayDiagnostics.js.map +1 -1
  41. package/build/services/DisplayProcessor.d.ts +2 -0
  42. package/build/services/DisplayProcessor.d.ts.map +1 -1
  43. package/build/services/DisplayProcessor.js +15 -1
  44. package/build/services/DisplayProcessor.js.map +1 -1
  45. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  46. package/build/services/GlassesTouchInput.js +4 -2
  47. package/build/services/GlassesTouchInput.js.map +1 -1
  48. package/build/services/JSCrashloopReportService.d.ts.map +1 -1
  49. package/build/services/JSCrashloopReportService.js +5 -0
  50. package/build/services/JSCrashloopReportService.js.map +1 -1
  51. package/build/services/JSRouter.d.ts +11 -3
  52. package/build/services/JSRouter.d.ts.map +1 -1
  53. package/build/services/JSRouter.js +36 -6
  54. package/build/services/JSRouter.js.map +1 -1
  55. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  56. package/build/services/LocalDisplayManager.js +5 -2
  57. package/build/services/LocalDisplayManager.js.map +1 -1
  58. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  59. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  60. package/build/services/LocalMiniappRuntime.js +61 -22
  61. package/build/services/LocalMiniappRuntime.js.map +1 -1
  62. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  63. package/build/services/LocalSttFallbackCoordinator.js +2 -0
  64. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  65. package/build/services/LocalTranslationService.d.ts +2 -0
  66. package/build/services/LocalTranslationService.d.ts.map +1 -1
  67. package/build/services/LocalTranslationService.js +22 -0
  68. package/build/services/LocalTranslationService.js.map +1 -1
  69. package/build/services/MiniappEngine.d.ts +9 -0
  70. package/build/services/MiniappEngine.d.ts.map +1 -1
  71. package/build/services/MiniappEngine.js +60 -27
  72. package/build/services/MiniappEngine.js.map +1 -1
  73. package/build/services/MiniappLauncher.d.ts +2 -0
  74. package/build/services/MiniappLauncher.d.ts.map +1 -1
  75. package/build/services/MiniappLauncher.js +6 -0
  76. package/build/services/MiniappLauncher.js.map +1 -1
  77. package/build/services/NavigationService.d.ts +1 -0
  78. package/build/services/NavigationService.d.ts.map +1 -1
  79. package/build/services/NavigationService.js +20 -6
  80. package/build/services/NavigationService.js.map +1 -1
  81. package/build/services/OfflineActivity.d.ts +54 -0
  82. package/build/services/OfflineActivity.d.ts.map +1 -0
  83. package/build/services/OfflineActivity.js +86 -0
  84. package/build/services/OfflineActivity.js.map +1 -0
  85. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  86. package/build/services/PhoneCalendarService.js +8 -0
  87. package/build/services/PhoneCalendarService.js.map +1 -1
  88. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  89. package/build/services/PhoneNotificationsSync.js +7 -3
  90. package/build/services/PhoneNotificationsSync.js.map +1 -1
  91. package/build/services/STTModelManager.d.ts +1 -0
  92. package/build/services/STTModelManager.d.ts.map +1 -1
  93. package/build/services/STTModelManager.js +6 -0
  94. package/build/services/STTModelManager.js.map +1 -1
  95. package/build/services/SceneRenderer.js +1 -1
  96. package/build/services/SceneRenderer.js.map +1 -1
  97. package/build/services/appStateBackupFile.d.ts +32 -0
  98. package/build/services/appStateBackupFile.d.ts.map +1 -0
  99. package/build/services/appStateBackupFile.js +135 -0
  100. package/build/services/appStateBackupFile.js.map +1 -0
  101. package/build/services/controllers.d.ts.map +1 -1
  102. package/build/services/controllers.js +12 -3
  103. package/build/services/controllers.js.map +1 -1
  104. package/build/services/effectiveCapabilities.js +6 -1
  105. package/build/services/effectiveCapabilities.js.map +1 -1
  106. package/build/services/pages/pageEligibility.d.ts.map +1 -1
  107. package/build/services/pages/pageEligibility.js +8 -1
  108. package/build/services/pages/pageEligibility.js.map +1 -1
  109. package/build/services/settingsBridge.d.ts.map +1 -1
  110. package/build/services/settingsBridge.js +1 -0
  111. package/build/services/settingsBridge.js.map +1 -1
  112. package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
  113. package/build/services/watch/WatchLink.d.ts.map +1 -0
  114. package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
  115. package/build/services/watch/WatchLink.js.map +1 -0
  116. package/build/stores/apps.d.ts.map +1 -1
  117. package/build/stores/apps.js +22 -10
  118. package/build/stores/apps.js.map +1 -1
  119. package/build/stores/settings.d.ts.map +1 -1
  120. package/build/stores/settings.js +63 -6
  121. package/build/stores/settings.js.map +1 -1
  122. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  123. package/build/types/capabilities/even-realities-g1.js +3 -0
  124. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  125. package/build/types/capabilities/memomind-one.d.ts +3 -0
  126. package/build/types/capabilities/memomind-one.d.ts.map +1 -0
  127. package/build/types/capabilities/memomind-one.js +42 -0
  128. package/build/types/capabilities/memomind-one.js.map +1 -0
  129. package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
  130. package/build/types/capabilities/rayneo-io.js +3 -2
  131. package/build/types/capabilities/rayneo-io.js.map +1 -1
  132. package/build/types/enums.d.ts +7 -2
  133. package/build/types/enums.d.ts.map +1 -1
  134. package/build/types/enums.js +5 -0
  135. package/build/types/enums.js.map +1 -1
  136. package/build/types/hardware.d.ts +3 -1
  137. package/build/types/hardware.d.ts.map +1 -1
  138. package/build/types/hardware.js +3 -1
  139. package/build/types/hardware.js.map +1 -1
  140. package/build/utils/display/index.d.ts +2 -1
  141. package/build/utils/display/index.d.ts.map +1 -1
  142. package/build/utils/display/index.js +3 -2
  143. package/build/utils/display/index.js.map +1 -1
  144. package/build/utils/display/profiles/index.d.ts +2 -1
  145. package/build/utils/display/profiles/index.d.ts.map +1 -1
  146. package/build/utils/display/profiles/index.js +3 -2
  147. package/build/utils/display/profiles/index.js.map +1 -1
  148. package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
  149. package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
  150. package/build/utils/display/profiles/rayneo-io.js +1 -1
  151. package/build/utils/display/profiles/rayneo-io.js.map +1 -1
  152. package/build/utils/display/scene/index.d.ts +1 -1
  153. package/build/utils/display/scene/index.d.ts.map +1 -1
  154. package/build/utils/display/scene/index.js +1 -1
  155. package/build/utils/display/scene/index.js.map +1 -1
  156. package/build/utils/display/scene/process.d.ts +6 -0
  157. package/build/utils/display/scene/process.d.ts.map +1 -1
  158. package/build/utils/display/scene/process.js +22 -7
  159. package/build/utils/display/scene/process.js.map +1 -1
  160. package/build/utils/miniappGlobals.d.ts +13 -17
  161. package/build/utils/miniappGlobals.d.ts.map +1 -1
  162. package/build/utils/miniappGlobals.js +24 -56
  163. package/build/utils/miniappGlobals.js.map +1 -1
  164. package/package.json +7 -7
  165. package/src/engine.ts +7 -4
  166. package/src/facades/ai.ts +4 -0
  167. package/src/facades/backup.ts +28 -0
  168. package/src/facades/glasses.ts +5 -5
  169. package/src/generated/releaseMetadata.ts +4 -4
  170. package/src/index.ts +9 -0
  171. package/src/services/AppRegistry.ts +36 -9
  172. package/src/services/AppStateBackup.ts +283 -0
  173. package/src/services/BlobStore.ts +5 -11
  174. package/src/services/CloudClientService.ts +35 -4
  175. package/src/services/DisplayDiagnostics.ts +4 -2
  176. package/src/services/DisplayProcessor.ts +20 -8
  177. package/src/services/GlassesTouchInput.ts +6 -2
  178. package/src/services/JSCrashloopReportService.ts +5 -0
  179. package/src/services/JSRouter.ts +37 -6
  180. package/src/services/LocalDisplayManager.ts +5 -2
  181. package/src/services/LocalMiniappRuntime.ts +61 -22
  182. package/src/services/LocalSttFallbackCoordinator.ts +4 -0
  183. package/src/services/LocalTranslationService.ts +20 -0
  184. package/src/services/MiniappEngine.ts +62 -27
  185. package/src/services/MiniappLauncher.ts +7 -0
  186. package/src/services/NavigationService.ts +28 -8
  187. package/src/services/OfflineActivity.ts +131 -0
  188. package/src/services/PhoneCalendarService.ts +7 -0
  189. package/src/services/PhoneNotificationsSync.ts +8 -3
  190. package/src/services/STTModelManager.ts +6 -0
  191. package/src/services/SceneRenderer.ts +1 -1
  192. package/src/services/appStateBackupFile.ts +171 -0
  193. package/src/services/controllers.ts +13 -3
  194. package/src/services/effectiveCapabilities.ts +6 -1
  195. package/src/services/pages/pageEligibility.ts +9 -1
  196. package/src/services/settingsBridge.ts +1 -0
  197. package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
  198. package/src/stores/apps.ts +22 -11
  199. package/src/stores/settings.ts +70 -10
  200. package/src/types/capabilities/even-realities-g1.ts +4 -0
  201. package/src/types/capabilities/memomind-one.ts +43 -0
  202. package/src/types/capabilities/rayneo-io.ts +3 -2
  203. package/src/types/enums.ts +5 -0
  204. package/src/types/hardware.ts +14 -1
  205. package/src/utils/display/index.ts +4 -2
  206. package/src/utils/display/profiles/index.ts +9 -2
  207. package/src/utils/display/profiles/rayneo-io.ts +6 -1
  208. package/src/utils/display/scene/index.ts +1 -1
  209. package/src/utils/display/scene/process.ts +22 -7
  210. package/src/utils/miniappGlobals.ts +32 -64
  211. package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
  212. package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
@@ -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,21 @@ 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
+ // Watch (Apple Watch, Wear OS): draw the glasses' current frame behind the watch
852
+ // trackpad. Frames cross the watch link while the watch app is up, so it stays off
853
+ // unless asked for.
854
+ watch_mirror: {
855
+ key: "watch_mirror",
856
+ defaultValue: () => false,
857
+ writable: true,
858
+ saveOnServer: false,
859
+ persist: true,
860
+ },
861
+ // Deprecated: the setting above under its Apple-only name; read once to migrate.
846
862
  apple_watch_mirror: {
847
863
  key: "apple_watch_mirror",
848
864
  defaultValue: () => false,
849
- writable: true,
865
+ writable: false,
850
866
  saveOnServer: false,
851
867
  persist: true,
852
868
  },
@@ -904,6 +920,23 @@ export const SETTINGS: Record<string, Setting> = {
904
920
  saveOnServer: false,
905
921
  persist: true,
906
922
  },
923
+ // Debug: keep each miniapp's switcher-card screenshot on disk so the card
924
+ // comes back after an app restart. Off keeps captures in memory only, for
925
+ // checking the capture path itself. Device-local, default on.
926
+ persist_miniapp_screenshots: {
927
+ key: "persist_miniapp_screenshots",
928
+ defaultValue: () => true,
929
+ writable: true,
930
+ saveOnServer: false,
931
+ persist: true,
932
+ },
933
+ home_offline_card: {
934
+ key: "home_offline_card",
935
+ defaultValue: () => true,
936
+ writable: true,
937
+ saveOnServer: false,
938
+ persist: true,
939
+ },
907
940
  // Gesture-dots debug indicator: flash a glyph on the glasses when a touch
908
941
  // gesture is received while the display is empty. Works on any
909
942
  // scene-capable device, not only CFW. Device-local debug knob, default off.
@@ -1128,6 +1161,13 @@ export const SETTINGS: Record<string, Setting> = {
1128
1161
  saveOnServer: true,
1129
1162
  persist: true,
1130
1163
  },
1164
+ notifications_batch_seconds: {
1165
+ key: "notifications_batch_seconds",
1166
+ defaultValue: () => 0,
1167
+ writable: true,
1168
+ saveOnServer: true,
1169
+ persist: true,
1170
+ },
1131
1171
  // Hold popups while another glasses feature (navigation, a miniapp) is on screen.
1132
1172
  notifications_hold_during_apps: {
1133
1173
  key: "notifications_hold_during_apps",
@@ -1205,7 +1245,8 @@ export const BLUETOOTH_SETTING_KEYS: string[] = [
1205
1245
  SETTINGS.calendar_events.key,
1206
1246
  SETTINGS.cfw_scene_renderer.key,
1207
1247
  SETTINGS.cfw_fast_ble.key,
1208
- SETTINGS.g2_perf_logs.key,
1248
+ SETTINGS.perf_logs.key,
1249
+ SETTINGS.memomind_accessory_picker.key,
1209
1250
  SETTINGS.ring_via_glasses.key,
1210
1251
  SETTINGS.display_max_fps.key,
1211
1252
  SETTINGS.twelve_hour_time.key,
@@ -1276,10 +1317,13 @@ export interface SettingsState {
1276
1317
  }
1277
1318
 
1278
1319
  const getDefaultSettings = () =>
1279
- Object.keys(SETTINGS).reduce((acc, key) => {
1280
- acc[key] = SETTINGS[key].defaultValue()
1281
- return acc
1282
- }, {} as Record<string, any>)
1320
+ Object.keys(SETTINGS).reduce(
1321
+ (acc, key) => {
1322
+ acc[key] = SETTINGS[key].defaultValue()
1323
+ return acc
1324
+ },
1325
+ {} as Record<string, any>,
1326
+ )
1283
1327
 
1284
1328
  // Single-flight for loadAllSettings: the host fires it at module load and
1285
1329
  // engine.start()'s device-store hydration awaits it — without the memo the
@@ -1619,6 +1663,22 @@ export const useSettingsStore = create<SettingsState>()(
1619
1663
  }
1620
1664
  if (menuMigrated) storage.save(MENU_SETTINGS_MIGRATION_KEY, true)
1621
1665
  }
1666
+
1667
+ // The watch mirror switch lost its Apple-only name when Wear OS joined: carry a
1668
+ // turned-on `apple_watch_mirror` over to `watch_mirror` once.
1669
+ const WATCH_MIRROR_MIGRATION_KEY = "migration:watch_mirror_v1"
1670
+ const watchMirrorMigrationDone = storage.load<boolean>(WATCH_MIRROR_MIGRATION_KEY)
1671
+ if (watchMirrorMigrationDone.is_error() || !watchMirrorMigrationDone.value) {
1672
+ let watchMirrorMigrated = true
1673
+ if (get().getSetting(SETTINGS.apple_watch_mirror.key) === true) {
1674
+ const result = await get().setSetting(SETTINGS.watch_mirror.key, true, false)
1675
+ if (result.is_error()) {
1676
+ console.log("SETTINGS: watch_mirror migration failed:", result.error)
1677
+ watchMirrorMigrated = false
1678
+ }
1679
+ }
1680
+ if (watchMirrorMigrated) storage.save(WATCH_MIRROR_MIGRATION_KEY, true)
1681
+ }
1622
1682
  })
1623
1683
  loadAllSettingsInFlight = inFlight
1624
1684
  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,15 @@ 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",
22
27
  }
@@ -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
  }
@@ -7,7 +7,7 @@
7
7
  * same API surface.
8
8
  */
9
9
 
10
- import {Dimensions, Platform} from "react-native"
10
+ import {Platform} from "react-native"
11
11
 
12
12
  export interface MiniappSafeArea {
13
13
  top: number
@@ -16,6 +16,11 @@ export interface MiniappSafeArea {
16
16
  right: number
17
17
  }
18
18
 
19
+ /**
20
+ * The host's capsule menu in the WebView's own coordinate space. `corner`
21
+ * floats over the page's top-right; `rail` sits in the iPhone Duo's status
22
+ * rail, inside the side safe-area inset and clear of the page content.
23
+ */
19
24
  export interface CapsuleMenuRect {
20
25
  top: number
21
26
  right: number
@@ -23,36 +28,7 @@ export interface CapsuleMenuRect {
23
28
  left: number
24
29
  width: number
25
30
  height: number
26
- }
27
-
28
- /**
29
- * The capsule menu bounding rect in the WebView's own coordinate space.
30
- *
31
- * Keep in sync with CapsuleMenu.tsx:
32
- * - CapsuleButton height ≈ h-7.5 (30px), width ≈ 73px
33
- * - Positioned at right-2 (8px), top = theme.spacing.s2 (8px) below insets.top
34
- */
35
- const CAPSULE_MENU_HEIGHT = 30
36
- const CAPSULE_MENU_WIDTH = 73
37
- const CAPSULE_MENU_PADDING = 8
38
-
39
- /**
40
- * @param topInsetOffset extra top offset to add when the WebView container does
41
- * NOT already pad by the safe-area inset (i.e. WebView fills edge-to-edge under
42
- * the status bar). Pass `insets.top` in that case, or 0 when the container
43
- * already applies top padding.
44
- */
45
- export function getCapsuleMenuRect(topInsetOffset = 0): CapsuleMenuRect {
46
- const screenWidth = Dimensions.get("window").width
47
- const top = topInsetOffset + CAPSULE_MENU_PADDING
48
- return {
49
- top,
50
- right: CAPSULE_MENU_PADDING,
51
- bottom: top + CAPSULE_MENU_HEIGHT,
52
- left: screenWidth - CAPSULE_MENU_PADDING - CAPSULE_MENU_WIDTH,
53
- width: CAPSULE_MENU_WIDTH,
54
- height: CAPSULE_MENU_HEIGHT,
55
- }
31
+ placement: "corner" | "rail"
56
32
  }
57
33
 
58
34
  export type MiniappColorScheme = "light" | "dark"
@@ -63,37 +39,48 @@ export interface BuildMiniappGlobalsOptions {
63
39
  miniappLocal?: boolean
64
40
  miniappDeveloperMode?: boolean
65
41
  safeAreaInsets: MiniappSafeArea
66
- /**
67
- * True when the WebView container renders edge-to-edge (no top padding for
68
- * the status bar). In that case the capsule menu rect needs to shift down by
69
- * the top inset so its coords match where it's actually drawn on screen.
70
- */
71
- webviewFillsStatusBar?: boolean
42
+ /** Where the host draws its capsule menu over this WebView. */
43
+ capsuleMenu: CapsuleMenuRect
72
44
  /** Current host color scheme. Miniapps may follow this to match the phone. */
73
45
  colorScheme?: MiniappColorScheme
74
46
  }
75
47
 
76
- function safeAreaCssVars(insets: MiniappSafeArea): Record<string, string> {
48
+ // CSS custom properties that mirror the safe-area and capsule menu data, so
49
+ // miniapp CSS (or Tailwind arbitrary values) can read them without touching
50
+ // JS: e.g. `style="margin-top: var(--glassly-capsule-top)"`.
51
+ function layoutCssVars(insets: MiniappSafeArea, capsule: CapsuleMenuRect): Record<string, string> {
77
52
  return {
78
53
  "--glassly-safe-top": `${insets.top}px`,
79
54
  "--glassly-safe-bottom": `${insets.bottom}px`,
80
55
  "--glassly-safe-left": `${insets.left}px`,
81
56
  "--glassly-safe-right": `${insets.right}px`,
57
+ "--glassly-capsule-top": `${capsule.top}px`,
58
+ "--glassly-capsule-bottom": `${capsule.bottom}px`,
59
+ "--glassly-capsule-left": `${capsule.left}px`,
60
+ "--glassly-capsule-right": `${capsule.right}px`,
61
+ "--glassly-capsule-width": `${capsule.width}px`,
62
+ "--glassly-capsule-height": `${capsule.height}px`,
63
+ "--glassly-capsule-center-y": `${capsule.top + capsule.height / 2}px`,
64
+ // Right-side gutter to reserve so content doesn't slide under the
65
+ // capsule: its width + 16px breathing room, none when it sits in the rail.
66
+ "--glassly-capsule-gutter": `${capsule.placement === "rail" ? 0 : capsule.width + 16}px`,
82
67
  }
83
68
  }
84
69
 
85
70
  /**
86
- * JS to inject into a running miniapp when the host's safe area changes (an
87
- * iPhone Duo folding moves its status rail): updates window.Glassly and the
88
- * CSS variables, then fires `glassly:safe-area-change`.
71
+ * JS to inject into a running miniapp when the host's safe area or capsule
72
+ * menu moves (an iPhone Duo folding, or its rail's reserved regions growing):
73
+ * updates window.Glassly and the CSS variables, then fires
74
+ * `glassly:safe-area-change`.
89
75
  */
90
- export function buildSafeAreaUpdateScript(insets: MiniappSafeArea): string {
91
- const sets = Object.entries(safeAreaCssVars(insets))
76
+ export function buildSafeAreaUpdateScript(insets: MiniappSafeArea, capsuleMenu: CapsuleMenuRect): string {
77
+ const sets = Object.entries(layoutCssVars(insets, capsuleMenu))
92
78
  .map(([k, v]) => `document.documentElement.style.setProperty(${JSON.stringify(k)}, ${JSON.stringify(v)});`)
93
79
  .join("\n ")
94
80
  return `
95
81
  window.Glassly = window.Glassly || {};
96
82
  window.Glassly.safeAreaInsets = ${JSON.stringify(insets)};
83
+ window.Glassly.capsuleMenu = ${JSON.stringify(capsuleMenu)};
97
84
  try {
98
85
  ${sets}
99
86
  } catch (e) { /* ignore */ }
@@ -115,37 +102,18 @@ export function buildSafeAreaUpdateScript(insets: MiniappSafeArea): string {
115
102
  * assign to it before the native bridge wires in.
116
103
  */
117
104
  export function buildMiniappGlobalsScript(opts: BuildMiniappGlobalsOptions): string {
118
- const capsuleTopOffset = opts.webviewFillsStatusBar ? opts.safeAreaInsets.top : 0
119
105
  const globals: Record<string, unknown> = {
120
106
  platform: Platform.OS,
121
107
  capabilities: opts.capabilities ?? ["share", "open_url", "copy_clipboard", "download"],
122
108
  safeAreaInsets: opts.safeAreaInsets,
123
- capsuleMenu: getCapsuleMenuRect(capsuleTopOffset),
109
+ capsuleMenu: opts.capsuleMenu,
124
110
  }
125
111
  if (opts.packageName) globals.packageName = opts.packageName
126
112
  if (opts.miniappLocal) globals.miniappLocal = true
127
113
  if (opts.miniappDeveloperMode) globals.miniappDeveloperMode = true
128
114
  if (opts.colorScheme) globals.colorScheme = opts.colorScheme
129
115
 
130
- // CSS custom properties that mirror the capsule menu / safe-area data.
131
- // Miniapp CSS (or Tailwind arbitrary values) can read these without
132
- // touching JS: e.g. `style="margin-top: var(--glassly-capsule-top)"`.
133
- const capsule = globals.capsuleMenu as CapsuleMenuRect
134
- const insets = opts.safeAreaInsets
135
- const capsuleCenter = capsule.top + capsule.height / 2
136
- const cssVars: Record<string, string> = {
137
- ...safeAreaCssVars(insets),
138
- "--glassly-capsule-top": `${capsule.top}px`,
139
- "--glassly-capsule-bottom": `${capsule.bottom}px`,
140
- "--glassly-capsule-left": `${capsule.left}px`,
141
- "--glassly-capsule-right": `${capsule.right}px`,
142
- "--glassly-capsule-width": `${capsule.width}px`,
143
- "--glassly-capsule-height": `${capsule.height}px`,
144
- "--glassly-capsule-center-y": `${capsuleCenter}px`,
145
- // Right-side gutter to reserve so content doesn't slide under the
146
- // capsule: capsule width + 16px breathing room.
147
- "--glassly-capsule-gutter": `${capsule.width + 16}px`,
148
- }
116
+ const cssVars = layoutCssVars(opts.safeAreaInsets, opts.capsuleMenu)
149
117
  const cssVarsBlock = Object.entries(cssVars)
150
118
  .map(([k, v]) => `${k}: ${v};`)
151
119
  .join(" ")
@@ -1 +0,0 @@
1
- {"version":3,"file":"AppleWatchLink.d.ts","sourceRoot":"","sources":["../../../src/services/appleWatch/AppleWatchLink.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,wBAAwB,CAAA;AAElE,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAA;AAOpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,aAAa,CAAA;AAO1C,eAAO,MAAM,cAAc,0BAA0B,CAAA;AACrD,eAAO,MAAM,aAAa,QAAQ,CAAA;AAQlC,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAC,CAAA;IAC7D,IAAI,EAAE,SAAS,SAAS,EAAE,CAAA;IAC1B,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,GAAG;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,EAAE,CAMhG;AAED,wBAAgB,UAAU,CAAC,EAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAC,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUpG;AAOD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CA4EpF;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,SAAS,SAAS,EAAE,CAAA;IAChC,KAAK,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/C,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACxC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IAChD,GAAG,EAAE,MAAM,MAAM,CAAA;CAClB;AAWD,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,sBAAsB,EAC7B,IAAI,GAAE,qBAAmC,GACxC,OAAO,CAAC,IAAI,CAAC,CA4Bf;AA6DD,wBAAgB,mBAAmB,IAAI,IAAI,CAqC1C;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"AppleWatchLink.js","sourceRoot":"","sources":["../../../src/services/appleWatch/AppleWatchLink.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Apple Watch remote's phone half: the watch app's status context (glasses, the miniapp in\n * front, launchable miniapps, whether Assist takes questions, the mirror setting), mirror frames\n * while the setting is on, and the commands the watch sends back. Native adds whether the active\n * watch is paired and keeps the newest frame for when the watch app opens. Gestures ride\n * `touch_event` like any controller's.\n */\nimport type {AppleWatchCommandEvent} from \"@glassly/bluetooth-sdk\"\nimport BluetoothSdk from \"@glassly/bluetooth-sdk/internal\"\nimport type {MirrorSnapshot} from \"@glassly/miniapp\"\nimport {MiniappStreamType} from \"@glassly/miniapp\"\nimport {Platform} from \"react-native\"\n\nimport {useAppStatusStore} from \"../../stores/apps\"\nimport {useGlassesStore} from \"../../stores/glasses\"\nimport {SETTINGS, useSettingsStore} from \"../../stores/settings\"\nimport type {ClientApp} from \"../../types\"\nimport {BgTimer} from \"../../utils/timers\"\nimport {isGlassesConnected} from \"../GlassesReadiness\"\nimport localMiniappRuntime from \"../LocalMiniappRuntime\"\nimport {mirrorService} from \"../MirrorService\"\nimport {keyEventsForTextChange} from \"../neuralBand/NeuralBandInput\"\n\nexport const ASSIST_PACKAGE = \"com.glassly.assistant\"\nexport const ASSIST_ACTION = \"ask\"\n/** WatchConnectivity messages top out near 64 KB; frames past this are skipped, not truncated. */\nconst MAX_FRAME_BYTES = 48_000\nconst MAX_IMAGE_CHARS = 16_000\nconst MAX_APPS = 24\nconst STATE_DEBOUNCE_MS = 250\nconst MIRROR_INTERVAL_MS = 500\n\nexport interface WatchStateInput {\n glasses: {connected: boolean; model: string; battery: number}\n apps: readonly ClientApp[]\n assist: boolean\n mirror: boolean\n}\n\n/** Miniapps the watch can open: shown, compatible, and drawing on the glasses. */\nexport function launchableApps(apps: readonly ClientApp[]): {packageName: string; name: string}[] {\n return apps\n .filter((app) => app.type === \"standard\" && !app.hidden && app.compatibility?.isCompatible !== false)\n .filter((app) => app.packageName && !app.packageName.includes(\"@empty\"))\n .slice(0, MAX_APPS)\n .map((app) => ({packageName: app.packageName, name: app.name}))\n}\n\nexport function watchState({glasses, apps, assist, mirror}: WatchStateInput): Record<string, unknown> {\n const front = apps.find((app) => app.foregrounded && app.running && app.type === \"standard\")\n const state: Record<string, unknown> = {\n glasses: {connected: glasses.connected, name: glasses.model || \"Glasses\", battery: glasses.battery},\n apps: launchableApps(apps),\n assist,\n mirror,\n }\n if (front) state.app = {packageName: front.packageName, name: front.name}\n return state\n}\n\ntype WatchElement = Record<string, string | number | boolean | number[]>\n\nconst level = (color: unknown, fallback = 15) =>\n typeof color === \"number\" ? Math.max(0, Math.min(15, color)) : fallback\n\n/** The glasses frame in the watch's drawing vocabulary; null when there is nothing to draw. */\nexport function mirrorFrame(snapshot: MirrorSnapshot): Record<string, unknown> | null {\n const canvas = snapshot.canvas\n if (!canvas) return null\n const content = snapshot.content\n if (content.kind === \"bitmap\") {\n if (content.data.length > MAX_FRAME_BYTES) return null\n return {width: canvas.width, height: canvas.height, bitmap: content.data, elements: []}\n }\n if (content.kind !== \"scene\") return null\n const elements: WatchElement[] = []\n for (const element of content.elements) {\n const {x, y, w, h} = element.box\n const style = element.style ?? {}\n switch (element.type) {\n case \"text\":\n if (!element.text) break\n elements.push({\n type: \"text\",\n x,\n y,\n w,\n h,\n text: element.text,\n size: style.fontSize ?? 20,\n color: level(style.color),\n })\n break\n case \"rect\":\n elements.push({\n type: \"rect\",\n x,\n y,\n w,\n h,\n fill: style.fill === true,\n border: style.border ?? 1,\n radius: style.radius ?? 0,\n color: level(style.color),\n })\n break\n case \"circle\":\n elements.push({\n type: \"circle\",\n x,\n y,\n w,\n h,\n fill: style.fill === true,\n border: style.border ?? 1,\n color: level(style.color),\n })\n break\n case \"line\":\n case \"triangle\":\n case \"quad\": {\n const points = (element.points ?? []).flatMap((point) => [point.x, point.y])\n if (points.length < 4) break\n const closed = element.type !== \"line\"\n elements.push({\n type: \"line\",\n points,\n width: style.width ?? style.border ?? 1,\n color: level(style.color),\n closed,\n fill: closed && style.fill === true,\n })\n break\n }\n case \"image\":\n if (element.data && element.data.length <= MAX_IMAGE_CHARS)\n elements.push({type: \"image\", x, y, w, h, data: element.data})\n break\n }\n }\n const frame = {width: canvas.width, height: canvas.height, elements}\n return JSON.stringify(frame).length > MAX_FRAME_BYTES ? null : frame\n}\n\nexport interface AppleWatchCommandDeps {\n apps: () => readonly ClientApp[]\n start: (app: ClientApp) => Promise<unknown>\n stop: (packageName: string) => Promise<unknown>\n ask: (query: string) => Promise<unknown>\n type: (stream: string, payload: unknown) => void\n now: () => number\n}\n\nconst defaultDeps: AppleWatchCommandDeps = {\n apps: () => useAppStatusStore.getState().apps,\n start: (app) => useAppStatusStore.getState().start(app),\n stop: (packageName) => useAppStatusStore.getState().stop(packageName),\n ask: (query) => localMiniappRuntime.invokeActionFromHost(ASSIST_PACKAGE, ASSIST_ACTION, {query}),\n type: (stream, payload) => localMiniappRuntime.forwardEvent(stream, payload),\n now: () => Date.now(),\n}\n\nexport async function handleAppleWatchCommand(\n event: AppleWatchCommandEvent,\n deps: AppleWatchCommandDeps = defaultDeps,\n): Promise<void> {\n switch (event.command) {\n case \"launch\": {\n const app = launchableApps(deps.apps()).some((entry) => entry.packageName === event.packageName)\n ? deps.apps().find((entry) => entry.packageName === event.packageName)\n : undefined\n if (!app) {\n console.warn(`AppleWatch: launch of unknown miniapp ${event.packageName}`)\n return\n }\n await deps.start(app)\n return\n }\n case \"stop\": {\n const front = deps.apps().find((app) => app.foregrounded && app.running && app.type === \"standard\")\n if (front) await deps.stop(front.packageName)\n return\n }\n case \"assist\": {\n const query = event.text.trim()\n if (query) await deps.ask(query)\n return\n }\n case \"text\":\n for (const key of keyEventsForTextChange(\"\", event.text, deps.now()))\n deps.type(MiniappStreamType.KEYBOARD_EVENT, key)\n return\n }\n}\n\nlet stops: (() => void)[] = []\nlet stopMirror: (() => void) | null = null\n\nfunction currentState(): Record<string, unknown> {\n const glasses = useGlassesStore.getState()\n return watchState({\n glasses: {\n connected: isGlassesConnected(glasses.connection),\n model: glasses.deviceModel,\n battery: glasses.batteryLevel,\n },\n apps: useAppStatusStore.getState().apps,\n assist: localMiniappRuntime.hostActionAvailable(ASSIST_PACKAGE, ASSIST_ACTION),\n mirror: useSettingsStore.getState().getSetting(SETTINGS.apple_watch_mirror.key) === true,\n })\n}\n\nfunction syncMirror(enabled: boolean): void {\n if (!enabled) {\n if (stopMirror) {\n stopMirror()\n stopMirror = null\n void BluetoothSdk.setWatchMirror(null).catch(() => {})\n }\n return\n }\n if (stopMirror) return\n let lastSentAt = 0\n let pending: MirrorSnapshot | null = null\n let timer: ReturnType<typeof BgTimer.setTimeout> | null = null\n const send = (snapshot: MirrorSnapshot) => {\n lastSentAt = Date.now()\n void BluetoothSdk.setWatchMirror(mirrorFrame(snapshot)).catch((err) =>\n console.warn(\"AppleWatch: mirror push failed:\", err),\n )\n }\n const unsubscribe = mirrorService.subscribe((snapshot) => {\n const wait = lastSentAt + MIRROR_INTERVAL_MS - Date.now()\n if (wait <= 0 && !timer) {\n send(snapshot)\n return\n }\n pending = snapshot\n timer ??= BgTimer.setTimeout(\n () => {\n timer = null\n if (pending) send(pending)\n pending = null\n },\n Math.max(wait, 0),\n )\n })\n stopMirror = () => {\n unsubscribe()\n if (timer) BgTimer.clearTimeout(timer)\n timer = null\n }\n}\n\nexport function startAppleWatchLink(): void {\n if (Platform.OS !== \"ios\" || stops.length > 0) return\n let lastKey = \"\"\n let timer: ReturnType<typeof BgTimer.setTimeout> | null = null\n const push = () => {\n timer = null\n const state = currentState()\n const key = JSON.stringify(state)\n if (key === lastKey) return\n lastKey = key\n void BluetoothSdk.setWatchState(state).catch((err) => console.warn(\"AppleWatch: state push failed:\", err))\n }\n const schedule = () => {\n timer ??= BgTimer.setTimeout(push, STATE_DEBOUNCE_MS)\n }\n const mirrorEnabled = () => useSettingsStore.getState().getSetting(SETTINGS.apple_watch_mirror.key) === true\n const commands = BluetoothSdk.addListener(\"apple_watch_command\", (event) => {\n handleAppleWatchCommand(event).catch((err) => console.warn(`AppleWatch: ${event.command} failed:`, err))\n })\n stops = [\n () => commands.remove(),\n useGlassesStore.subscribe(schedule),\n useAppStatusStore.subscribe(schedule),\n useSettingsStore.subscribe(\n (s) => s.getSetting(SETTINGS.apple_watch_mirror.key),\n () => {\n syncMirror(mirrorEnabled())\n schedule()\n },\n ),\n () => {\n if (timer) BgTimer.clearTimeout(timer)\n timer = null\n },\n ]\n syncMirror(mirrorEnabled())\n schedule()\n}\n\nexport function stopAppleWatchLink(): void {\n stops.splice(0).forEach((stop) => stop())\n syncMirror(false)\n}\n"]}