Initial release

This commit is contained in:
Martino Ferrari
2026-05-29 13:29:59 +02:00
commit 617b5bd712
110 changed files with 29234 additions and 0 deletions
+241
View File
@@ -0,0 +1,241 @@
# DebugService
`DebugService` is a MARTe2 `Object` that instruments a running MARTe2 application for
real-time signal tracing, forcing, and breakpoints — **without modifying the application
source code**.
It lives at `Source/Components/Interfaces/DebugService/` in this repository and is built
alongside `TCPLogger` at `Source/Components/Interfaces/TCPLogger/`.
---
## Architecture
### Core Mechanism: Registry Patching
On `Initialise()`, `PatchRegistry()` replaces the `ObjectBuilder` for every standard
`MemoryMap*Broker` class in the `ClassRegistryDatabase`. When
`RealTimeApplication::ConfigureApplication()` runs afterward it instantiates wrapped broker
objects instead of the originals. The application sees no difference.
Patched broker types:
| Class | Wrapper |
|---|---|
| `MemoryMapInputBroker` | `DebugBrokerWrapper<MemoryMapInputBroker>` |
| `MemoryMapOutputBroker` | `DebugBrokerWrapper<MemoryMapOutputBroker>` |
| `MemoryMapSynchronisedInputBroker` | `DebugBrokerWrapper<MemoryMapSynchronisedInputBroker>` |
| `MemoryMapSynchronisedOutputBroker` | `DebugBrokerWrapper<MemoryMapSynchronisedOutputBroker>` |
| `MemoryMapMultiBufferBroker` | `DebugBrokerWrapper<MemoryMapMultiBufferBroker>` |
| `MemoryMapMultiBufferOutputBroker` | `DebugBrokerWrapper<MemoryMapMultiBufferOutputBroker>` |
| `MemoryMapAsynchronousInputBroker` | `DebugBrokerWrapperAsync<MemoryMapAsynchronousInputBroker>` |
| `MemoryMapAsynchronousOutputBroker` | `DebugBrokerWrapperAsync<MemoryMapAsynchronousOutputBroker>` |
| `MemoryMapInterpolatedInputBroker` | `DebugBrokerWrapper<MemoryMapInterpolatedInputBroker>` |
| `MemoryMapStatefulOutputBroker` | `DebugBrokerWrapper<MemoryMapStatefulOutputBroker>` |
| `MemoryMapStatefulInputBroker` | `DebugBrokerWrapper<MemoryMapStatefulInputBroker>` |
### DebugServiceI Abstraction
`DebugServiceI.h` defines an abstract singleton interface decoupling the broker injection
layer from the transport:
- **RT-path API** (called every RT cycle): `RegisterSignal`, `ProcessSignal`, `RegisterBroker`,
`IsPaused`/`SetPaused`, `ConsumeStepIfNeeded`
- **Control-path API**: `ForceSignal`/`UnforceSignal`, `TraceSignal`, `SetBreak`/`ClearBreak`,
`RegisterMonitorSignal`/`UnmonitorSignal`
### Signal Registration and Aliases
Each signal is registered with two names:
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*`. Alias lookup is bidirectional — a short unqualified
name matches any longer canonical or alias path.
### Communication Channels
| Port | Protocol | Purpose |
|------|----------|---------|
| 8080 (default) | TCP text | Command and control |
| 8081 (default) | UDP binary | High-speed telemetry stream |
| 8082 (default) | TCP stream | Log forwarding via `TcpLogger` |
### TraceRingBuffer
Single-producer/single-consumer circular byte buffer (4 MB default) defined in `DebugCore.h`.
Buffer layout per entry: `[ID:4][Timestamp:8][Size:4][Data:N]`.
`Push()` is serialised by `tracePushMutex` (multiple RT threads write).
`Pop()` is called exclusively by the Streamer thread.
---
## Configuration
Add `DebugService` as a **sibling** of `+App` (not inside it):
```text
+DebugService = {
Class = DebugService
ControlPort = 8080
UdpPort = 8081
LogPort = 8082
}
+Logger = {
Class = TcpLogger
Port = 8082
}
+App = {
Class = RealTimeApplication
...
}
```
Call `SetFullConfig(cdb)` after `ConfigureApplication()` to enable the `CONFIG` and `INFO`
commands.
---
## TCP Command Interface (port 8080)
Commands are newline-terminated (`\n`) UTF-8 text strings. One client is served at a time.
Rate limit: 100 commands/second. Idle timeout: 30 seconds.
### `DISCOVER`
List all registered signals.
```
Request: DISCOVER\n
Response: {"Signals":[{"id":<n>,"name":"…","alias":"…","type":"…","elements":<n>},...]}
OK DISCOVER
```
### `TREE`
Return the live `ObjectRegistryDatabase` hierarchy as JSON.
```
Request: TREE\n
Response: {"name":"Root","children":[…]}
OK TREE
```
### `INFO <path>`
Return metadata for a specific ORD node, enriched with config fields.
### `LS [path]`
List the immediate children of an ORD node.
### `TRACE <name> <0|1> [decimation]`
Enable or disable high-speed tracing. Optional decimation controls how many RT cycles are
skipped between samples (default 1 = every cycle).
```
Request: TRACE App.Data.DDB.Counter 1\n
Response: OK TRACE <match_count>\n
```
### `FORCE <name> <value>`
Inject a persistent value into a signal's memory location every RT cycle.
```
Request: FORCE App.Data.DDB.Counter 9999\n
Response: OK FORCE <match_count>\n
```
### `UNFORCE <name>`
Remove a forced value; the signal resumes normal data-flow.
### `BREAK <name> <op> <threshold>`
Set a conditional breakpoint. Supported operators: `>`, `<`, `==`, `>=`, `<=`, `!=`, `OFF`.
```
Request: BREAK App.Data.DDB.Counter > 1000\n
Response: OK BREAK <match_count>\n
```
### `PAUSE` / `RESUME`
Halt or release all patched RT threads.
### `STEP <n> [thread]`
Resume for exactly `n` RT output-broker cycles, then pause again.
### `VALUE <name>`
Read the current raw value of a signal. Arrays capped at 256 elements.
### `CONFIG`
Return the full application configuration as JSON (requires `SetFullConfig()`).
### `MSG <object> <function> [key=value …]`
Send a MARTe2 `Message` to any object in the ORD.
### `SERVICE_INFO`
Return metadata about the debug service (ports, transport type).
---
## UDP Telemetry Format (port 8081)
Packets are little-endian and packed.
### `TraceHeader` (20 bytes)
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | uint32 | `magic` | Always `0xDA7A57AD` |
| 4 | uint32 | `seq` | Monotonically incrementing sequence number |
| 8 | uint64 | `timestamp` | High-resolution hardware timestamp |
| 16 | uint32 | `count` | Number of samples in this datagram |
### Sample Entry (per signal)
| Offset | Type | Field | Description |
|---|---|---|---|
| 0 | uint32 | `id` | Signal ID from `DISCOVER` |
| 4 | uint64 | `timestamp` | Per-sample RT timestamp |
| 12 | uint32 | `size` | Payload size in bytes |
| 16 | bytes | `data` | Raw signal memory |
Multiple samples are packed into one datagram up to `STREAMER_MTU = 1400` bytes.
---
## Log Forwarding (TcpLogger, port 8082)
`TcpLogger` connects to the MARTe2 global `LoggerI` and forwards every `REPORT_ERROR` call:
```
<level>|<object>|<function>|<message>\n
```
Up to 8 simultaneous log clients supported.
---
## Performance Hardening
| Fix | Change |
|---|---|
| Multi-producer ring | `tracePushMutex` serialises `TraceRingBuffer::Push()` |
| Break evaluation | Break indices copied to stack-local array before evaluating outside the lock |
| Decimation counter | `Atomic::Add` for lock-free per-signal decimation in multi-producer `ProcessSignal` |
| Ring corruption | `Pop()` discards all entries only when `size >= bufferSize` |
| TCP rate limit | Disconnect clients sending > 100 commands/second |
| Idle timeout | Disconnect active TCP client after 30 s of silence |
| TCP framing | `inputBuffer` accumulates partial commands across `Read()` calls; bounded at 8 KiB |
| VALUE output size | Caps output at 256 elements; includes `"Truncated"` flag |
---
## Key Source Files
| File | Purpose |
|---|---|
| `DebugCore.h` | `DebugSignalInfo` struct, `TraceRingBuffer`, `BreakOp` enum |
| `DebugServiceI.h` | Abstract singleton interface, `SignalAlias`, `BrokerInfo` |
| `DebugBrokerWrapper.h` | `DebugBrokerHelper`, wrapper templates, `DebugBrokerBuilder` |
| `DebugService.h/.cpp` | TCP/UDP transport implementation; `Server()` and `Streamer()` threads |
| `DebugServiceBase.h/.cpp` | Shared signal registry and alias matching logic |
| `TcpLogger.h/.cpp` | `LoggerConsumerI` forwarding `REPORT_ERROR` events to TCP clients |
+208
View File
@@ -0,0 +1,208 @@
# UDPStreamer Wire Protocol
This document specifies the binary protocol used between UDPStreamer (server) and any
compatible client (the included Go WebUI, a Python script, etc.).
All multi-byte integers are **little-endian**.
---
## Packet Header (17 bytes, packed)
Every datagram begins with a 17-byte header:
```
Offset Size Type Field
────── ──── ────── ────────────────────────────────────────────────────
0 4 uint32 magic = 0x53504455 ('UDPS' LE)
4 1 uint8 type see Packet Types below
5 4 uint32 counter per-update sequence number
(same across all fragments of one update)
9 2 uint16 fragmentIdx 0-based index of this fragment
11 2 uint16 totalFragments number of fragments for this update
13 4 uint32 payloadBytes bytes of payload following this header
```
**Total header size:** 17 bytes
**Magic:** `0x55 0x44 0x50 0x53` (`UDPS`)
---
## Packet Types
| Value | Direction | Name | Description |
|-------|-----------|------|-------------|
| 0 | Server → Client | DATA | Signal data (may be fragmented) |
| 1 | Server → Client | CONFIG | Signal metadata sent on connect |
| 2 | Client → Server | ACK | Acknowledge a data counter (reserved) |
| 3 | Client → Server | CONNECT | Request a session |
| 4 | Client → Server | DISCONNECT | End the session |
---
## Session Flow
```
Client Server
────── ──────
CONNECT (type=3) →
← CONFIG (type=1)
← DATA (type=0) ┐
← DATA (type=0) │ repeated every RT cycle
← DATA (type=0) ┘
DISCONNECT (type=4) →
```
1. Client sends a 17-byte CONNECT packet (`payloadBytes = 0`).
2. Server responds immediately with one or more CONFIG fragments describing all signals.
3. Server sends DATA fragments on every `Synchronise()` call while a client is connected.
4. Client sends DISCONNECT to terminate cleanly. A new CONNECT replaces an existing session.
---
## CONFIG Payload
The CONFIG payload is sent as one or more fragmented packets (`type = 1`).
After reassembly the layout is:
```
Offset Size Type Field
────── ──── ─────── ────────────────────────────────────
0 4 uint32 numSignals
── for each signal (136 bytes) ──────────────────────────────
0 64 char[64] name null-terminated
64 1 uint8 typeCode see Type Codes
65 1 uint8 quantType see Quantization Types
66 1 uint8 numDimensions 0 = scalar, 1 = 1-D array, 2 = matrix
67 4 uint32 numRows 0 or 1 for scalar/1-D
71 4 uint32 numCols number of elements along fastest axis
75 8 float64 rangeMin
83 8 float64 rangeMax
91 1 uint8 timeMode see Time Modes
92 8 float64 samplingRate Hz (0 if PacketTime)
100 4 uint32 timeSignalIdx index of the time-reference signal;
0xFFFFFFFF = PacketTime (no reference)
104 32 char[32] unit null-terminated physical unit string
── (total per signal: 136 bytes) ────────────────────────────
```
### Type Codes
| Code | C type | Bytes/element |
|------|--------|---------------|
| 0 | uint8 | 1 |
| 1 | int8 | 1 |
| 2 | uint16 | 2 |
| 3 | int16 | 2 |
| 4 | uint32 | 4 |
| 5 | int32 | 4 |
| 6 | uint64 | 8 |
| 7 | int64 | 8 |
| 8 | float32 | 4 |
| 9 | float64 | 8 |
### Quantization Type Codes (wire side)
| Code | Wire type | Description |
|------|-----------|-------------|
| 0 | — | No quantization; raw type as above |
| 1 | uint8 | Linear map `[rangeMin, rangeMax]``[0, 255]` |
| 2 | int8 | Linear map `[rangeMin, rangeMax]``[-127, 127]` |
| 3 | uint16 | Linear map `[rangeMin, rangeMax]``[0, 65535]` |
| 4 | int16 | Linear map `[rangeMin, rangeMax]``[-32767, 32767]` |
### Time Mode Codes
| Code | Name | Meaning |
|------|------|---------|
| 0 | PacketTime | HRT timestamp at `Synchronise()` — see DATA payload |
| 1 | FullArray | `timeSignalIdx` signal has same `numElements`; element `[k]` time = `timeSignal[k]` |
| 2 | FirstSample | `timeSignalIdx` is scalar; `t[k] = t[0] + k / samplingRate` |
| 3 | LastSample | `timeSignalIdx` is scalar; `t[k] = t[N-1] - (N-1-k) / samplingRate` |
---
## DATA Payload
After reassembly, the DATA payload layout is:
```
Offset Size Type Field
────── ──── ────── ────────────────────────────────────────────────────
0 8 uint64 hrtTimestamp hardware reference timer count at Synchronise()
── for each signal (in config order) ────────────────────────────────────
varies N×sz — signal data N = numRows×numCols, sz = element size
(wire size if quantized, raw size otherwise)
```
Signal data for quantized signals uses the wire element size (see Quantization Type Codes),
not the original MARTe2 type size.
### Dequantization
To recover physical values from quantized integers:
```
// uint16 → float
span = rangeMax - rangeMin
physical = rangeMin + (wire_uint16 / 65535.0) × span
// int16 → float
physical = rangeMin + ((wire_int16 + 32767) / 65534.0) × span
```
---
## Fragmentation
When a payload exceeds `MaxPayloadSize` bytes, it is split into fragments:
```
chunkSize = MaxPayloadSize - 17 // usable bytes per datagram
numFragments = ceil(payloadSize / chunkSize)
```
Fragment `i` carries bytes `[i × chunkSize .. min((i+1) × chunkSize, payloadSize))`.
All fragments share the same `counter`; `fragmentIdx` and `totalFragments` allow
the client to reassemble them in any order.
**Example:** `MaxPayloadSize = 1400`, payload = 8016 B
`chunkSize = 1383`, `numFragments = ceil(8016/1383) = 6`
---
## Minimal Python Client Example
```python
import socket, struct, time
MAGIC = 0x53504455
HDR_FMT = '<IBHHI' # magic, type, counter, fragIdx, totalFrags, payloadBytes
HDR_SIZE = 17
def build_connect():
return struct.pack(HDR_FMT, MAGIC, 3, 0, 0, 1, 0)
def parse_header(data):
return struct.unpack_from(HDR_FMT, data)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', 44900))
sock.sendto(build_connect(), ('127.0.0.1', 44500))
sock.settimeout(5.0)
fragments = {}
while True:
data, _ = sock.recvfrom(65536)
magic, ptype, counter, frag_idx, total_frags, payload_bytes = parse_header(data)
payload = data[HDR_SIZE:]
if ptype == 1: # CONFIG
print(f"CONFIG fragment {frag_idx+1}/{total_frags}")
elif ptype == 0: # DATA
fragments.setdefault(counter, {})[frag_idx] = payload
if len(fragments[counter]) == total_frags:
full = b''.join(fragments.pop(counter)[i] for i in range(total_frags))
hrt = struct.unpack_from('<Q', full)[0]
print(f"DATA counter={counter} hrt={hrt} payload={len(full)}B")
```
+104
View File
@@ -0,0 +1,104 @@
# SineArrayGAM
`SineArrayGAM` is a helper GAM bundled with the UDPStreamer library. It fills a `float32`
output array with a continuous sinusoidal waveform, maintaining phase continuity across
RT cycles. It is intended for testing and demonstrating the UDPStreamer's packed-burst
signal capability.
> **Library:** The class is compiled into `UDPStreamer.so` alongside the UDPStreamer
> DataSource. A `SineArrayGAM.so → UDPStreamer.so` symlink is required for MARTe2's
> auto-loader (created automatically by `Test/MARTeApp/run.sh`).
---
## Configuration
```
+Ch1GAM = {
Class = SineArrayGAM
Frequency = 1000.0 // Signal frequency [Hz] (default: 1.0)
Amplitude = 1.0 // Peak amplitude (default: 1.0)
Offset = 0.0 // DC offset (default: 0.0)
Phase = 0.0 // Initial phase [radians] (default: 0.0)
SamplingRate = 1000000.0 // Sample rate [Hz] (default: 1000000.0)
// Must match the SamplingRate declared in the
// UDPStreamer signal config.
OutputSignals = {
Ch1 = {
DataSource = DDB
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000 // N samples per RT cycle
}
}
}
```
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `Frequency` | float64 | 1.0 | Signal frequency in Hz |
| `Amplitude` | float64 | 1.0 | Peak amplitude (half peak-to-peak) |
| `Offset` | float64 | 0.0 | DC offset added to every sample |
| `Phase` | float64 | 0.0 | Phase shift in radians |
| `SamplingRate` | float64 | 1 000 000.0 | Sample rate in Hz; must be > 0 |
### Output signal
Exactly **one** output signal of type **`float32`** is required. The signal must have
`NumberOfDimensions = 1` and `NumberOfElements = N` where N ≥ 1.
---
## Waveform formula
Each `Execute()` call fills elements `[0 .. N-1]` using:
```
v[k] = Amplitude × sin(2π × Frequency × (offset_total + k) / SamplingRate + Phase) + Offset
```
where `offset_total` is a cumulative sample counter that increments by `N` after each call.
This guarantees a gapless, phase-continuous waveform across RT cycles.
---
## Bandwidth and fragmentation
For a 1 MSps burst at 10 kHz RT rate with 1000 samples per cycle:
```
N = SamplingRate / RT_rate = 1 000 000 / 1000 = 1000 samples per cycle
Wire bytes = 1000 × 4 (float32) = 4000 B per channel per cycle
At 10 kHz: 4000 × 10 000 = 40 MB/s raw (UDP, before quantization)
```
Using `QuantizedType = uint16` halves the wire bandwidth to 20 MB/s.
With `MaxPayloadSize = 1400` the 4 000-byte payload spans 3 UDP datagrams
(`ceil(4008 / 1383) = 3`).
---
## Example: two-channel quadrature pair
```
+Ch1GAM = {
Class = SineArrayGAM
Frequency = 1000.0; Amplitude = 1.0; Phase = 0.0; SamplingRate = 10000000.0
OutputSignals = {
Ch1 = { DataSource = DDB; Type = float32; NumberOfDimensions = 1; NumberOfElements = 1000 }
}
}
+Ch2GAM = {
Class = SineArrayGAM
Frequency = 1000.0; Amplitude = 1.0; Phase = 1.5708; SamplingRate = 10000000.0
OutputSignals = {
Ch2 = { DataSource = DDB; Type = float32; NumberOfDimensions = 1; NumberOfElements = 1000 }
}
}
```
This produces two signals 90° out of phase, useful for verifying IQ-style signal chains.
+493
View File
@@ -0,0 +1,493 @@
# Tutorial: MARTe2 Integrated Components
This guide covers two complementary use-cases:
- **Part A** — Real-time signal streaming with `UDPStreamer` and the web client
- **Part B** — Signal tracing, forcing, and breakpoints with `DebugService`
---
## Part A: Streaming Signals with UDPStreamer
### A.1 Setting up the environment
2. Running the demo application
3. Visualising signals in the browser
4. Adding UDPStreamer to your own MARTe2 application
5. Streaming high-frequency packed signals
**Prerequisites:** MARTe2 and MARTe2-components must already be built.
See the [MARTe2 installation guide](https://vcis.f4e.europa.eu/marte2-docs/) if needed.
---
## 1. Environment Setup
Edit `marte_env.sh` in the repository root to point at your MARTe2 installations:
```bash
# marte_env.sh (key variables)
export MARTe2_DIR="$HOME/workspace/MARTe2"
export MARTe2_Components_DIR="$HOME/workspace/MARTe2-components"
```
Then source it in your shell:
```bash
cd /path/to/MARTe_IO_components
source marte_env.sh
```
Verify the environment is correct:
```bash
echo $MARTe2_DIR
ls $MARTe2_DIR/Build/x86-linux/App/MARTeApp.ex # should exist
```
---
## 2. Running the Demo Application
The demo is in `Test/MARTeApp/`. It runs a 10 kHz MARTe2 application that streams:
- **Counter** and **Time** — scalar counters from the Linux timer
- **Sine1** — 1 Hz sine wave (float32, amplitude 10, quantized to uint16 on wire)
- **Sine2** — 0.3 Hz sine wave (float32, amplitude 5, raw float32 on wire)
- **Ch1**, **Ch2** — 1 kHz sine bursts packed as 1000 samples/packet (10 MSps)
Start everything with one command:
```bash
cd Test/MARTeApp
./run.sh --webui
```
The script will:
1. Build the UDPStreamer shared library.
2. Build the Go WebUI binary (first run only).
3. Start the WebUI relay on `http://localhost:8080`.
4. Launch the MARTe2 application.
Press `Ctrl+C` to stop both processes.
---
## 3. Visualising Signals in the Browser
Open `http://localhost:8080` in any modern browser.
### Add your first plot
1. Click **+ Add Plot** in the toolbar.
2. A blank plot panel appears with a "Drop signals here" hint.
### Plot a signal
1. In the left sidebar find **Sine1** (listed as `Sine1 · f32`).
2. Click and drag it onto the plot panel.
3. The sine wave appears immediately.
### Overlay multiple signals
Drag **Sine2** onto the same plot — it is added as a second trace.
### Adjust the time window
Use the **Window** dropdown in the top bar to change the rolling display window
(1 s, 5 s, 10 s, 30 s, 60 s).
### Plot layout
Use the layout buttons (`1×1`, `2×1`, `2×2`, …) to split the screen into multiple
plot panels. Each panel is independent — drag different signals onto each.
### High-frequency signals
Drag **Ch1** (shown as `Ch1 · [1000] f32`) onto a plot. Each UDP packet carries
1000 samples at 10 MSps; the WebUI reconstructs per-sample timestamps and displays
the continuous waveform.
### Export data
Click **⬇** on any plot to download the visible window as a CSV file.
---
## 4. Adding UDPStreamer to Your Own Application
### Step 1 — Declare the DataSource
Add UDPStreamer to the `+Data` section of your MARTe2 configuration:
```
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB
+DDB = { Class = GAMDataSource }
+Streamer = {
Class = UDPStreamer
Port = 44500
MaxPayloadSize = 1400
Signals = {
Voltage = {
Type = float32
Unit = "V"
RangeMin = -10.0
RangeMax = 10.0
QuantizedType = uint16 // 16-bit quantized on wire
}
Current = {
Type = float32
Unit = "A"
}
}
}
+Timings = { Class = TimingDataSource }
}
```
### Step 2 — Route signals with IOGAM
Use IOGAM to copy signals from your inter-GAM DDB into the Streamer:
```
+StreamerGAM = {
Class = IOGAM
InputSignals = {
Voltage = { DataSource = DDB; Type = float32 }
Current = { DataSource = DDB; Type = float32 }
}
OutputSignals = {
Voltage = { DataSource = Streamer; Type = float32 }
Current = { DataSource = Streamer; Type = float32 }
}
}
```
Add `StreamerGAM` at the **end** of the thread's `Functions` list so it runs after
your control GAMs have written their outputs.
### Step 3 — Add the library to LD_LIBRARY_PATH
In your run script, add the UDPStreamer build directory:
```bash
export LD_LIBRARY_PATH="/path/to/Build/x86-linux/Components/DataSources/UDPStreamer:$LD_LIBRARY_PATH"
```
### Step 4 — Start the WebUI and connect
```bash
# From the Client/WebUI directory:
./udpstreamer-webui --streamer 127.0.0.1:44500 --listen :8080 --clientport 44900
```
Open `http://localhost:8080`, drag your signals onto a plot, and you're done.
---
## 5. Streaming High-Frequency Packed Signals
This section shows how to stream 1000 samples per RT cycle at 1 MSps.
### Overview
At 1 kHz RT rate with 1000 samples per cycle the effective sample rate is 1 MSps.
Each UDP packet carries a burst of 1000 samples; the client reconstructs timestamps
using the anchor timestamp and `SamplingRate`.
### Step 1 — Generate burst data with SineArrayGAM
`SineArrayGAM` (bundled in `UDPStreamer.so`) produces a continuous float32 array:
```
+Ch1GAM = {
Class = SineArrayGAM
Frequency = 1000.0 // 1 kHz signal
Amplitude = 1.0
Phase = 0.0
SamplingRate = 1000000.0 // must match Streamer config below
OutputSignals = {
Ch1 = {
DataSource = DDB
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
}
}
}
```
### Step 2 — Add a time reference signal
Add a scalar time signal that will anchor the first sample's timestamp:
```
+TimerGAM = {
Class = IOGAM
InputSignals = {
Time = { DataSource = Timer; Type = uint32; Frequency = 1000 }
}
OutputSignals = {
Time = { DataSource = DDB; Type = uint32 }
}
}
```
### Step 3 — Configure UDPStreamer for packed signals
```
+Streamer = {
Class = UDPStreamer
Port = 44500
MaxPayloadSize = 1400
Signals = {
Time = { Type = uint32; Unit = "us" } // time reference (scalar)
Ch1 = {
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000
Unit = "V"
TimeMode = FirstSample // Time = timestamp of first sample
TimeSignal = Time
SamplingRate = 1000000.0 // Hz
}
}
}
```
### Step 4 — Wire everything in the thread
```
+Thread1 = {
Class = RealTimeThread
Functions = { TimerGAM Ch1GAM StreamerGAM }
}
```
Where `StreamerGAM` is the IOGAM that copies `Time` and `Ch1` from DDB to Streamer.
### Step 5 — Fragmentation note
A single 1000-element float32 channel plus a uint32 time signal produces:
```
payload = 8 B (HRT) + 4 B (Time/uint32) + 4000 B (float32×1000) = 4012 B
```
With `MaxPayloadSize = 1400`:
```
fragments = ceil(4012 / 1383) = 3 datagrams per cycle
```
At 1 kHz that is 3000 UDP datagrams/second per channel — well within typical LAN capacity.
### Step 6 — Create the SineArrayGAM symlink
MARTe2 tries to `dlopen("SineArrayGAM.so")` the first time it encounters the class.
Create the symlink in your build directory:
```bash
UDPSTREAMER_LIB=/path/to/Build/x86-linux/Components/DataSources/UDPStreamer
ln -sf "${UDPSTREAMER_LIB}/UDPStreamer.so" "${UDPSTREAMER_LIB}/SineArrayGAM.so"
```
---
## 6. Writing a Custom UDP Client
A minimal Python client that receives and prints signal data:
```python
import socket, struct
MAGIC = 0x53504455
HDR = struct.Struct('<IBHHI') # 17 bytes: magic, type, counter, fragIdx, total, payloadBytes
SERVER = ('127.0.0.1', 44500)
MY_PORT = 44900
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', MY_PORT))
sock.settimeout(5.0)
# Send CONNECT
sock.sendto(HDR.pack(MAGIC, 3, 0, 0, 1, 0), SERVER)
print("CONNECT sent")
reassembly = {}
while True:
data, _ = sock.recvfrom(65536)
if len(data) < 17:
continue
magic, ptype, counter, frag_idx, total_frags, payload_bytes = HDR.unpack_from(data)
if magic != MAGIC:
continue
payload = data[17:17 + payload_bytes]
# Accumulate fragments
bucket = reassembly.setdefault((ptype, counter), {})
bucket[frag_idx] = payload
if len(bucket) < total_frags:
continue
full = b''.join(bucket[i] for i in range(total_frags))
del reassembly[(ptype, counter)]
if ptype == 1: # CONFIG
num_sigs = struct.unpack_from('<I', full)[0]
print(f"CONFIG: {num_sigs} signals")
elif ptype == 0: # DATA
hrt = struct.unpack_from('<Q', full)[0]
print(f"DATA counter={counter} hrt={hrt} payload={len(full)}B")
```
For a full-featured client with CONFIG parsing and dequantization see
[`Client/WebUI/protocol.go`](../Client/WebUI/protocol.go) (Go)
and the [Protocol reference](Protocol.md).
---
---
## Part B: Debugging with DebugService
### B.1 Add DebugService to Your Config
Add it as a **sibling** of the `+App` node (not inside it):
```text
+DebugService = {
Class = DebugService
ControlPort = 8080
UdpPort = 8081
LogPort = 8082
}
+Logger = {
Class = TcpLogger
Port = 8082
}
+App = {
Class = RealTimeApplication
...
}
```
Call `SetFullConfig(cdb)` after `ConfigureApplication()` to enable the `CONFIG` and `INFO` commands.
### B.2 Start the Debug Web Client
```bash
cd Client/debugger
go build ./...
./debugger --listen :9090
```
Open `http://localhost:9090` in any browser.
### B.3 Exploring the Object Tree
The **Application Tree** panel on the left mirrors your live MARTe2 `ObjectRegistryDatabase`.
1. Expand `Root → App → Data` to find data sources.
2. Click **Info** next to any node to see its class, config, and signals.
3. Click **List** to show immediate children.
### B.4 Real-Time Signal Tracing
1. Locate a signal, e.g. `Root.App.Data.Timer.Counter`.
2. Click **Trace**. The signal appears in the **Traced Signals** list with its live last value.
3. Click **Plot** to open it in the real-time graph. Use **Follow** to keep the time axis scrolling.
4. To set decimation (e.g. every 10th sample):
```
TRACE App.Data.Timer.Counter 1 10
```
5. Click **Trace** again (or send `TRACE … 0`) to stop.
### B.5 Signal Forcing
1. Find a signal, e.g. `Root.App.Data.DDB.Counter`.
2. Click **Force**, enter a value (e.g. `9999`), click **Apply**.
3. The signal is locked at that value every RT cycle.
4. Click **Unforce** to release.
### B.6 Conditional Breakpoints
1. Click **Break** next to a signal.
2. Select an operator (`>`, `<`, `==`, `>=`, `<=`, `!=`) and enter a threshold.
3. When the condition fires, the application pauses. The status bar shows **PAUSED**.
4. Use **Step** to advance one cycle at a time, or **Resume** to continue.
5. Click **Break OFF** to clear.
### B.7 Execution Stepping
While paused (after a breakpoint or manual **Pause**):
1. Enter a step count (e.g. `5`) and click **Step**.
2. The RT loop runs exactly 5 output-broker cycles, then pauses again.
3. The status SSE event (`{"type":"status","remaining":…}`) keeps the UI updated.
### B.8 Scripted / Programmatic Access
Both `DebugService` and the web client accept plain-text TCP commands:
```bash
# Direct TCP
echo -e "DISCOVER\nTRACE App.Data.DDB.Counter 1" | nc localhost 8080
# Via web client API
curl -s -X POST http://localhost:9090/api/command \
-H "Content-Type: text/plain" \
-d "DISCOVER"
```
See `Docs/DebugService.md` for the full command reference.
---
## Part C: Using UDPStreamer and DebugService Together
Both can run simultaneously in the same application:
```text
+DebugService = { Class = DebugService; ControlPort = 8080; UdpPort = 8081; LogPort = 8082 }
+Logger = { Class = TcpLogger; Port = 8082 }
+App = {
Class = RealTimeApplication
+Data = {
Class = ReferenceContainer
DefaultDataSource = DDB
+DDB = { Class = GAMDataSource }
+Streamer = { Class = UDPStreamer; Port = 44500; MaxPayloadSize = 1400; ... }
+Timings = { Class = TimingDataSource }
}
...
}
```
- `UDPStreamer` provides continuous high-speed streaming of selected signals.
- `DebugService` provides on-demand tracing, forcing, and breakpoints for any signal.
- Both use the UDPS binary protocol format (see `Docs/Protocol.md`).
---
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| `Failed dlopen(): UDPStreamer.so` | Library not in `LD_LIBRARY_PATH` | Source `env.sh`; add build dir |
| `Failed dlopen(): SineArrayGAM.so` | Symlink missing | `ln -sf UDPStreamer.so SineArrayGAM.so` in build dir |
| WebUI shows "No data" | UDPStreamer not running / wrong port | Check port numbers; check MARTe2 logs |
| Plots only show ~167 ms of HF data | Browser buffer too small | Reduce decimation or increase `TEMPORAL_CAP` in JS |
| Fragmentation error / missing data | MTU too small | Reduce `MaxPayloadSize` to 1200 or smaller |
| DebugService: DISCOVER returns empty | `PatchRegistry` called too late | Ensure `DebugService` is initialised before `ConfigureApplication()` |
| Integration tests timeout | MARTe2 libs not on `LD_LIBRARY_PATH` | Source `env.sh` before running tests |
+200
View File
@@ -0,0 +1,200 @@
# UDPStreamer DataSource
`UDPStreamer` is a MARTe2 output DataSource that streams signals from a real-time application
to a single connected UDP client. It is fully asynchronous from the RT thread: the RT cycle
only performs a fast spinlock + memcpy, while all network I/O runs on a dedicated background
thread.
## Key Features
- **Zero-copy RT path** — `Synchronise()` only locks, copies signal memory, and posts a semaphore.
- **Single-client model** — one client at a time; a new CONNECT replaces the previous session.
- **Packet fragmentation** — large payloads are split into ≤ `MaxPayloadSize`-byte datagrams,
each with a header carrying fragment index and total count so the client can reassemble them.
- **Signal quantization** — `float32`/`float64` signals can be linearly quantized to
`uint8`, `int8`, `uint16`, or `int16` on the wire, reducing bandwidth significantly.
- **Temporal arrays** — signals with `NumberOfElements > 1` can carry per-sample time
metadata via `TimeMode` and `TimeSignal`, enabling high-frequency burst transmission
(e.g. 1 000 samples per RT cycle at 1 MSps).
---
## Configuration
```
+Streamer = {
Class = UDPStreamer
// Network
Port = 44500 // UDP port the server listens on (default: 44500)
MaxPayloadSize = 1400 // Maximum bytes per UDP datagram (default: 1400)
// Must be > 17 (header size). Tune for MTU.
// Background thread (optional)
CPUMask = 0x2 // CPU affinity mask for the network thread
StackSize = 1048576 // Stack size in bytes (default: 1 MiB)
Signals = {
// ── Scalar signal ────────────────────────────────────────────────────
Time = {
Type = uint32
Unit = "us" // Optional: physical unit string (informational)
}
// ── Float signal with quantization ───────────────────────────────────
Pressure = {
Type = float32
Unit = "Pa"
RangeMin = 0.0 // Required when QuantizedType is set
RangeMax = 1000000.0 // Required when QuantizedType is set
QuantizedType = uint16 // none | uint8 | int8 | uint16 | int16
}
// ── Temporal array (packed burst) ────────────────────────────────────
Channel1 = {
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000 // N samples per RT cycle
Unit = "V"
TimeMode = FirstSample // see Time Modes below
TimeSignal = Time // name of a scalar signal in this DataSource
SamplingRate = 1000000.0 // Hz — used by client to reconstruct timestamps
}
}
}
```
### Top-level Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `Port` | uint16 | 44500 | UDP server port |
| `MaxPayloadSize` | uint32 | 1400 | Max payload bytes per UDP datagram (min 18) |
| `CPUMask` | uint32 | 0 (any) | Background thread CPU affinity |
| `StackSize` | uint32 | 1 048 576 | Background thread stack size in bytes |
### Per-signal Parameters
| Parameter | Type | Default | Applies to |
|-----------|------|---------|------------|
| `Unit` | string | `""` | Any type — informational, forwarded to client in CONFIG |
| `RangeMin` | float64 | 0.0 | float32/float64 with `QuantizedType` |
| `RangeMax` | float64 | 1.0 | float32/float64 with `QuantizedType` |
| `QuantizedType` | string | `none` | float32/float64 only |
| `TimeMode` | string | `PacketTime` | Signals with `NumberOfElements > 1` |
| `TimeSignal` | string | — | Required when `TimeMode``PacketTime` |
| `SamplingRate` | float64 | 0.0 | Required when `TimeMode` = `FirstSample` or `LastSample` |
### Quantization Types
| Value | Wire type | Bit depth | Notes |
|-------|-----------|-----------|-------|
| `none` | same as source | — | Raw copy, no quantization |
| `uint8` | uint8 | 8-bit | Maps `[RangeMin, RangeMax]``[0, 255]` |
| `int8` | int8 | 8-bit | Maps `[RangeMin, RangeMax]``[-127, 127]` |
| `uint16` | uint16 | 16-bit | Maps `[RangeMin, RangeMax]``[0, 65 535]` |
| `int16` | int16 | 16-bit | Maps `[RangeMin, RangeMax]``[-32 767, 32 767]` |
Quantization formula (unsigned, e.g. uint16):
```
normalized = clamp((value - RangeMin) / (RangeMax - RangeMin), 0.0, 1.0)
wire_value = (uint16)(normalized × 65535)
```
### Time Modes
| Value | Meaning | Requirements |
|-------|---------|--------------|
| `PacketTime` | The HRT counter captured at `Synchronise()` time is used as the packet timestamp. No per-signal time metadata. | — |
| `FullArray` | `TimeSignal` carries one timestamp per element (same `NumberOfElements`). | `TimeSignal` must have the same `NumberOfElements`. |
| `FirstSample` | `TimeSignal` is a scalar giving the timestamp of element `[0]`. Elements `[1..N-1]` are inferred at `1/SamplingRate` intervals. | Scalar `TimeSignal`; `SamplingRate > 0`. |
| `LastSample` | Same as `FirstSample` but `TimeSignal` is the timestamp of element `[N-1]`. | Scalar `TimeSignal`; `SamplingRate > 0`. |
---
## Broker
UDPStreamer uses `MemoryMapSynchronisedOutputBroker` for output signals. This broker is
called automatically by the MARTe2 scheduler after all GAMs in the thread have executed.
> **Note:** Input signals are not supported — `GetBrokerName()` returns `""` for
> `InputSignals`.
---
## Lifecycle
```
PrepareNextState() ← opens UDP server socket, starts background thread
[RT thread, each cycle]
GAMs execute ← write into Streamer signal memory via broker
Synchronise() ← spinlock + memcpy to readyBuffer + post dataSem
[Background thread]
Poll serverSocket ← receive CONNECT / DISCONNECT / ACK
Wait dataSem ← woken by Synchronise()
QuantizeAndSerialize() ← build wire payload
SendFragmented() ← send DATA fragments to client
```
---
## Performance Notes
- The RT path (`Synchronise()`) performs only: `FastLock()` + `memcpy` + `FastUnLock()` + `EventSem.Post()`.
No socket calls, no heap allocation.
- `readyBuffer` and `wireBuffer` are allocated once in `AllocateMemory()`.
- If no client is connected the background thread skips serialisation entirely.
- Packet loss is tolerated silently. ACK tracking is reserved for future use.
---
## Example: minimal scalar streaming
```
+Data = {
Class = ReferenceContainer
+Streamer = {
Class = UDPStreamer
Port = 44500
Signals = {
Counter = { Type = uint32 }
Voltage = { Type = float32; Unit = "V"; RangeMin = -10.0; RangeMax = 10.0; QuantizedType = uint16 }
}
}
}
```
## Example: high-frequency burst
```
+Streamer = {
Class = UDPStreamer
Port = 44500
MaxPayloadSize = 1400
Signals = {
T0 = { Type = uint32; Unit = "us" }
Ch1 = {
Type = float32
NumberOfDimensions = 1
NumberOfElements = 1000 // 1000 samples per RT cycle
Unit = "V"
TimeMode = FirstSample
TimeSignal = T0
SamplingRate = 1000000.0 // 1 MSps
}
}
}
```
With `MaxPayloadSize = 1400`, a single 1000-element float32 signal produces:
```
payload = 8 B (HRT timestamp) + 4 B (T0/uint32) + 4000 B (float32×1000) = 4012 B
fragments = ceil(4012 / 1383) = 3
```
+300
View File
@@ -0,0 +1,300 @@
# WebUI Client
The WebUI is a Go binary that acts as a bridge between UDPStreamer and any web browser.
It receives UDP packets from UDPStreamer, reassembles fragmented data, and re-publishes
decoded signal values over a WebSocket to the browser. The browser renders live plots
using [uPlot](https://github.com/leeoniya/uPlot).
```
MARTe2 RT app
│ UDP (binary protocol)
udpstreamer-webui (Go)
│ WebSocket (binary frames)
Browser (index.html + uPlot)
```
---
## Building
```bash
cd Client/WebUI
go build -o udpstreamer-webui ./...
```
Requires Go ≥ 1.21. The only external dependency is `gorilla/websocket`
(declared in `go.mod`; fetched automatically by `go build`).
---
## Running
```bash
./udpstreamer-webui \
--streamer 127.0.0.1:44500 \ # address:port of the UDPStreamer server
--listen :8080 \ # HTTP / WebSocket listen address
--clientport 44900 # local UDP port for receiving from streamer
```
Open `http://localhost:8080` in any modern browser.
### Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--streamer` | `127.0.0.1:44500` | UDP address of the UDPStreamer DataSource |
| `--listen` | `:8080` | HTTP server bind address |
| `--clientport` | `44900` | Local UDP port for receiving data |
---
## Browser UI
### Layout
```
┌───────────────────────────────────────────────────────────────────────────────┐
│ ☰ UDP Scope │ ⊞ 1×1 ▾ │ A: — B: — ΔT: — │ Window: [5s▾] Fit ⬇ CSV ⚡ Trigger ⏸ Pause │
├──────────────────┬────────────────────────────────────────────────────────────┤
│ Signals │ [Plot title] ○ ○ ○ ← signal badges │
│──────────────────│────────────────────────────────────────────────────────── │
│ Counter · u32 │ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ Time · f64 │ │ Plot 1 │ │ Plot 2 │ │
│ Sine1 · f32 │ │ (drop signals here) │ │ (drop signals here) │ │
│ Sine2 · f32 │ │ │ │ │ │
│ Ch1 · [1000] f32 │ └─────────────────────┘ └─────────────────────────────┘ │
└──────────────────┴────────────────────────────────────────────────────────────┘
│ ● Streaming [📊 Stats] v1.0.0 │
└───────────────────────────────────────────────────────────────────────────────┘
```
### Signal Sidebar
Signals received in the CONFIG packet are listed in the sidebar:
- **Scalar signals** — appear as single draggable items (e.g. `Sine1 · f32`).
- **Temporal arrays** — high-frequency burst signals with `TimeMode ≠ PacketTime`.
Displayed as a single draggable item showing element count: `Ch1 · [1000] f32`.
- **Spatial arrays** — `TimeMode = PacketTime` arrays are shown as an expandable
group; individual elements (`Ch1[0]`, `Ch1[1]`, …) can be dragged independently.
Click the sidebar toggle button (☰) to collapse/expand the signal list.
### Adding Plots
1. Select a layout from the layout menu (⊞ button in the top bar).
2. Drag a signal from the sidebar onto a plot panel.
3. Multiple signals can be overlaid on the same plot.
4. Click the **×** inside a signal badge to remove a trace.
### Plot Layout
The plot grid supports multiple layouts selectable from the layout menu (⊞):
| Layout | Description |
|--------|-------------|
| 1×1 | Single plot |
| 2×1 | Two columns |
| 1×2 | Two rows |
| 2×2 | Four plots |
| 3×1 | Three columns |
| … | More layouts available |
**Resizing plots**: When multiple plots are shown, drag the dividers between them
to resize. Vertical dividers resize column widths; horizontal dividers resize row
heights. Sizes are stored as fractional grid units (fr).
### Plot Configuration
Click the **plot title** to open the configuration toolbar:
- **Title** — edit the plot's display name.
- **Mode** — select the plot display mode:
- **Normal** — standard time-series oscilloscope view (default).
- **Mixed** — signals are arranged in horizontal bands (like digital mode), but
each signal can independently be displayed as analog (auto-scaled within its band)
or digital (quantized to high/low within its band).
- **Digital** — logic-analyzer style; each signal occupies a fixed horizontal band
and is quantized to high/low based on its data range midpoint as threshold.
### Signal Badges and Selection
Each signal assigned to a plot appears as a colored badge in the plot header.
- **Click a badge** — selects the signal and opens the V-Scale toolbar for that
signal. The selected signal is drawn on top with increased line width.
- **Click again** — deselects the signal and closes the V-Scale toolbar.
- **Right-click a badge** — opens the style context menu (color, line width, dash
style, markers).
- **Click ×** on a badge — removes the signal from the plot.
### V-Scale Toolbar
When a signal is selected (via badge click), an inline toolbar appears below the
plot header showing per-signal vertical scale controls:
| Control | Description |
|---------|-------------|
| **Auto** | Automatically fits the signal vertically |
| **Range** | Shows V/div and Pos controls for manual positioning |
| **Manual** | Fixed V/div with free positioning |
| **V/div** | Volts (or units) per division |
| **Pos (div)** | Screen position in divisions (draggable offset marker on Y axis) |
| **Type** (Mixed mode only) | Toggle between **Analog** and **Digital** for this signal |
| **✕** | Close the toolbar and deselect the signal |
Offset markers (small triangles on the Y axis) show each signal's position and can
be dragged to reposition signals without opening the toolbar.
### Plot Controls
| Control | Action |
|---------|--------|
| **⏸ / ▶** (per plot) | Pause / resume that plot; paused plots allow zoom and pan |
| **⬇** (per plot) | Export all visible traces to CSV |
| **🗑** (per plot) | Delete the plot |
| **⏸ Pause** (top bar) | Pause all plots simultaneously |
| **↺ Auto** (top bar) | Resume all paused plots and snap back to live view |
| **Window** (top bar) | Adjust the rolling time window (1 s 60 s) |
| **Fit** (top bar) | Fit all plots to their current data range |
| **⬇ CSV** (top bar) | Export all signals from all plots to CSV |
| Layout button | Switch between grid layouts |
| Sidebar **☰** | Toggle the signal list panel |
### Cursor System
Two vertical cursors (A and B) can be placed on plots:
- Enable with the **Cursor** button (shown when a plot is paused/zoomed).
- The top bar readout shows **A**, **B**, and **ΔT** (time between cursors).
- Cursors follow the mouse within the plot area.
### Zoom
- **Drag** left or right on a paused plot to zoom into a time range.
- **← Back** button steps back through zoom history.
- **Fit** returns to the full data view.
- When zooming into a region with few or no data points, the nearest data points
outside the zoom window are included so that connecting lines are drawn across
the view rather than showing blank space.
### Trigger System
Click **⚡ Trigger** in the top bar to open the trigger bar:
| Control | Description |
|---------|-------------|
| **Signal** | Select the trigger source signal |
| **Edge** | Rising ↑, Falling ↓, or Both ↕ |
| **Threshold** | Trigger level |
| **Window** | Capture duration after trigger (100 µs 10 s) |
| **Pre** | Pre-trigger buffer percentage (0100%) |
| **Mode** | **Normal** (re-arms automatically) or **Single** (fires once) |
| **Rearm** | Manually re-arm the trigger |
| **Stop** | Cancel waiting trigger |
For array signals, clicking the trigger signal selector prompts for the element
index to use as the trigger source.
### Status Bar
The status bar at the bottom shows:
- **LED indicator**: red (disconnected), orange pulsing (connected, no data), green pulsing (streaming).
- **Status text**: connection state and data age.
- **📊 Stats** button: opens the source statistics panel (packet counts, rates, errors).
- **Build version**: server build tag.
---
## Data Buffering
Signal buffers are sized based on the signal's configured sampling rate from the
CONFIG packet:
```
capacity = min(600 000, ceil(samplingRate × 60 s × 1.5))
```
This ensures up to 60 seconds of history is retained for any signal, regardless of
sample rate. If a signal's sampling rate is not configured, a default capacity of
100 000 samples is used. Temporal arrays (burst signals) use a fixed capacity of
500 000 samples.
Buffers grow automatically during streaming if a higher effective sample rate is
detected. Existing data is preserved during growth.
---
## Architecture (Go side)
### Goroutines
```
main()
├── hub.Run() ← event loop: register/unregister WS clients, batch data at 30 Hz
├── udpClient.Run() ← reconnects on silence; parses UDP packets; feeds hub.dataCh
└── http.ListenAndServe()
├── GET / ← serves embedded static files
└── GET /ws ← upgrades to WebSocket; launches per-client read/write pumps
```
### WebSocket Message Format
Two message types are sent from server to browser.
#### `config` message (JSON)
Sent immediately when a browser client connects (if a CONFIG has been received from
UDPStreamer) and whenever UDPStreamer sends a new CONFIG packet.
```json
{
"type": "config",
"signals": [
{
"name": "Sine1",
"typeCode": 8,
"quantType": 3,
"numDimensions": 0,
"numRows": 1,
"numCols": 1,
"rangeMin": -10.0,
"rangeMax": 10.0,
"timeMode": 0,
"samplingRate": 1000.0,
"timeSignalIdx": 4294967295,
"unit": "V"
}
]
}
```
#### `data` message (binary)
Sent at ≤ 30 Hz, batching all UDP packets received since the last tick.
Uses a compact binary format: a fixed header followed by per-signal blocks.
Each signal block contains:
- Signal name (length-prefixed)
- Number of samples
- Timestamp array (float64 LE, Unix seconds)
- Value array (float64 LE)
For **temporal arrays**, each packet contributes `N` samples (one per array element).
For **spatial arrays**, keys are `"Ch1[0]"`, `"Ch1[1]"`, etc.
---
## Reconnection Behaviour
- **UDP reconnect:** The Go client reconnects to UDPStreamer automatically after 5 s
of silence. This handles MARTe2 restarts transparently.
- **WebSocket keepalive:** The server sends a WebSocket ping every 30 s. The browser
auto-responds; if no pong is received within 10 s the connection is closed and the
browser reconnects with exponential backoff (starting at 1 s, capped at 30 s).
- **Buffer preservation:** Browser-side signal buffers are only reset when the signal
layout changes (name, type, or dimensions differ). A reconnect with the same CONFIG
keeps existing data visible in the plots.