Status: DRAFT / IN PROGRESS · Owner: Chris · Started 2026‑07‑19 · Branch: phoenix
This plan captures a large, multi‑codebase change: split the desktop app into a headless backend service (motion engine + full control API + MCP, zero GPU) and a detachable UI client, and finish the half‑implemented Phase‑3 on‑device demo/session system. It also records the smaller bridge/firmware fixes that surfaced during live hardware testing on 2026‑07‑19.
Live HW testing exposed several structural problems:
- Hard GPU dependency. The app is one process where the motion/update loop
runs on the OpenGL render thread. When a game (the real motion source, e.g.
Assetto Corsa) saturates the RTX 4070, the app's GL context stack‑overflows
(
nvoglv64.dll,0xc00000fd) and the app dies ~7–13 s after launch. In production this machine runs the game and the motion feeder at once, so the feeder must not depend on the GPU. - Monolithic engine + UI. State (entities, motion, HIL streaming, cueing) lives in the GUI process. You can't run the motion pipeline without the whole GUI, and you can't attach a second view without a second engine (which would fight over the HIL device).
- Phase‑3 on‑device demo/session system is stubbed. DEMO playback reports
playing but produces no servo motion; burned files don't become the active
sequence;
LOOPis unimplemented; device file listing isn't surfaced. - Tuning can't reach the device. In raw‑LIVE the mini runs the cue engine,
but the app's
mca_set/intensity/tiltonly write local config and the bridge never relaysMCA:commands — so changing motion cue does nothing. - Friction: LIVE needed a manual Play; duplicate HIL entities from malformed reconnects; args‑nesting client bug (all fixed 2026‑07‑19).
┌──────────────────────────────────────────┐
│ stewartd (headless backend service) │
│ ── motion engine (plugins → cueing → │
│ HIL/SIL streaming), no GL, no window │
│ ── control API (TCP 8770, line‑JSON) │
│ ── MCP server (stewart_* tools) │
│ ── runs as a Windows service / tray app │
└───────────────┬──────────────────────────┘
│ same TCP/JSON API
┌───────────────┴───────────┐ ┌──────────────┐
│ UI client (ImGui/GL) │ │ MCP / agent │
│ attaches on demand, thin │ │ (Claude) │
│ view+control, GPU only │ └──────────────┘
│ while open │
└────────────────────────────┘
- One engine, many views. The service owns all state and the single HIL connection. UIs (and the MCP/agent) are clients over the existing 8770 API.
- Zero GPU when headless. During gaming, only
stewartdruns. - Full API + MCP on the service, not the UI — the UI becomes optional.
Small, no firmware flash. Gets live tuning working today.
- 1a
control_api.py: LIVE sendsPLAY:START(notPLAY:STOP) so "enabled" alone moves the rig. (done) - 1b
control_api.py: new whitelistedcmdverb → relaysMCA:/SERVO:/CONFIG:/ query strings to the mini so tuning reaches the on‑device cue engine. (done) - 1c commit+push → redeploy
/tmp/bridgeon Voron (192.168.1.168) → restart → verify one‑click LIVE +MCA:reaches the mini. - 1d live feel‑tuning pass (trim heave, lift surge/sway + tilt) via
MCA:with camera as ground truth.
mini/main/main.cpp (ESP‑IDF). Requires build + flash (via bridge flash_firmware
or esptool over USB on the Voron).
- 2a DEMO plays
laps123_moderate(69367 samples) but rig is physically static — investigate PlaybackTask: is it advancing? is servo output gated by a state that DEMO doesn't set? is the TEL tap on the cue path (bypassed during baked playback)? Fix so baked playback drives servos. - 2b Burned file must become selectable/active:
SELECT:<name>+ device‑side file store; DEMO plays the selected file, not only the embedded one. - 2c Implement
PLAY:LOOP(PLAY:ERR unknown 'LOOP'today). - 2d Surface device file list +
memas RESP events the bridge/app parse.
app/src/ui_panels.cpp, control_server.cpp.
- 3a Replace the single
sequence.m6ptext box with a list: local available sessions (recordings +.m6p), each with bake/upload actions. - 3b Show the device's internal file list (from Phase 2d) with size/active markers; select / delete.
- 3c Robust upload with real progress + success/fail feedback (chunked handshake, CRC verify, retry).
- 3d Generate the 3 Nürburgring session files (currently only
laps123_moderate.m6pexists — 3 laps combined). Produce gentle / moderate / aggressive variants (or per‑lap split) as first‑class selectable sessions. - 3e Resolve "Geometry out of sync with device" (push/confirm geometry).
The core architectural refactor. Sub‑phased to stay shippable throughout.
- 4a
--headlessflag: run engine + control server + MCP with no window / no GL. The update/motion loop moves off the render thread into a plain loop. Immediate zero‑GPU backend for production gaming. (ship this first) - 4b Extract the engine into a service target (
stewartd) that always runs headless; package as a Windows service / tray launcher with autostart. - 4c Make the ImGui app a thin client: it renders state fetched from the service over the 8770 API and sends commands, instead of owning the engine. Attach/detach at will; only consumes GPU while open.
- 4d Interim GPU mitigation (do now, reversible): pin the app to the
integrated AMD GPU via Windows per‑app graphics preference
(
HKCU\...\DirectX\UserGpuPreferences,GpuPreference=1) so the 4070 stays 100 % for the game even before 4a lands. - 4e Robustness: handle GL device‑loss/reset gracefully; optional Mesa
llvmpipesoftware‑GL fallback for a UI with no usable GPU.
- Phase 1 now — tiny, unblocks the original goal (tuning) today.
- Phase 4a + 4d early — kills the GPU crash risk during any further testing (headless backend + iGPU pin) with low effort.
- Phase 2 — firmware is the gate for the demo actually working; needs a build/flash cycle, so batch 2a–2d together.
- Phase 3 — UI on top of the now‑working device file system.
- Phase 4b/4c — the full service/thin‑client split, once the engine is proven headless.
- Firmware flash on the live rig (Phase 2) — keep a known‑good binary; flash via bridge or USB with recovery path. Verify servo safety (home/limits) first.
- Headless engine loop (4a) — the motion/update currently assumes render‑ thread cadence; must re‑time it (fixed‑rate loop) without the GL vsync clock.
- Thin‑client state sync (4c) — 8770 API must expose enough state at a good rate; may need a streaming/subscribe channel beyond request/response.
- Deploy discipline — all changes via GitHub commit+push; bridge redeployed from a pushed commit (no ad‑hoc edits on the Voron).
| Phase | Item | State |
|---|---|---|
| 1a | LIVE→PLAY:START | ✅ coded |
| 1b | cmd MCA passthrough |
✅ coded |
| 1c | deploy + verify bridge | ⏳ next |
| 1d | feel‑tuning pass | ⛔ blocked on 1c |
| 2a–2d | firmware DEMO/session | ⛔ not started |
| 3a–3e | Device Demos UI + sessions | ⛔ not started |
| 4a | --headless flag |
⛔ not started |
| 4b–4c | service + thin client | ⛔ not started |
| 4d | pin to iGPU | ⛔ quick, not started |
Verified findings, decisions, and the control‑API arg contract live in the repo
history and ~/.claude memory (stewart-control-api-args).