docs: the TCPStreamer source kind, the client's TCP mode, and its probes

This commit is contained in:
Martino Ferrari committed 2026-11-06 13:58:48 +01:00
1 parent 3dad65c487
commit a65f953f17
5 files changed
+110 -7

No files matched your search

+25
View File
@@ -327,6 +327,27 @@ delivers one frame per datagram. The source is connected on the first valid data
was verified on a loopback group (`239.255.42.99` joined on `127.0.0.1`, although this machine's `lo` does not was verified on a loopback group (`239.255.42.99` joined on `127.0.0.1`, although this machine's `lo` does not
carry the `MULTICAST` flag); a real network, its switches and IGMP were not exercised. carry the `MULTICAST` flag); a real network, its switches and IGMP were not exercised.
The fourth built-in kind is **TCPStreamer** (`--source tcps`), a receiver for the MARTe2 `TCPStreamer`
datasource, standalone or through a shared `TCPStreamService`. Both speak the UDPStreamer framing over a TCP
connection, so it is the UDPS client in a TCP mode: the same CONFIG and DATA, the same channels, calibration and
timing. The form takes the server (default 127.0.0.1), the port (44500) and an optional **stream name**: leave
it empty for a standalone `TCPStreamer`, which starts sending on connect; name the stream for a
`TCPStreamService`, which wants a `SUBSCRIBE` first (the client sends one only when a name is set). On the
command line these are `--source tcps --source-opt server=... --source-opt port=... --source-opt stream=...`.
The source is disconnected after 2 s of silence or a closed connection and reconnects every 0.5 s; a stream
whose frames are more than 2 s apart therefore reconnects continually, as UDPS does. It is verified against
`tools/mock_tcpstreamer.py`, not against a MARTe2 application. Its limits:
- **One stream per connection.** A multiplexed `SUBSCRIBE` list (`a,b,c`) is refused by the form, because a
source is single-timebase; merge streams on the server with `TCPStreamJoin` and name the result.
- **No packet capture.** The capture is of the UDP socket; the button stays disabled.
- **A gap is reported at once.** TCP is ordered, so there is no reorder window: a counter gap is a server-side
drop and is counted as lost immediately.
- **A frame is at most `UDPS_MAX_DATAGRAM_BYTES` (about 65 KB)**, which the server's `MaxPayloadSize` default of
60000 satisfies.
- **After a reconnect to a server whose time base restarted** the app prints `the clock stalled at ...`
warnings, exactly as for a restarted UDPS stream.
A session file written before kinds existed has no `source_kind`, and is read as UDPS; a UDPS file is A session file written before kinds existed has no `source_kind`, and is read as UDPS; a UDPS file is
written exactly as before. A kind's capability panels (for instance a source's own controls) appear as written exactly as before. A kind's capability panels (for instance a source's own controls) appear as
pages of the side stack, and are insensitive rather than hidden while no source is connected. pages of the side stack, and are insensitive rather than hidden while no source is connected.
@@ -3299,6 +3320,10 @@ silence timeout and then resumes, which is the EMI case end to end.
exactly faithful in `strict` mode, and the only reading of the wire that does exactly faithful in `strict` mode, and the only reading of the wire that does
not invent samples in `accumulate` mode. The info popup and the CONFIG dump on not invent samples in `accumulate` mode. The info popup and the CONFIG dump on
stderr report the element counts. stderr report the element counts.
- **The TCPStreamer source takes one stream per connection and has no packet capture.** A multiplexed
`SUBSCRIBE` list is not supported (merge on the server with `TCPStreamJoin`), the capture button stays
disabled, a gap is reported at once (no reorder window) and a frame is limited to about 65 KB; see "Data
sources".
- **The event window can be shorter than the buffer.** The trigger latches a window - **The event window can be shorter than the buffer.** The trigger latches a window
of the event-span selection; the ring holds far more, so with trigger mode off the of the event-span selection; the ring holds far more, so with trigger mode off the
rest is scrollable history. rest is scrollable history.
+9 -1
View File
@@ -32,8 +32,16 @@ What feeds `Acquisition` is a `DataSource`, one of the registered *kinds*.
source-neutral, and its pointers are valid **only during `on_data`**: copy what is kept. source-neutral, and its pointers are valid **only during `on_data`**: copy what is kept.
- The GTK thread reaches a source **only** through `DataSource::post(Command)`; the queue is drained - The GTK thread reaches a source **only** through `DataSource::post(Command)`; the queue is drained
at the top of `poll()`. A panel never calls a source directly. at the top of `poll()`. A panel never calls a source directly.
- `UdpsSource` wraps `deps/udpsclient` unchanged; `udps_*` names appear only in - `UdpsSource` wraps `deps/udpsclient` and takes a `Transport` (UDP or TCP); `udps_*` names appear only in
`src/source/udps_*.cpp` (`grep -rn "udps_" src` outside those files is a leak of the old coupling). `src/source/udps_*.cpp` (`grep -rn "udps_" src` outside those files is a leak of the old coupling).
- **The TCPStreamer kind (`tcps`) is `UdpsSource` with `Transport::Tcp`**, not a new `DataSource`: the
transport argument sets `use_tcp` / `stream_name` in the client config, defaults the port to 44500 and
ignores multicast and interface. `src/source/tcps_kind.cpp` is only the form and the identity (`host:port` or
`host:port/stream`). One stream per connection (no multiplexed `SUBSCRIBE` list: a source is
single-timebase; merge on the server with `TCPStreamJoin`), and the form refuses a name with a comma.
`has_datagrams()` is false, so the capture button stays disabled. `Disconnected` follows the client's 2 s
silence timeout or a closed connection, and it reconnects every 0.5 s; a stream whose frames are further
apart than 2 s therefore reconnects continually, as UDPS does.
- The frame is **single-timebase**. A multi-timebase source (ABScope) will change the interface and - The frame is **single-timebase**. A multi-timebase source (ABScope) will change the interface and
the data path together; do not bend one frame to carry two clocks. the data path together; do not bend one frame to carry two clocks.
- A kind's capability panels are pages of the slide-over (`slideover_add_page`), named `source-<id>`. A disconnected source - A kind's capability panels are pages of the slide-over (`slideover_add_page`), named `source-<id>`. A disconnected source
+22 -2
View File
@@ -164,6 +164,16 @@ python3 tools/mock_flatudp.py --port 44588 --rate 1000 --signals 'Trigger:uint8,
(Quote the `signals` value: zsh globs the `[4]`. The mock takes the same text with or without the trailing `w`; (Quote the `signals` value: zsh globs the `[4]`. The mock takes the same text with or without the trailing `w`;
the datagram is the same, `w` only says how the app shows it.) the datagram is the same, `w` only says how the app shows it.)
`--source tcps` receives a MARTe2 `TCPStreamer` (standalone) or `TCPStreamService` (named stream) over
TCP: `server` (default 127.0.0.1), `port` 44500, and `stream` (optional; a `SUBSCRIBE` frame is sent only when it is
set). `tools/mock_tcpstreamer.py` serves one client with the `mock_udpstreamer.py` signals (`--port`, `--stream`
to require a `SUBSCRIBE`, `--rate`, `--batch`, `--freq`, `--close-after S`, `--drop-every N`).
```sh
python3 tools/mock_tcpstreamer.py --port 44500 --stream fast &
./build/udpscope --source tcps --source-opt port=44500 --source-opt stream=fast
```
`tools/mock_udpstreamer.py` has waveform-injection options used by the probes: `tools/mock_udpstreamer.py` has waveform-injection options used by the probes:
- `--square-harmonics N` shapes `square`: `N` odd harmonics of the ideal step's Fourier - `--square-harmonics N` shapes `square`: `N` odd harmonics of the ideal step's Fourier
@@ -201,7 +211,8 @@ form for a new host. Every probe below passes `--server`, so none of them sees i
`deps/plotter` is built from source with only the GTK4 adapter enabled `deps/plotter` is built from source with only the GTK4 adapter enabled
(`PLOT_BUILD_GTK4`); its tests and examples are forced off. `deps/udpsclient` is a (`PLOT_BUILD_GTK4`); its tests and examples are forced off. `deps/udpsclient` is a
vendored C99 source pair compiled into a static lib. `deps/udpsclient` gained one vendored C99 source pair compiled into a static lib. `deps/udpsclient` gained a TCP
transport mode (item 6 below) and one
additive entry point for the packet capture — `udps_client_set_datagram_cb()`, firing additive entry point for the packet capture — `udps_client_set_datagram_cb()`, firing
inside `udps_client_poll()` immediately after the UDP `recv()` returns, before the inside `udps_client_poll()` immediately after the UDP `recv()` returns, before the
magic check or reassembly — plus the public `UDPS_MAX_DATAGRAM_BYTES` the capture magic check or reassembly — plus the public `UDPS_MAX_DATAGRAM_BYTES` the capture
@@ -227,7 +238,7 @@ handler clears either pointer without knowing which dialog it is. Presenting eit
app's plain `GtkApplicationWindow` is supported and warning-free; it was checked. app's plain `GtkApplicationWindow` is supported and warning-free; it was checked.
`deps/udpsclient` is a vendored C99 source pair and its diff from the upstream source `deps/udpsclient` is a vendored C99 source pair and its diff from the upstream source
is intentional. Five things were changed: is intentional. Six things were changed:
1. **A reorder ring of reassembled payloads.** When a packet arrives with a higher counter 1. **A reorder ring of reassembled payloads.** When a packet arrives with a higher counter
than expected, the raw datagram is held in a fixed-size ring rather than decoded immediately. than expected, the raw datagram is held in a fixed-size ring rather than decoded immediately.
@@ -258,6 +269,15 @@ is intentional. Five things were changed:
would be the same silent loss by another route. `stats.reorder_overflows` counts the event, would be the same silent loss by another route. `stats.reorder_overflows` counts the event,
so an undersized ring is a reading rather than a mystery; it is surfaced in the exit dump and so an undersized ring is a reading rather than a mystery; it is surfaced in the exit dump and
the `[reseq-probe]` line as `overflows=`. the `[reseq-probe]` line as `overflows=`.
6. **A TCP transport mode.** `udps_client_config_t::use_tcp` / `stream_name` select a TCP
connection to a MARTe2 `TCPStreamer` or `TCPStreamService`: `connect_tcp()` (a 3 s connect
bound through `SO_SNDTIMEO`, a 1 s receive bound, an optional `SUBSCRIBE` frame,
`UDPS_PKT_SUBSCRIBE = 5`, sent only when `stream_name` is set), the existing
`read_tcp_frame()` for the frames, the reorder window forced to 0 (TCP is ordered, so a
counter gap is a server-side drop and is reported at once), multicast forced off, no
DISCONNECT frame on close (a write to a closed peer would raise SIGPIPE), no keepalive ACK
and no datagram tap. A frame must fit `UDPS_MAX_DATAGRAM_BYTES` (the server's
`MaxPayloadSize` default of 60000 does). A stream name over 255 bytes is refused at create.
`deps/plotter` is upstream's own repository and this app's figure support was added `deps/plotter` is upstream's own repository and this app's figure support was added
there rather than worked around here: `Figure::set_min_y_gutter()` (a floor under the there rather than worked around here: `Figure::set_min_y_gutter()` (a floor under the
+37 -3
View File
@@ -2117,8 +2117,9 @@ and `regained` reads 400 ms later.
## `[source-kinds]` ## `[source-kinds]`
``` ```
[source-kinds] kinds=udps,e1608,flatudp,probe [source-kinds] kinds=udps,tcps,e1608,flatudp,probe
[source-kinds] form udps: round_trip=1 valid=1 [source-kinds] form udps: round_trip=1 valid=1
[source-kinds] form tcps: round_trip=1 valid=1
[source-kinds] form e1608: round_trip=1 valid=1 [source-kinds] form e1608: round_trip=1 valid=1
[source-kinds] form flatudp: round_trip=1 valid=1 [source-kinds] form flatudp: round_trip=1 valid=1
[source-kinds] form probe: round_trip=1 valid=1 [source-kinds] form probe: round_trip=1 valid=1
@@ -2132,10 +2133,10 @@ and `regained` reads 400 ms later.
source-plugin path from the registry to the producer thread. The variable also registers the test-only `probe` source-plugin path from the registry to the producer thread. The variable also registers the test-only `probe`
kind, so a run without it never shows that kind. The `form` line appears once per registered kind. kind, so a run without it never shows that kind. The `form` line appears once per registered kind.
- `kinds=udps,e1608,flatudp,probe`: a missing kind means `register_builtin_sources()` was not called before `parse_args()`, - `kinds=udps,tcps,e1608,flatudp,probe`: a missing kind means `register_builtin_sources()` was not called before `parse_args()`,
or the env var gate is wrong. or the env var gate is wrong.
- `round_trip=0`: a form's `write` and `read` disagree about a key -- a renamed `SourceConfig` key, or an - `round_trip=0`: a form's `write` and `read` disagree about a key -- a renamed `SourceConfig` key, or an
absent key that was not defaulted. `valid=0` is a default configuration the form refuses. The e1608 form's sample sets address, port, scan_rate, mode, ai1, trigger, dir1, plot0 and gen_rate; a mismatch names one of those. The flatudp sample sets port, bind, multicast, iface, rate, endian and signals. absent key that was not defaulted. `valid=0` is a default configuration the form refuses. The e1608 form's sample sets address, port, scan_rate, mode, ai1, trigger, dir1, plot0 and gen_rate; a mismatch names one of those. The flatudp sample sets port, bind, multicast, iface, rate, endian and signals. The tcps sample sets server, port and stream.
- `panel disconnected: sensitive=0 present=1`: **disable, don't hide.** `present=0` is a panel that was hidden - `panel disconnected: sensitive=0 present=1`: **disable, don't hide.** `present=0` is a panel that was hidden
or never added; `sensitive=1` is a panel that stays live with no source behind it. or never added; `sensitive=1` is a panel that stays live with no source behind it.
- `panel connected: sensitive=1`: anything else means `apply_source_panels` acted on the wrong widget (it must - `panel connected: sensitive=1`: anything else means `apply_source_panels` acted on the wrong widget (it must
@@ -2302,6 +2303,39 @@ same reason.
- `resumed: ok=1`: `Connected` was raised again and frames resumed. `connected=0` here means the source never - `resumed: ok=1`: `Connected` was raised again and frames resumed. `connected=0` here means the source never
recovered after the sender came back. recovered after the sender came back.
## `[tcps]`
```
[tcps] stream: connected=1 signals=5 names=time,sine,square,ramp,noise frames=373 ok=1
[tcps] capture button: present=1 sensitive=0
[tcps] reconnect: connected=1 frames_before=211 after=301 reconnects_before=0 after=1 ok=1
```
`UDPSCOPE_PROBE_TCPS=1500` (the first two lines; counts vary) and `UDPSCOPE_PROBE_TCPS_RECONNECT=2000` (the last)
run as `--source tcps --source-opt port=44500` against `tools/mock_tcpstreamer.py` with the options in
`verification.md`. A service run (`--stream fast` on both sides) reads the same two lines; the mock logs
`SUBSCRIBE 'fast'`. With no mock the stream line reads `connected=0 signals=0 names= frames=0 ok=0`: a reading,
not a failure (the retries are quiet on stderr).
- `stream: ok=1`: connected, the five names the mock sends (its `time` master channel is a signal of its own, so
the list is `time,sine,square,ramp,noise`, not four), and frames counted. `connected=0` with a mock running is
the connect or the `SUBSCRIBE` failing -- a mock started with `--stream` and an app without `stream=` (or a
different name) is closed by the mock (at once on a wrong name, after 5 s on none) and the app reconnects
forever; the mock's log says `no valid SUBSCRIBE`. `names` wrong or `signals=0` with `connected=1` is a CONFIG that was not decoded.
- `capture button: present=1 sensitive=0`: the capture is of the UDP socket and a TCP source reports no
datagrams (`has_datagrams()` false), so it is disabled, not hidden. `sensitive=1` is the button enabled for a
kind that has nothing to capture.
- `reconnect: ok=1`: after the mock closed the connection (`--close-after 3`) the client came back
(`connected=1`), frames advanced past `frames_before` and the `reconnects` stat advanced past
`reconnects_before`. The probe polls every 100 ms for up to 6 s, so it reads the first moment all three hold.
`ok=0` is a client that did not come back after the close, or `Acquisition` not mapping `reconnects`;
`reconnects_before=1` is the probe's baseline falling **after** the close (`UDPSCOPE_PROBE_TCPS_RECONNECT` must
be earlier than the mock's `--close-after`).
- **Not a defect:** after the reconnect the mock restarts its time base at 0 and the app prints repeating
`warning: the clock stalled at ... samples stamped [...] are all behind it`. That is `Acquisition`'s behaviour
for any stream whose clock goes backwards, identical to restarting the UDPS mock; the line is a reading, not a
failure of this probe.
## `[prefs-probe]` ## `[prefs-probe]`
``` ```
+17 -1
View File
@@ -576,7 +576,7 @@ precedent, not an exception.
- `UDPSCOPE_PROBE_SOURCE_KINDS=2000` with `UDPSCOPE_EXIT_AFTER_MS=5000`, run as `--source probe --source-opt rate=500` - `UDPSCOPE_PROBE_SOURCE_KINDS=2000` with `UDPSCOPE_EXIT_AFTER_MS=5000`, run as `--source probe --source-opt rate=500`
-- the source-plugin path end to end. The variable's presence also registers the test-only `probe` kind (a -- the source-plugin path end to end. The variable's presence also registers the test-only `probe` kind (a
synthetic sine with one panel), so it is the only way a run sees that kind; its value is the delay in ms. synthetic sine with one panel), so it is the only way a run sees that kind; its value is the delay in ms.
Nine `[source-kinds]` lines (one `form` line per registered kind: `udps`, `e1608`, `flatudp`, `probe`); the Ten `[source-kinds]` lines (one `form` line per registered kind: `udps`, `tcps`, `e1608`, `flatudp`, `probe`); the
reading's own entry is in `probe-readings.md`. reading's own entry is in `probe-readings.md`.
- `UDPSCOPE_PROBE_E1608=1500` with `UDPSCOPE_EXIT_AFTER_MS`, run as `--source e1608` against - `UDPSCOPE_PROBE_E1608=1500` with `UDPSCOPE_EXIT_AFTER_MS`, run as `--source e1608` against
`tools/mock_e1608.py` -- the MCC E-1608 end to end: panels present and sensitive, a DAC write, a `tools/mock_e1608.py` -- the MCC E-1608 end to end: panels present and sensitive, a DAC write, a
@@ -637,6 +637,22 @@ precedent, not an exception.
- **no sender:** `connected=0` throughout, a reading and not a failure. - **no sender:** `connected=0` throughout, a reading and not a failure.
- `UDPSCOPE_PROBE_FLATUDP_GAP=1500` -- starts the silence poll described above (2 s after the source's last - `UDPSCOPE_PROBE_FLATUDP_GAP=1500` -- starts the silence poll described above (2 s after the source's last
datagram it must be disconnected; then it must reconnect and the frame count move on). datagram it must be disconnected; then it must reconnect and the frame count move on).
- `UDPSCOPE_PROBE_TCPS=1500` with `UDPSCOPE_EXIT_AFTER_MS`, run as `--source tcps --source-opt port=44500`
against `tools/mock_tcpstreamer.py --port 44500` -- the TCPStreamer source end to end: the connection, the
five channel names the mock sends (`time,sine,square,ramp,noise`), frames flowing and the capture button
present but disabled. It enables the signals itself, so do **not** add `UDPSCOPE_ENABLE_ALL_MS`. Runs, each
short and serial, with the mock killed afterwards:
- **standalone:** the mock with no `--stream`, `UDPSCOPE_PROBE_TCPS=1500 UDPSCOPE_EXIT_AFTER_MS=4000`;
- **service:** mock `--stream fast`, app `--source-opt stream=fast`, the same environment; the mock logs
`SUBSCRIBE 'fast'`;
- **reconnect:** mock `--close-after 3`, `UDPSCOPE_PROBE_TCPS=1500 UDPSCOPE_PROBE_TCPS_RECONNECT=2000
UDPSCOPE_EXIT_AFTER_MS=8000`; stderr also carries `the clock stalled at ...` warnings after the
reconnect (see `probe-readings.md`);
- **no mock:** `connected=0` throughout, a reading and not a failure, with no retry flood on stderr.
- `UDPSCOPE_PROBE_TCPS_RECONNECT=2000` -- records the frame and reconnect counts, then polls every 100 ms
(up to 6 s) until connected, frames and reconnects have all advanced, and prints. **It must fall before the
mock's `--close-after`** (the baseline is read before the close); the poll, not a fixed delay, absorbs
when the close lands.
- `UDPSCOPE_PROBE_SEARCH_MS` — runs a buffer search for the armed condition, cross-checks the live - `UDPSCOPE_PROBE_SEARCH_MS` — runs a buffer search for the armed condition, cross-checks the live
trigger's own `t0` against the nearest hit (`agree=1` is the load-bearing reading), and jumps to trigger's own `t0` against the nearest hit (`agree=1` is the load-bearing reading), and jumps to
hit 0. Reports the resulting `mode=`, `axis=` and `marks placed=`. hit 0. Reports the resulting `mode=`, `axis=` and `marks placed=`.