@glassly/engine 0.3.5-dev.841 → 0.3.5-dev.843

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.
@@ -27,6 +27,13 @@ import {useSettingsStore, SETTINGS} from "../stores/settings"
27
27
  import {hasDefaultDevice} from "../services/DeviceStoreHydration"
28
28
  import micStateCoordinator from "../services/MicStateCoordinator"
29
29
  import {projectPairingIdentity} from "../services/PairingIdentity"
30
+ import {
31
+ fixtureControllerList,
32
+ fixtureControllerStatuses,
33
+ fixtureInfo,
34
+ fixtureStatus,
35
+ isScreenshotMode,
36
+ } from "../services/ScreenshotFixture"
30
37
  import {isGlassesConnected, isGlassesReady} from "../services/GlassesReadiness"
31
38
  import {pushAllBluetoothSettings} from "../services/GlassesSettingsSync"
32
39
  import {isControllerReady, pairedControllersFrom, readyControllers} from "../services/controllers"
@@ -43,25 +50,33 @@ import {glassesSettings} from "./glassesSettings"
43
50
 
44
51
  let micLevelSubscriptions = 0
45
52
 
53
+ // The screenshot fixture (ScreenshotFixture.ts) dresses these read-models
54
+ // while the identity is the simulated device; it is a no-op outside that build.
46
55
  function projectStatus() {
47
56
  const s = useGlassesStore.getState()
48
- return {
49
- state: s.connection.state,
50
- fullyBooted: isGlassesReady(s.connection),
51
- battery: s.batteryLevel,
52
- charging: s.charging,
53
- case: {battery: s.caseBatteryLevel, charging: s.caseCharging, open: s.caseOpen, removed: s.caseRemoved},
54
- signal: s.signalStrength,
55
- micEnabled: s.micEnabled,
56
- vadEnabled: s.voiceActivityDetectionEnabled,
57
- btClassic: s.bluetoothClassicConnected,
58
- // iOS: glasses may read the phone's ANCS stream ("Share System Notifications").
59
- ancsAuthorized: Boolean(s.ancsAuthorized),
60
- }
57
+ return fixtureStatus(
58
+ {
59
+ state: s.connection.state,
60
+ fullyBooted: isGlassesReady(s.connection),
61
+ battery: s.batteryLevel,
62
+ charging: s.charging,
63
+ case: {battery: s.caseBatteryLevel, charging: s.caseCharging, open: s.caseOpen, removed: s.caseRemoved},
64
+ signal: s.signalStrength,
65
+ micEnabled: s.micEnabled,
66
+ vadEnabled: s.voiceActivityDetectionEnabled,
67
+ btClassic: s.bluetoothClassicConnected,
68
+ // iOS: glasses may read the phone's ANCS stream ("Share System Notifications").
69
+ ancsAuthorized: Boolean(s.ancsAuthorized),
70
+ },
71
+ projectPairingIdentity(),
72
+ )
61
73
  }
62
74
 
63
75
  function projectControllerList(): PairedController[] {
64
- return pairedControllersFrom(useSettingsStore.getState().getSetting(SETTINGS.controllers.key))
76
+ return fixtureControllerList(
77
+ pairedControllersFrom(useSettingsStore.getState().getSetting(SETTINGS.controllers.key)),
78
+ projectPairingIdentity(),
79
+ )
65
80
  }
66
81
 
67
82
  // Copies: snapshots must not hand callers mutable references into the store.
@@ -70,7 +85,27 @@ function projectControllerStatuses(): Record<string, ControllerStatus> {
70
85
  for (const [id, status] of Object.entries(useGlassesStore.getState().controllers ?? {})) {
71
86
  statuses[id] = {...status}
72
87
  }
73
- return statuses
88
+ return fixtureControllerStatuses(statuses, projectPairingIdentity())
89
+ }
90
+
91
+ // Dedupe on the projected snapshot: the store updates on many fields; fire
92
+ // only when the projection changes. The fixture's read-models also follow the
93
+ // identity (a settings key), so that build watches the settings store too.
94
+ function subscribeProjection<T>(project: () => T, cb: (snapshot: T) => void): () => void {
95
+ let last = JSON.stringify(project())
96
+ const notify = () => {
97
+ const snap = project()
98
+ const key = JSON.stringify(snap)
99
+ if (key === last) return
100
+ last = key
101
+ cb(snap)
102
+ }
103
+ const unsubscribeGlasses = useGlassesStore.subscribe(notify)
104
+ const unsubscribeSettings = isScreenshotMode() ? useSettingsStore.subscribe(notify) : () => {}
105
+ return () => {
106
+ unsubscribeGlasses()
107
+ unsubscribeSettings()
108
+ }
74
109
  }
