# 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.
