Files
2026-06-26 09:11:10 +02:00

128 lines
6.4 KiB
Markdown

# 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<Source>` + `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.