Implemented new C++ logic
This commit is contained in:
+359
-49
@@ -6,13 +6,15 @@ This document describes the internal architecture of the MARTe2 Integrated Compo
|
||||
|
||||
## 1. Repository Overview
|
||||
|
||||
### DebugService path (legacy introspection / breakpoints)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ MARTe2 Application │
|
||||
│ ┌──────────┐ ┌───────────────────────┐ ┌──────────────────┐ │
|
||||
│ │ GAM(s) │ │ DebugBrokerWrapper │ │ FastScheduler │ │
|
||||
│ │ │◄──│ (Registry-patched) │ │ (unmodified) │ │
|
||||
│ └──────────┘ └──────────┬────────────┘ └──────────────────┘ │
|
||||
│ MARTe2 Application │
|
||||
│ ┌──────────┐ ┌───────────────────────┐ ┌──────────────────┐ │
|
||||
│ │ GAM(s) │ │ DebugBrokerWrapper │ │ FastScheduler │ │
|
||||
│ │ │◄──│ (Registry-patched) │ │ (unmodified) │ │
|
||||
│ └──────────┘ └──────────┬────────────┘ └──────────────────┘ │
|
||||
│ │ RT-path API │
|
||||
└────────────────────────────┼───────────────────────────────────────┘
|
||||
│
|
||||
@@ -32,28 +34,41 @@ This document describes the internal architecture of the MARTe2 Integrated Compo
|
||||
│ 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 │ │
|
||||
│ └──────────────────────┬──────────────────────┘ │
|
||||
│ MARTe2 Application │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ UDPStreamer DataSource │ │
|
||||
│ │ - Receives signals from GAMs via IOGAM │ │
|
||||
│ │ - Serialises to UDPS binary protocol │ │
|
||||
│ │ - Manages per-client sessions │ │
|
||||
│ └──────────────────────┬──────────────────────┘ │
|
||||
└─────────────────────────┼──────────────────────────────────────────┘
|
||||
UDP 44500
|
||||
UDP 44500 (unicast or multicast)
|
||||
│
|
||||
┌───────────▼──────────────┐
|
||||
│ Common/Client/go │
|
||||
│ udpsprotocol package │
|
||||
│ (decode CONFIG+DATA) │
|
||||
└───────────┬──────────────┘
|
||||
│ WebSocket
|
||||
┌───────────▼──────────────┐
|
||||
│ Browser oscilloscope │
|
||||
│ (Chart.js plots) │
|
||||
└──────────────────────────┘
|
||||
┌───────────────▼───────────────┐
|
||||
│ 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) │
|
||||
└──────────────────┘ └───────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
@@ -65,45 +80,72 @@ 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 |
|
||||
| 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]
|
||||
[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
|
||||
### Quantization / Dequantization
|
||||
|
||||
When `QuantizedType` is set on a signal, the server maps `[RangeMin, RangeMax]` to the
|
||||
full integer range of the quantized type before transmission. The Go client reverses this
|
||||
using the `Scale` and `Offset` fields from `UDPSSignalDescriptor`.
|
||||
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)` |
|
||||
|
||||
---
|
||||
|
||||
@@ -112,8 +154,8 @@ using the `Scale` and `Offset` fields from `UDPSSignalDescriptor`.
|
||||
### 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
|
||||
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`,
|
||||
@@ -134,10 +176,11 @@ ConfigureApplication()
|
||||
```
|
||||
|
||||
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`
|
||||
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
|
||||
@@ -166,9 +209,9 @@ Entry format: `[ID:4][Timestamp:8][Size:4][Data:N]`.
|
||||
|
||||
### 3.5 DebugService Threads
|
||||
|
||||
| Thread | Role |
|
||||
|---|---|
|
||||
| `Server()` | Accepts one TCP client; reads text commands; writes JSON/text responses |
|
||||
| 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
|
||||
@@ -194,21 +237,273 @@ DebugServiceI *svc = DebugServiceI::GetInstance(); // broker wrapper retrieves i
|
||||
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
|
||||
- The Go client interpolates per-sample timestamps as `t0 + e/SamplingRate * dt`
|
||||
- Clients interpolate per-sample timestamps as `t0 + e/SamplingRate`
|
||||
- `UDPStreamer` packs all elements contiguously with no per-element header
|
||||
|
||||
---
|
||||
|
||||
## 5. Go Client Packages
|
||||
## 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -216,25 +511,40 @@ Pure Go decoder for UDPS packets:
|
||||
### `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)
|
||||
|
||||
---
|
||||
|
||||
## 6. Key Design Decisions
|
||||
## 9. Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|---|---|
|
||||
| Shared `UDPSProtocol.h` in `Common/UDP/` | Eliminates duplication between `UDPStreamer` and any future UDP producer; Go packages decode the same binary layout |
|
||||
|----------|-----------|
|
||||
| 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++ components | MARTe2 coding convention; `Vec<T>` used instead of `std::vector` |
|
||||
| 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 |
|
||||
| Go for clients | Minimal dependencies, single binary, easy cross-compilation; no Node/npm toolchain required |
|
||||
| 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 |
|
||||
|
||||
Reference in New Issue
Block a user