# StreamHub Qt Client Implementation Plan > **For agentic workers:** implement task-by-task. Each task ends with a buildable > deliverable. Build with `cmake -B build && cmake --build build -j` from > `Client/streamhub-qt/`. **Goal:** Native Qt5/6 oscilloscope client equivalent to the ImGui `Client/streamhub/` client, same StreamHub WS protocol. **Architecture:** Qt Widgets + custom `QPainter` plot widget + `QtWebSockets`. Reuse `../streamhub/Protocol.{h,cpp}` and `SignalBuffer.h` verbatim. Single GUI thread; `QWebSocket` signals drive a model; 60 Hz `QTimer` repaints plots. **Tech Stack:** C++17, Qt5 or Qt6 (autodetect), CMake ≥3.16. ## Global Constraints - Must compile against Qt5 **and** Qt6 — use `Qt${QT_VERSION_MAJOR}::` targets, no version-specific APIs without a guard. - Do not modify `Client/streamhub/` (reference). Reuse its `Protocol.cpp`, `Protocol.h`, `SignalBuffer.h` by include path / direct source compile. - Domain model uses `QColor` (not `ImVec4`). - Reuse `LTTBDecimate` and `SignalBuffer` from `SignalBuffer.h`. --- ### Task 1: Project scaffold + CMake autodetect + empty MainWindow **Files:** Create `CMakeLists.txt`, `main.cpp`, `MainWindow.{h,cpp}`, `Theme.{h,cpp}`, `Model.h`. - CMake: `find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets WebSockets)`, `find_package(Qt${QT_VERSION_MAJOR} ...)`, `CMAKE_AUTOMOC ON`, C++17. Sources include `${CMAKE_CURRENT_SOURCE_DIR}/../streamhub/Protocol.cpp`; include dir `../streamhub`. Link `Qt${QT_VERSION_MAJOR}::Widgets`, `Qt${QT_VERSION_MAJOR}::WebSockets`. - `Model.h`: `SignalView{SignalMeta meta; SignalBuffer buf; QColor color; double lineWidth; int marker; bool visible;}`, `Source`, `VScale`, `PlotAssignment`, `PlotLayout` enum + `layoutDims`/`layoutNames`/`layoutCount`. - `Theme`: apply Catppuccin-Mocha `QPalette` + stylesheet; `tracePalette(i)`. - `main.cpp`: parse `-host`/`-port`, build `QApplication`, apply theme, show `MainWindow`. - **Deliverable:** builds, opens an empty dark window. ### Task 2: WsClient + Hub controller (data model + WS dispatch) **Files:** Create `WsClient.{h,cpp}`, `Hub.{h,cpp}`. - `WsClient`: wraps `QWebSocket`; `connectTo(host,port)`, `reconnect`, `sendText`; 3 s reconnect `QTimer`; signals `connectedChanged(bool)`, `textReceived(QString)`, `binaryReceived(QByteArray)`. - `Hub`: owns `WsClient` + `std::vector` + `TriggerCfgState` + `CaptureFrame` + per-plot zoom caches + `HistoryInfoMsg`. On connect sends `getSources/getStats/historyInfo`. Routes text via `ParseType` to `onSources/onConfig/onStats/onTriggerState/onZoom/onHistoryZoom/onHistoryInfo/` `onMaxPointsUpdated`; binary via first byte (v2 capture vs v1 push). Command senders mirror `Build*` in `Protocol.h`. Emits signals listed in the spec. Holds a `findSource/findSignal/slotKey`. Zoom caches keyed by plot index (`requestZoom`, `requestHistoryZoom`, `nextZoomReqId`). - **Deliverable:** builds; connecting to a hub logs received message types (temporary `qDebug`), model populates (verified via a debug dump). ### Task 3: PlotWidget — static render + live scroll **Files:** Create `PlotWidget.{h,cpp}`. - `PlotWidget(Hub*, int plotIdx, QWidget*)`. `paintEvent`: draw frame, grid, ±4-div Y axis with 9 ticks, X axis with 10 ticks. Live mode: X = `[wallNow-windowSec, wallNow]`. For each assigned slot: read ring (`buf.readLast(maxPoints)`), clip to visible X, `LTTBDecimate` to 2400, normalize (normal/digital/mixed via ported `resolveVScale`/`normalizeY`/ `bandNormalize`), map to pixels, `drawPolyline` in trace color/width; optional markers. Badge row painted at top (trace name + V/div). - **Deliverable:** dropping handled later; for now a hard-coded assignment shows a live trace scrolling against wall clock. ### Task 4: PlotWidget — interaction (zoom/pan/cursors/active/vmode) + drag-drop **Files:** Modify `PlotWidget.{h,cpp}`; add small per-plot toolbar (Live/Back/ Fit, N/D/M, V-scale controls) as child widgets or painted buttons. - Mouse: wheel = Y zoom active trace (X zoom if none); Ctrl+wheel = X zoom/window; Shift+wheel = Y pan; right-drag = X pan (live→non-live); box-zoom optional. Click a badge = toggle active slot; context menu on badge = color/ width/marker/digital-in-mixed/remove. Zoom history Back/Fit/Live. - Global A/B cursors (drawn + draggable) with readouts; pause snapshot. - `dragEnterEvent`/`dropEvent` accept a signal-ref mime → append assignment. - Hi-res WS zoom + history zoom requests through `Hub` (live-narrow throttle, non-live debounce) and prefer cached hi-res/history data when it covers view. - **Deliverable:** full single-plot interaction parity. ### Task 5: PlotGrid + SourceSidebar + MainWindow toolbar **Files:** Create `PlotGrid.{h,cpp}`, `SourceSidebar.{h,cpp}`; expand `MainWindow.{h,cpp}`. - `PlotGrid`: nested `QSplitter`s realizing each `PlotLayout`; rebuild on change. - `SourceSidebar`: `QTreeWidget` sources→signals; drag a signal (mime with src/sig index); "Add source" + remove; reflects `Hub` signals. - MainWindow toolbar: sidebar toggle, layout picker (menu of the 8 layouts), pause, cursors toggle, trigger-bar toggle, history-bar toggle, window-preset combo, stats button, connection (host/port + reconnect), status LED. - Add-source dialog (label/host/port/multicast/dataPort) → `Hub::addSource`. - **Deliverable:** drag-drop from sidebar to any layout cell works end to end. ### Task 6: TriggerBar + StatsDialog + HistoryBar **Files:** Create `TriggerBar.{h,cpp}`, `StatsDialog.{h,cpp}`, `HistoryBar.{h,cpp}`; wire into `MainWindow`. - `TriggerBar`: signal combo (full keys incl array index), edge combo, threshold spin, window combo (100 µs…10 s), pre% slider, normal/single → `setTrigger`; Arm/Disarm/Rearm/Stop; badge from `triggerStateChanged`. Capture frame routes to plots' trigger view. - `StatsDialog`: table of metrics per source + custom 20-bin histogram widget (`QPainter` bars), refreshed on `statsChanged`. - `HistoryBar`: Live, range readout, pan ←/→, jump presets, All; drives plots' non-live X range + history-zoom. - **Deliverable:** trigger capture, stats histogram, history browse all match the ImGui client. ### Task 7: Build, run, verify, document - Clean Release build on Qt6 (and confirm Qt5 configure path). - Launch against a live hub; verify the spec's parity checklist. - Add a `Client/streamhub-qt` build note to `CLAUDE.md` and `ARCHITECTURE.md`. - **Deliverable:** working client + docs.