diff --git a/CLAUDE.md b/CLAUDE.md index 8dacb34..823f6cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co it reports headset/environment sensors to Home Assistant, offers services Home Assistant can call (e.g. notifications in the headset), and shows a Home Assistant dashboard as a SteamVR dashboard tab. Status: the overlay platform works; the Home Assistant parts are being built -(implementation iteration 1: settings file and the "HA Dashboard" web UI tab). +(implementation iteration 1: settings file; the web UI tab was dropped, see REQUIREMENTS.md D40). **Start every session by reading `docs/PROGRESS.md`** (current state, next steps, log). @@ -61,7 +61,7 @@ cmake --preset native-debug && cmake --build --preset native-debug # system Qt ctest --preset native-debug # all tests build/native-debug/bin/tst_inputbridge clickUsesGlCoordinates # one QtTest function build/native-debug/bin/frame-assistant --desktop # UI preview in a window, no SteamVR -build/native-debug/bin/frame-assistant --snapshot x.png # x-0.png (main tab), x-dashboard.png +build/native-debug/bin/frame-assistant --snapshot x.png # one frame via the real offscreen path scripts/container-build.sh cross # x86_64 → aarch64 against the Frame's system Qt (default) scripts/container-build.sh native # same for the host arch (use on the Frame) scripts/container-build.sh cross-bundle # fallback: self-contained, Qt bundled (also native-bundle) @@ -72,9 +72,8 @@ build/native-debug/bin/frame-assistant --graphics-api vulkan --benchmark 600 # ``` - Use `--snapshot` and then read the PNGs to check UI or rendering changes without a headset. - With several surfaces it writes `-.png` per surface (`-0` for the main tab's - empty id). Snapshot and benchmark don't load the settings, so the "HA Dashboard" tab shows - its "no URL" hint there. + With several surfaces it writes `-.png` per surface (`-0` for an empty id). + Snapshot and benchmark don't load the settings. - Overlay and desktop mode need a settings file; pass a test one with `--config `. - `tst_render` runs once per backend (`VRDASH_TEST_API`, separate CTest entries, because the Qt graphics API is process-global). It QSKIPs without a GPU context or when Vulkan isn't @@ -198,16 +197,16 @@ into Qt events. `create()` (e.g. give the constructor a required `QObject* parent` and init it `{nullptr}`). - **App code layout:** logic without SteamVR or rendering goes into the static library `frameassistant_lib` (`src/config/`, …), which the app and its tests link. QML types - (`QML_ELEMENT`, e.g. `src/ha/HaWebUi`) go into `QML_SOURCES` of `vrdash_add_dashboard()`: + (`QML_ELEMENT`) go into `QML_SOURCES` of `vrdash_add_dashboard()`: they must be part of the QML module. The generated type registration includes their headers by file name only, so their directory is on the app's include path. - **Settings** (`src/config/Config`) are loaded once in `Dashboard::prepare()` (overlay and desktop mode) and passed on as an immutable value (DESIGN.md, D19). -- **Qt WebEngine** ("HA Dashboard" tab): `QtWebEngineQuick::initialize()` runs in `main()` - before the app object, because it must precede the `QGuiApplication`. `HaWebUi` creates a - persistent profile under `~/.local/share/frameassistant/webengine` with the Qt 6.8 API - (setters on `QQuickWebEngineProfile`). Qt ≥ 6.9 logs a hint to use - `WebEngineProfilePrototype` instead, which the Frame's Qt 6.8 doesn't have. +- **No Qt WebEngine** (REQUIREMENTS.md, D40): the Frame's system WebEngine can't be loaded, and bundling our own + failed (STEAM-FRAME.md). Don't link it: the whole app would fail to start on the Frame. +- **Check Frame libraries by loading, not by file:** before relying on a system library, check + `ldd | grep "not found"` and that its package version matches the rest of Qt + (`pacman -Q | grep qt6-`). - **moc and raw string literals:** a raw string containing `//` (e.g. a URL) makes moc treat the rest as a comment; it then misses `Q_OBJECT` classes in that file (link error "vtable for …"). In moc'ed files, write such text as normal string literals (`tst_config` uses @@ -333,5 +332,3 @@ Check these when testing on the device, and update this list: - The restructured app code (base class `DashboardApplication`, `Surface`, `Overlay` base, active-surface navigation) has only been tested off the device so far. - Several dashboard tabs from one process (works in `tst_surfaces`, not tried in SteamVR). -- Qt WebEngine in the off-screen renderer (Vulkan and OpenGL), the SteamVR keyboard in web - page input fields, and the persistent login ("HA Dashboard" tab, DSH-1). diff --git a/CMakePresets.json b/CMakePresets.json index b83f485..dd55c75 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -41,8 +41,7 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", "VRDASH_DEPLOY_QT": "ON", - "VRDASH_BUILD_TESTS": "OFF", - "VRDASH_WEBENGINE_PROCESS": "/opt/qt6-arm64/usr/lib/qt6/libexec/QtWebEngineProcess" + "VRDASH_BUILD_TESTS": "OFF" } }, { diff --git a/README.md b/README.md index 97c5386..517dc06 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,7 @@ the headset to Home Assistant in both directions: - **Dashboard:** a SteamVR dashboard tab to view and control Home Assistant from inside VR. > **Status:** early development. The overlay platform works on the Frame. The Home Assistant -> parts are being built: so far a settings file and an "HA Dashboard" tab with Home -> Assistant's web UI. See [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md), +> parts are being built: so far the settings file. See [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md), > [docs/DESIGN.md](docs/DESIGN.md) and [docs/PROGRESS.md](docs/PROGRESS.md). ### Platform @@ -34,8 +33,8 @@ FrameAssistant grew out of a SteamVR dashboard-overlay template, which it still ## Quick start Requirements: -- CMake ≥ 3.22, Ninja, a C++20 compiler, and Qt ≥ 6.5 (Quick, QuickControls2, OpenGL, - WebEngineQuick; Test for the tests) +- CMake ≥ 3.22, Ninja, a C++20 compiler, and Qt ≥ 6.5 (Quick, QuickControls2, OpenGL; + Test for the tests) - Vulkan headers (optional; without them the build is OpenGL only) - Docker or Podman, for the Frame builds @@ -47,7 +46,7 @@ cmake --preset native-debug && cmake --build --preset native-debug ctest --preset native-debug build/native-debug/bin/frame-assistant --desktop # UI in windows, no SteamVR -build/native-debug/bin/frame-assistant --snapshot out.png # one frame per tab through the VR path +build/native-debug/bin/frame-assistant --snapshot out.png # one frame through the VR path build/native-debug/bin/frame-assistant # as dashboard tab (SteamVR running) build/native-debug/bin/frame-assistant --install-manifest # register + auto-start with SteamVR ``` @@ -89,15 +88,12 @@ FrameAssistant reads its settings from `~/.config/frameassistant/config.json` | `mqtt.tls.ca_file` | no | system certificates | CA of the broker's certificate | | `mqtt.tls.cert_file`, `mqtt.tls.key_file` | no | | Client certificate | | `homeassistant.discovery_prefix` | no | `homeassistant` | | -| `homeassistant.url` | no | | Home Assistant web UI for the "HA Dashboard" tab | +| `homeassistant.url` | no | | Home Assistant's address; reserved for the dashboard, not used yet | | `notifications.timeout_s` | no | 10 | How long notifications stay | Invalid or missing settings stop the app with an error message and exit code 1. Unknown keys only produce a warning. Keep the file private (`chmod 600`) when it holds a password. -The "HA Dashboard" tab keeps the Home Assistant login in -`~/.local/share/frameassistant/webengine`. - ## Development - **App identity** (app key `net.matzman.frameassistant`, tab name, executable name @@ -149,7 +145,6 @@ VK_DRIVER=lvp scripts/container-test.sh # same on the lavapipe software driver |------|---------| | `src/main.cpp`, `src/Dashboard.*`, `src/qml/` | **The app** | | `src/config/` | Settings file (`frameassistant_lib`) | -| `src/ha/` | Home Assistant parts: `HaWebUi` (web UI tab) | | `src/app/` | `DashboardApplication` (app base class), `Surface` (one view) | | `src/render/` | Off-screen Qt Quick renderer; Vulkan and OpenGL backends | | `src/vr/` | SteamVR connection, overlays (dashboard tab) | @@ -162,6 +157,5 @@ VK_DRIVER=lvp scripts/container-test.sh # same on the lavapipe software driver ## License -MIT, see [LICENSE](LICENSE). Third-party components (OpenVR SDK, Qt incl. Qt WebEngine; in -bundled builds also their dependencies) are listed in -[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). +MIT, see [LICENSE](LICENSE). Third-party components (OpenVR SDK, Qt; in bundled builds also +their dependencies) are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 0a21361..a25d79f 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -7,7 +7,6 @@ This project is MIT-licensed (see `LICENSE`). `` below is the CMake pro |-----------|---------|----------------| | [OpenVR SDK](https://github.com/ValveSoftware/openvr) (`libopenvr_api.so`, `openvr.h`) | BSD-3-Clause | Shipped in `lib/`; license in `share/doc//third-party/openvr.LICENSE` | | [Qt 6](https://www.qt.io/) (Core, Gui, Qml, Quick, Quick Controls, Wayland/XCB plugins) | LGPL-3.0 (Qt open-source license) | Linked dynamically. Default builds use the system's Qt (not shipped). Bundled builds ship Qt's shared libraries unmodified in `lib/`, `plugins/`, `qml/`: replace them to use another compatible Qt build | -| [Qt WebEngine](https://doc.qt.io/qt-6/qtwebengine-index.html) (Quick), for the "HA Dashboard" tab | LGPL-3.0; contains Chromium (BSD-3-Clause and further licenses, listed by Qt WebEngine) | Linked dynamically, from the system's Qt; not shipped. Bundled builds don't support it yet | **Bundled builds** (`--bundle-qt`) also ship further shared libraries that Qt depends on, taken unmodified from Debian. Their copyright and license files are in diff --git a/cmake/BundleQt.cmake b/cmake/BundleQt.cmake index 5abb177..f26d070 100644 --- a/cmake/BundleQt.cmake +++ b/cmake/BundleQt.cmake @@ -30,14 +30,6 @@ set(VRDASH_BUNDLE_PLUGINS wayland-shell-integration/*.so wayland-graphics-integration-client/*.so wayland-decoration-client/*.so xcbglintegrations/*.so) -# Qt WebEngine (bundled only if a dashboard needs it): its helper process, resources and -# locales. Cross builds point VRDASH_WEBENGINE_PROCESS at the target's QtWebEngineProcess -# (Debian can't install the arm64 one next to the host's; see docker/cross-arm64.Dockerfile). -set(VRDASH_WEBENGINE_PROCESS "" CACHE FILEPATH - "QtWebEngineProcess to bundle (default: the one of the Qt used for the build)") -# Locales shipped with WebEngine (the full set is about 35 MB). -set(VRDASH_WEBENGINE_LOCALES en-US de) - function(vrdash_bundle_qt) get_property(targets GLOBAL PROPERTY VRDASH_DASHBOARD_TARGETS) get_property(qml_dirs GLOBAL PROPERTY VRDASH_DASHBOARD_QML_DIRS) @@ -60,14 +52,6 @@ function(vrdash_bundle_qt) set(qt_libdir ${qt_prefix}/lib/${CMAKE_LIBRARY_ARCHITECTURE}) # Debian multiarch endif() - # Used by the install script only if a bundled library is Qt WebEngine. - set(webengine_process ${VRDASH_WEBENGINE_PROCESS}) - if(NOT webengine_process) - set(webengine_process ${QT6_INSTALL_PREFIX}/${QT6_INSTALL_LIBEXECS}/QtWebEngineProcess) - endif() - set(webengine_resources ${QT6_INSTALL_PREFIX}/${QT6_INSTALL_DATA}/resources) - set(webengine_locales ${QT6_INSTALL_PREFIX}/${QT6_INSTALL_TRANSLATIONS}/qtwebengine_locales) - # The regexes end up inside a quoted string in the generated script: escape backslashes. string(REPLACE "\\" "\\\\" bundle_exclude "${VRDASH_BUNDLE_EXCLUDE}") diff --git a/cmake/bundle-install.cmake.in b/cmake/bundle-install.cmake.in index 4be871c..839033a 100644 --- a/cmake/bundle-install.cmake.in +++ b/cmake/bundle-install.cmake.in @@ -14,9 +14,6 @@ set(qt_plugins "@QT6_INSTALL_PREFIX@/@QT6_INSTALL_PLUGINS@") set(qt_qml "@QT6_INSTALL_PREFIX@/@QT6_INSTALL_QML@") set(patchelf "@VRDASH_PATCHELF@") set(exclude "@bundle_exclude@") -set(webengine_process "@webengine_process@") -set(webengine_resources "@webengine_resources@") -set(webengine_locales "@webengine_locales@") message(STATUS "Bundling Qt into ${prefix}") @@ -81,35 +78,6 @@ file(GET_RUNTIME_DEPENDENCIES RESOLVED_DEPENDENCIES_VAR resolved UNRESOLVED_DEPENDENCIES_VAR unresolved) -# --- Qt WebEngine: helper process (and its libraries), resources, locales ---------------- -# Only if a dashboard uses it. Found through qt.conf (LibraryExecutables, Data, -# Translations; written below). -set(helper_executables "") -if(resolved MATCHES "libQt6WebEngineCore") - message(STATUS "Bundling Qt WebEngine (${webengine_process})") - # TYPE PROGRAM: a plain file(INSTALL) would drop the executable bit. - file(INSTALL "${webengine_process}" DESTINATION "${prefix}/libexec" TYPE PROGRAM - FOLLOW_SYMLINK_CHAIN) - list(APPEND helper_executables "${prefix}/libexec/QtWebEngineProcess") - file(GET_RUNTIME_DEPENDENCIES - EXECUTABLES ${helper_executables} - DIRECTORIES "@qt_libdir@" "${libdir}" - PRE_EXCLUDE_REGEXES ${exclude} - POST_EXCLUDE_REGEXES "^${libdir}/" - RESOLVED_DEPENDENCIES_VAR helper_resolved - UNRESOLVED_DEPENDENCIES_VAR helper_unresolved) - list(APPEND resolved ${helper_resolved}) - list(APPEND unresolved ${helper_unresolved}) - list(REMOVE_DUPLICATES resolved) - list(REMOVE_DUPLICATES unresolved) - file(GLOB files LIST_DIRECTORIES false "${webengine_resources}/*") - file(INSTALL ${files} DESTINATION "${prefix}/resources") - foreach(locale @VRDASH_WEBENGINE_LOCALES@) - file(INSTALL "${webengine_locales}/${locale}.pak" - DESTINATION "${prefix}/translations/qtwebengine_locales") - endforeach() -endif() - if(unresolved) message(WARNING "Unresolved (expected on target system): ${unresolved}") endif() @@ -131,7 +99,7 @@ foreach(lib IN LISTS bundled_libs) set_rpath("${lib}" "\$ORIGIN") endif() endforeach() -foreach(file IN LISTS qt_plugin_files qml_plugins helper_executables) +foreach(file IN LISTS qt_plugin_files qml_plugins) get_filename_component(dir "${file}" DIRECTORY) file(RELATIVE_PATH rel "${dir}" "${libdir}") set_rpath("${file}" "\$ORIGIN/${rel}") @@ -144,14 +112,11 @@ find_program(dpkg dpkg) if(dpkg) set(docdir "${prefix}/@CMAKE_INSTALL_DOCDIR@/third-party") set(packages "") - foreach(file IN LISTS resolved qt_plugin_files qml_plugins helper_executables) + foreach(file IN LISTS resolved qt_plugin_files qml_plugins) # Ask dpkg about the original file: map installed plugins back, resolve # symlinks and /lib -> /usr/lib (usrmerge: packages register /usr paths). string(REPLACE "${plugindir}" "${qt_plugins}" original "${file}") string(REPLACE "${qmldir}" "${qt_qml}" original "${original}") - if(file STREQUAL "${prefix}/libexec/QtWebEngineProcess") - set(original "${webengine_process}") - endif() file(REAL_PATH "${original}" original) execute_process(COMMAND "${dpkg}" -S "${original}" OUTPUT_VARIABLE owner ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) @@ -193,9 +158,6 @@ Prefix = .. Libraries = @CMAKE_INSTALL_LIBDIR@ Plugins = plugins QmlImports = qml -LibraryExecutables = libexec -Data = . -Translations = translations ") list(LENGTH resolved n_libs) diff --git a/docker/cross-arm64.Dockerfile b/docker/cross-arm64.Dockerfile index 51088e3..26a66fe 100644 --- a/docker/cross-arm64.Dockerfile +++ b/docker/cross-arm64.Dockerfile @@ -17,13 +17,4 @@ RUN dpkg --add-architecture arm64 \ qml6-module-qtquick-layouts:arm64 qml6-module-qtquick-templates:arm64 \ qml6-module-qtqml-workerscript:arm64 qml6-module-qtquick-window:arm64 \ qt6-qpa-plugins:arm64 qt6-wayland:arm64 \ - qt6-webengine-dev:arm64 qml6-module-qtwebengine:arm64 \ && rm -rf /var/lib/apt/lists/* - -# Qt WebEngine's helper process for arm64 (bundled builds): its package is Multi-Arch: foreign -# and can't be installed next to the amd64 one, so unpack it into its own directory. -RUN apt-get update \ - && cd /tmp && apt-get download libqt6webenginecore6-bin:arm64 \ - && dpkg-deb -x libqt6webenginecore6-bin_*_arm64.deb /opt/qt6-arm64 \ - && rm -f libqt6webenginecore6-bin_*_arm64.deb && rm -rf /var/lib/apt/lists/* \ - && test -x /opt/qt6-arm64/usr/lib/qt6/libexec/QtWebEngineProcess diff --git a/docker/native.Dockerfile b/docker/native.Dockerfile index f0a582f..9e517e5 100644 --- a/docker/native.Dockerfile +++ b/docker/native.Dockerfile @@ -13,5 +13,4 @@ RUN apt-get update \ qml6-module-qtquick qml6-module-qtquick-controls qml6-module-qtquick-layouts \ qml6-module-qtquick-templates qml6-module-qtqml-workerscript qml6-module-qtquick-window \ qml6-module-qttest qt6-qpa-plugins qt6-wayland \ - qt6-webengine-dev qml6-module-qtwebengine \ && rm -rf /var/lib/apt/lists/* diff --git a/docs/DESIGN.md b/docs/DESIGN.md index c6f6e3d..0893887 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -57,18 +57,15 @@ Three layers; dependencies only go downward (D17). - **MQTT is the only channel for everything device-related:** entities, services, availability (D24). -- MQTT can't show HA as a whole (other entities' states, service calls for them), which the - dashboard (DSH-1) needs. The dashboard gets **its own channel, separate from MQTT**: +- MQTT can't show HA as a whole (other entities' states, service calls for them), which a + dashboard needs. The dashboard gets **its own channel, separate from MQTT**: - Q2a, HA web UI in Qt WebEngine: the browser talks to HA itself (HTTP/WebSocket, HA's own - login); config: HA URL. + login); config: HA URL. **Failed on the Frame (D40)**: the system WebEngine can't be + loaded, a bundled one didn't work (STEAM-FRAME.md). - Q2b, native QML dashboard: HA's WebSocket API with a token (Q3) as another I/O component; config: HA URL and token. -- Both fit the three layers. The MVP contains the web UI test (DSH-1, D28), which also serves - as the spike on WebEngine in the off-screen renderer; Q2/Q3 are decided for DSH-2. -- Known DSH-1 risks: WebEngine must be initialised before the `QGuiApplication` (created by the - platform's base constructor), so the platform needs a small change; WebEngine with our - off-screen renderer, especially Vulkan, is untested; text fields inside the page probably - don't open the SteamVR keyboard (fallback: log in once in a desktop mode). +- Both fit the three layers. The web UI test (DSH-1, D28) answered Q2a with "no"; Q2/Q3 are + decided for DSH-2, with the native dashboard as the candidate. ## Start-up and modes @@ -102,7 +99,7 @@ Two overlay kinds in the platform (D25): | Main tab "Frame Assistant" (UI-1, later NOT-7, do-not-disturb) | dashboard tab | always there; SteamVR shows/hides it | ✓ | | Notifications | world overlay | the app (notification manager) shows/hides it | ✓ | | Status tab (SRV-6) | dashboard tab | exists only while cards exist (D9) | ✗ | -| "HA Dashboard" (DSH-1, web UI test) | dashboard tab | always there | ✓ | +| HA dashboard (DSH-2) | dashboard tab | always there | ✗ (web UI test DSH-1 removed, D40) | - **World overlays (MVP):** a new overlay kind next to the dashboard tab (CLAUDE.md, "Planned extension"). The surface gets a kind and a placement; renders only while visible (NFR-3). @@ -150,7 +147,7 @@ and signals across threads are delivered through the receiver's event loop. | **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 tab (DSH-1; Q2 for DSH-2). `src/ha/HaWebUi`, a QML singleton in the app itself (not `frameassistant_lib`: QML types must be part of the QML module). | ✓ (DSH-1) | +| **HA web UI** | Was: Qt WebEngine for the dashboard tab (DSH-1). Dropped (D40); a native dashboard (DSH-2) would need its own object class. | ✗ | | **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. | ✓ | @@ -307,7 +304,7 @@ Layout follows common practice (e.g. Zigbee2MQTT's `…//set`); one topi | `mqtt.tls.ca_file` | no | system certificates | own CA, e.g. a self-signed broker certificate | | `mqtt.tls.cert_file` / `key_file` | no | none | client certificate, if the broker requires one | | `homeassistant.discovery_prefix` | no | `homeassistant` | | -| `homeassistant.url` | for DSH-1 only | — | missing: the dashboard tab shows a hint instead of the web UI | +| `homeassistant.url` | no | — | reserved for the dashboard (DSH-2); not used yet (D40) | | `notifications.timeout_s` | no | 10 | D33 | - Errors (missing required values, invalid JSON, wrong types): exit with a message and exit @@ -447,4 +444,4 @@ Collected 2026-10-05 for the notification design. | D36 | 2026-10-05 | NFR-3 measured as own cost (CPU idle, frames while hidden, GPU per frame) plus an A/B comparison of the game's compositor statistics; manual in the MVP, a `--vr-stats` mode later | Measurable acceptance for NFR-3 without extra code in the MVP | | D37 | 2026-10-05 | The MVP is implemented in four iterations: 1 DSH-1 + configuration, 2 MQTT/HA device/UI-1, 3 sensors, 4 notifications | Small iterations (working rules); DSH-1 first because the user wants to see it early and it doesn't depend on MQTT | | D38 | 2026-10-05 | App-specific command-line options: the platform's base constructor takes an optional list of extra options, parses them with its own and exposes their values | The command line is parsed in the base constructor, before the derived app can contribute (`--config`); backward compatible, `--help` stays complete | -| D39 | 2026-10-05 | DSH-1: `QtWebEngineQuick::initialize()` in `main.cpp` (no platform change); a named, persistent WebEngine profile under `~/.local/share/frameassistant/`; tab name "HA Dashboard" | WebEngine must be initialised before the `QGuiApplication`; the HA login must survive restarts | +| D39 | 2026-10-05 | *Superseded by D40 (REQUIREMENTS.md): tab and WebEngine removed.* DSH-1: `QtWebEngineQuick::initialize()` in `main.cpp` (no platform change); a named, persistent WebEngine profile under `~/.local/share/frameassistant/`; tab name "HA Dashboard" | WebEngine must be initialised before the `QGuiApplication`; the HA login must survive restarts | diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 68e1bcf..f72b290 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -5,12 +5,11 @@ CLAUDE.md ("Working rules"). ## Current state -- **Implementation iteration 1 (DSH-1 and configuration), phase 3 (implementation) done, - waiting for the user's approval.** All plan steps are implemented and committed; next is - phase 4, testing on the Frame (plan item 10). -- Started by SteamVR, the app now logs to `logs/frame-assistant.log` in the install folder: - the manifest passes `--log-file` (CFG-2 wants the settings error in a log file). Whether - SteamVR on the Frame honours the manifest's `arguments` is checked in phase 4. +- **Implementation iteration 1, phase 4 (testing on the Frame).** DSH-1 is done with a + negative result: HA's web UI via Qt WebEngine doesn't work on the Frame, so the "HA + Dashboard" tab and the WebEngine dependency were removed (D40). What remains of iteration 1 + (platform hooks, settings file, `--config`, log file for SteamVR-started instances) still + needs its test on the Frame with the system-Qt package. ## Next steps @@ -19,7 +18,7 @@ Requirements and design cover the whole MVP; it is **implemented in four small i | Iteration | Content | Platform change | Checked by | |---|---|---|---| -| 1 | DSH-1: HA web UI (Qt WebEngine) in a dashboard tab; configuration (D34) with start-up hook and `--config` | start-up hook (D21), WebEngine initialisation before the `QGuiApplication` | HA web UI in the tab on the Frame; result documented | +| 1 | ~~DSH-1: HA web UI in a dashboard tab~~ (failed, removed: D40); configuration (D34) with start-up hook and `--config`; log file for SteamVR-started instances | start-up hook (D21), app command-line options (D38) | DSH-1 result documented; app starts on the Frame with the settings file, settings errors in the log | | 2 | MQTT client (own thread), HA device with discovery and availability, libmosquitto in the package, status display UI-1 | modes (D22) | device appears in HA, online/offline right, reconnect works | | 3 | OpenVR wrappers and the 13 sensor entities | `--desktop` with optional SteamVR (D22) | sensor values in HA on the Frame | | 4 | notification manager, `notify` entity, world overlay | world overlay, premultiplied flag (D25) | notification from HA in VR, also in a game | @@ -80,6 +79,12 @@ Known limitation: the bundled-Qt package does not support WebEngine in this iter present (relevant for Q2/Q3). - libmosquitto ABI check: Debian's 2.0.21 needs at most `OPENSSL_3.0.0`/`GLIBC_2.34`; the Frame has OpenSSL 3.2.1 and glibc 2.39. No libcjson dependency. D14 holds. +- DSH-1 closed (D40): the bundling experiment failed (sandbox, ICU resources, Vulkan abort, + empty page with OpenGL and in a desktop window; details in STEAM-FRAME.md). At the user's + decision the "HA Dashboard" tab, `HaWebUi`, the WebEngine build dependency and the + bundler's WebEngine support were removed (reverts of the corresponding commits). Qt WebView + is no alternative (WebEngine backend on Linux). `homeassistant.url` stays reserved for + DSH-2, where a native dashboard is the candidate. - Phase 4 (testing on the Frame), first findings: - `deploy-to-frame.sh` exited silently without a bundled package in `dist/` (fixed). - The system-Qt package doesn't start: the Frame's Qt WebEngine is 6.7.0 (rest of Qt 6.8.0) diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 22ef7b1..3557ba7 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -103,7 +103,7 @@ FrameAssistant. | ID | Requirement | Prio | MVP | Acceptance criteria | |----|-------------|------|-----|---------------------| -| DSH-1 | A dashboard tab shows HA's web UI (Qt WebEngine) at a configured URL, as a "does it work out of the box" test. | M | ✓ | The page loads in VR and can be operated with the laser; whatever doesn't work out of the box is documented and moved to a later iteration. | +| DSH-1 | A dashboard tab shows HA's web UI (Qt WebEngine) at a configured URL, as a "does it work out of the box" test. | M | ✓ | The page loads in VR and can be operated with the laser; whatever doesn't work out of the box is documented and moved to a later iteration. **Result (D40):** fails on the Frame; tab removed. | | DSH-2 | The full dashboard to control HA entities in VR: everything beyond DSH-1, e.g. login comfort, opening a given HA view (SRV-4), possibly a native approach (Q2, Q3). | S | | Defined after design exploration. | ### Non-functional (NFR) @@ -133,7 +133,7 @@ FrameAssistant. | # | Question | Status | |---|----------|--------| | Q1 | Several notifications at once: stack, queue, replace by tag? | open, design exploration | -| Q2 | Dashboard: HA's own web UI vs. native VR UI? | open, design exploration (Qt WebEngine exists on the Frame, Appendix C) | +| Q2 | Dashboard: HA's own web UI vs. native VR UI? | open; HA's web UI via Qt WebEngine failed on the Frame (D40), so native is the candidate | | Q3 | Dashboard data: HA WebSocket API (token) in addition to MQTT, or MQTT only? | open, design | | Q4 | Which MQTT client library? Licence, availability on SteamOS, TLS/login support. | answered: libmosquitto, shipped in the package (D14, Appendix B) | | Q5 | Status card: own tab or part of the main tab? | answered: own tab, on demand (D9) | @@ -163,6 +163,7 @@ FrameAssistant. | D15 | 2026-10-05 | HA MQTT discovery: **own small layer on Qt's JSON classes**, using device-based discovery (one `/device//config` topic with all entities under `cmps`) | No suitable library (Appendix B); HA recommends device-based discovery for multi-entity devices (NFR-7) | | D20 | 2026-10-05 | New requirement UI-1 (status display in the main tab), MVP | Helps testing on the device; little effort | | D28 | 2026-10-05 | DSH-1 split: DSH-1 (MVP) = HA web UI in a tab as an out-of-the-box test; DSH-2 (Should) = the full dashboard. The MVP (D8) now also contains DSH-1 | The user wants to see early whether the simple variant works | +| D40 | 2026-10-05 | DSH-1 done with a negative result: HA's web UI via Qt WebEngine doesn't work on the Frame (system WebEngine not loadable; bundling our own failed). The "HA Dashboard" tab and the WebEngine dependency are removed; the MVP no longer contains a dashboard. `homeassistant.url` stays reserved for DSH-2 | The out-of-the-box test answered its question; a WebEngine dependency would keep the whole app from starting on the Frame | Design decisions (D16 onward) are in [DESIGN.md](DESIGN.md). @@ -273,7 +274,7 @@ use Qt's private API, which requires the exact Qt version. | Sounds, audio from URL | NOT-4, SRV-8 | Qt Multimedia + QtNetwork | ✓ `libQt6Multimedia`, FFmpeg plugin | no | | HTTPS (downloads, WebSocket) | SRV-8, Q3 | Qt TLS plugin | ✓ `libqopensslbackend.so` | no | | Translations | NFR-4 | `QTranslator`, `lupdate`/`lrelease` | ✓ (build-time tools) | no | -| Dashboard, HA web UI | DSH-1, DSH-2, Q2 | Qt WebEngine (Quick) | **✗ present but not loadable**: 6.7.0 next to Qt 6.8.0, built against FFmpeg 6, which the Frame lacks (STEAM-FRAME.md) | yes, only bundled with all of Qt (experiment, see PROGRESS.md) | +| Dashboard, HA web UI | DSH-1, DSH-2, Q2 | Qt WebEngine (Quick); **dropped (D40)** | **✗ present but not loadable**: 6.7.0 next to Qt 6.8.0, built against FFmpeg 6, which the Frame lacks (STEAM-FRAME.md) | bundling it with all of Qt was tried and failed (STEAM-FRAME.md) | | Dashboard, native UI | DSH-2, Q2/Q3 | HA WebSocket API via Qt WebSockets | ✓ `libQt6WebSockets` | no | | Haptics, fade, apps | SRV-1/2 | OpenVR | ✓ | no | | Tests | NFR-8 | QtTest | ✓ (host only) | no | @@ -286,5 +287,5 @@ Also present and possibly useful later: `libQt6TextToSpeech`, Qt's `networkinfor Open points: - The Debian build container needs the matching `-dev` packages (amd64 and arm64) for every module used. Not checked yet. -- WebEngine and Multimedia come with SteamOS's desktop (KDE) packages; a SteamOS update could - remove them. +- Multimedia comes with SteamOS's desktop (KDE) packages; a SteamOS update could remove it. + (WebEngine likewise, but it isn't usable anyway, D40.) diff --git a/docs/STEAM-FRAME.md b/docs/STEAM-FRAME.md index 504bfc3..87ae562 100644 --- a/docs/STEAM-FRAME.md +++ b/docs/STEAM-FRAME.md @@ -70,7 +70,8 @@ Without the deploy script: copy the `.tar.gz`, unpack it on the Frame, run `./in Checked 2026-10-05; details per requirement in [REQUIREMENTS.md](REQUIREMENTS.md), Appendix C. - **System Qt 6.8.0** under `/usr/lib` includes, besides the base modules: DBus, Network, - Multimedia (FFmpeg plugin), WebSockets, WebEngine (Core, Quick), WebView, TextToSpeech, + Multimedia (FFmpeg plugin), WebSockets, WebEngine (Core, Quick; not loadable, see below), + WebView, TextToSpeech, Positioning, Sensors, Svg, VirtualKeyboard. TLS plugins: `libqopensslbackend.so`, `libqcertonlybackend.so`. - It apparently comes with the KDE Plasma desktop mode (plugins for plasma, kwin, discover). @@ -84,6 +85,13 @@ Checked 2026-10-05; details per requirement in [REQUIREMENTS.md](REQUIREMENTS.md depend on it. A program linking WebEngine fails to start ("libavcodec.so.60: cannot open shared object file"). Other Qt modules are 6.8.0 (`pacman -Q | grep qt6-`). Qt Multimedia's FFmpeg plugin resolves all its libraries (`ldd`, no "not found"). +- **Bundling our own Qt WebEngine failed** (experiment, 2026-10-05; Debian's Qt 6.8.2 incl. + WebEngine with its own FFmpeg, 141 MB package): the helper process (`QtWebEngineProcess`, + needs the executable bit) only starts without Chromium's sandbox (`--no-sandbox`); without + `QTWEBENGINE_RESOURCES_PATH` its child processes find no ICU data ("Invalid file descriptor + to ICU data"); with Vulkan the app aborts ("free(): invalid pointer"); with OpenGL the + dashboard tab stays empty; in a desktop window the page stays white and Chromium's + remote-debugging port never opens. FrameAssistant uses no web engine (D40). - **Qt add-on modules must come from the system Qt.** Shipping one fails: they use Qt's private API, and QtCore aborts on any version mismatch ("Cannot mix incompatible Qt library"). - **Not present:** any MQTT library (no `libmosquitto`, no Qt MQTT). FrameAssistant ships diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f48f40..5892416 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -58,20 +58,14 @@ target_link_libraries(frameassistant_lib PUBLIC Qt6::Core) target_compile_options(frameassistant_lib PRIVATE -Wall -Wextra) # The app: its UI is the QML module "FrameAssistant" (src/qml/). -# Qt WebEngine shows HA's web UI (DSH-1); on the Frame it comes with the system Qt. -find_package(Qt6 REQUIRED COMPONENTS WebEngineQuick) vrdash_add_dashboard(frameassistant OUTPUT_NAME ${VRDASH_EXECUTABLE_NAME} QML_URI FrameAssistant SOURCES main.cpp Dashboard.h Dashboard.cpp - QML_SOURCES ha/HaWebUi.h ha/HaWebUi.cpp QML_FILES qml/Main.qml - qml/Card.qml - qml/HaDashboard.qml) -target_link_libraries(frameassistant PRIVATE frameassistant_lib Qt6::WebEngineQuick) -# The generated QML type registration includes QML_SOURCES headers by file name. -target_include_directories(frameassistant PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ha) + qml/Card.qml) +target_link_libraries(frameassistant PRIVATE frameassistant_lib) install(FILES ${OPENVR_LIBRARY} DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/src/Dashboard.cpp b/src/Dashboard.cpp index 7279887..81ade1c 100644 --- a/src/Dashboard.cpp +++ b/src/Dashboard.cpp @@ -17,9 +17,6 @@ Dashboard::Dashboard(int& argc, char** argv) .arg(Config::defaultPath()), QStringLiteral("path"))}) { addSurface({.qmlRootType = QStringLiteral("Main")}); - addSurface({.id = QStringLiteral("dashboard"), - .qmlRootType = QStringLiteral("HaDashboard"), - .displayName = QStringLiteral("HA Dashboard")}); } bool Dashboard::prepare(QString* error) { @@ -37,5 +34,3 @@ bool Dashboard::prepare(QString* error) { << m_config.deviceName << "(" + m_config.deviceId + ")"; return true; } - -void Dashboard::loading() { m_haWebUi.setUrl(m_config.haUrl); } diff --git a/src/Dashboard.h b/src/Dashboard.h index 16fede5..10d6eed 100644 --- a/src/Dashboard.h +++ b/src/Dashboard.h @@ -2,7 +2,6 @@ #include "app/DashboardApplication.h" #include "config/Config.h" -#include "ha/HaWebUi.h" // The dashboard app. Its UI is the QML module set up in src/CMakeLists.txt // (QML_URI FrameAssistant, files in src/qml/). @@ -17,10 +16,7 @@ class Dashboard : public DashboardApplication { protected: // Loads the settings (overlay and desktop mode; docs/DESIGN.md, D22). bool prepare(QString* error) override; - void loading() override; private: Config m_config; - // QML singleton (HaWebUi); a member, so it outlives all QML. - HaWebUi m_haWebUi{nullptr}; }; diff --git a/src/ha/HaWebUi.cpp b/src/ha/HaWebUi.cpp deleted file mode 100644 index f765ccb..0000000 --- a/src/ha/HaWebUi.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "ha/HaWebUi.h" - -#include -#include -#include - -namespace { - -HaWebUi* g_instance = nullptr; - -} // namespace - -HaWebUi::HaWebUi(QObject* parent) : QObject(parent) { - Q_ASSERT(!g_instance); - g_instance = this; -} - -HaWebUi::~HaWebUi() { g_instance = nullptr; } - -QQuickWebEngineProfile* HaWebUi::profile() { - if (!m_profile) { - m_profile = new QQuickWebEngineProfile(this); - m_profile->setStorageName(QStringLiteral("frameassistant")); - m_profile->setOffTheRecord(false); - m_profile->setPersistentStoragePath(storagePath()); - m_profile->setCachePath(cachePath()); - m_profile->setPersistentCookiesPolicy( - QQuickWebEngineProfile::AllowPersistentCookies); - } - return m_profile; -} - -QString HaWebUi::storagePath() { - return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + - QStringLiteral("/frameassistant/webengine"); -} - -QString HaWebUi::cachePath() { - return QStandardPaths::writableLocation( - QStandardPaths::GenericCacheLocation) + - QStringLiteral("/frameassistant/webengine"); -} - -HaWebUi* HaWebUi::create(QQmlEngine*, QJSEngine*) { - Q_ASSERT(g_instance); - // Owned by the app, not by the QML engine. - QJSEngine::setObjectOwnership(g_instance, QJSEngine::CppOwnership); - return g_instance; -} diff --git a/src/ha/HaWebUi.h b/src/ha/HaWebUi.h deleted file mode 100644 index 4cd02eb..0000000 --- a/src/ha/HaWebUi.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -class QJSEngine; -class QQmlEngine; - -// HA's web UI in the "HA Dashboard" tab (DSH-1): the URL from the settings -// and a persistent WebEngine profile, so the HA login survives restarts -// (docs/DESIGN.md, D39). A QML singleton owned by the app (Dashboard). -class HaWebUi : public QObject { - Q_OBJECT - QML_ELEMENT - QML_SINGLETON - // Empty: no URL configured (the tab shows a hint). - Q_PROPERTY(QUrl url READ url CONSTANT) - Q_PROPERTY(QQuickWebEngineProfile* profile READ profile CONSTANT) - - public: - // Not default-constructible, so QML uses create() instead of making its - // own instance. - explicit HaWebUi(QObject* parent); - ~HaWebUi() override; - - // Call before the QML is loaded. - void setUrl(const QUrl& url) { m_url = url; } - - QUrl url() const { return m_url; } - // Created on first use: only a tab that shows the web UI needs it. - QQuickWebEngineProfile* profile(); - - // Where the profile keeps its data (cookies, local storage) and cache. - static QString storagePath(); - static QString cachePath(); - - // QML singleton factory: the instance the app created. - static HaWebUi* create(QQmlEngine*, QJSEngine*); - - private: - QUrl m_url; - QQuickWebEngineProfile* m_profile = nullptr; -}; diff --git a/src/main.cpp b/src/main.cpp index 49fe89d..b2aa26b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,11 +1,6 @@ -#include - #include "Dashboard.h" int main(int argc, char* argv[]) { - // Must run before the Qt application object exists (the Dashboard - // constructor creates it). - QtWebEngineQuick::initialize(); Dashboard app(argc, argv); return app.exec(); } diff --git a/src/qml/HaDashboard.qml b/src/qml/HaDashboard.qml deleted file mode 100644 index 7657ca4..0000000 --- a/src/qml/HaDashboard.qml +++ /dev/null @@ -1,40 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Controls.Material -import QtWebEngine - -// "HA Dashboard" tab (DSH-1): HA's own web UI at the URL from the settings. -// Without a URL it shows a hint instead. -Pane { - id: root - - Material.theme: Material.Dark - padding: 0 - font.pixelSize: 24 - - Loader { - anchors.fill: parent - sourceComponent: HaWebUi.url.toString() !== "" ? webUi : hint - } - - Component { - id: webUi - - WebEngineView { - profile: HaWebUi.profile - url: HaWebUi.url - } - } - - Component { - id: hint - - Label { - padding: 48 - wrapMode: Text.WordWrap - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - text: qsTr("No Home Assistant URL configured.\n\nSet \"homeassistant.url\" in the settings file and restart.") - } - } -}