Implemented qt port + e2e

This commit is contained in:
Martino Ferrari
2026-06-26 09:11:10 +02:00
parent 0d7d8f396b
commit 4702d0a217
146 changed files with 57272 additions and 128 deletions
@@ -0,0 +1,134 @@
# StreamHub Qt Client — Design Spec
**Date:** 2026-06-25
**Sub-project:** D (port `Client/streamhub/` ImGui/SDL2/ImPlot desktop client to Qt5/6)
**Status:** Approved-by-default (user away; standing instruction: "implement the specs, do not wait for my approval").
> Decisions below were made autonomously per the standing instruction. They are
> recorded with rationale so they can be revisited.
## Goal
Produce a native Qt desktop oscilloscope client (`Client/streamhub-qt/`) that is
feature- and UX-equivalent to the existing ImGui client, talking the identical
StreamHub WebSocket protocol (JSON commands/events + binary v1 push / v2 trigger
capture frames), and builds against **either Qt5 or Qt6** (autodetected), for
back-compatibility with older Linux distributions.
The existing ImGui client (`Client/streamhub/`) stays untouched as a reference.
## Framework decisions (with rationale)
1. **Qt Widgets, not QML/Qt Quick.**
The client is a dense desktop tool: multi-panel layouts, drag-and-drop of
signals, context menus, splitters, modal dialogs. Widgets is the natural fit,
renders without a GPU/OpenGL pipeline (important on old hardware), and has the
most stable API surface shared between Qt5 and Qt6.
2. **Custom `QPainter`-based `PlotWidget`, not QCustomPlot/QtCharts.**
The oscilloscope plotting is highly bespoke (fixed ±4-division Y space,
wall-clock live X, LTTB decimation, normal/digital/mixed band normalization,
per-trace V/div + screen position, A/B cursors, box/scroll zoom + pan, zoom
history, drag-drop, hi-res WS zoom overlay, trigger-capture view). Replicating
this on top of a general charting library fights the library as much as it
helps. A focused `QPainter` widget:
- gives exact behavioral parity with the ImPlot version;
- adds **zero external dependencies** (max portability on old Linux);
- avoids GPL/commercial licensing entanglement (repo is EUPL v1.1);
- is straightforward to test in isolation.
3. **`QtWebSockets` (`QWebSocket`), not the hand-rolled POSIX RFC6455 client.**
`QWebSocket` integrates with the Qt event loop, exposes
`textMessageReceived`/`binaryMessageReceived` signals, and removes the need
for a manual receive thread + mutex-guarded queue. Auto-reconnect is a 3 s
`QTimer`. Both Qt5 and Qt6 ship the WebSockets module (verified present).
4. **Qt5/6 autodetect in CMake.**
`find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets WebSockets)` then
`find_package(Qt${QT_VERSION_MAJOR} ...)`; link `Qt${QT_VERSION_MAJOR}::*`.
This is the upstream-recommended idiom; prefers Qt6 when present, falls back
to Qt5. `CMAKE_AUTOMOC ON`.
5. **Reuse, do not fork, the wire layer.**
`Protocol.h`, `Protocol.cpp`, and `SignalBuffer.h` in `Client/streamhub/` are
pure C++17/STL with **no ImGui/SDL dependency** (verified). The Qt project
compiles `../streamhub/Protocol.cpp` directly and adds `../streamhub` to its
include path. Single source of truth for the protocol — any future protocol
change is picked up by both clients. The only ImGui type leaking into the
reference *domain* model is `ImVec4 color`; the Qt project defines its own
domain model (`Model.h`) using `QColor` instead.
## Threading model
Single-threaded around the Qt event loop:
- `QWebSocket` delivers frames as signals on the GUI thread → parsed via the
reused `Protocol` functions → mutate the model directly (no locks needed,
unlike the ImGui version whose WS ran on a background thread).
- A `QTimer` at ~60 Hz calls `update()` on visible `PlotWidget`s for smooth
live scrolling (decoupled from data arrival rate).
This is simpler and safer than the ImGui client's background-thread + drain-queue
model, and is the idiomatic Qt approach.
## Module / file structure (`Client/streamhub-qt/`)
| File | Responsibility |
|------|----------------|
| `CMakeLists.txt` | Qt5/6 autodetect, sources, reuse `../streamhub/Protocol.cpp`, install rules |
| `main.cpp` | `QApplication`, arg parse (`-host`/`-port`), apply dark theme, show `MainWindow` |
| `Theme.h/.cpp` | Catppuccin-Mocha `QPalette` + stylesheet, trace color palette |
| `Model.h` | Domain types: `Signal` (with `QColor`), `Source`, `TriggerCfgState`, `VScale`, `PlotAssignment`, `PlotLayout` enum + dims |
| `Hub.h/.cpp` | `QObject` controller: owns `QWebSocket`, model (`std::vector<Source>`), trigger state, capture, zoom caches, history info. Parses WS messages (reused `Protocol`), exposes command senders, emits Qt signals (`sourcesChanged`, `configChanged`, `statsChanged`, `triggerStateChanged`, `captureReceived`, `zoomReceived`, `historyInfoChanged`, `connectedChanged`). |
| `WsClient.h/.cpp` | Thin `QWebSocket` wrapper: connect/reconnect (`QTimer`), `sendText`, `textReceived`/`binaryReceived`/`connectedChanged` signals |
| `PlotWidget.h/.cpp` | Custom `QPainter` oscilloscope panel (one per grid cell). Owns per-plot view state (live/window, stored X range, zoom history, vMode, active slot, zoom caches, paused snapshot, trig-zoom). Renders, handles mouse zoom/pan + drag-drop, draws cursors, requests hi-res/history zoom through `Hub`. |
| `PlotGrid.h/.cpp` | Grid of `PlotWidget`s using nested `QSplitter`s per `PlotLayout`; rebuilds on layout change; routes drops |
| `SourceSidebar.h/.cpp` | `QTreeWidget` of sources→signals (drag source), connect/remove, "Add source" dialog launcher |
| `TriggerBar.h/.cpp` | Trigger config widgets (signal combo, edge, threshold, window combo, pre% slider, normal/single) + Arm/Disarm/Rearm/Stop + state badge |
| `StatsDialog.h/.cpp` | Per-source metrics table + 20-bin histogram (custom `QPainter` bar widget) |
| `HistoryBar.h/.cpp` | Live/range/pan/jump-preset/All history navigation |
| `MainWindow.h/.cpp` | `QMainWindow`: toolbar (sidebar toggle, layout picker, pause, cursors, trigger, history, window presets, stats, connection, status LED), assembles sidebar + grid + bars, owns `Hub`, the 60 Hz repaint `QTimer`, and the add-source dialog |
## Feature parity checklist (must match the ImGui client)
- Live wall-clock X window with presets {1,5,10,30,60 s} + Ctrl+scroll resize.
- Plot layouts {1×1, 2×1, 1×2, 3×1, 1×3, 2×2, 4×1, 1×4} via splitters.
- Drag a signal from the sidebar onto a plot; per-trace color/width/marker via
context menu; remove-from-plot; "active" trace selection.
- V-scale per trace: Auto / Range / Manual (V/div, offset, screen pos); Y ticks
labelled from the active trace; ±4-division fixed Y space.
- Per-plot V-mode: Normal / Digital / Mixed band normalization (ports
`applyDigitalNorm`/`applyMixedNorm` math already in `PlotPanel.cpp`).
- Mouse: scroll = Y zoom of active trace (or X zoom if none); Ctrl+scroll = X
zoom / window resize; Shift+scroll = Y pan; right-drag = X pan (live→non-live).
- Zoom history Back / Fit / Live per plot.
- Global pause (freeze view snapshot; rings keep filling); global A/B cursors
with ΔT, 1/ΔT, per-trace A/B/Δ readouts.
- Hi-res zoom over WS (`zoom` cmd, 2400 pts) for live-narrow and zoomed views;
history zoom (`historyZoom`) for disk-backed ranges; "HIST" indicator.
- Trigger: full config → `setTrigger`; Arm/Disarm/Rearm/Stop; state badge
(idle/armed/collecting/triggered); v2 capture frame → capture view rendered in
`[-pre,+post]` with trigger marker at t=0; capture-view zoom + Reset.
- Stats dialog: state, totalReceived, totalLost, rateHz±std, frags/bytes per
cycle, cycle avg/std/min/max ms, 20-bin cycle-time histogram.
- Add/remove source; connection host/port edit + reconnect; status LED.
- LTTB decimation to ≤2400 pts/trace before drawing (reuse `LTTBDecimate`).
## Build & run
```bash
cd Client/streamhub-qt
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/StreamHubQtClient -host 127.0.0.1 -port 8090
```
Verification: run `./run_streamhub.sh` (or the recorder E2E streamer) to get a
live hub, then confirm live streaming, zoom (live + box), trigger capture, stats
histogram, and history browse all behave like the ImGui client.
## Out of scope
- No new protocol features; byte-for-byte the same WS protocol.
- Sub-project E (Go `Client/debugger/` → Qt) is a separate spec.
- Font Awesome glyph icons are replaced by Qt standard icons / unicode; exact
icon glyphs are not required for parity.
@@ -0,0 +1,167 @@
# Streaming-Chain E2E Test Suite — Design
**Date:** 2026-06-25
**Status:** Approved (autonomous implementation authorised)
**Sub-project:** A (StreamHub full-chain waveform E2E)
## Goal
A curated end-to-end test suite that validates the streaming chain
**MARTe2 App (`UDPStreamer`) → `StreamHub` → client** with *waveform* validation,
across a covering matrix of `UDPStreamer` configuration options, plus
StreamHub↔client behavioural tests (live stream, zoom in/out, time window, all
trigger modes). Output: one combined PDF report **and** a machine-readable
`results.json`.
## Decisions (from brainstorming)
1. **Matrix strategy** — curated "covering" set (~4070 scenarios): every option
value appears at least once, plus deliberately chosen high-risk interactions.
2. **Oracle** — hybrid, applied per case:
- **Analytic (A)**: clean cases reconstruct expected values from the
generator formula on the received timestamps; tolerance
`max|err| ≤ quant_step/2 + eps`; decimated streams assert only the samples
that should survive; LTTB-decimated views use a shape metric (RMSE /
correlation), not point equality.
- **Fed-reference (B)**: subtle modes (Accumulate, FullArray, First/LastSample)
compare received-vs-fed using a `FileWriter` tap of the exact signals MARTe
fed into the `UDPStreamer`.
3. **Client testing** — Go mock client (grown from `Test/E2E/streamhub/main.go`)
is the authoritative functional/waveform gate; plus a thin Qt `QTest` GUI
smoke test (offscreen) for GUI-level confidence.
4. **"to disk to client"** — the mock client persists the stream it received to
disk as a report artifact for offline comparison/plots.
5. **Output** — one orchestrator + one combined PDF **+** `results.json`.
## Architecture
New directory `Test/E2E/chain/`. A shell entry point delegates the matrix to a
Python driver, matching the existing `Test/E2E/datasources/` and
`Test/E2E/recorder/` conventions (shell + Python + Typst). Artifacts go to
`Build/x86-linux/E2E/chain/` (never the source tree).
### Two-process stack (per scenario)
```
gen_data.py -> /tmp/chain_e2e/input_<id>.bin (ground truth + fed reference)
gen_cfg.py -> marte_<id>.cfg (LinuxTimer + FileReader -> IOGAM -> UDPStreamer
[+ optional FileWriter tap])
-> hub_<id>.cfg (StreamHub Source pointing at the UDPStreamer)
StreamHub.ex -cfg hub_<id>.cfg [process 1, started first]
MARTeApp.ex -l RealTimeLoader -f marte_<id>.cfg -s Running [process 2]
chain-client (Go) -hub 127.0.0.1:<wsport> -scenario <id> -out <dir>
-> records live pushes, runs zoom/window/trigger checks,
dumps received_<id>.bin + checks_<id>.json
validate_waveform.py input_<id>.bin received_<id>.bin [+ tap_<id>.bin]
-> metrics_<id>.json (+ overlay/diff plots)
```
### Components & responsibilities
| File | Responsibility |
|------|----------------|
| `Test/E2E/chain/scenarios.py` | Declarative list of ~50 scenario dicts (the covering set) + validity rules |
| `Test/E2E/chain/gen_data.py` | scenario → deterministic `input_<id>.bin` for all 10 MARTe2 types & shapes; knows the analytic formula per signal |
| `Test/E2E/chain/gen_cfg.py` | scenario → MARTe2 app `.cfg` and StreamHub `.cfg` (unicast/multicast, Strict/Accumulate/Decimate, per-signal type/shape/timemode/quant, optional FileWriter tap) |
| `Test/E2E/chain/client/` | Go mock client (grown from `streamhub/main.go`): record live, zoom, window, all trigger modes; dump `received_<id>.bin` + `checks_<id>.json` |
| `Test/E2E/chain/validate_waveform.py` | ground-truth/fed-reference vs received; per-effect tolerance; emit `metrics_<id>.json` |
| `Test/E2E/chain/plots.py` | matplotlib overlays (truth/received/diff), trigger-capture, zoom/window figures |
| `Test/E2E/chain/run_chain_e2e.sh` | orchestrator: build, loop scenarios over the two-process stack, validate, aggregate `results.json`, compile PDF |
| `Test/E2E/chain/E2E_Report.typ` | Typst report template → `E2E_Report.pdf` |
| `Client/streamhub-qt/test/` | Qt `QTest` smoke (offscreen), run via ctest |
## Covering matrix (~50 scenarios)
Grouped so each option value is hit at least once, with targeted interactions.
Config-surface reference (verified against source):
- **Publishing modes**: `Strict`, `Accumulate` (+`MinRefreshRate`), `Decimate` (+`Ratio`).
- **Network**: unicast (`Port`); multicast (`MulticastGroup` + `DataPort`).
- **Signal types**: uint8/int8/uint16/int16/uint32/int32/uint64/int64/float32/float64.
- **Time modes** (per signal): `PacketTime`, `FullArray`, `FirstSample`,
`LastSample` (+ `TimeSignal`, `SamplingRate`).
- **Quantisation** (float only): `none`, `uint8`, `int8`, `uint16`, `int16`
(+`RangeMin`/`RangeMax`).
- **Shapes**: scalar, 100-element array, 5000-element array (fragmentation).
- **Payload**: default 1400, small (512, forces fragmentation), jumbo (65507).
- **Sources**: single and multiple sources (two UDPStreamer blocks → two hub sources).
Representative buckets (illustrative, ~50 total):
- Per-type scalar Strict unicast (10) — type fidelity.
- Time-mode sweep on arrays (FullArray/FirstSample/LastSample/PacketTime × float32/uint64-time) (~8).
- Quantisation sweep (none/uint8/int8/uint16/int16 with matching RangeMin/Max) (~6).
- Publishing modes (Strict/Accumulate@{10,100,1000Hz}/Decimate@{1,5,10}) (~8).
- Multicast variants of the above (~6).
- Shapes/fragmentation (scalar/100/5000 × payload 512/1400/65507) (~6).
- Multi-source (23 sources, mixed configs) (~3).
- Edge/robustness (mixed-type multi-signal source, large array + quant) (~3).
## Oracle details
Ground truth is the analytic generator. For a received sample at time `t` for
signal `s`, the expected value is `f_s(t)` reconstructed from the generator's
per-signal formula and the row/cycle clock.
- **Type fidelity (no quant)**: integer/float types must be bit-exact after the
type round-trip (`err == 0`).
- **Quantised float**: `quant_step = (RangeMax-RangeMin)/levels` (levels = 255,
254, 65535, 65534 for uint8/int8/uint16/int16). Require
`max|recv truth| ≤ quant_step/2 + 1e-6·range`.
- **Decimate (Ratio R)**: only every R-th producer cycle is transmitted; assert
the surviving subsequence matches truth; received count ≈ produced/R.
- **Accumulate**: batched samples must reconstruct the original per-cycle
sequence within float eps; validated against the **fed reference** tap.
- **Time modes**: reconstructed timestamps must be monotonic, within wall-clock
bounds, and spaced by `1/SamplingRate` (First/LastSample) or equal the time
array (FullArray); compared to fed reference where reconstruction is subtle.
- **LTTB live/zoom views**: shape metric — Pearson correlation ≥ 0.99 and
normalised RMSE ≤ tolerance against truth resampled onto returned timestamps.
## Client behavioural validation (Go mock, authoritative)
Per scenario the client performs and records:
- **Live**: ≥N binary v1 frames; per-signal wall-clock + monotonic time; dump
`received_<id>.bin`; waveform vs truth (shape metric).
- **Zoom**: ≥2 ranges (narrow "in" and wide "out"); points within `[t0,t1]`;
count ≤ requested `n`; shape vs truth. Also `historyZoom` when history enabled.
- **Time window**: set `windowSec`; assert returned/clamped span matches.
- **Triggers**: matrix of `edge ∈ {rising,falling,both}` × `mode ∈
{normal,single}` on an oscillating signal; validate triggerState transitions,
v2 capture window `[trigTimepre, trigTime+post]`, the edge actually crosses
threshold in the correct direction at `trigTime`, and captured waveform vs
truth. Normal mode must re-arm; single must not until `rearm`.
- Emit `checks_<id>.json` (per-check pass/fail + measured values).
## Qt GUI smoke (thin)
`Client/streamhub-qt/test/smoke_test.cpp` using `QTest`, `QT_QPA_PLATFORM=offscreen`:
construct `MainWindow`, point at a live StreamHub, spin the event loop, assert it
received ≥1 source and rendered ≥1 frame, exercise a layout change and pause
toggle without crashing. Built via the existing CMake; run via `ctest`.
## Report
`E2E_Report.typ` → `E2E_Report.pdf`:
- Title + environment + matrix summary table (scenario, config, oracle, result).
- Per-scenario waveform overlays (truth / received / diff) for a representative
subset; aggregate pass/fail.
- Trigger-capture plots (one per edge/mode).
- Zoom/window plots.
- Qt smoke result.
`results.json` — aggregate machine-readable: list of scenarios with config
summary, per-check metrics, pass/fail, and overall status (for CI gating).
## Failure & robustness
- Each scenario runs under `timeout`; stack always torn down via trap.
- A scenario that fails to produce data fails that scenario only; the suite
continues and the report records it.
- Multicast scenarios are skipped (recorded as SKIP) if the loopback multicast
route is unavailable, rather than hanging.
## Out of scope (covered elsewhere)
- Per-component standalone Typst reports → Sub-project C.
- DebugService E2E → Sub-project B.
- Real GUI click-automation beyond the QTest smoke.