Files
MARTe-Integrated-Components/ARCHITECTURE.md
2026-06-26 09:11:10 +02:00

614 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Architecture
This document describes the internal architecture of the MARTe2 Integrated Components.
---
## 1. Repository Overview
### DebugService path (legacy introspection / breakpoints)
```
┌────────────────────────────────────────────────────────────────────┐
│ MARTe2 Application │
│ ┌──────────┐ ┌───────────────────────┐ ┌──────────────────┐ │
│ │ GAM(s) │ │ DebugBrokerWrapper │ │ FastScheduler │ │
│ │ │◄──│ (Registry-patched) │ │ (unmodified) │ │
│ └──────────┘ └──────────┬────────────┘ └──────────────────┘ │
│ │ RT-path API │
└────────────────────────────┼───────────────────────────────────────┘
┌────────▼────────┐
│ DebugServiceI │ ← Abstract singleton interface
└────────┬────────┘
┌────────▼────────┐
│ DebugService │
│ TCP/UDP │
└────────┬────────┘
┌───────────────┼──────────────┐
│ │ │
TCP 8080 UDP 8081 TCP 8082
(commands) (telemetry) (TcpLogger)
│ │
┌────────▼───────────────▼──────┐
│ Client/debugger │
│ Go web client (browser) │
└───────────────────────────────┘
```
### UDPStreamer / StreamHub path (high-throughput signal streaming)
```
┌────────────────────────────────────────────────────────────────────┐
│ MARTe2 Application │
│ ┌─────────────────────────────────────────────┐ │
│ │ UDPStreamer DataSource │ │
│ │ - Receives signals from GAMs via IOGAM │ │
│ │ - Serialises to UDPS binary protocol │ │
│ │ - Manages per-client sessions │ │
│ └──────────────────────┬──────────────────────┘ │
└─────────────────────────┼──────────────────────────────────────────┘
UDP 44500 (unicast or multicast)
┌───────────────▼───────────────┐
│ StreamHub │
│ Source/Applications/StreamHub│
│ - N simultaneous sources │
│ - Ring buffers per signal │
│ - LTTB decimation │
│ - Trigger engine (FSM) │
│ - M WebSocket clients │
└──────────────┬────────────────┘
WebSocket 8090
┌──────────────┴──────────────┐
│ │
┌───────▼──────────┐ ┌────────────▼──────────────┐
│ Browser (any) │ │ ImGui Desktop Client │
│ Client/udpstreamer│ │ Client/streamhub/ │
│ Go web server + │ │ C++ native window │
│ JS oscilloscope │ │ (SDL2 + OpenGL + ImPlot) │
└──────────────────┘ └───────────────────────────┘
```
---
## 2. UDPS Binary Protocol
Defined in `Common/UDP/UDPSProtocol.h` and shared between:
- `UDPStreamer` (C++ producer)
- `DebugService` (C++ producer for trace packets)
- `Common/Client/go/udpsprotocol` (Go decoder)
- `Source/Components/Interfaces/UDPStream/UDPSClient` (C++ consumer)
### Packet Header (17 bytes, little-endian, packed)
| Offset | Size | Type | Field | Description |
| ------ | ---- | ------ | ---------------- | -------------------------------------------------------------- |
| 0 | 4 | uint32 | `magic` | Always `0x53504455` ('UDPS' LE) |
| 4 | 1 | uint8 | `type` | Packet type (DATA=0, CONFIG=1, ACK=2, CONNECT=3, DISCONNECT=4) |
| 5 | 4 | uint32 | `counter` | Per-update sequence number (same across all fragments) |
| 9 | 2 | uint16 | `fragmentIdx` | 0-based fragment index |
| 11 | 2 | uint16 | `totalFragments` | Total fragments for this update |
| 13 | 4 | uint32 | `payloadBytes` | Bytes of payload following this header |
### Signal Descriptor (136 bytes, packed)
| Field | Type | Description |
| -------------- | ------- | --------------------------------------------------------- |
| `name` | char[64]| Null-terminated signal name |
| `typeCode` | uint8 | UDPS_TYPECODE_* (0=u8, 1=i8, … 9=f32, 10=f64) |
| `quantType` | uint8 | UDPS_QUANT_* (0=none, 1=u8, 2=i8, 3=u16, 4=i16) |
| `numDimensions`| uint8 | 0=scalar, 1=array, 2=matrix |
| `numRows` | uint32 | Element count for 1D; rows for 2D |
| `numCols` | uint32 | 1 for scalar/1D; columns for 2D |
| `rangeMin` | float64 | Physical range minimum (for dequantisation) |
| `rangeMax` | float64 | Physical range maximum (for dequantisation) |
| `timeMode` | uint8 | UDPS_TIMEMODE_* (0=packet, 1=first, 2=last, 3=full_array)|
| `samplingRate` | float64 | Hz; used for first/last sample interpolation |
| `timeSignalIdx`| uint32 | Index of time-ref signal; 0xFFFFFFFF if none |
| `unit` | char[32]| Null-terminated physical unit string |
### CONFIG Payload
Sent when the signal set changes or a client connects:
```
[uint32 numSigs]
numSigs × UDPSSignalDescriptor (136 bytes each, packed)
[uint8 publishMode] 0=Strict/Decimate, 1=Accumulate
```
### DATA Payload (Strict / Decimate modes)
```
[uint64 HRT timestamp]
per-signal data in CONFIG order (quantised or raw, no inter-signal padding)
```
### DATA Payload (Accumulate mode)
```
[uint64 HRT timestamp]
[uint32 numSamples]
for each signal: if scalar → numSamples elements; else → NumElements once
```
### Quantization / Dequantization
When `quantType != QUANT_NONE`, the server maps `[rangeMin, rangeMax]` to the full
integer range. Clients reverse with:
| quantType | Formula |
|-----------|---------|
| QUANT_UINT8 | `rangeMin + (q / 255.0) × (rangeMax rangeMin)` |
| QUANT_INT8 | `rangeMin + ((q + 127.0) / 254.0) × (rangeMax rangeMin)` |
| QUANT_UINT16 | `rangeMin + (q / 65535.0) × (rangeMax rangeMin)` |
| QUANT_INT16 | `rangeMin + ((q + 32767.0) / 65534.0) × (rangeMax rangeMin)` |
---
## 3. DebugService Architecture
### 3.1 Registry Patching (Zero-Code-Change Instrumentation)
`DebugService::PatchRegistry()` replaces the `ObjectBuilder` for all `MemoryMap*Broker`
types in the MARTe2 `ClassRegistryDatabase`. Any subsequent `ConfigureApplication()` call
will instantiate `DebugBrokerWrapper<T>` objects instead of the originals. No application
source changes are needed.
Wrapped types: `MemoryMapInputBroker`, `MemoryMapOutputBroker`,
`MemoryMapSynchronisedInputBroker`, `MemoryMapSynchronisedOutputBroker`,
`MemoryMapMultiBufferBroker`, `MemoryMapMultiBufferOutputBroker`,
`MemoryMapAsynchronousInputBroker`, `MemoryMapAsynchronousOutputBroker`,
`MemoryMapInterpolatedInputBroker`, `MemoryMapStatefulOutputBroker`,
`MemoryMapStatefulInputBroker`.
### 3.2 Signal Registration
```
ConfigureApplication()
└─► DebugBrokerWrapper::Init()
└─► DebugBrokerHelper::InitSignals()
├─► DebugServiceI::RegisterSignal() (canonical + GAM alias)
└─► DebugServiceI::RegisterBroker()
```
Each signal is registered twice:
1. **Canonical**: `<DataSourcePath>.<SignalName>` (e.g. `App.Data.DDB.Counter`)
2. **GAM alias**: `<GAMPath>.In.<SignalName>` or `<GAMPath>.Out.<SignalName>`
Both map to the same `DebugSignalInfo*`. `AliasMatch()` in `DebugServiceBase.cpp`
performs bidirectional suffix matching so short unqualified names work in commands.
### 3.3 RT Hot Path
```
RealTimeThread::Execute()
└─► DebugBrokerWrapper::Execute()
├─► Base::Execute() (actual data movement)
└─► DebugBrokerHelper::Process()
├─► For each active signal:
│ └─► DebugServiceI::ProcessSignal()
│ ├─► if isForcing: memcpy forcedValue → signal memory
│ ├─► if isTracing & decimation fires: push to TraceRingBuffer
│ └─► if breakOp set: evaluate condition → SetPaused(true)
└─► (output brokers only) ConsumeStepIfNeeded()
```
### 3.4 TraceRingBuffer
Single-producer/single-consumer circular byte buffer (4 MB default).
Entry format: `[ID:4][Timestamp:8][Size:4][Data:N]`.
- `Push()` serialised by `tracePushMutex` (multiple RT threads may write)
- `Pop()` called exclusively by the Streamer thread
- Corrupt entries detected by `size >= bufferSize`; discarded gracefully
### 3.5 DebugService Threads
| Thread | Role |
| ------------ | -------------------------------------------------------------------------------- |
| `Server()` | Accepts one TCP client; reads text commands; writes JSON/text responses |
| `Streamer()` | Drains `TraceRingBuffer`; assembles/sends UDP datagrams; polls monitored signals |
### 3.6 DebugServiceI Abstraction
`DebugServiceI.h` defines a pure-virtual singleton so transport implementations
(`DebugService` TCP/UDP, `WebDebugService` HTTP/SSE) share the same broker injection layer.
```cpp
DebugServiceI::SetInstance(this); // concrete implementation registers itself
DebugServiceI *svc = DebugServiceI::GetInstance(); // broker wrapper retrieves it
```
---
## 4. UDPStreamer DataSource
`UDPStreamer` is a standard MARTe2 `DataSourceI` that:
1. Maintains a list of registered client sessions (UDP source address + port)
2. On each `Synchronise()` call (once per RT cycle):
- Serialises all configured signals into one or more UDPS DATA packets
- Sends to all connected clients
3. Handles `CONNECT` / `DISCONNECT` / `ACK` packets from clients
4. Sends `CONFIG` packets when the signal list changes or a new client connects
5. Applies quantization (`QuantizedType`, `RangeMin`, `RangeMax`) per signal
6. Supports unicast and multicast UDP delivery (configurable per instance)
### Packed Signals (Accumulate mode)
When a signal has `NumberOfElements > 1` and `SamplingRate` is configured:
- `TimeMode = FirstSample`: the `TimeSignal` value anchors the burst timestamp
- Clients interpolate per-sample timestamps as `t0 + e/SamplingRate`
- `UDPStreamer` packs all elements contiguously with no per-element header
---
## 5. StreamHub C++ Application
`StreamHub` (`Source/Applications/StreamHub/`) is a **headless standalone process** that
bridges N MARTe2 UDPStreamer sources to M simultaneous WebSocket clients. It replaces the
Go-based `Client/udpstreamer/` hub for deployments where a Go toolchain is unavailable
or where the hub must run as a C++ service.
### Components
| File | Purpose |
|------|---------|
| `main.cpp` | Entry point; parses `-cfg`, `-port`, `-maxPoints` flags |
| `StreamHub.h/.cpp` | Top-level orchestrator; push loop; WS command dispatch |
| `UDPSourceSession.h/.cpp` | One UDPS source: `UDPSClient` + ring buffers + wall-clock calibration + stats |
| `TriggerEngine.h/.cpp` | FSM: IDLE / ARMED / COLLECTING / TRIGGERED (web-client semantics) |
| `WSServer.h/.cpp` | RFC 6455 WebSocket server; multi-client accept+read threads |
| `WSFrame.h` | WebSocket frame encode/decode helpers |
| `SignalRingBuffer.h` | Thread-safe fixed-capacity circular buffer (float64 t, v) |
| `UDPSourceStats.h` | Per-source statistics struct |
| `LTTB.h` | Largest Triangle Three Buckets decimation (header-only) |
| `SHA1.h` | Inline SHA-1 for WebSocket handshake Accept key |
| `Base64.h` | Inline Base64 encoder for WebSocket handshake |
### Time Base (wall-clock calibration)
All ring-buffer timestamps are **Unix wall-clock seconds** (float64). Each
`UDPSourceSession` calibrates source time to the hub's `CLOCK_REALTIME` on the
first packet (offset = wallNow sourceTime) and re-anchors automatically if the
computed time drifts more than 2 s from arrival time (source restart). Per-sample
time depends on the signal's UDPS `timeMode`:
| timeMode | Per-sample timestamp |
|----------|---------------------|
| `FirstSample` / `LastSample` | calibrated time-signal anchor ± k/samplingRate |
| `FullArray` | calibrated per-element time-signal array |
| Packet (scalar) | packet arrival wall time |
| Packet (array, n>1) | elements span `(lastPktWall, wallNow]`, interpolated from the inter-packet gap |
### Push Loop
Runs at `PushRate` Hz (default 30 Hz):
1. For each configured source:
- On first CONFIG: broadcast `sources` + `config`, reset push cursors
- For each signal: read **only the samples written since the last tick**
(per-signal monotonic cursor into the ring; overrun clamps to oldest)
- LTTB-decimate temporal signals to `MaxPushPoints` (default 50); scalars
and packet-timed arrays go verbatim
- Serialise one binary v1 frame per source; broadcast to all WS clients
- Cursors advance even with zero clients (no backlog burst on connect)
2. Trigger servicing (capture finalisation, auto-rearm, state events)
3. Every `PushRate / StatsRate` ticks: broadcast JSON stats frame
Pushing only-new samples is essential: re-reading overlapping windows and
re-LTTBing them picks different points per window and corrupts the traces.
### Trigger Engine
Hub-side trigger with the web client's semantics (config: signal key
`"src:sig"` or `"src:sig[i]"`, edge rising/falling/both, threshold, window
0.1 ms10 s, pre-trigger percent, mode normal/single):
```
IDLE →[arm]→ ARMED
ARMED →[edge crossing]→ COLLECTING (latches trigTime, pre/postSec)
COLLECTING →[post window + margin elapsed]→ TRIGGERED (broadcast binary v2 capture)
TRIGGERED →[auto-rearm (normal, ~200 ms) | rearm (single)]→ ARMED
any →[disarm]→ IDLE
```
`CheckSample()` is called from the UDPSClient receive thread for every decoded
sample of the configured signal. The capture is assembled in the push loop from
`ReadSignalRange(trigTimepreSec, trigTime+postSec)` over **all** signals,
LTTB-capped at 20 000 points/signal, and broadcast as a binary version-2 frame.
A `stopped` flag (`trigStop`) freezes auto-rearm.
### Configuration File (MARTe2 cfg format)
```
Hub = {
WSPort = 8090
MaxPoints = 20000 // zoom/live window cap (setMaxPoints)
PushRate = 30 // push loop Hz
MaxPushPoints = 50 // LTTB cap per signal per tick
StatsRate = 1 // stats broadcast Hz
RingTemporal = 1000000 // ring capacity (points) for multi-element signals
RingScalar = 100000 // ring capacity (points) for scalar signals
SourcesFile = "streamhub_sources.json" // dynamic-source persistence
Sources = {
App1 = {
Label = "MARTe2 App 1"
Addr = "127.0.0.1"
Port = 44500
}
App2 = {
Label = "MARTe2 App 2"
Addr = "192.168.1.10"
Port = 44501
MulticastGroup = "239.0.0.1"
DataPort = 44504
}
}
}
```
Sources added at runtime (`addSource`) get generated ids `s1, s2, …`;
`saveSources` persists them to `SourcesFile` (JSON array of
`{label, addr, multicastGroup?, dataPort?}`), reloaded at start-up.
### Build
```bash
MARTe2_DIR=/path/to/MARTe2 \
make -C Source/Applications/StreamHub -f Makefile.gcc TARGET=x86-linux
# Produces: Build/x86-linux/StreamHub/StreamHub.ex
```
---
## 6. StreamHub WebSocket Protocol
All client↔hub communication uses WebSocket (RFC 6455). Text frames carry JSON;
binary frames carry data push payloads.
### Commands (client → hub, JSON text frames)
| `type` field | Additional fields | Action |
|------------------|-------------------|--------|
| `ping` | — | Hub replies `{"type":"pong"}` |
| `addSource` | `label`, `addr` (`"host:port"`), `multicastGroup?`, `dataPort?` | Connect to a new UDPS source; hub assigns id `s1, s2, …` |
| `removeSource` | `id` | Disconnect and remove a source |
| `saveSources` | — | Persist the current dynamic source list to `SourcesFile` (JSON) |
| `getSources` | — | Trigger `sources` broadcast |
| `getConfig` | `sourceId` | Trigger `config` broadcast for one source |
| `getStats` | — | Trigger `stats` broadcast |
| `setTrigger` | `signal` (`"src:sig"` or `"src:sig[i]"`), `edge` (`"rising"`\|`"falling"`\|`"both"`), `threshold`, `windowSec`, `prePercent`, `mode` (`"normal"`\|`"single"`) | Configure the hub-side trigger |
| `arm` | — | IDLE → ARMED |
| `disarm` | — | Any state → IDLE |
| `rearm` | — | TRIGGERED → ARMED (used in single mode) |
| `trigStop` | `stopped?` (bool; absent = toggle) | Suppress auto-rearm in normal mode |
| `zoom` | `reqId`, `t0`, `t1`, `n?`, `signals?` (comma-separated `"src:sig"` keys; absent = all) | Hi-res window read; reply is **unicast** to the requester. `n` absent → 2400; `n≤0` → no decimation |
| `setMaxPoints` | `maxPoints` | Resize all ring buffers (applied in push loop; cursors reset) |
### Events (hub → client, JSON text frames)
| `type` field | Fields | When sent |
|--------------------|--------|-----------|
| `sources` | `sources:[{id, label, addr:"host:port", state}]` | On connect; after add/remove/getSources; on first CONFIG |
| `config` | `sourceId`, `publishMode`, `signals:[{name, typeCode, quantType, numDimensions, numRows, numCols, rangeMin, rangeMax, timeMode, samplingRate, timeSignalIdx, unit}]` | After CONFIG received from source |
| `stats` | `sources:{id:{state, totalReceived, totalLost, rateHz, rateStdHz, fragsPerCycle, bytesPerCycle, cycleAvgMs, cycleStdMs, cycleMinMs, cycleMaxMs, cycleHistMin, cycleHistMax, cycleHist:[20]}}` | At `StatsRate` Hz |
| `triggerState` | `state` (`"idle"`\|`"armed"`\|`"collecting"`\|`"triggered"`), `mode`, `stopped`, `trigTime?` | On any trigger FSM transition |
| `zoom` | `reqId`, `signals:{"src:sig":{t:[…], v:[…]}}` (`t` printed `%.17g`, `v` `%.9g`) | Unicast reply to `zoom` |
| `maxPointsUpdated` | `maxPoints` | After ring buffer resize |
| `pong` | — | In reply to `ping` |
### Binary Push Frame (version 1, hub → client, binary WS frame)
Little-endian throughout. Sent at `PushRate` Hz per source; contains **only
samples new since the previous push** (per-signal cursors), decimated to
`MaxPushPoints` per signal.
```
[1] version = 1
[1] sourceIdLen (L)
[L] sourceId (UTF-8, no null terminator)
[4] numSignals (uint32 LE)
Per signal:
[2] keyLen (uint16 LE) (K)
[K] key = signal name (UTF-8; "name[i]" per element for multi-element PACKET signals)
[4] pairCount (uint32 LE) (N)
[N×8] time array (float64 LE, Unix wall-clock seconds)
[N×8] value array (float64 LE, physical units)
```
### Binary Capture Frame (version 2, hub → client, binary WS frame)
Broadcast once per trigger capture (FSM COLLECTING → TRIGGERED). Each signal is
LTTB-decimated to at most 20 000 points.
```
[1] version = 2
[8] trigTime (float64 LE, Unix seconds)
[8] preSec (float64 LE)
[8] postSec (float64 LE)
[4] numSignals (uint32 LE)
Per signal:
[2] keyLen (uint16 LE) (K)
[K] fullKey = "src:sig" (UTF-8)
[4] pairCount (uint32 LE) (N)
[N×8] time array (float64 LE, Unix seconds)
[N×8] value array (float64 LE)
```
---
## 7. ImGui Desktop Client
`Client/streamhub/` is a **native C++ desktop oscilloscope** that connects directly to
StreamHub via WebSocket and provides the same feature set as the browser client.
### Technology Stack
| Component | Library | Notes |
|-----------|---------|-------|
| UI framework | Dear ImGui (v1.91.x) | Fetched by CMake FetchContent |
| Time-series plots | ImPlot (v0.17.x) | Fetched by CMake FetchContent |
| Window + input | SDL2 | System package (`sdl2`) |
| GPU rendering | OpenGL 3.3 core | System package |
| WebSocket client | Custom (POSIX) | Reuses `WSFrame.h`, `SHA1.h`, `Base64.h` from StreamHub |
### Features
| Feature | Implementation |
|---------|---------------|
| Source sidebar | `ImGui::TreeNodeEx` per source; draggable signal leaves |
| Drag & drop signals | `ImGui::BeginDragDropSource``ImGui::BeginDragDropTarget` on plots |
| Multi-plot layout | 1×1, 1×2, 2×1, 1×3, 3×1, 2×2, 1×4, 4×1 grid; `ImGui::BeginTable` |
| Live data streaming | Binary v1 frame parser → per-signal ring buffer → ImPlot; live x-axis follows wall clock |
| LTTB decimation | Client-side, before plotting |
| Pause / resume | Per-plot toggle; frozen data still rendered |
| Zoom | ImPlot scroll+drag with history stack (Back / Fit / Reset-to-live); hub WS `zoom` for hi-res when zoomed in |
| Cursor A / B | ImPlot `DragLineX` annotations + ΔT and per-trace readouts |
| Trigger panel | Signal/edge/threshold/window/pre%/mode controls; Arm/Disarm/Rearm/Stop; badge (IDLE/ARMED/COLLECTING/TRIGGERED) |
| Trigger capture view | Binary v2 frames rendered relative to trigTime in `[-preSec, +postSec]`, marker at 0 |
| V-scale modes | normal / digital / mixed normalisation (parity with web client) |
| Stats panel | Per-source rate/cycle stats + 20-bin cycle-time histogram (`ImPlot::PlotBars`) |
| Add / remove source | Modal dialog; sends `addSource` / `removeSource` JSON |
| Signal styles | Per-signal color picker, line width, markers |
| Auto-reconnect | WSClient receive thread retries every 3 s |
### Build
```bash
# Install SDL2 (Arch Linux)
sudo pacman -S sdl2
cd Client/streamhub
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
# Produces: build/StreamHubClient
# Run (StreamHub must already be running on port 8090)
./build/StreamHubClient -host 127.0.0.1 -port 8090
```
---
## 7b. Qt Desktop Client
`Client/streamhub-qt/` is a **native Qt Widgets desktop oscilloscope** — a
feature/UX-equivalent alternative to the ImGui client, speaking the identical
StreamHub WebSocket protocol. It targets deployments that prefer a system Qt
runtime over bundled ImGui/SDL2/OpenGL, and it builds against either Qt6
(preferred) or Qt5 for old-Linux back-compatibility.
### Technology Stack
| Component | Library | Notes |
|-----------|---------|-------|
| UI framework | Qt Widgets | Autodetect Qt6 → Qt5 via `find_package(QT NAMES Qt6 Qt5 ...)`, then `Qt${QT_VERSION_MAJOR}::` targets |
| Time-series plots | Custom `QPainter` (`PlotWidget`) | No QtCharts/QCustomPlot — for ImPlot parity, zero extra deps, EUPL-clean |
| WebSocket client | `QtWebSockets` `QWebSocket` (`WsClient`) | Signals delivered on the GUI thread |
| Wire layer | Reused verbatim from `../streamhub/` | `Protocol.{h,cpp}`, `SignalBuffer.h` (framework-free C++17) |
### Threading & keyword model
- **Single GUI thread.** `QWebSocket` text/binary signals arrive on the GUI
thread, so no locks are needed (unlike the ImGui client's background receive
thread + drain). A 60 Hz `QTimer` (16 ms) drives repaint and panel refresh.
- **`QT_NO_KEYWORDS`.** The reused `Protocol.h`/`Model.h` structs have members
named `signals` (e.g. `ZoomResponse::signals`), which collide with Qt's
`signals`/`slots`/`emit` macros. The build defines `QT_NO_KEYWORDS`; all Qt
classes here use `Q_SIGNALS:` / `Q_SLOTS:` / `Q_EMIT` instead. This keeps the
shared wire layer unmodified.
### Components
| File | Responsibility |
|------|----------------|
| `Hub.{h,cpp}` | Domain model + command builders; owns `WsClient`; re-emits parsed events as Qt signals |
| `WsClient.{h,cpp}` | `QWebSocket` wrapper; auto-reconnect (3 s timer) |
| `PlotWidget.{h,cpp}` | One QPainter plot; live/stored/trigger modes, cursors, zoom cache |
| `PlotGrid.{h,cpp}` | Persistent pool of 8 `PlotWidget`s mounted into nested `QSplitter`s per layout |
| `SourceSidebar.{h,cpp}` | `QTreeWidget` of sources/signals; drag source = mime `application/x-shq-signal` carrying `qint32[2]` {srcIdx, sigIdx} (LittleEndian) |
| `TriggerBar.{h,cpp}` | Trigger config/arm controls + state badge |
| `StatsDialog.{h,cpp}` | Per-source stats table + 20-bin QPainter histogram |
| `HistoryBar.{h,cpp}` | Live / pan / jump-ago / show-all history navigation |
| `MainWindow.{h,cpp}` | Toolbars, docks, layout menu, connection controls, 60 Hz tick |
### Build & run
```bash
cd Client/streamhub-qt
cmake -B build # autodetects Qt6, falls back to Qt5
cmake --build build -j$(nproc)
# Produces: build/StreamHubQtClient
# Run (StreamHub must already be running on port 8090)
./build/StreamHubQtClient --host 127.0.0.1 --port 8090
```
> Use long `--host`/`--port` (or short `-H`/`-p`). A single-dash `-host` is
> misparsed by `QCommandLineParser` as clustered short flags.
Verified: builds and links cleanly against both Qt6 (6.11) and Qt5 (5.15); the
Qt6 binary connects to a live StreamHub (server logs *"WebSocket client
connected"*) and runs without error.
---
## 8. Go Client Packages
### `Common/Client/go/udpsprotocol`
Pure Go decoder for UDPS packets:
- `Decoder` — reassembles fragments; returns complete CONFIG or DATA payloads
- `SignalDescriptor` — mirrors `UDPSSignalDescriptor` from C++
- `DataPacket` — decoded signal values with per-sample timestamps
### `Common/Client/go/wshub`
WebSocket broadcast hub:
- Multiple browser clients share one UDP → WebSocket relay
- JSON-encodes signal samples and broadcasts to all connected browsers
### `Client/debugger`
Go HTTP server providing the debug web UI:
- `main.go` — CLI entry point; starts TCP relay and HTTP server
- `martecontrol.go` — TCP client to `DebugService`; routes commands from browser
- `static/` — Single-page application (HTML/JS/CSS) with Chart.js plots
### `Client/udpstreamer`
Go HTTP server providing the UDP streamer oscilloscope web UI:
- `main.go` — CLI entry point; connects to one or more UDPStreamer ports
- `static/` — Full-featured SPA (uPlot plots, LTTB decimation, trigger UI, zoom)
- Compatible with StreamHub's WebSocket protocol (identical binary frame format)
---
## 9. Key Design Decisions
| Decision | Rationale |
|----------|-----------|
| Shared `UDPSProtocol.h` in `Common/UDP/` | Eliminates duplication between `UDPStreamer`, `UDPSClient`, and any future UDPS producer; Go packages decode the same binary layout |
| `DebugServiceI` abstract interface | Allows `DebugService` (TCP/UDP) and `WebDebugService` (HTTP/SSE) to share broker injection without coupling |
| No STL in C++ MARTe2 components | MARTe2 coding convention; `FastPollingMutexSem` and fixed arrays used throughout |
| STL allowed in ImGui client | `Client/streamhub/` is not a MARTe2 component; C++17 + STL reduce boilerplate without any architectural conflict |
| `FastPollingMutexSem` on hot path | Lowest-latency synchronisation primitive available in MARTe2; avoids OS scheduler involvement |
| Separate `TCPLogger` component | Decoupled from `DebugService`; can be used with any MARTe2 application independently |
| C++ StreamHub instead of Go hub | Single binary, no Go toolchain required on deployment host; reuses MARTe2 `UDPSClient` directly; embeds trigger engine |
| ImPlot for time-series in native client | Best-in-class ImGui plotting extension; handles millions of points with GPU acceleration; native zoom/pan without custom code |
| Reuse `WSFrame.h` / `SHA1.h` / `Base64.h` in ImGui client | Avoids duplicating the WebSocket framing implementation; CMake include path points directly at `Source/Applications/StreamHub/` |
| POSIX WebSocket client (no external library) | Keeps the client self-contained; RFC 6455 frame parsing is already implemented in `WSFrame.h`; only handshake SHA-1 and Base64 are needed |
| Go for browser-based clients | Minimal dependencies, single binary, easy cross-compilation; no Node/npm toolchain required for the web UI path |