D20: requirement UI-1 (status in the main tab). D21: failing start-up hook prepare() in the platform. D22: modes incl. --desktop with optional SteamVR, --desktop-novr, --config. D23: one class per HA entity type, native types preferred, own JSON service only as fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.3 KiB
Design
Status: in progress, iteration 1, phase 2 (planning and design). High-level architecture first, then the details for the MVP. Requirements and their decisions (D1–D15): REQUIREMENTS.md. Design decisions start at D16.
Process and threads
- One process started by SteamVR does everything: HA connection, sensors, services, all overlays (D16).
- Several threads inside it for concurrent work. Which work runs in which thread is still open.
Layers
Three layers; dependencies only go downward (D17).
Presentation QML (view and controller)
▼
Business logic entities, services, managers
▼
I/O HA connection (MQTT), SteamVR, system (network, audio)
- Qt Quick already enforces separating presentation from logic.
- The overlay platform (
vrdashboard_core) spans two layers: overlays, rendering and input are presentation infrastructure; SteamVR access for sensors and actions is I/O.
Start-up and modes
-
Start-up hook (D21): the platform (
DashboardApplicationinvrdashboard_core) gets a third hook, e.g.virtual bool prepare(QString* error).exec()calls it once mode and app key are known, before the QML engine, the surfaces andVR_Init. Onfalse,exec()ends with exit code 1 and reports the error through the existing path (log and stderr). The default returnstrue, so other apps built on the template don't change. FrameAssistant loads its configuration there and decides frommode()whether it needs it (CFG-2). -
Modes (D22):
Mode Configuration + HA connection SteamVR overlay (default) yes required (overlay app) --desktopyes connects as background app if SteamVR runs; otherwise VR entities are unavailable (SEN-15) --desktop-novryes never --snapshot,--benchmarkno no --desktopon the Frame over RDP shows the live UI with real data without wearing the headset. Stop the SteamVR-started instance by hand first: two instances with the same device name would knock each other off the broker.--config <path>loads another configuration file instead of~/.config/frameassistant/config.json, e.g. on a PC with its own device name (CON-5).
Object classes
The app is HA-centric, so its building blocks follow HA's concepts (D18). Objects talk to each other through Qt signals and slots (Qt's observer pattern). Senders don't know their receivers, and signals across threads are delivered through the receiver's event loop.
| Class | Role | MVP |
|---|---|---|
| HA entity | One HA entity, both directions. Entity → HA: state and JSON attributes. HA → entity: commands; HA's MQTT integration turns its own services into them (e.g. button.press, switch.turn_on, number.set_value, notify.send_message). Observes value signals of other objects and publishes them; turns MQTT commands into signals other objects act on. Native entity types are preferred wherever their meaning fits (D23). |
✓ |
| HA service | Fallback where no entity type fits (D23): a JSON command on an own MQTT command topic (HA side: the standard mqtt.publish action), e.g. rich notifications or vibration with parameters. Results go back asynchronously, e.g. through an event entity. How the user wraps it in HA (scripts, automations, pyscript) is their choice; FrameAssistant only requires stock HA with MQTT (NFR-6). |
depends on the NOT-1 choice |
| HA device | Owns the MQTT connection, discovery and availability, and owns all entity and service objects. One per headset (CON-5). | ✓ |
| I/O wrappers | Data sources and actuators; know nothing about HA. One wrapper object per OpenVR interface used (e.g. IVRSystem, IVRApplications, IVRChaperone, IVRSettings). System wrappers (network, audio) later. |
OpenVR ✓, system ✗ |
| Overlays | View and controller of MVC (QML), as dashboard tabs or world overlays. Get their data (the model) from other objects. | ✓ |
| Managers | One object per stateful feature that manages and coordinates it, using entities, services and overlays. Notification manager (MVP); status card manager (SRV-6/7) later. | notifications ✓ |
| HA web UI | Encapsulates Qt WebEngine for the dashboard (Q2). | ✗ |
| Configuration | Settings from the JSON file, loaded once by the app as an immutable value. Each component gets the part it needs through its constructor (D19). | ✓ |
| App | The managing object: owns all of the above as components and wires them together. | ✓ |
Entities and services (D23)
-
One class per HA entity type on a common base. The base does the discovery part, topics, availability and throttling, so SEN-14, SEN-15 and NFR-7 are handled in one place.
-
A concrete entity is an instance with a description (id, name, device class, unit, throttle interval), not a class of its own.
-
Our actions map to native types where the meaning fits:
Action Type HA service Sensors (SEN-*) sensor,binary_sensor— Notification (NOT-1) notifynotify.send_messageScreenshot, standby, quit app, show boundary (SRV-1/2/3) buttonbutton.pressDo-not-disturb (SRV-5) switchswitch.turn_on/offVolume (SRV-3) numbernumber.set_valueLaunch app by key (SRV-2) texttext.set_valueAttention signal with duration/volume (SRV-1), to check sirensiren.turn_onFeedback (NOT-5, NOT-10) event— -
Wiring: the app wires simple entities (I/O wrapper signal ↔ entity). Managers get their entities and services from the HA device and wire them internally. The HA device owns them all (D18).
-
MQTT
notifydrops the title: HA'sasync_send_message()renders onlymessageinto the payload (homeassistant/components/mqtt/notify.py). NOT-1 needs title and text; options (MVP detail): first line is the title, JSON in the message, or an own service.
Notes for later:
- OpenVR delivers all events through one queue; one dispatcher (today
VrRuntime) hands them to the wrappers. - OpenVR is not documented as thread-safe (relevant for the thread decision).
Open
- High-level points 3–7 (see PROGRESS.md, "Next steps").
Decisions
| # | Date | Decision | Reason |
|---|---|---|---|
| D16 | 2026-10-05 | One process started by SteamVR does everything; several threads inside it for concurrent work | Meets NFR-2 without extra work, no IPC; the platform already supports several surfaces per process |
| D17 | 2026-10-05 | Three layers: presentation (QML), business logic, I/O. The overlay platform spans presentation (overlays, rendering, input) and I/O (SteamVR access) | Simple and enough for the start; Qt Quick already enforces separating presentation from logic |
| D18 | 2026-10-05 | HA-centric object classes: HA entity, HA service, HA device, I/O wrappers, overlays, managers, HA web UI, configuration, app; communication through signals and slots | The app is HA-centric; signals/slots decouple objects and work across threads |
| D19 | 2026-10-05 | Configuration is injected: the app loads it once as an immutable value and passes each component the part it needs; no singleton | Explicit dependencies, trivial tests with own configs, thread-safe; the app creates all components anyway |
| D21 | 2026-10-05 | Start-up hook in the platform that may fail (prepare()), before the engine, surfaces and VR_Init; failure → exit code 1 |
CFG-2 needs a clean abort; loading()/started() can't fail; backward compatible |
| D22 | 2026-10-05 | Overlay and both desktop modes load the configuration and connect to HA; --desktop also connects to SteamVR as a background app if it runs; --desktop-novr never does; --snapshot/--benchmark stay without configuration; new option --config <path> |
Live development over RDP on the Frame with real data; PC development without SteamVR under its own device name |
| D23 | 2026-10-05 | One class per HA entity type on a common base; concrete entities are described instances. Native entity types (and the HA services the MQTT integration provides for them) are preferred; an own JSON command service only as fallback | Mirrors HA's model; discovery, availability and throttling in one place; actions are visible and selectable in HA without mqtt.publish |