FIrs
This commit is contained in:
@@ -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")
|
||||
```
|
||||
@@ -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.
|
||||
@@ -0,0 +1,359 @@
|
||||
# Tutorial: Streaming Signals with UDPStreamer
|
||||
|
||||
This tutorial walks you through:
|
||||
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| `Failed dlopen(): UDPStreamer.so` | Library not in `LD_LIBRARY_PATH` | Add build dir to `LD_LIBRARY_PATH` |
|
||||
| `Failed dlopen(): WaveformSin.so` | Symlink missing | Run `run.sh`; it creates the symlinks automatically |
|
||||
| `Failed dlopen(): SineArrayGAM.so` | Symlink missing | Create `SineArrayGAM.so → UDPStreamer.so` in the build dir |
|
||||
| WebUI shows "No data for Xs" | UDPStreamer not running / wrong port | Check port numbers; check MARTe2 logs |
|
||||
| Plots only show ~167 ms of HF data | Browser buffer too small | Use `TEMPORAL_CAP` in JS (already 600 000 in current WebUI) |
|
||||
| Fragmentation error / missing data | MTU too small | Reduce `MaxPayloadSize` to 1200 or smaller |
|
||||
@@ -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
|
||||
```
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
# 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 [Plotly.js](https://plotly.com/javascript/).
|
||||
|
||||
```
|
||||
MARTe2 RT app
|
||||
│ UDP (binary protocol)
|
||||
▼
|
||||
udpstreamer-webui (Go)
|
||||
│ WebSocket (JSON)
|
||||
▼
|
||||
Browser (index.html + Plotly.js)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ MARTe2 UDP Streamer ● Streaming Window: [5 s▾] ⏸ │ ← top bar
|
||||
├──────────┬──────────────────────────────────────────────┤
|
||||
│ Signals │ Plots [1×1][2×1][2×2]… [+Add] │
|
||||
│──────────│──────────────────────────────────────────────│
|
||||
│ Counter │ ┌─────────────────┐ ┌──────────────────┐ │
|
||||
│ Time │ │ Plot 1 │ │ Plot 2 │ │
|
||||
│ Sine1 │ │ (drop signals) │ │ (drop signals) │ │
|
||||
│ Sine2 │ │ │ │ │ │
|
||||
│ Ch1[1000]│ └─────────────────┘ └──────────────────┘ │
|
||||
│ Ch2[1000]│ │
|
||||
└──────────┴──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### Adding Plots
|
||||
|
||||
1. Click **+ Add Plot** in the toolbar.
|
||||
2. Drag a signal from the sidebar onto a plot panel.
|
||||
3. Multiple signals can be overlaid on the same plot.
|
||||
4. Click the **×** badge to remove a trace.
|
||||
|
||||
### 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 All** (top bar) | Pause all plots simultaneously |
|
||||
| **Window** (top bar) | Adjust the rolling time window (1 s – 60 s) |
|
||||
| Layout buttons | Switch between 1×1, 2×1, 1×2, 2×2, 3×1, … grid layouts |
|
||||
| Sidebar **←** | Collapse the signal list to maximise plot area |
|
||||
|
||||
### Status LED
|
||||
|
||||
| Colour | Meaning |
|
||||
|--------|---------|
|
||||
| Red (solid) | No WebSocket connection to browser |
|
||||
| Orange (pulsing) | WebSocket connected but no data received in > 1 s |
|
||||
| Green (pulsing) | Data is being received normally |
|
||||
|
||||
---
|
||||
|
||||
## 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 index.html
|
||||
└── GET /ws ← upgrades to WebSocket; launches per-client read/write pumps
|
||||
```
|
||||
|
||||
### WebSocket Message Format
|
||||
|
||||
All messages are JSON. Two types are sent from server to browser:
|
||||
|
||||
#### `config` message
|
||||
|
||||
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": 0.0,
|
||||
"timeSignalIdx": 4294967295,
|
||||
"unit": "V"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `data` message
|
||||
|
||||
Sent at ≤ 30 Hz, batching all UDP packets received since the last tick.
|
||||
Each signal carries its own time axis to support mixed scalar + temporal-array signals.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "data",
|
||||
"signals": {
|
||||
"Sine1": { "t": [1747123456.001, 1747123456.002], "v": [3.14, 2.71] },
|
||||
"Counter": { "t": [1747123456.001, 1747123456.002], "v": [12340, 12341] },
|
||||
"Ch1": { "t": [1747123456.000, 1747123456.0001, ...], "v": [0.12, 0.13, ...] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `t` — Unix timestamp in seconds (float64) for each sample.
|
||||
- `v` — physical value (after dequantization if applicable).
|
||||
- For **temporal arrays**, `t` and `v` have `N × batchSize` entries
|
||||
(up to 2000 per 30 Hz tick after server-side decimation).
|
||||
- For **spatial arrays** the 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.
|
||||
Reference in New Issue
Block a user