Implemented client datasource
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
// UDPStreamerClient — E2E Test Report
|
||||
// Author: Martino Ferrari
|
||||
// Date: June 2026
|
||||
#set document(
|
||||
title: "UDPStreamerClient — End-to-End Test Report",
|
||||
author: "Martino Ferrari",
|
||||
date: datetime(year: 2026, month: 6, day: 24),
|
||||
)
|
||||
#set page(numbering: "1 / 1", margin: (left: 2.5cm, right: 2.5cm, top: 2cm, bottom: 2cm))
|
||||
#set heading(numbering: "1.")
|
||||
#set par(justify: true)
|
||||
#show link: underline
|
||||
#show raw.where(block: true): set block(inset: 8pt, radius: 4pt, fill: luma(240))
|
||||
#set table(stroke: 0.5pt, inset: 8pt)
|
||||
|
||||
// ── Live validation data (emitted by validate_binary.py --json) ──
|
||||
#let uni = json("e2e_unicast.json")
|
||||
#let multi = json("e2e_multicast.json")
|
||||
#let fidx(v) = if v < 0 { [—] } else { [#v] }
|
||||
#let pct(n, d) = if d > 0 { [#(calc.round(100 * n / d, digits: 1))%] } else { [—] }
|
||||
#let status-badge(d) = {
|
||||
let c = if d.passed { green.darken(20%) } else { red.darken(10%) }
|
||||
text(fill: c, weight: "bold")[#d.status]
|
||||
}
|
||||
// Validation metrics table for one mode's json record.
|
||||
#let metrics-table(d) = table(
|
||||
columns: (auto, auto, auto),
|
||||
align: (left, right, left),
|
||||
[*Metric*], [*Value*], [*Notes*],
|
||||
[Output rows], [#d.n_rows_out], [Cycles captured by `FileWriter`],
|
||||
[Matching rows], [#d.matching_rows (#pct(d.matching_rows, d.n_rows_out))], [Non-zero rows equal to an input row],
|
||||
[Zero rows], [#d.zero_rows (#pct(d.zero_rows, d.n_rows_out))], [Startup transient before first `DATA`],
|
||||
[Mismatching rows], [#d.mismatching_rows (#pct(d.mismatching_rows, d.n_rows_out))], [Non-zero rows matching no input → corruption],
|
||||
[First matching row], [#fidx(d.first_matching_row)], [Index of first transported row],
|
||||
[First zero row], [#fidx(d.first_zero_row)], [Index of first all-zero row],
|
||||
[First mismatching row], [#fidx(d.first_mismatch_row)], [`—` when no corruption],
|
||||
[Status], [#status-badge(d)], [#d.message],
|
||||
)
|
||||
|
||||
// ── Title page ──
|
||||
#align(center)[
|
||||
#v(4cm)
|
||||
#text(size: 28pt, weight: "bold")[UDPStreamerClient]
|
||||
#v(0.5cm)
|
||||
#text(size: 18pt)[End-to-End Test Report]
|
||||
#v(1.5cm)
|
||||
#text(size: 11pt, fill: luma(120))[
|
||||
MARTe2 Input DataSource for receiving signal data from UDPStreamer server \
|
||||
Unicast and multicast modes with event-driven thread triggering
|
||||
]
|
||||
#v(3cm)
|
||||
#text(size: 10pt)[Martino Ferrari — June 2026]
|
||||
]
|
||||
#pagebreak()
|
||||
|
||||
#outline(indent: 1.5em, depth: 3)
|
||||
#pagebreak()
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 1. Architecture
|
||||
// ═══════════════════════════════════════
|
||||
= Architecture Overview
|
||||
|
||||
== End-to-End Dataflow
|
||||
|
||||
#figure(
|
||||
caption: [Pipeline from binary file input to binary file output across two MARTe2 threads.],
|
||||
{
|
||||
set text(size: 9pt)
|
||||
grid(
|
||||
columns: (1fr, 1fr, 1fr, 1fr, 1fr),
|
||||
rows: (auto, auto, auto, auto, auto, auto, auto, auto, auto),
|
||||
gutter: 4pt,
|
||||
// Header row
|
||||
grid.cell(colspan: 5, align(center)[*Thread 1 — 1 kHz, CPU 0x1*]),
|
||||
grid.cell(colspan: 5, align(center)[#line(length: 100%)]),
|
||||
// Row 1: sources
|
||||
align(center)[#block(fill: luma(220), inset: 4pt, radius: 3pt, width: 100%)[`LinuxTimer`\ Counter, Time]],
|
||||
align(center)[#text(fill: luma(140))[→]],
|
||||
align(center)[#block(fill: luma(220), inset: 4pt, radius: 3pt, width: 100%)[`FileReader`\ `Signal[10000]`]],
|
||||
align(center)[],
|
||||
align(center)[],
|
||||
// Row 2: IOGAM
|
||||
grid.cell(colspan: 5, align(center)[
|
||||
#block(fill: luma(210), inset: 6pt, radius: 4pt, width: 100%)[
|
||||
*IOGAM* `ReaderGAM` \
|
||||
_Input:_ `Counter, Time, Signal` from `DDB` + `FileReaderDS` \
|
||||
_Output:_ `Counter, Time, Signal` to `DDB` + `Streamer`
|
||||
]
|
||||
]),
|
||||
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↕ memcpy]]),
|
||||
// Row 3: UDPStreamer
|
||||
grid.cell(colspan: 5, align(center)[
|
||||
#block(fill: luma(200), inset: 8pt, radius: 4pt, width: 100%)[
|
||||
*UDPStreamer* (port `44600`)\
|
||||
`Synchronise()` copies `memory` → `readyBuffer` → posts `dataSem`\
|
||||
`Execute()` (background) waits on `dataSem`, serializes, sends UDP
|
||||
]
|
||||
]),
|
||||
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↓ UDP datagrams ↓]]),
|
||||
// Row 4: Network
|
||||
grid.cell(colspan: 5)[#block(fill: luma(235), inset: 6pt, radius: 3pt, width: 100%)[#align(center)[*Network* — localhost loopback, unicast or multicast]]],
|
||||
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↓ UDP datagrams ↓]]),
|
||||
// Row 5: UDPStreamerClient
|
||||
grid.cell(colspan: 5, align(center)[
|
||||
#block(fill: luma(200), inset: 8pt, radius: 4pt, width: 100%)[
|
||||
*UDPStreamerClient* (owns a shared `UDPSClient` — same receiver as the StreamHub hub)\
|
||||
`UDPSClient` background thread receives UDP, reassembles fragments, auto-reconnects,\
|
||||
then invokes `OnUDPSConfig()` / `OnUDPSData()` → decode to `scratchBuffer` → `readyBuffer`, post `dataSem`\
|
||||
`Synchronise()` (RT) blocks on `dataSem.ResetWait()` — _no `LinuxTimer` needed_
|
||||
]
|
||||
]),
|
||||
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↕ memcpy]]),
|
||||
// Row 6: IOGAM
|
||||
grid.cell(colspan: 5, align(center)[
|
||||
#block(fill: luma(210), inset: 6pt, radius: 4pt, width: 100%)[
|
||||
*IOGAM* `ClientGAM` \
|
||||
_Input:_ `Signal` from `ClientDS` \
|
||||
_Output:_ `Signal` to `FileWriterDS`
|
||||
]
|
||||
]),
|
||||
grid.cell(colspan: 5, align(center)[#text(fill: luma(140))[↕ async write]]),
|
||||
// Row 7: FileWriter
|
||||
grid.cell(colspan: 5, align(center)[#block(fill: luma(220), inset: 4pt, radius: 3pt, width: 100%)[`FileWriter`\ async flush to binary file]]),
|
||||
// Footer
|
||||
grid.cell(colspan: 5, align(center)[#line(length: 100%)]),
|
||||
grid.cell(colspan: 5, align(center)[*Thread 2 — Event-driven, CPU 0x2*]),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
== Event-Driven Thread Trigger
|
||||
|
||||
Thread 2 does _not_ use a `LinuxTimer`. Execution is driven entirely by data arrival
|
||||
via the `EventSem` pattern (also used by `SDNSubscriber`, `NI6368ADC`, `UARTDataSource`).
|
||||
Crucially, `UDPStreamerClient` does *not* reimplement the network stack: it owns a shared
|
||||
`MARTe::UDPSClient` (the very same receiver the StreamHub hub uses) and only implements the
|
||||
`UDPSClientListener` callbacks. Transport, fragment reassembly, multicast join and
|
||||
auto-reconnect are therefore identical to the hub by construction, with the wire format
|
||||
shared through `Common/UDP/UDPSProtocol.h`.
|
||||
|
||||
#enum(
|
||||
numbering: "1.",
|
||||
[`UDPSClient` background thread (`SingleThreadService`) receives datagrams, reassembles fragments and auto-reconnects],
|
||||
[On a complete payload it invokes the listener: `OnUDPSConfig()` validates the server CONFIG against the local signals; `OnUDPSData()` decodes one snapshot],
|
||||
[`OnUDPSData()` decodes (incl. dequantisation / accumulate) into a private `scratchBuffer`, then copies to `readyBuffer` under `FastPollingMutexSem`],
|
||||
[Posts `EventSem dataSem` to wake the real-time thread],
|
||||
[`UDPStreamerClient::Synchronise()` (RT) blocks on `dataSem.ResetWait(10 ms)`, copies `readyBuffer` to `memory`],
|
||||
[GAM executes, data flows to `FileWriter`],
|
||||
)
|
||||
|
||||
#pagebreak()
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 2. Latency Budget
|
||||
// ═══════════════════════════════════════
|
||||
= Latency Budget
|
||||
|
||||
#figure(
|
||||
image("latency_budget.png", width: 100%),
|
||||
caption: [Estimated per-cycle latency. Total: 54 ms → ∼18 Hz max throughput. Bottlenecks: `FileWriter` async flush (50 ms) and poll sleeps (2 ms).],
|
||||
)
|
||||
|
||||
== Breakdown
|
||||
|
||||
#table(
|
||||
columns: (auto, auto, auto),
|
||||
[*Stage*], [*Latency (ms)*], [*Notes*],
|
||||
[`FileReader::Synchronise()`], [1.0], [Blocking read from OS buffer],
|
||||
[`IOGAM` (memcpy)], [0.1], [24 KB copy (6100 float32)],
|
||||
[`UDPStreamer::Synchronise()`], [1.0], [Copy `memory` → `readyBuffer` + post semaphore],
|
||||
[`UDPStreamer::Execute()` (bg)], [1.0], [`Sleep::MSec(1)` poll interval],
|
||||
[Network (localhost)], [0.05], [Loopback, negligible],
|
||||
[`UDPSClient` receiver (bg)], [1.0], [`select()` timeout + decode in `OnUDPSData()`],
|
||||
[`UDPStreamerClient::Synchronise()`], [0.01], [`ResetWait(10ms)`, copy, return],
|
||||
[`IOGAM` (memcpy)], [0.1], [24 KB copy (6100 floats)],
|
||||
[`FileWriter` (async flush)], [50.0], [Disk I/O, buffer count configurable],
|
||||
[*Total*], [*54.3*], [*18 Hz max throughput*],
|
||||
)
|
||||
|
||||
== Observations
|
||||
|
||||
#list(
|
||||
tight: false,
|
||||
[1 ms poll sleeps in both `Execute()` loops minimize software latency. Total poll overhead: 2 ms.],
|
||||
[`FileWriter` async flush dominates at 50 ms; reducing `NumberOfBuffers` or using CSV format lowers this.],
|
||||
[Maximum theoretical throughput with zero sleeps and sync FileWriter: ∼500 Hz (limited by 24 KB memcpy).],
|
||||
[The `EventSem` pattern eliminates timer jitter — cycle rate exactly matches network data rate.],
|
||||
)
|
||||
|
||||
#pagebreak()
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 3. Test Results
|
||||
// ═══════════════════════════════════════
|
||||
= End-to-End Test Results
|
||||
|
||||
== Input Data
|
||||
|
||||
Multi-signal test file with three channels of different sizes to verify
|
||||
no data scrambling across UDP transport:
|
||||
|
||||
#table(
|
||||
columns: (auto, auto, auto, auto),
|
||||
[*Signal*], [*Type*], [*Elements*], [*Value Range*],
|
||||
[`Signal_100`], [`float32`], [`100`], [`(row*1000 + col) / 100.0`],
|
||||
[`Signal_1K`], [`float32`], [`1000`], [`(row*500 + col) / 50.0`],
|
||||
[`Signal_5K`], [`float32`], [`5000`], [`(row*200 + col) / 20.0`],
|
||||
)
|
||||
|
||||
Format: MARTe2 binary (42 B signal descriptor) + 6100 floats per row (24.4 KB/row).
|
||||
100 rows total, 2.44 MB data.
|
||||
|
||||
#figure(
|
||||
image("e2e_plots.png", width: 100%),
|
||||
caption: [3×3 grid: Input, the matching received Output, and their Difference per signal. The plot picks the first non-zero output row that matches an input row (skipping startup zero rows), so the near-zero Difference column confirms lossless, unscrambled transport.],
|
||||
)
|
||||
|
||||
== Latency Distribution
|
||||
|
||||
#figure(
|
||||
image("latency_histogram.png", width: 100%),
|
||||
caption: [Left: End-to-end latency histogram (median 54 ms, P95 103 ms, P99 129 ms). Right: Per-component boxplot showing `FileWriter` async flush dominates the distribution.],
|
||||
)
|
||||
|
||||
== Unicast Test
|
||||
|
||||
#table(
|
||||
columns: (auto, auto),
|
||||
[*Parameter*], [*Value*],
|
||||
[Configuration], [`E2ETest.cfg`],
|
||||
[Signals], [`3` (100 / 1000 / 5000 float32)],
|
||||
[Server port], [`44600`],
|
||||
[`MaxPayloadSize`], [`65507` (UDP max, no fragmentation)],
|
||||
[`PublishingMode`], [`Strict`],
|
||||
[Client thread], [Event-driven (no `LinuxTimer`)],
|
||||
)
|
||||
|
||||
#block(fill: luma(240), inset: 10pt, radius: 4pt)[
|
||||
*Status*: #status-badge(uni) --- #uni.message
|
||||
]
|
||||
|
||||
#metrics-table(uni)
|
||||
|
||||
== Multicast Test
|
||||
|
||||
#table(
|
||||
columns: (auto, auto),
|
||||
[*Parameter*], [*Value*],
|
||||
[Configuration], [`E2EMulticastTest.cfg`],
|
||||
[Signals], [`3` (100 / 1000 / 5000 float32)],
|
||||
[Server], [TCP control on `44600`, UDP DATA on `239.0.0.1:44610`],
|
||||
[`MaxPayloadSize`], [`65507` (UDP max, no fragmentation)],
|
||||
[`PublishingMode`], [`Strict`],
|
||||
[Client thread], [Event-driven (no `LinuxTimer`)],
|
||||
)
|
||||
|
||||
#block(fill: luma(240), inset: 10pt, radius: 4pt)[
|
||||
*Status*: #status-badge(multi) --- #multi.message
|
||||
]
|
||||
|
||||
#metrics-table(multi)
|
||||
|
||||
== Result Interpretation
|
||||
|
||||
Both transports *pass*: every non-zero output row is byte-identical to an input row
|
||||
(*zero mismatching rows*), confirming the `UDPSClient`-based transport is lossless and
|
||||
does not scramble the three different-sized signals
|
||||
(#uni.matching_rows of #uni.n_rows_out rows matched for unicast,
|
||||
#multi.matching_rows of #multi.n_rows_out for multicast).
|
||||
|
||||
The only non-matching rows are the leading all-zero rows (#uni.zero_rows for unicast;
|
||||
first real match at row #uni.first_matching_row). These are an expected start-up
|
||||
transient: `FileWriter` begins capturing cycles the instant the application reaches
|
||||
`Running`, a few cycles before the client has received its first `CONFIG` + `DATA`,
|
||||
so the `MemoryDataSourceI` signal memory is still zero-initialised. Once data arrives
|
||||
the output tracks the input exactly, hence *zero* mismatching rows.
|
||||
|
||||
=== Pass / Fail Criteria
|
||||
|
||||
`validate_binary.py` sorts every output row into exactly one bucket --- *zero*
|
||||
(all-zero startup), *matching* (equals some input row) or *mismatching* (non-zero but
|
||||
matches no input row) --- and fails on genuine corruption:
|
||||
|
||||
#table(
|
||||
columns: (auto, auto),
|
||||
[*Condition*], [*Verdict*],
|
||||
[Signal count / per-signal size / row size differ, or a file is unreadable/empty], [*FAIL*],
|
||||
[`matching == 0` (nothing transported, incl. all-zero output)], [*FAIL*],
|
||||
[`mismatching > 0` (a non-zero row matches no input row)], [*FAIL* --- corruption],
|
||||
[`matching > 0`, `mismatching == 0`, with some zero rows], [*PASS* (WARN)],
|
||||
[`matching == n_rows_out`], [*PASS*],
|
||||
)
|
||||
#pagebreak()
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 4. Implementation
|
||||
// ═══════════════════════════════════════
|
||||
= Implementation Summary
|
||||
|
||||
== Source Code
|
||||
|
||||
#table(
|
||||
columns: (auto, auto, auto),
|
||||
[*File*], [*Lines*], [*Description*],
|
||||
[`UDPStreamerClient.h`], [`204`], [Class + `UDPStreamerClientSignal` metadata declaration],
|
||||
[`UDPStreamerClient.cpp`], [`564`], [CONFIG/DATA decode, double-buffering, `Synchronise()`],
|
||||
[`Makefile.inc`], [`60`], [Includes + links `-lUDPStream`, `-lMARTe2`],
|
||||
[`Makefile.gcc`], [`25`], [GCC compiler rules],
|
||||
[`Makefile.cov`], [`25`], [Coverage rules],
|
||||
[*Total*], [*878*], [],
|
||||
)
|
||||
|
||||
The transport, fragment reassembly, multicast and auto-reconnect logic is *not* counted
|
||||
here: it lives in the shared `Source/Components/Interfaces/UDPStream/UDPSClient` library
|
||||
that the StreamHub hub also uses, so the DataSource itself stays thin.
|
||||
|
||||
== Protocol Support
|
||||
|
||||
#table(
|
||||
columns: (auto, auto, auto),
|
||||
[*Packet*], [*Direction*], [*Status*],
|
||||
[`CONNECT` (3)], [Client → Server], [✓],
|
||||
[`CONFIG` (1)], [Server → Client], [✓ parse + validate],
|
||||
[`DATA` (0)], [Server → Client], [✓ deserialize + dequantize + accumulate],
|
||||
[`DISCONNECT` (4)], [Bidirectional], [✓],
|
||||
[`ACK` (2)], [Client → Server], [✓ optional],
|
||||
)
|
||||
|
||||
== Features
|
||||
|
||||
#table(
|
||||
columns: (auto, auto),
|
||||
[*Feature*], [*Status*],
|
||||
[Reuses StreamHub hub code base (shared `UDPSClient`)], [✓],
|
||||
[Unicast mode], [✓],
|
||||
[Multicast mode (TCP control + UDP DATA join)], [✓],
|
||||
[Fragment reassembly (delegated to `UDPSClient`)], [✓],
|
||||
[Auto-reconnect on silence (delegated to `UDPSClient`)], [✓],
|
||||
[CONFIG validation against local signals], [✓],
|
||||
[Dequantization (uint8 / int8 / uint16 / int16)], [✓],
|
||||
[Accumulate mode batch deserialization], [✓],
|
||||
[Event-driven thread trigger (`EventSem`, no `LinuxTimer`)], [✓],
|
||||
[RT-safe double buffering (`FastPollingMutexSem`)], [✓],
|
||||
[`CLASS_REGISTER("1.0")`], [✓],
|
||||
[`MemoryMapSynchronisedInputBroker`], [✓],
|
||||
[Integrated into root `Makefile.gcc` `core`/`clean`], [✓],
|
||||
)
|
||||
|
||||
== Test Infrastructure
|
||||
|
||||
#table(
|
||||
columns: (auto, auto),
|
||||
[*File*], [*Description*],
|
||||
[`E2ETest.cfg`], [Unicast MARTe2 config with 3 multi-size signals],
|
||||
[`E2EMulticastTest.cfg`], [Multicast MARTe2 config (`239.0.0.1:44610`)],
|
||||
[`run_e2e_report.sh`], [Builds, runs unicast+multicast, validates, plots, compiles this report],
|
||||
[`validate_binary.py`], [Row-bucket comparison + `--json` metrics export],
|
||||
[`gen_test_data.py`], [Multi-signal binary file generator],
|
||||
)
|
||||
|
||||
== Build
|
||||
|
||||
#block(fill: luma(235), inset: 10pt, radius: 4pt)[
|
||||
```sh
|
||||
# Built as part of the library via the repo root (Interfaces/UDPStream first,
|
||||
# since UDPStreamerClient links -lUDPStream):
|
||||
$ make -f Makefile.gcc core
|
||||
|
||||
# Or the component on its own:
|
||||
$ make -C Source/Components/DataSources/UDPStreamerClient -f Makefile.gcc
|
||||
g++ -std=c++98 -Wall -Werror -Wno-invalid-offsetof \
|
||||
-fPIC -fno-strict-aliasing -frtti -pthread -g \
|
||||
-I. -I$ROOT/Common/UDP \
|
||||
-I$ROOT/Source/Components/Interfaces/UDPStream \
|
||||
UDPStreamerClient.cpp -o UDPStreamerClient.o
|
||||
g++ -shared UDPStreamerClient.o \
|
||||
-L$ROOT/Build/x86-linux/Components/Interfaces/UDPStream -lUDPStream \
|
||||
-L$MARTe2_DIR/Build/x86-linux/Core -lMARTe2 -o UDPStreamerClient.so
|
||||
```
|
||||
]
|
||||
|
||||
Builds clean under `-Werror`; the DataSource reuses the hub's `UDPSClient` rather than
|
||||
duplicating any socket code.
|
||||
|
||||
#pagebreak()
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 5. Next Steps
|
||||
// ═══════════════════════════════════════
|
||||
= Next Steps
|
||||
|
||||
== Short-Term
|
||||
|
||||
#list(
|
||||
[*Reduce poll latency*: Lower `RECV_TIMEOUT_MS` from 10 to 1 ms. Lower `ResetWait` timeout from 1000 to 100 ms.],
|
||||
[*Add GTest unit tests*: Fragment reassembly (2/5/100 fragments), dequantization accuracy, CONFIG parsing, accumulate mode.],
|
||||
)
|
||||
|
||||
== Medium-Term
|
||||
|
||||
#list(
|
||||
[*Benchmark throughput*: Measure with varying signal sizes (100 / 1K / 10K / 100K floats) and plot curve.],
|
||||
[*Multicast multi-client*: Verify multiple `UDPStreamerClient` instances join same group simultaneously.],
|
||||
[*Remove poll sleeps entirely*: Use continuous `select()` with zero timeout + `EventSem` back-pressure.],
|
||||
)
|
||||
|
||||
== Long-Term
|
||||
|
||||
#list(
|
||||
[*CI integration*: Add E2E test runner with automated comparison and regression detection.],
|
||||
[*Performance profiling*: Identify exact memcpy and serialization costs with `perf`.],
|
||||
)
|
||||
Reference in New Issue
Block a user