# 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 ms–10 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(trigTime−preSec, 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 |
