30 KiB
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:
- Canonical:
<DataSourcePath>.<SignalName>(e.g.App.Data.DDB.Counter) - 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 bytracePushMutex(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.
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:
- Maintains a list of registered client sessions (UDP source address + port)
- On each
Synchronise()call (once per RT cycle):- Serialises all configured signals into one or more UDPS DATA packets
- Sends to all connected clients
- Handles
CONNECT/DISCONNECT/ACKpackets from clients - Sends
CONFIGpackets when the signal list changes or a new client connects - Applies quantization (
QuantizedType,RangeMin,RangeMax) per signal - 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: theTimeSignalvalue anchors the burst timestamp- Clients interpolate per-sample timestamps as
t0 + e/SamplingRate UDPStreamerpacks 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):
- 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)
- On first CONFIG: broadcast
- Trigger servicing (capture finalisation, auto-rearm, state events)
- Every
PushRate / StatsRateticks: 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
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
# 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.
QWebSockettext/binary signals arrive on the GUI thread, so no locks are needed (unlike the ImGui client's background receive thread + drain). A 60 HzQTimer(16 ms) drives repaint and panel refresh. QT_NO_KEYWORDS. The reusedProtocol.h/Model.hstructs have members namedsignals(e.g.ZoomResponse::signals), which collide with Qt'ssignals/slots/emitmacros. The build definesQT_NO_KEYWORDS; all Qt classes here useQ_SIGNALS:/Q_SLOTS:/Q_EMITinstead. 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 PlotWidgets mounted into nested QSplitters 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
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-hostis misparsed byQCommandLineParseras 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 payloadsSignalDescriptor— mirrorsUDPSSignalDescriptorfrom 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 servermartecontrol.go— TCP client toDebugService; routes commands from browserstatic/— 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 portsstatic/— 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 |