From a65f953f17109138876715c83ce053cdb299458d Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Fri, 6 Nov 2026 13:58:48 +0100 Subject: [PATCH] docs: the TCPStreamer source kind, the client's TCP mode, and its probes --- README.md | 25 ++++++++++++++++++++++ docs/claude/architecture.md | 10 ++++++++- docs/claude/build-and-run.md | 24 +++++++++++++++++++-- docs/claude/probe-readings.md | 40 ++++++++++++++++++++++++++++++++--- docs/claude/verification.md | 18 +++++++++++++++- 5 files changed, 110 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e831ccc..396d711 100644 --- a/README.md +++ b/README.md @@ -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 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 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. @@ -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 not invent samples in `accumulate` mode. The info popup and the CONFIG dump on 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 of the event-span selection; the ring holds far more, so with trigger mode off the rest is scrollable history. diff --git a/docs/claude/architecture.md b/docs/claude/architecture.md index 6cddb85..8552ecb 100644 --- a/docs/claude/architecture.md +++ b/docs/claude/architecture.md @@ -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. - 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. -- `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). +- **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 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-`. A disconnected source diff --git a/docs/claude/build-and-run.md b/docs/claude/build-and-run.md index 2db2f58..f3971b0 100644 --- a/docs/claude/build-and-run.md +++ b/docs/claude/build-and-run.md @@ -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`; 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: - `--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 (`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 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 @@ -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. `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 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, 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=`. +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 there rather than worked around here: `Figure::set_min_y_gutter()` (a floor under the diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 9d1c697..b12c956 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -2117,8 +2117,9 @@ and `regained` reads 400 ms later. ## `[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 tcps: 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 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` 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. - `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 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 @@ -2302,6 +2303,39 @@ same reason. - `resumed: ok=1`: `Connected` was raised again and frames resumed. `connected=0` here means the source never 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]` ``` diff --git a/docs/claude/verification.md b/docs/claude/verification.md index 14ac1ac..5f15b7e 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -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` -- 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. - 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`. - `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 @@ -637,6 +637,22 @@ precedent, not an exception. - **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 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 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=`.