Initial release
This commit is contained in:
@@ -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 |
|
||||
@@ -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,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 |
|
||||
@@ -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
@@ -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 (0–100%) |
|
||||
| **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.
|
||||
Reference in New Issue
Block a user