@glassly/engine 0.1.0-dev.61 → 0.1.0-dev.89

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 (147) hide show
  1. package/README.md +6 -2
  2. package/build/engine.d.ts +7 -2
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +3 -0
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/display.d.ts +10 -3
  7. package/build/facades/display.d.ts.map +1 -1
  8. package/build/facades/display.js +11 -1
  9. package/build/facades/display.js.map +1 -1
  10. package/build/facades/displayMirror.d.ts +2 -6
  11. package/build/facades/displayMirror.d.ts.map +1 -1
  12. package/build/facades/displayMirror.js +7 -14
  13. package/build/facades/displayMirror.js.map +1 -1
  14. package/build/facades/glasses.d.ts +1 -0
  15. package/build/facades/glasses.d.ts.map +1 -1
  16. package/build/facades/glasses.js +2 -0
  17. package/build/facades/glasses.js.map +1 -1
  18. package/build/facades/ota.d.ts +3 -1
  19. package/build/facades/ota.d.ts.map +1 -1
  20. package/build/facades/ota.js +4 -2
  21. package/build/facades/ota.js.map +1 -1
  22. package/build/facades/phoneNotifications.d.ts +12 -2
  23. package/build/facades/phoneNotifications.d.ts.map +1 -1
  24. package/build/facades/phoneNotifications.js +30 -10
  25. package/build/facades/phoneNotifications.js.map +1 -1
  26. package/build/generated/releaseMetadata.js +3 -3
  27. package/build/generated/releaseMetadata.js.map +1 -1
  28. package/build/index.d.ts +2 -2
  29. package/build/index.d.ts.map +1 -1
  30. package/build/index.js +2 -2
  31. package/build/index.js.map +1 -1
  32. package/build/react/MentraLiveOtaFlow.d.ts +6 -1
  33. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  34. package/build/react/MentraLiveOtaFlow.js +136 -14
  35. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  36. package/build/react/useMentraLiveOta.d.ts +5 -1
  37. package/build/react/useMentraLiveOta.d.ts.map +1 -1
  38. package/build/react/useMentraLiveOta.js +135 -58
  39. package/build/react/useMentraLiveOta.js.map +1 -1
  40. package/build/services/CustomDashboardService.d.ts +11 -10
  41. package/build/services/CustomDashboardService.d.ts.map +1 -1
  42. package/build/services/CustomDashboardService.js +70 -69
  43. package/build/services/CustomDashboardService.js.map +1 -1
  44. package/build/services/DashboardCoordinator.d.ts +26 -0
  45. package/build/services/DashboardCoordinator.d.ts.map +1 -0
  46. package/build/services/DashboardCoordinator.js +125 -0
  47. package/build/services/DashboardCoordinator.js.map +1 -0
  48. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  49. package/build/services/DeviceEventRouter.js +5 -7
  50. package/build/services/DeviceEventRouter.js.map +1 -1
  51. package/build/services/DisplayProcessor.d.ts +3 -3
  52. package/build/services/DisplayProcessor.js +3 -3
  53. package/build/services/DisplayProcessor.js.map +1 -1
  54. package/build/services/LocalDisplayManager.d.ts +12 -2
  55. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  56. package/build/services/LocalDisplayManager.js +68 -14
  57. package/build/services/LocalDisplayManager.js.map +1 -1
  58. package/build/services/OtaInstallCoordinator.d.ts +2 -0
  59. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  60. package/build/services/OtaInstallCoordinator.js +21 -1
  61. package/build/services/OtaInstallCoordinator.js.map +1 -1
  62. package/build/services/PhoneNotificationsSync.d.ts +13 -0
  63. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  64. package/build/services/PhoneNotificationsSync.js +101 -9
  65. package/build/services/PhoneNotificationsSync.js.map +1 -1
  66. package/build/services/PhonePhotoCoordinator.js +1 -1
  67. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  68. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  69. package/build/services/PhoneStreamCoordinator.js +9 -6
  70. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  71. package/build/services/SceneRenderer.d.ts.map +1 -1
  72. package/build/services/SceneRenderer.js +5 -0
  73. package/build/services/SceneRenderer.js.map +1 -1
  74. package/build/services/StreamLifecycleController.d.ts +4 -0
  75. package/build/services/StreamLifecycleController.d.ts.map +1 -1
  76. package/build/services/StreamLifecycleController.js +4 -0
  77. package/build/services/StreamLifecycleController.js.map +1 -1
  78. package/build/services/effectiveCapabilities.d.ts +1 -1
  79. package/build/services/effectiveCapabilities.d.ts.map +1 -1
  80. package/build/services/effectiveCapabilities.js +9 -4
  81. package/build/services/effectiveCapabilities.js.map +1 -1
  82. package/build/services/iosNotificationAppCatalog.d.ts +13 -0
  83. package/build/services/iosNotificationAppCatalog.d.ts.map +1 -0
  84. package/build/services/iosNotificationAppCatalog.js +28 -0
  85. package/build/services/iosNotificationAppCatalog.js.map +1 -0
  86. package/build/services/otaInstallPolicy.d.ts +4 -0
  87. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  88. package/build/services/otaInstallPolicy.js +4 -0
  89. package/build/services/otaInstallPolicy.js.map +1 -1
  90. package/build/stores/display.d.ts +4 -4
  91. package/build/stores/display.d.ts.map +1 -1
  92. package/build/stores/display.js +12 -36
  93. package/build/stores/display.js.map +1 -1
  94. package/build/stores/glasses.d.ts.map +1 -1
  95. package/build/stores/glasses.js +2 -0
  96. package/build/stores/glasses.js.map +1 -1
  97. package/build/stores/settings.d.ts.map +1 -1
  98. package/build/stores/settings.js +53 -2
  99. package/build/stores/settings.js.map +1 -1
  100. package/build/types/applet.d.ts +1 -1
  101. package/build/types/applet.js.map +1 -1
  102. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  103. package/build/types/capabilities/even-realities-g2.js +2 -2
  104. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  105. package/build/types/hardware.d.ts.map +1 -1
  106. package/build/types/hardware.js +1 -0
  107. package/build/types/hardware.js.map +1 -1
  108. package/build/types/index.d.ts +1 -1
  109. package/build/types/index.d.ts.map +1 -1
  110. package/build/types/index.js +1 -1
  111. package/build/types/index.js.map +1 -1
  112. package/build/utils/display/composer/ColumnComposer.d.ts +1 -1
  113. package/build/utils/display/composer/ColumnComposer.js +1 -1
  114. package/build/utils/display/composer/ColumnComposer.js.map +1 -1
  115. package/package.json +8 -7
  116. package/src/engine.ts +3 -0
  117. package/src/facades/display.ts +11 -1
  118. package/src/facades/displayMirror.ts +7 -15
  119. package/src/facades/glasses.ts +2 -0
  120. package/src/facades/ota.ts +4 -2
  121. package/src/facades/phoneNotifications.ts +32 -10
  122. package/src/generated/releaseMetadata.ts +3 -3
  123. package/src/index.ts +2 -1
  124. package/src/react/MentraLiveOtaFlow.tsx +226 -41
  125. package/src/react/useMentraLiveOta.ts +146 -60
  126. package/src/services/CustomDashboardService.ts +67 -66
  127. package/src/services/DashboardCoordinator.ts +121 -0
  128. package/src/services/DeviceEventRouter.ts +5 -7
  129. package/src/services/DisplayProcessor.ts +3 -3
  130. package/src/services/LocalDisplayManager.ts +68 -16
  131. package/src/services/OtaInstallCoordinator.ts +21 -0
  132. package/src/services/PhoneNotificationsSync.ts +125 -9
  133. package/src/services/PhonePhotoCoordinator.ts +1 -1
  134. package/src/services/PhoneStreamCoordinator.ts +10 -6
  135. package/src/services/SceneRenderer.ts +4 -0
  136. package/src/services/StreamLifecycleController.ts +4 -0
  137. package/src/services/effectiveCapabilities.ts +9 -4
  138. package/src/services/iosNotificationAppCatalog.ts +39 -0
  139. package/src/services/otaInstallPolicy.ts +4 -0
  140. package/src/stores/display.ts +16 -48
  141. package/src/stores/glasses.ts +2 -0
  142. package/src/stores/settings.ts +53 -2
  143. package/src/types/applet.ts +1 -1
  144. package/src/types/capabilities/even-realities-g2.ts +3 -2
  145. package/src/types/hardware.ts +1 -0
  146. package/src/types/index.ts +1 -1
  147. package/src/utils/display/composer/ColumnComposer.ts +1 -1
