Files
MARTe-Integrated-Components/docs/superpowers/specs/2026-06-25-streamhub-qt-client-design.md
T
2026-06-26 09:11:10 +02:00

8.1 KiB
Raw Blame History

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 PlotWidgets 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 PlotWidgets using nested QSplitters 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

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.