Initial release
This commit is contained in:
@@ -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 |
|
||||
Reference in New Issue
Block a user