128 lines
6.4 KiB
Markdown
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.
|