@@ -1,24 +1,50 @@
1
1
  /**
2
- * Phone-notifications sync — engine-owned. Pushes the notification-forwarding
3
- * operational kill switch + config (`android_notification_listener_enabled`
4
- * and `notifications_blocklist`) to the native
5
- * NotificationListener via `CrustModule.setNotificationConfig`, so
2
+ * Phone-notifications sync — engine-owned. Started by `engine.start()`.
3
+ *
4
+ * Android: pushes the notification-forwarding operational kill switch + config
5
+ * (`android_notification_listener_enabled` and `notifications_blocklist`) to
6
+ * the native NotificationListener via `CrustModule.setNotificationConfig`, so
6
7
  * the blocklist reaches the listener for ANY host — not just the Glassly App,
7
8
  * where this used to live in MantleManager. The Android service is desired by
8
9
  * default now that its cold start is isolated from React Native. Native code
9
10
  * keeps the component discoverable in Android's notification-access Settings,
10
11
  * but only binds the service after access is granted. The debug setting remains
11
12
  * a hidden emergency kill switch; Notify's running state gates presentation,
12
- * not capture. Android-only; a no-op elsewhere. Started by `engine.start()`.
13
+ * not capture.
14
+ *
15
+ * iOS: the G2 consumes notifications on-firmware via ANCS, so the phone's job
16
+ * is config, not capture. This sync (a) records the apps the glasses report
17
+ * posting notifications (`notification_app_seen` → `ios_notification_seen_apps`)
18
+ * and (b) maps the shared `notifications_blocklist` + the G2 display settings
19
+ * onto the native `notification_filter` / `notification_ctrl` DeviceStore keys,
20
+ * which the bluetooth-sdk pushes to the glasses (and replays on reconnect).
21
+ * The firmware filter is allow-based and fail-open: an empty blocklist disables
22
+ * whitelisting entirely; any block enables it with allowlist = (catalog ∪ seen)
23
+ * minus blocked.
13
24
  */
