148 lines
8.1 KiB
Markdown
148 lines
8.1 KiB
Markdown
# 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 | The user declares the nominal cycle rate in Hz (`rate`, required). The existing pipeline stamps the frames: anchored at arrival, samples spaced by the declared cadence, corrected to the measured cadence when the arrivals are more than 25% off it; `--relative-time` gives a purely counted clock. There is no `time_mode` switch: `acquisition.cpp` has no stamping path that ignores the declared cadence, and `sampling_rate = 0` would size the ring and the ingest decimation for 1 kHz (`model.cpp:60`). Loss is never reported (`lost = 0`). |
|
|
| 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 | Nominal cycle rate in Hz. Required; sizes the ring and the decimation and spaces samples within a frame. |
|
|
| `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` finite and positive.
|
|
|
|
## 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`).
|
|
- **Batching.** When every row is scalar, decoded datagrams are queued and delivered
|
|
`block` at a time in one `Frame` (`num_samples = block`), where
|
|
`block = clamp(round(rate / 100), 1, 1024)`, about 100 frames per second as the E-1608 does. A
|
|
partial block is flushed after 100 ms so a slow or stopped sender does not hold samples back.
|
|
When any row is a vector, a vector signal carries one sample per `Frame`, so each datagram is
|
|
its own frame (`num_samples = 1`).
|
|
- `Frame`: `counter` a running frame count, `lost = 0`, `late = false`, `recv_time` the arrival
|
|
clock at delivery.
|
|
- `set_datagram_tap` is honoured for every datagram, valid or not, and `has_datagrams()` is
|
|
true, so packet capture works.
|
|
- `on_config` is called from the first `poll()` with one `SignalInfo` per row (`rows = count`,
|
|
`cols = 1`, `time_mode = kTimePacket`, `sampling_rate = rate`).
|
|
|
|
### 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` is raised on the first valid datagram and `Disconnected` after 2 s of silence; a
|
|
later datagram raises `Connected` again.
|
|
- 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: a rate spin.
|
|
- 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`; the declared rate reaching the model; 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).
|
|
|
|
## Amendment 2026-11-02: how a vector signal is shown (decided with the user during Task 2)
|
|
|
|
Task 2 found that treating every vector row as one channel of `count` consecutive samples zero-pads
|
|
the scalar channels of a mixed list (the model spans a frame by its largest element block, and
|
|
`Frame::value` is 0 past a scalar's one value) and declares the wrong rate for the vector channel.
|
|
Both readings of a vector are useful depending on the sender, so each vector row picks one:
|
|
|
|
- **`channels` (default).** `Wave:float32[16]` becomes 16 scalar channels named `Wave[0]` .. `Wave[15]`,
|
|
one sample per cycle each. All such lists are batched like an all-scalar list.
|
|
- **`waveform`.** `Wave:float32[16]w` is ONE channel receiving a block of `count` consecutive samples per
|
|
cycle: `SignalInfo{rows=count, cols=1, sampling_rate=rate*count}`, one datagram per `Frame`
|
|
(`num_samples = 1`).
|
|
|
|
Rules:
|
|
- The `signals` string takes an optional trailing `w` after the count; `format` emits it only for a waveform row.
|
|
- A waveform row needs `count > 1`.
|
|
- A list with a waveform row must contain only waveform rows, all with the same `count`; otherwise it is
|
|
invalid, with a message the form shows. (No zero-padding, no holding scalars across a block.)
|
|
- The channel total is capped at 256: scalars count 1, a `channels` row counts `count`, a waveform row counts 1.
|
|
Over the cap is invalid.
|
|
- `block` batching applies whenever the list has no waveform row; with a waveform row it is 1.
|
|
- The form shows a "Vector as" dropdown (`Channels` / `Waveform`) on rows whose count is above 1, and
|
|
the channel total in the subtitle.
|
|
- The earlier "Data flow" text saying every vector row forces `num_samples = 1` applies to waveform rows only.
|