8.1 KiB
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)
-
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.
-
Custom
QPainter-basedPlotWidget, 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 focusedQPainterwidget:- 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.
-
QtWebSockets(QWebSocket), not the hand-rolled POSIX RFC6455 client.QWebSocketintegrates with the Qt event loop, exposestextMessageReceived/binaryMessageReceivedsignals, and removes the need for a manual receive thread + mutex-guarded queue. Auto-reconnect is a 3 sQTimer. Both Qt5 and Qt6 ship the WebSockets module (verified present). -
Qt5/6 autodetect in CMake.
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets WebSockets)thenfind_package(Qt${QT_VERSION_MAJOR} ...); linkQt${QT_VERSION_MAJOR}::*. This is the upstream-recommended idiom; prefers Qt6 when present, falls back to Qt5.CMAKE_AUTOMOC ON. -
Reuse, do not fork, the wire layer.
Protocol.h,Protocol.cpp, andSignalBuffer.hinClient/streamhub/are pure C++17/STL with no ImGui/SDL dependency (verified). The Qt project compiles../streamhub/Protocol.cppdirectly and adds../streamhubto 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 isImVec4 color; the Qt project defines its own domain model (Model.h) usingQColorinstead.
Threading model
Single-threaded around the Qt event loop:
QWebSocketdelivers frames as signals on the GUI thread → parsed via the reusedProtocolfunctions → mutate the model directly (no locks needed, unlike the ImGui version whose WS ran on a background thread).- A
QTimerat ~60 Hz callsupdate()on visiblePlotWidgets 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/applyMixedNormmath already inPlotPanel.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 (
zoomcmd, 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.