diff --git a/docs/superpowers/specs/2026-11-02-flat-udp-source-design.md b/docs/superpowers/specs/2026-11-02-flat-udp-source-design.md new file mode 100644 index 0000000..52f6b04 --- /dev/null +++ b/docs/superpowers/specs/2026-11-02-flat-udp-source-design.md @@ -0,0 +1,124 @@ +# 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 `Write` of the datasource memory per `Synchronise()`: **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 of `size * elements`. +- There is no header, counter, timestamp or signal description. In IndependentThread mode the + first signal is a `uint8` `Trigger`; 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}` -- the `signals` string codec: parse, format, validate, size + arithmetic. No GTK, so a probe can round-trip it. +- `flatudp_source.{hpp,cpp}` -- the `DataSource`. +- `flatudp_kind.{hpp,cpp}` -- the `SourceKind`, the form, and the registration in + `register_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 one `Frame`: `num_samples = 1`, `counter` a running count, + `lost = 0`, `late = false`, `recv_time` the arrival clock. +- `set_datagram_tap` is honoured and `has_datagrams()` is true, so packet capture works. +- `on_config` is called once on open with one `SignalInfo` per row (`rows = count`, + `cols = 1`). + +### Time + +- `rate`: every signal has `time_mode = kTimePacket` and `sampling_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 with `sampling_rate = 0` + really 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 `Error` + event ("datagram of N bytes, expected M, check the signal list") and increments `stale`. +- `connected` becomes true after the first valid datagram and false after 2 s of silence. +- A bind or multicast-join failure raises an `Error` and the source stays disconnected. + +### Connect form + +Built from `AdwPreferencesRow`s, 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 `signals` string 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_FLATUDP` in `src/ui_probe.cpp`: stream names and count; a hand-computed + decode of known values including a vector signal and a `uint64`; rate-mode spacing; the + wrong-size error reported once and counted in `stale`; disconnect after silence; the form's + validity rules; the `signals` codec round trip. A multicast loopback check runs if the + sandbox allows it. +- Baselines unchanged: exactly 3 build warnings, `gtk warnings: 0`, no `Gtk-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).