Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6.0 KiB
Flat UDP source plugin (sub-project 2 of 4)
Date: 2026-11-02
Branch: to be cut from feature/source-plugins
Context
Sub-project 1 added the DataSource / SourceKind / SourceRegistry framework. Sub-project 3
added the E-1608. This sub-project adds flatudp: a receiver for the MARTe2 UDPSender
datasource (MARTe2-components/Source/Components/DataSources/UDP/UDPSender.{h,cpp}).
The wire format
Read from UDPSender::Synchronise() and MemoryDataSourceI:
- One
Writeof the datasource memory perSynchronise(): one datagram is one cycle. - The memory is the signals concatenated in declaration order, no padding, in the sender's
native byte order.
signalOffsets[s]is the running sum ofsize * elements. - There is no header, counter, timestamp or signal description. In IndependentThread mode the
first signal is a
uint8Trigger; it is sent like any other signal. - The user must therefore enter the exact signal list by hand.
Decisions
| Question | Decision |
|---|---|
| Time base | Both. time_mode=rate (default): the user declares the cycle rate in Hz; frames are spaced 1/rate and no loss is ever reported. time_mode=arrival: each datagram is stamped with its receive time. |
| Signal list entry | A row editor: name entry, type dropdown, element count and a remove button per row, plus an "Add signal" row. |
| Network | Port and optional bind address, plus an optional multicast group and interface. No source-address filter. |
| Byte order | little (default) or big. |
| Out of scope | Auto-detecting the list, importing a MARTe2 .cfg, a source filter, loss detection. |
Config keys
Persisted in the session, settable with --source-opt. Kind id: flatudp.
| Key | Default | Meaning |
|---|---|---|
port |
44488 | UDP port to listen on (the sender's own default). |
bind |
empty | Local address to bind; empty is any. |
multicast |
empty | Group to join; empty is plain unicast. |
iface |
empty | Local interface address for the join. |
rate |
none | Cycle rate in Hz. Required when time_mode=rate. |
time_mode |
rate |
rate or arrival. |
endian |
little |
little or big. |
signals |
none | name:type[count],..., e.g. Trigger:uint8,Ia:float32,Wave:float32[16]. A persistence format only: the form builds and parses it. |
Types: uint8/16/32/64, int8/16/32/64, float32, float64. Count 1 is a scalar signal; N
is a vector signal. Expected datagram size is the sum of size * count over the rows.
Validity (form and --source-opt alike): at least one signal; names non-empty and unique;
count 1-4096; total size at most 65507 bytes; rate > 0 when time_mode=rate.
Architecture
New files under src/source/flatudp/:
flatudp_signals.{hpp,cpp}-- thesignalsstring codec: parse, format, validate, size arithmetic. No GTK, so a probe can round-trip it.flatudp_source.{hpp,cpp}-- theDataSource.flatudp_kind.{hpp,cpp}-- theSourceKind, the form, and the registration inregister_builtin_sources().
Threading is unchanged: the source lives on the producer thread and poll() does all the work.
Data flow
poll(timeout_ms) waits on a non-blocking UDP socket and drains it.
- A datagram of exactly the expected size is decoded row by row to doubles (byte-swapped when
endian=big) and delivered as oneFrame:num_samples = 1,countera running count,lost = 0,late = false,recv_timethe arrival clock. set_datagram_tapis honoured andhas_datagrams()is true, so packet capture works.on_configis called once on open with oneSignalInfoper row (rows = count,cols = 1).
Time
rate: every signal hastime_mode = kTimePacketandsampling_rate = rate, as the E-1608 does. Spacing comes from the declared rate; jitter does not appear in the trace.arrival:sampling_rate = 0, so the existing arrival-clock path stamps each frame.- To confirm in the plan: that
acquisition.cpp's packet path withsampling_rate = 0really stamps on arrival and does not degenerate. If it does not, the plan adds the small change there.
Errors and state
- A wrong-size datagram is dropped, never decoded. Each distinct size raises one
Errorevent ("datagram of N bytes, expected M, check the signal list") and incrementsstale. connectedbecomes true after the first valid datagram and false after 2 s of silence.- A bind or multicast-join failure raises an
Errorand the source stays disconnected.
Connect form
Built from AdwPreferencesRows, with the destroy-notify guard used by the E-1608 form.
- Network group: port, bind, multicast, interface.
- Timing group: rate spin and time-mode combo. The rate spin is disabled, not hidden, in arrival mode.
- Byte order combo.
- Signals group: one row per signal, "Add signal" at the bottom, and a live subtitle with the
implied size ("41 bytes per datagram").
valid()is false while the list breaks a validity rule. - The
signalsstring is rebuilt from the rows on every change;write()parses it back.
Verification
No unit suite; the self-check is the contract.
tools/mock_flatudp.py: emits packed structs for a given signal spec and rate; options for big-endian and for one wrong-size datagram.UDPSCOPE_PROBE_FLATUDPinsrc/ui_probe.cpp: stream names and count; a hand-computed decode of known values including a vector signal and auint64; rate-mode spacing; the wrong-size error reported once and counted instale; disconnect after silence; the form's validity rules; thesignalscodec round trip. A multicast loopback check runs if the sandbox allows it.- Baselines unchanged: exactly 3 build warnings,
gtk warnings: 0, noGtk-CRITICAL.
Documentation
README (the kind and its limits: no loss detection, the signal list is the user's
responsibility), docs/claude/build-and-run.md (options and mock usage),
docs/claude/architecture.md (source plugins), docs/claude/probe-readings.md and
docs/claude/verification.md (the new probe lines).