Files
Martino Ferrari e3389f932b FIrs
2026-05-15 17:42:14 +02:00

9.5 KiB
Raw Permalink Blame History

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 if needed.


1. Environment Setup

Edit marte_env.sh in the repository root to point at your MARTe2 installations:

# 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:

cd /path/to/MARTe_IO_components
source marte_env.sh

Verify the environment is correct:

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:

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:

export LD_LIBRARY_PATH="/path/to/Build/x86-linux/Components/DataSources/UDPStreamer:$LD_LIBRARY_PATH"

Step 4 — Start the WebUI and connect

# 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.

MARTe2 tries to dlopen("SineArrayGAM.so") the first time it encounters the class. Create the symlink in your build directory:

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:

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 (Go) and the Protocol reference.


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