25
+ import {Platform} from "react-native"
14
26
  import {shallow} from "zustand/shallow"
15
27
  import CrustModule from "@glassly/crust"
28
+ import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
29
+ import type {
30
+ G2NotificationCtrlSetting,
31
+ G2NotificationFilterSetting,
32
+ NotificationAppSeenEvent,
33
+ } from "@glassly/bluetooth-sdk/internal"
16
34
 
17
35
  import {useSettingsStore, SETTINGS} from "../stores/settings"
36
+ import {IOS_NOTIFICATION_APP_CATALOG} from "./iosNotificationAppCatalog"
37
+
38
+ export interface SeenNotificationApp {
39
+ appId: string
40
+ displayName: string
41
+ lastSeen: number
42
+ }
18
43
 
19
44
  let unsubscribe: (() => void) | null = null
45
+ let seenAppsSubscription: {remove(): void} | null = null
20
46
 
21
- function pushConfig(): void {
47
+ function pushAndroidConfig(): void {
22
48
  const s = useSettingsStore.getState()
23
49
  const listenerEnabled = Boolean(s.getSetting(SETTINGS.android_notification_listener_enabled.key))
24
50
  const blocklist = s.getSetting(SETTINGS.notifications_blocklist.key)
@@ -27,20 +53,110 @@ function pushConfig(): void {
27
53
  )
28
54
  }
29
55
 
56
+ export function getSeenNotificationApps(): SeenNotificationApp[] {
57
+ const seen = useSettingsStore.getState().getSetting(SETTINGS.ios_notification_seen_apps.key)
58
+ return Array.isArray(seen) ? (seen as SeenNotificationApp[]) : []
59
+ }
60
+
61
+ /** Catalog ∪ seen apps, seen display names winning. Sorted by name. */
62
+ export function getKnownNotificationApps(): Array<{appId: string; displayName: string}> {
63
+ const merged = new Map<string, string>()
64
+ for (const app of IOS_NOTIFICATION_APP_CATALOG) {
65
+ merged.set(app.appId, app.displayName)
66
+ }
67
+ for (const app of getSeenNotificationApps()) {
68
+ if (app.appId) merged.set(app.appId, app.displayName || app.appId)
69
+ }
70
+ return [...merged.entries()]
71
+ .map(([appId, displayName]) => ({appId, displayName}))
72
+ .sort((a, b) => a.displayName.localeCompare(b.displayName))
73
+ }
74
+
75
+ export function computeG2NotificationFilter(): G2NotificationFilterSetting {
76
+ const blocklistValue = useSettingsStore.getState().getSetting(SETTINGS.notifications_blocklist.key)
77
+ const blocklist = Array.isArray(blocklistValue) ? (blocklistValue as string[]) : []
78
+ const blocked = new Set(blocklist)
79
+ // The list is included even while filtering is off, so the on-glass file
80
+ // already holds the user's current list when a first block enables it.
81
+ const apps = getKnownNotificationApps()
82
+ .filter((app) => !blocked.has(app.appId))
83
+ .map((app) => ({id: app.appId, name: app.displayName}))
84
+ return {enabled: blocked.size > 0, apps}
85
+ }
86
+
87
+ function computeG2NotificationCtrl(): G2NotificationCtrlSetting {
88
+ const s = useSettingsStore.getState()
89
+ return {
90
+ enabled: Boolean(s.getSetting(SETTINGS.g2_notifications_enabled.key)),
91
+ auto_display: Boolean(s.getSetting(SETTINGS.g2_notification_auto_display.key)),
92
+ disp_time: Number(s.getSetting(SETTINGS.g2_notification_disp_time.key)) || 0,
93
+ avoid_disturb: Boolean(s.getSetting(SETTINGS.g2_notification_avoid_disturb.key)),
94
+ }
95
+ }
96
+
97
+ function pushIosConfig(): void {
98
+ // The native DeviceStore dedupes unchanged values and replays these to the
99
+ // glasses on every reconnect, so pushing eagerly is cheap.
100
+ void Promise.resolve(
101
+ BluetoothSdk.updateBluetoothSettings({
102
+ notification_ctrl: computeG2NotificationCtrl(),
103
+ notification_filter: computeG2NotificationFilter(),
104
+ }),
105
+ ).catch((err: unknown) =>
106
+ console.warn(`PhoneNotificationsSync: notification config push failed: ${(err as Error)?.message ?? err}`),
107
+ )
108
+ }
109
+
110
+ /** Record a glasses-reported source app. Writes only on new app / renamed app. */
111
+ function recordSeenApp(event: NotificationAppSeenEvent): void {
112
+ if (!event.appId) return
113
+ const seen = getSeenNotificationApps()
114
+ const existing = seen.find((app) => app.appId === event.appId)
115
+ const displayName = event.displayName || existing?.displayName || event.appId
116
+ if (existing && existing.displayName === displayName) return
117
+ const next = seen
118
+ .filter((app) => app.appId !== event.appId)
119
+ .concat([{appId: event.appId, displayName, lastSeen: event.timestamp || Date.now()}])
120
+ useSettingsStore.getState().setSetting(SETTINGS.ios_notification_seen_apps.key, next)
121
+ }
122
+
30
123
  export function startPhoneNotificationsSync(): void {
31
124
  if (unsubscribe) return
32
- pushConfig() // initial sync so the listener has the current config
125
+
126
+ if (Platform.OS === "android") {
127
+ pushAndroidConfig() // initial sync so the listener has the current config
128
+ unsubscribe = useSettingsStore.subscribe(
129
+ (state) => ({
130
+ listenerEnabled: state.getSetting(SETTINGS.android_notification_listener_enabled.key),
131
+ blocklist: state.getSetting(SETTINGS.notifications_blocklist.key),
132
+ }),
133
+ () => pushAndroidConfig(),
134
+ {equalityFn: shallow},
135
+ )
136
+ return
137
+ }
138
+
139
+ if (Platform.OS !== "ios") return
140
+
141
+ pushIosConfig()
33
142
  unsubscribe = useSettingsStore.subscribe(
34
143
  (state) => ({
35
- listenerEnabled: state.getSetting(SETTINGS.android_notification_listener_enabled.key),
36
144
  blocklist: state.getSetting(SETTINGS.notifications_blocklist.key),
145
+ seenApps: state.getSetting(SETTINGS.ios_notification_seen_apps.key),
146
+ enabled: state.getSetting(SETTINGS.g2_notifications_enabled.key),
147
+ autoDisplay: state.getSetting(SETTINGS.g2_notification_auto_display.key),
148
+ dispTime: state.getSetting(SETTINGS.g2_notification_disp_time.key),
149
+ avoidDisturb: state.getSetting(SETTINGS.g2_notification_avoid_disturb.key),
37
150
  }),
38
- () => pushConfig(),
151
+ () => pushIosConfig(),
39
152
  {equalityFn: shallow},
40
153
  )
154
+ seenAppsSubscription = BluetoothSdk.addListener("notification_app_seen", recordSeenApp)
41
155
  }
42
156
 
43
157
  export function stopPhoneNotificationsSync(): void {
44
158
  unsubscribe?.()
45
159
  unsubscribe = null
160
+ seenAppsSubscription?.remove()
161
+ seenAppsSubscription = null
46
162
  }
@@ -166,7 +166,7 @@ export class PhonePhotoCoordinator {
166
166
  // to time out. Fail fast with a typed error.
167
167
  //
168
168
  // We DON'T pre-check `hasCamera` here — the canonical capability data
169
- // lives in `getModelCapabilities(deviceModel)` in the engine types and
169
+ // lives in `getModelCapabilities(deviceModel)` from @glassly/engine and
170
170
  // pulling that into this file would add a cross-package import. If a
171
171
  // cameraless device receives the BLE photo command, the glasses-side
172
172
  // handler will return a photo_response error within ~1s and the gated
@@ -796,12 +796,6 @@ export class PhoneStreamCoordinator {
796
796
  for (const reject of entry.hlsReadyRejecters) reject(pendingErr)
797
797
  entry.hlsReadyResolvers = []
798
798
  entry.hlsReadyRejecters = []
799
- // Cloudflare DELETE is fire-and-forget — failing it doesn't undo the
800
- // BLE stop we're about to do, and Cloudflare's idle-input scavenger
801
- // will eventually clean up leaked inputs.
802
- teardownManagedStream(entry.liveInputId).catch((err) => {
803
- console.warn("[STREAM] teardownManagedStream failed:", err)
804
- })
805
799
  }
806
800
 
807
801
  try {
@@ -816,6 +810,16 @@ export class PhoneStreamCoordinator {
816
810
  // Only clear if we're still the active entry (defensive — runExclusive
817
811
  // serializes us, so this should always be true).
818
812
  if (this.current === entry) this.current = null
813
+
814
+ if (entry.kind === "managed") {
815
+ // Start remote cleanup only after the publisher has stopped, but do not
816
+ // hold the local transition lock on an unbounded network request. The
817
+ // runtime's durable cleanup queue owns retries if this request fails or
818
+ // the app exits before Cloudflare finishes finalizing the recording.
819
+ void teardownManagedStream(entry.liveInputId).catch((err) => {
820
+ console.warn("[STREAM] teardownManagedStream failed:", err)
821
+ })
822
+ }
819
823
  }
820
824
  }
821
825
  }
@@ -273,6 +273,10 @@ class SceneRenderer {
273
273
  }
274
274
 
275
275
  private sendFrame(frame: SceneFrame): void {
276
+ // Single native view: only the visible surface's frames go out. Suppressed
277
+ // main frames stay committed; setDashboardActive(false) replays them.
278
+ const dashboardActive = useDisplayStore.getState().dashboardActive
279
+ if (dashboardActive !== (frame.view === "dashboard")) return
276
280
  try {
277
281
  // Frames carry the canvas they were laid out against; only a frame from
278
282
  // a path that predates the tag falls back to the current capabilities.
@@ -2,6 +2,10 @@
2
2
  * Drives RTMP stream liveliness checks via a keep-alive heartbeat:
3
3
  * `setActive(true)` starts the timer; each tick sends a keep-alive with a
4
4
  * fresh ackId; `maxMissedAcks` consecutive timeouts fire `onTimeout`.
5
+ *
6
+ * Phone-owned successor to the retired cloud stream lifecycle controller.
7
+ * It uses the local `LifecycleLogger` interface because pino is not a phone
8
+ * dependency.
5
9
  */
6
10
 
7
11
  export interface LifecycleLogger {
@@ -9,7 +9,8 @@
9
9
  * lifted alongside the canvas, bounded only to keep frames sane.
10
10
  */
11
11
  import {useGlassesStore} from "../stores/glasses"
12
- import {getModelCapabilities, type Capabilities, type DeviceTypes} from "../types"
12
+ import {DeviceTypes, getModelCapabilities, type Capabilities} from "../types"
13
+ import {isGlasslyCfwFirmwareVersion} from "./CustomDashboardModel"
13
14
 
14
15
  const CFW_MAX_TEXT_ELEMENTS = 24
15
16
  const CFW_MAX_IMAGE_ELEMENTS = 16
@@ -28,13 +29,17 @@ export function runtimeSceneCanvas(): SceneCanvasSize | null {
28
29
  export function getEffectiveCapabilities(model: DeviceTypes): Capabilities {
29
30
  const base = getModelCapabilities(model)
30
31
  const canvas = runtimeSceneCanvas()
31
- if (!base.display || !canvas) return base
32
+ // The firmware marker lifts the caps even when no canvas has been published:
33
+ // CFW classification rides the version string, and a G2 running Glassly CFW
34
+ // always rasterizes phone-side regardless of whether native reported dims.
35
+ const cfw = model === DeviceTypes.G2 && isGlasslyCfwFirmwareVersion(useGlassesStore.getState().firmwareVersion)
36
+ if (!base.display || (!canvas && !cfw)) return base
32
37
  return {
33
38
  ...base,
34
39
  display: {
35
40
  ...base.display,
36
- width: canvas.width,
37
- height: canvas.height,
41
+ width: canvas?.width ?? base.display.width,
42
+ height: canvas?.height ?? base.display.height,
38
43
  maxTextElements: Math.max(base.display.maxTextElements ?? 0, CFW_MAX_TEXT_ELEMENTS),
39
44
  maxImageElements: Math.max(base.display.maxImageElements ?? 0, CFW_MAX_IMAGE_ELEMENTS),
40
45
  },
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Seeded catalog of common iOS apps that post notifications, keyed by bundle
3
+ * id. iOS has no installed-apps enumeration — the filter UI shows this catalog
4
+ * plus apps the glasses have actually observed via ANCS — and the on-glass
5
+ * allowlist push needs known ids so a blocked app doesn't take every
6
+ * not-yet-seen app down with it (the firmware filter is allow-based).
7
+ */
8
+ export interface IosNotificationApp {
9
+ appId: string
10
+ displayName: string
11
+ }
12
+
13
+ export const IOS_NOTIFICATION_APP_CATALOG: IosNotificationApp[] = [
14
+ {appId: "com.apple.MobileSMS", displayName: "Messages"},
15
+ {appId: "com.apple.mobilephone", displayName: "Phone"},
16
+ {appId: "com.apple.mobilemail", displayName: "Mail"},
17
+ {appId: "com.apple.mobilecal", displayName: "Calendar"},
18
+ {appId: "com.apple.reminders", displayName: "Reminders"},
19
+ {appId: "com.apple.facetime", displayName: "FaceTime"},
20
+ {appId: "net.whatsapp.WhatsApp", displayName: "WhatsApp"},
21
+ {appId: "ph.telegra.Telegraph", displayName: "Telegram"},
22
+ {appId: "org.whispersystems.signal", displayName: "Signal"},
23
+ {appId: "com.google.Gmail", displayName: "Gmail"},
24
+ {appId: "com.microsoft.Office.Outlook", displayName: "Outlook"},
25
+ {appId: "com.tinyspeck.chatlyio", displayName: "Slack"},
26
+ {appId: "com.hammerandchisel.discord", displayName: "Discord"},
27
+ {appId: "com.microsoft.skype.teams", displayName: "Teams"},
28
+ {appId: "com.facebook.Messenger", displayName: "Messenger"},
29
+ {appId: "com.facebook.Facebook", displayName: "Facebook"},
30
+ {appId: "com.burbn.instagram", displayName: "Instagram"},
31
+ {appId: "com.atebits.Tweetie2", displayName: "X"},
32
+ {appId: "com.zhiliaoapp.musically", displayName: "TikTok"},
33
+ {appId: "com.toyopagroup.picaboo", displayName: "Snapchat"},
34
+ {appId: "com.linkedin.LinkedIn", displayName: "LinkedIn"},
35
+ {appId: "com.google.ios.youtube", displayName: "YouTube"},
36
+ {appId: "com.spotify.client", displayName: "Spotify"},
37
+ {appId: "com.ubercab.UberClient", displayName: "Uber"},
38
+ {appId: "com.venmo.TouchFree", displayName: "Venmo"},
39
+ ]
@@ -32,6 +32,8 @@ export const PING_INTERVAL_MS = 10_000
32
32
  export const QUERY_REPLY_TIMEOUT_MS = 6000
33
33
  /** 15s lockout on the Continue button after a BES restart, to prevent an accidental tap. */
34
34
  export const BES_CONTINUE_LOCKOUT_MS = 15_000
35
+ /** Fail closed if a BES reboot never reconnects; normal restarts complete in a fraction of this window. */
36
+ export const BES_RESTART_TIMEOUT_MS = 120_000
35
37
 
36
38
  // --- Legacy old-build (< MINIMUM_OTA_STATUS_BUILD) OTA policy (WP 8C) ---
37
39
  //
@@ -51,6 +53,7 @@ export const LEGACY_MTK_INSTALL_TIMEOUT_MS = MTK_INSTALL_TIMEOUT_MS + LEGACY_EXT
51
53
  export const LEGACY_GLOBAL_OTA_TIMEOUT_MS = GLOBAL_OTA_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS
52
54
  export const LEGACY_PING_INTERVAL_MS = PING_INTERVAL_MS + LEGACY_EXTRA_TIMEOUT_MS
53
55
  export const LEGACY_BES_CONTINUE_LOCKOUT_MS = BES_CONTINUE_LOCKOUT_MS + LEGACY_EXTRA_TIMEOUT_MS
56
+ export const LEGACY_BES_RESTART_TIMEOUT_MS = BES_RESTART_TIMEOUT_MS + LEGACY_EXTRA_TIMEOUT_MS
54
57
  /**
55
58
  * After a legacy apk `install FINISHED` observed in-flight, the legacy screen held the
56
59
  * completed state for 12s (+20s pad) so the ASG process restart settles (and fresh
@@ -122,6 +125,7 @@ export function selectOtaProtocolProfile(
122
125
  export const OtaProgressMessages = {
123
126
  stalledOrStuck: "Update may have failed. Ensure glasses have internet access and try again.",
124
127
  globalTimeout: "Update took too long. Please try again.",
128
+ restartTimeout: "Glasses did not finish restarting. Restart them before trying the update again.",
125
129
  sendOtaStartFailed: "Failed to communicate with glasses.",
126
130
  hotspotArtifactDownloadFailed: "Could not download the update to your phone. Check phone internet and try again.",
127
131
  hotspotArtifactVerifyFailed: "The update package could not be verified. Please try again later.",
@@ -29,54 +29,34 @@ const DISPLAY_COALESCE_MS = 150
29
29
 
30
30
  type DisplayEvent = Record<string, unknown> & {view?: string}
31
31
 
32
- function applyDisplayEvent(
33
- set: (partial: Partial<DisplayStore>) => void,
34
- get: () => DisplayStore,
35
- event: DisplayEvent,
36
- ) {
37
- const currentView = get().view
38
- const targetBucket = event.view === "dashboard" ? "dashboardEvent" : "mainEvent"
39
-
40
- const updates: Partial<DisplayStore> = {
41
- [targetBucket]: event,
42
- }
43
-
44
- // also update the current event if the view is the same:
45
- if (event.view === currentView) {
46
- updates.currentEvent = event
47
- }
48
-
49
- const visibleEvent = (updates.currentEvent ?? event) as DisplayEvent
50
- const textLines = extractDisplayText(visibleEvent)
32
+ function applyDisplayEvent(set: (partial: Partial<DisplayStore>) => void, event: DisplayEvent) {
33
+ const textLines = extractDisplayText(event)
51
34
  if (textLines.some((line) => line.trim() !== "")) {
52
35
  logE2EMetric("display_store_update", {
53
- view: visibleEvent.view ?? currentView,
54
- layout_type: (visibleEvent.layout as {layoutType?: string} | undefined)?.layoutType ?? "",
36
+ view: event.view ?? "main",
37
+ layout_type: (event.layout as {layoutType?: string} | undefined)?.layoutType ?? "",
55
38
  text_lines: textLines,
56
39
  })
57
40
  }
58
-
59
- set(updates)
41
+ set({currentEvent: event})
60
42
  }
61
43
 
62
44
  export interface DisplayStore {
45
+ /** The frame currently on the glasses — the mirror renders this. */
63
46
  currentEvent: any
64
- dashboardEvent: any
65
- mainEvent: any
47
+ /** True while the dashboard covers the main content (see DashboardCoordinator). */
48
+ dashboardActive: boolean
66
49
  setDisplayEvent: (event: DisplayEvent | string) => void
67
- view: string
68
- setView: (view: string) => void
50
+ setDashboardActive: (active: boolean) => void
69
51
  }
70
52
 
71
53
  let pendingDisplayEvent: DisplayEvent | null = null
72
54
  let displayCoalesceTimer: ReturnType<typeof BgTimer.setTimeout> | null = null
73
55
 
74
56
  export const useDisplayStore = create<DisplayStore>()(
75
- subscribeWithSelector((set, get) => ({
57
+ subscribeWithSelector((set) => ({
76
58
  currentEvent: {} as any,
77
- dashboardEvent: {} as any,
78
- mainEvent: {} as any,
79
- view: "main",
59
+ dashboardActive: false,
80
60
  setDisplayEvent: (event: DisplayEvent | string) => {
81
61
  const parsed: DisplayEvent = typeof event === "string" ? JSON.parse(event) : event
82
62
  pendingDisplayEvent = parsed
@@ -85,24 +65,12 @@ export const useDisplayStore = create<DisplayStore>()(
85
65
  displayCoalesceTimer = null
86
66
  const next = pendingDisplayEvent
87
67
  pendingDisplayEvent = null
88
- if (next) applyDisplayEvent(set, get, next)
68
+ if (next) applyDisplayEvent(set, next)
89
69
  }, DISPLAY_COALESCE_MS)
90
70
  },
91
- setView: (view: string) => {
92
- const currentView = get().view
93
- if (view === currentView) {
94
- return
95
- }
96
-
97
- // update the view and the currentEvent with the corresponding event:
98
- let newEvent
99
- if (view === "dashboard") {
100
- newEvent = get().dashboardEvent
101
- } else {
102
- newEvent = get().mainEvent
103
- }
104
- logE2EMetric("display_view_changed", {view})
105
- set({view, currentEvent: newEvent})
71
+ setDashboardActive: (active: boolean) => {
72
+ logE2EMetric("display_view_changed", {view: active ? "dashboard" : "main"})
73
+ set({dashboardActive: active})
106
74
  },
107
75
  })),
108
76
  )
@@ -116,6 +84,6 @@ export function flushDisplayCoalesceForTests(): void {
116
84
  if (pendingDisplayEvent) {
117
85
  const event = pendingDisplayEvent
118
86
  pendingDisplayEvent = null
119
- applyDisplayEvent(useDisplayStore.setState, useDisplayStore.getState, event)
87
+ applyDisplayEvent(useDisplayStore.setState, event)
120
88
  }
121
89
  }
@@ -165,6 +165,8 @@ const initialState: GlassesStore = {
165
165
  controllerMacAddress: "",
166
166
  controllerBatteryLevel: -1,
167
167
  controllerSignalStrength: -1,
168
+ // iOS ANCS consent (G2 consumes notifications on-firmware):
169
+ ancsAuthorized: false,
168
170
  }
169
171
 
170
172
  export const useGlassesStore = create<GlassesState>()(
@@ -140,6 +140,15 @@ export const SETTINGS: Record<string, Setting> = {
140
140
  saveOnServer: true,
141
141
  persist: true,
142
142
  },
143
+ // Tint the glasses display mirror text with the green chart color instead of
144
+ // the theme foreground. Toggled from Settings > Appearance.
145
+ mirror_green_text: {
146
+ key: "mirror_green_text",
147
+ defaultValue: () => false,
148
+ writable: true,
149
+ saveOnServer: true,
150
+ persist: true,
151
+ },
143
152
  debug_navigation_history: {
144
153
  key: "debug_navigation_history",
145
154
  defaultValue: () => false,
@@ -901,6 +910,47 @@ export const SETTINGS: Record<string, Setting> = {
901
910
  saveOnServer: true,
902
911
  persist: true,
903
912
  },
913
+ // iOS: apps observed posting notifications, reported by the glasses' ANCS
914
+ // client ({appId, displayName, lastSeen}[]). Backs the filter UI, since iOS
915
+ // has no installed-apps enumeration. Device-local by design.
916
+ ios_notification_seen_apps: {
917
+ key: "ios_notification_seen_apps",
918
+ defaultValue: () => [],
919
+ writable: true,
920
+ saveOnServer: false,
921
+ persist: true,
922
+ },
923
+ // G2 on-glass notification display behavior (NotificationControl):
924
+ g2_notifications_enabled: {
925
+ key: "g2_notifications_enabled",
926
+ defaultValue: () => true,
927
+ writable: true,
928
+ saveOnServer: true,
929
+ persist: true,
930
+ },
931
+ g2_notification_auto_display: {
932
+ key: "g2_notification_auto_display",
933
+ defaultValue: () => true,
934
+ writable: true,
935
+ saveOnServer: true,
936
+ persist: true,
937
+ },
938
+ // Popup duration in seconds; firmware accepts 0 (default), 5, 7, 10.
939
+ g2_notification_disp_time: {
940
+ key: "g2_notification_disp_time",
941
+ defaultValue: () => 5,
942
+ writable: true,
943
+ saveOnServer: true,
944
+ persist: true,
945
+ },
946
+ // Suppress popups while another on-glass feature (navigation, teleprompter) is active.
947
+ g2_notification_avoid_disturb: {
948
+ key: "g2_notification_avoid_disturb",
949
+ defaultValue: () => true,
950
+ writable: true,
951
+ saveOnServer: true,
952
+ persist: true,
953
+ },
904
954
  // Cached required version from server - used to enforce updates even when offline
905
955
  cached_required_version: {
906
956
  key: "cached_required_version",
@@ -942,8 +992,8 @@ export const BLUETOOTH_SETTING_KEYS: string[] = [
942
992
  SETTINGS.screen_disabled.key,
943
993
  SETTINGS.auth_email.key,
944
994
  SETTINGS.core_token.key,
945
- // glasses settings:
946
- SETTINGS.contextual_dashboard.key,
995
+ // glasses settings (contextual_dashboard is phone-side policy now — native
996
+ // no longer reads it):
947
997
  SETTINGS.head_up_angle.key,
948
998
  SETTINGS.brightness.key,
949
999
  SETTINGS.auto_brightness.key,
@@ -951,6 +1001,7 @@ export const BLUETOOTH_SETTING_KEYS: string[] = [
951
1001
  SETTINGS.dashboard_depth.key,
952
1002
  SETTINGS.menu_apps.key,
953
1003
  SETTINGS.calendar_events.key,
1004
+ // G2 native reads this to gate rendering while the ER firmware dashboard is open.
954
1005
  SETTINGS.use_native_dashboard.key,
955
1006
  SETTINGS.cfw_scene_renderer.key,
956
1007
  SETTINGS.twelve_hour_time.key,
@@ -1,6 +1,6 @@
1
1
  import type {ReactNode} from "react"
2
2
  /**
3
- * App/Applet types for client interfaces
3
+ * App and miniapp types owned by @glassly/engine.
4
4
  */
5
5
 
6
6
 
@@ -85,6 +85,9 @@ export const evenRealitiesG2: Capabilities = {
85
85
  // WiFi capabilities - G2 does not support WiFi
86
86
  hasWifi: false,
87
87
 
88
+ // OTA capabilities - G2 firmware updates are not driven by the ASG OTA flow
89
+ hasOta: false,
90
+
88
91
  // Dashboard - G2 renders Even Realities' native dashboard in firmware, so
89
92
  // Glassly does not manage the dashboard or expose dashboard settings for it
90
93
  hasNativeDashboard: true,
@@ -93,6 +96,4 @@ export const evenRealitiesG2: Capabilities = {
93
96
  // 576×188 overview map); magnetometer streams only while it is active
94
97
  hasNativeNavigation: true,
95
98
 
96
- // OTA capabilities - G2 firmware updates are not driven by the ASG OTA flow
97
- hasOta: false,
98
99
  }
@@ -189,6 +189,7 @@ export const HARDWARE_CAPABILITIES: Record<string, Capabilities> = {
189
189
  [mentraLive.modelName]: mentraLive,
190
190
  [simulatedGlasses.modelName]: simulatedGlasses,
191
191
  [vuzixZ100.modelName]: vuzixZ100,
192
+ [nimo.modelName]: nimo,
192
193
  [DeviceTypes.MACH1]: vuzixZ100, // Mach1 uses same Vuzix Ultralite hardware as Z100
193
194
  [nimo.modelName]: nimo,
194
195
  [DeviceTypes.AR99]: {
@@ -25,9 +25,9 @@ export {
25
25
  evenRealitiesG1,
26
26
  evenRealitiesG2,
27
27
  mentraLive,
28
+ nimo,
28
29
  vuzixZ100,
29
30
  mentraDisplay,
30
- nimo,
31
31
  } from "./hardware"
32
32
 
33
33
  // Applet types
@@ -11,7 +11,7 @@
11
11
  * 2. Merges columns line-by-line with pixel-precise space padding
12
12
  * 3. Returns a single pre-composed string ready for native to chunk & send
13
13
  *
14
- * @see cloud/issues/026-mobile-display-processor for design docs
14
+ * @see notes/miniapp-display-api-redesign.md for the current display design
15
15
  */
16
16
 
17
17
  import type {DisplayProfile} from "../profiles/types"