@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.
- package/README.md +6 -2
- package/build/engine.d.ts +7 -2
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +3 -0
- package/build/engine.js.map +1 -1
- package/build/facades/display.d.ts +10 -3
- package/build/facades/display.d.ts.map +1 -1
- package/build/facades/display.js +11 -1
- package/build/facades/display.js.map +1 -1
- package/build/facades/displayMirror.d.ts +2 -6
- package/build/facades/displayMirror.d.ts.map +1 -1
- package/build/facades/displayMirror.js +7 -14
- package/build/facades/displayMirror.js.map +1 -1
- package/build/facades/glasses.d.ts +1 -0
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +2 -0
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/ota.d.ts +3 -1
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +4 -2
- package/build/facades/ota.js.map +1 -1
- package/build/facades/phoneNotifications.d.ts +12 -2
- package/build/facades/phoneNotifications.d.ts.map +1 -1
- package/build/facades/phoneNotifications.js +30 -10
- package/build/facades/phoneNotifications.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +6 -1
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
- package/build/react/MentraLiveOtaFlow.js +136 -14
- package/build/react/MentraLiveOtaFlow.js.map +1 -1
- package/build/react/useMentraLiveOta.d.ts +5 -1
- package/build/react/useMentraLiveOta.d.ts.map +1 -1
- package/build/react/useMentraLiveOta.js +135 -58
- package/build/react/useMentraLiveOta.js.map +1 -1
- package/build/services/CustomDashboardService.d.ts +11 -10
- package/build/services/CustomDashboardService.d.ts.map +1 -1
- package/build/services/CustomDashboardService.js +70 -69
- package/build/services/CustomDashboardService.js.map +1 -1
- package/build/services/DashboardCoordinator.d.ts +26 -0
- package/build/services/DashboardCoordinator.d.ts.map +1 -0
- package/build/services/DashboardCoordinator.js +125 -0
- package/build/services/DashboardCoordinator.js.map +1 -0
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +5 -7
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/DisplayProcessor.d.ts +3 -3
- package/build/services/DisplayProcessor.js +3 -3
- package/build/services/DisplayProcessor.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts +12 -2
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +68 -14
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/OtaInstallCoordinator.d.ts +2 -0
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +21 -1
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/PhoneNotificationsSync.d.ts +13 -0
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +101 -9
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/PhonePhotoCoordinator.js +1 -1
- package/build/services/PhonePhotoCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +9 -6
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +5 -0
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/StreamLifecycleController.d.ts +4 -0
- package/build/services/StreamLifecycleController.d.ts.map +1 -1
- package/build/services/StreamLifecycleController.js +4 -0
- package/build/services/StreamLifecycleController.js.map +1 -1
- package/build/services/effectiveCapabilities.d.ts +1 -1
- package/build/services/effectiveCapabilities.d.ts.map +1 -1
- package/build/services/effectiveCapabilities.js +9 -4
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/build/services/iosNotificationAppCatalog.d.ts +13 -0
- package/build/services/iosNotificationAppCatalog.d.ts.map +1 -0
- package/build/services/iosNotificationAppCatalog.js +28 -0
- package/build/services/iosNotificationAppCatalog.js.map +1 -0
- package/build/services/otaInstallPolicy.d.ts +4 -0
- package/build/services/otaInstallPolicy.d.ts.map +1 -1
- package/build/services/otaInstallPolicy.js +4 -0
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/build/stores/display.d.ts +4 -4
- package/build/stores/display.d.ts.map +1 -1
- package/build/stores/display.js +12 -36
- package/build/stores/display.js.map +1 -1
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js +2 -0
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +53 -2
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +1 -1
- package/build/types/applet.js.map +1 -1
- package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
- package/build/types/capabilities/even-realities-g2.js +2 -2
- package/build/types/capabilities/even-realities-g2.js.map +1 -1
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +1 -0
- package/build/types/hardware.js.map +1 -1
- package/build/types/index.d.ts +1 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/index.js +1 -1
- package/build/types/index.js.map +1 -1
- package/build/utils/display/composer/ColumnComposer.d.ts +1 -1
- package/build/utils/display/composer/ColumnComposer.js +1 -1
- package/build/utils/display/composer/ColumnComposer.js.map +1 -1
- package/package.json +8 -7
- package/src/engine.ts +3 -0
- package/src/facades/display.ts +11 -1
- package/src/facades/displayMirror.ts +7 -15
- package/src/facades/glasses.ts +2 -0
- package/src/facades/ota.ts +4 -2
- package/src/facades/phoneNotifications.ts +32 -10
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +2 -1
- package/src/react/MentraLiveOtaFlow.tsx +226 -41
- package/src/react/useMentraLiveOta.ts +146 -60
- package/src/services/CustomDashboardService.ts +67 -66
- package/src/services/DashboardCoordinator.ts +121 -0
- package/src/services/DeviceEventRouter.ts +5 -7
- package/src/services/DisplayProcessor.ts +3 -3
- package/src/services/LocalDisplayManager.ts +68 -16
- package/src/services/OtaInstallCoordinator.ts +21 -0
- package/src/services/PhoneNotificationsSync.ts +125 -9
- package/src/services/PhonePhotoCoordinator.ts +1 -1
- package/src/services/PhoneStreamCoordinator.ts +10 -6
- package/src/services/SceneRenderer.ts +4 -0
- package/src/services/StreamLifecycleController.ts +4 -0
- package/src/services/effectiveCapabilities.ts +9 -4
- package/src/services/iosNotificationAppCatalog.ts +39 -0
- package/src/services/otaInstallPolicy.ts +4 -0
- package/src/stores/display.ts +16 -48
- package/src/stores/glasses.ts +2 -0
- package/src/stores/settings.ts +53 -2
- package/src/types/applet.ts +1 -1
- package/src/types/capabilities/even-realities-g2.ts +3 -2
- package/src/types/hardware.ts +1 -0
- package/src/types/index.ts +1 -1
- package/src/utils/display/composer/ColumnComposer.ts +1 -1
|
@@ -1,24 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Phone-notifications sync — engine-owned.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
() =>
|
|
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)`
|
|
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
|
|
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
|
-
|
|
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.",
|
package/src/stores/display.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
54
|
-
layout_type: (
|
|
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
|
-
|
|
65
|
-
|
|
47
|
+
/** True while the dashboard covers the main content (see DashboardCoordinator). */
|
|
48
|
+
dashboardActive: boolean
|
|
66
49
|
setDisplayEvent: (event: DisplayEvent | string) => void
|
|
67
|
-
|
|
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
|
|
57
|
+
subscribeWithSelector((set) => ({
|
|
76
58
|
currentEvent: {} as any,
|
|
77
|
-
|
|
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,
|
|
68
|
+
if (next) applyDisplayEvent(set, next)
|
|
89
69
|
}, DISPLAY_COALESCE_MS)
|
|
90
70
|
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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,
|
|
87
|
+
applyDisplayEvent(useDisplayStore.setState, event)
|
|
120
88
|
}
|
|
121
89
|
}
|
package/src/stores/glasses.ts
CHANGED
|
@@ -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>()(
|
package/src/stores/settings.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/types/applet.ts
CHANGED
|
@@ -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
|
}
|
package/src/types/hardware.ts
CHANGED
|
@@ -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]: {
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
|
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"
|