# 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`), 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.