75
110
 
76
111
  function projectKeyboardStatus() {
@@ -89,6 +124,10 @@ function projectKeyboardStatus() {
89
124
 
90
125
  function projectInfo() {
91
126
  const s = useGlassesStore.getState()
127
+ return fixtureInfo(projectRawInfo(s), projectPairingIdentity())
128
+ }
129
+
130
+ function projectRawInfo(s: ReturnType<typeof useGlassesStore.getState>) {
92
131
  return {
93
132
  model: s.deviceModel,
94
133
  style: s.style,
@@ -191,29 +230,9 @@ export const glasses = {
191
230
 
192
231
  // --- read-model (projected from the engine-owned glasses store) ---
193
232
  status: (): GlassesStatusSnapshot => projectStatus(),
194
- onStatus: (cb: (status: GlassesStatusSnapshot) => void): (() => void) => {
195
- // Dedupe: the glasses store updates on many fields; only fire when the
196
- // projected status snapshot actually changes.
197
- let last = JSON.stringify(projectStatus())
198
- return useGlassesStore.subscribe(() => {
199
- const snap = projectStatus()
200
- const key = JSON.stringify(snap)
201
- if (key === last) return
202
- last = key
203
- cb(snap)
204
- })
205
- },
233
+ onStatus: (cb: (status: GlassesStatusSnapshot) => void): (() => void) => subscribeProjection(projectStatus, cb),
206
234
  info: (): GlassesInfoSnapshot => projectInfo(),
207
- onInfo: (cb: (info: GlassesInfoSnapshot) => void): (() => void) => {
208
- let last = JSON.stringify(projectInfo())
209
- return useGlassesStore.subscribe(() => {
210
- const snap = projectInfo()
211
- const key = JSON.stringify(snap)
212
- if (key === last) return
213
- last = key
214
- cb(snap)
215
- })
216
- },
235
+ onInfo: (cb: (info: GlassesInfoSnapshot) => void): (() => void) => subscribeProjection(projectInfo, cb),
217
236
  capabilities: () => getModelCapabilities(useGlassesStore.getState().deviceModel as DeviceTypes),
218
237
  /** Ask the glasses to report fresh firmware/version info (updates the store). */
219
238
  requestVersionInfo: () => BluetoothSdk.requestVersionInfo(),
@@ -284,14 +303,11 @@ export const glasses = {
284
303
  statuses: (): Record<string, ControllerStatus> => projectControllerStatuses(),
285
304
  status: (id: string): ControllerStatus | undefined => projectControllerStatuses()[id],
286
305
  onStatus: (cb: (statuses: Record<string, ControllerStatus>) => void): (() => void) =>
287
- useGlassesStore.subscribe(
288
- (s) => s.controllers,
289
- () => cb(projectControllerStatuses()),
290
- ),
306
+ subscribeProjection(projectControllerStatuses, cb),
291
307
  /** Controllers delivering input (linked, route or claim done). */
292
308
  connected: (): ControllerStatus[] => readyControllers(projectControllerStatuses()),
293
309
  isConnected: (model?: string): boolean =>
294
- Object.values(useGlassesStore.getState().controllers ?? {}).some(
310
+ Object.values(projectControllerStatuses()).some(
295
311
  (status) => isControllerReady(status) && (model === undefined || status.model === model),
296
312
  ),
297
313
  connect: (id: string): Promise<void> => BluetoothSdk.connectController(id),
@@ -21,6 +21,7 @@ import {
21
21
  subscribePairingIdentity,
22
22
  type IdentitySnapshot,
23
23
  } from "../services/PairingIdentity"
24
+ import {fixtureIdentity} from "../services/ScreenshotFixture"
24
25
  import {isGlassesConnected, isGlassesLinkLayerBusy, isGlassesReady} from "../services/GlassesReadiness"
25
26
  import {
26
27
  logAutomaticReportSubmissionStatus,
@@ -278,10 +279,11 @@ export const pairing = {
278
279
  // --- pairing-identity lifecycle (the PairingIdentity read-model + the JS-owned
279
280
  // identity writes; promotion to `paired` only ever happens natively) ---
280
281
  /** The identity lifecycle snapshot: none | pending (chosen, never paired) | paired. */
281
- identity: (): IdentitySnapshot => projectPairingIdentity(),
282
+ identity: (): IdentitySnapshot => fixtureIdentity(projectPairingIdentity()),
282
283
  /** Subscribe to identity changes; fires only when the projected snapshot changes.
283
284
  * Returns an unsubscribe. */
284
- onIdentity: (cb: (identity: IdentitySnapshot) => void): (() => void) => subscribePairingIdentity(cb),
285
+ onIdentity: (cb: (identity: IdentitySnapshot) => void): (() => void) =>
286
+ subscribePairingIdentity((identity) => cb(fixtureIdentity(identity))),
285
287
  /** Mark the chosen model as the pending selection (the scan-entry write); the
286
288
  * host renders it as a finish-pairing affordance until pairing succeeds. */
287
289
  markPendingSelection: (model: string) => markPendingSelection(model),
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "0.3.5",
15
- "releaseIdentity": "0.3.5-dev.841",
16
- "releaseSetId": "glassly-0.3.5-dev.841",
17
- "sourceCommit": "4908f6a381a9bd658ae29557b5ba0000bc1ee780",
15
+ "releaseIdentity": "0.3.5-dev.843",
16
+ "releaseSetId": "glassly-0.3.5-dev.843",
17
+ "sourceCommit": "85bd06271a34f9bb21c4dc7b3c75cb4393c08aba",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Screenshot fixture — dresses Phone Mode up as a paired Even Realities G2
3
+ * with a Meta Neural Band and an R1 ring, for the store-screenshot build only
4
+ * (EXPO_PUBLIC_SCREENSHOT_MODE=1, set by scripts/maestro-screenshots-*.mjs
5
+ * for its own Release build and never by a shipped one).
6
+ *
7
+ * The fixture is applied where hosts READ — engine.pairing.identity and
8
+ * engine.glasses.status/info/controllers.* — not where native writes: the
9
+ * Simulated driver keeps running underneath (so the mirror has frames),
10
+ * nothing is persisted, and native never learns of the fixture, so no
11
+ * reconnect, demotion or driver path can act on it. It is active only while
12
+ * the underlying identity is the simulated device: home enters Phone Mode as
13
+ * usual (identity `none` passes through untouched), the simulated pairing
14
+ * then flips the read-models to the fixture, and real glasses on a fixture
15
+ * build stay real.
16
+ */
17
+ import type {ControllerStatus, PairedController} from "@glassly/bluetooth-sdk"
18
+
19
+ import {ControllerTypes, DeviceTypes} from "../types/enums"
20
+ import type {IdentitySnapshot} from "./PairingIdentity"
21
+
22
+ export function isScreenshotMode(): boolean {
23
+ return process.env.EXPO_PUBLIC_SCREENSHOT_MODE === "1"
24
+ }
25
+
26
+ export const FIXTURE_GLASSES = {
27
+ model: DeviceTypes.G2,
28
+ name: "Even G2_7F3A",
29
+ battery: 87,
30
+ caseBattery: 100,
31
+ signal: -58,
32
+ } as const
33
+
34
+ interface FixtureController extends PairedController {
35
+ batteryLevel: number
36
+ }
37
+
38
+ // The R1's id is its ring id (from the BLE name); the band's is an opaque
39
+ // peripheral id. Both are stable fixtures so flows can target
40
+ // `controller-tile-<id>`.
41
+ export const FIXTURE_CONTROLLERS: readonly FixtureController[] = [
42
+ {id: "7C1A2B9E", model: ControllerTypes.NEURAL_BAND, name: "Meta Neural Band", batteryLevel: 73},
43
+ {id: "CEC5BA", model: ControllerTypes.R1, name: "CEC5BA", batteryLevel: 58},
44
+ ]
45
+
46
+ /** The fixture dresses Phone Mode only. */
47
+ export function fixtureActive(identity: IdentitySnapshot): boolean {
48
+ return isScreenshotMode() && identity.kind === "paired" && identity.model.includes(DeviceTypes.SIMULATED)
49
+ }
50
+
51
+ export function fixtureIdentity(identity: IdentitySnapshot): IdentitySnapshot {
52
+ if (!fixtureActive(identity)) return identity
53
+ return {kind: "paired", model: FIXTURE_GLASSES.model, name: FIXTURE_GLASSES.name}
54
+ }
55
+
56
+ interface StatusLike {
57
+ battery: number
58
+ charging: boolean
59
+ case: {battery: number; charging: boolean; open: boolean; removed: boolean}
60
+ signal: number
61
+ }
62
+
63
+ export function fixtureStatus<T extends StatusLike>(status: T, identity: IdentitySnapshot): T {
64
+ if (!fixtureActive(identity)) return status
65
+ return {
66
+ ...status,
67
+ battery: FIXTURE_GLASSES.battery,
68
+ charging: false,
69
+ case: {battery: FIXTURE_GLASSES.caseBattery, charging: false, open: false, removed: false},
70
+ signal: FIXTURE_GLASSES.signal,
71
+ }
72
+ }
73
+
74
+ interface InfoLike {
75
+ model: string
76
+ firmwareUpdateAvailable: boolean
77
+ }
78
+
79
+ export function fixtureInfo<T extends InfoLike>(info: T, identity: IdentitySnapshot): T {
80
+ if (!fixtureActive(identity)) return info
81
+ return {...info, model: FIXTURE_GLASSES.model, firmwareUpdateAvailable: false}
82
+ }
83
+
84
+ /** Fixture controllers first, then any real records that are not the fixture's. */
85
+ export function fixtureControllerList(list: PairedController[], identity: IdentitySnapshot): PairedController[] {
86
+ if (!fixtureActive(identity)) return list
87
+ const fixture = FIXTURE_CONTROLLERS.map(({id, model, name}) => ({id, model, name}))
88
+ const ids = new Set(fixture.map((record) => record.id))
89
+ return [...fixture, ...list.filter((record) => !ids.has(record.id))]
90
+ }
91
+
92
+ export function fixtureControllerStatuses(
93
+ statuses: Record<string, ControllerStatus>,
94
+ identity: IdentitySnapshot,
95
+ ): Record<string, ControllerStatus> {
96
+ if (!fixtureActive(identity)) return statuses
97
+ const result = {...statuses}
98
+ for (const {id, model, name, batteryLevel} of FIXTURE_CONTROLLERS) {
99
+ result[id] = {
100
+ id,
101
+ model,
102
+ name,
103
+ connected: true,
104
+ fullyBooted: true,
105
+ searching: false,
106
+ batteryLevel,
107
+ charging: false,
108
+ macAddress: "",
109
+ signalStrength: -62,
110
+ }
111
+ }
112
+ return result
113
+ }
@@ -3,6 +3,7 @@ import {AsyncResult, result as Res, Result} from "typesafe-ts"
3
3
  import {create} from "zustand"
4
4
  import {subscribeWithSelector} from "zustand/middleware"
5
5
 
6
+ import {isScreenshotMode} from "../services/ScreenshotFixture"
6
7
  import {storage} from "../utils/storage"
7
8
  import {getBluetoothSettingKeysForDevice, nativeMenuApps} from "./bluetoothSettingKeys"
8
9
  import {useGlassesStore} from "./glasses"
@@ -144,6 +145,8 @@ export const SETTINGS: Record<string, Setting> = {
144
145
  home_mirror: {
145
146
  key: "home_mirror",
146
147
  defaultValue: () => true,
148
+ // The store-screenshot build always shows the mirror under its fixture G2.
149
+ override: () => (isScreenshotMode() ? true : undefined),
147
150
  writable: true,
148
151
  saveOnServer: true,
149
152
  persist: true,