flatudp: vector rows as channels or as a waveform, a re-openable source, probes for both
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
a523bc2bd9
commit
e3bc1f72f3
10 files changed
+446
-100
No files matched your search
@@ -140,20 +140,27 @@ concatenated in declaration order, no header, no padding). The datagram carries
|
||||
settings are the whole contract: `port` 44488, `bind`, `multicast` and `iface` (a group to join and the local
|
||||
interface for it), `rate` (the sender's cycle rate in Hz; **required**, it sizes the ring and the ingest
|
||||
decimation and spaces the samples within a frame), `endian` `little|big`, and `signals`
|
||||
`name:type[count],...` with types `uint8/16/32/64`, `int8/16/32/64`, `float32`, `float64` (a count above 1
|
||||
makes a vector signal: one channel whose `count` values per datagram are consecutive samples, as a UDPStreamer
|
||||
block signal), e.g. `Trigger:uint8,Ia:float32,Wave:float32[16]`. A datagram of any other size than
|
||||
`name:type[count][w],...` with types `uint8/16/32/64`, `int8/16/32/64`, `float32`, `float64`, e.g.
|
||||
`Trigger:uint8,Ia:float32,Wave:float32[16]`. A count above 1 makes a vector signal, shown one of two ways: by
|
||||
default as `count` **channels** `Wave[0]`..`Wave[15]` of one sample per cycle each (batched like any scalar
|
||||
list); with a trailing `w` (`Wave:float32[16]w`) as ONE **waveform** channel receiving `count` consecutive
|
||||
samples per cycle at `rate * count`, as a UDPStreamer block signal. A waveform row needs a count above 1, a
|
||||
list with one holds only waveform rows of one count (no zero-padding of scalars), and a list may make at most
|
||||
256 channels (a scalar 1, a `channels` row `count`, a waveform 1). A datagram of any other size than
|
||||
the list implies is dropped and counted as stale; each distinct wrong size is reported once on stderr as
|
||||
`[flatudp] ...`. Loss is never reported: the stream has no counter. `tools/mock_flatudp.py` is a sender for
|
||||
a signal spec (`--rate`, `--signals`, `--big-endian`, `--gap S,D`, `--bad-every S`, `--multicast G --mcast-if A`,
|
||||
`--log FILE`).
|
||||
|
||||
```sh
|
||||
python3 tools/mock_flatudp.py --port 44588 --rate 1000 --signals Trigger:uint8,Ia:float32,Wave:int16[4] &
|
||||
python3 tools/mock_flatudp.py --port 44588 --rate 1000 --signals 'Trigger:uint8,Ia:float32,Wave:int16[4]' &
|
||||
./build/udpscope --source flatudp --source-opt port=44588 --source-opt rate=1000 \
|
||||
--source-opt signals=Trigger:uint8,Ia:float32,Wave:int16[4]
|
||||
--source-opt 'signals=Trigger:uint8,Ia:float32,Wave:int16[4]'
|
||||
```
|
||||
|
||||
(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.)
|
||||
|
||||
`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
|
||||
|
||||
@@ -1913,46 +1913,61 @@ the one `[e1608] handshake with ...: Connection refused` line, `stream: ... sign
|
||||
```
|
||||
[flatudp] codec: ok=1
|
||||
[flatudp] datagram of 22 bytes, expected 21, check the signal list
|
||||
[flatudp] stream: connected=1 signals=4 names=Trigger,Ia,Cnt,Wave expect=Trigger,Ia,Cnt,Wave frames=3735 rate=1000.000 ok=1
|
||||
[flatudp] channels: count=4 expect=4 ok=1
|
||||
[flatudp] continuity: ch0_breaks=0 ch1_breaks=0 ok=1
|
||||
[flatudp] wrong size: count=3 message='datagram of 22 bytes, expected 21, check the signal list' ok=1
|
||||
[flatudp] stream: connected=1 signals=7 names=Trigger,Ia,Cnt,Wave[0],Wave[1],Wave[2],Wave[3] expect=Trigger,Ia,Cnt,Wave[0],Wave[1],Wave[2],Wave[3] frames=368 rate=1000.000 ok=1
|
||||
[flatudp] channels: count=7 expect=7 ok=1
|
||||
[flatudp] continuity: Trigger_breaks=0 Ia_breaks=0 Cnt_breaks=0 Wave[0]_breaks=0 Wave[1]-[0]_breaks=0 ok=1
|
||||
[flatudp] wrong size: count=4 message='datagram of 22 bytes, expected 21, check the signal list' ok=1
|
||||
[flatudp] capture button: present=1 sensitive=1
|
||||
[flatudp] state: valid=3735 wrong=3
|
||||
[flatudp] silence: connected=0 model_connected=0 ok=1
|
||||
[flatudp] resumed: connected=1 frames_before=4990 frames_after=7147 ok=1
|
||||
[flatudp] state: valid=3683 wrong=4
|
||||
[flatudp] silence: connected=0 model_connected=0 after_ms=4300 ok=1
|
||||
[flatudp] resumed: connected=1 frames_before=401 frames_after=410 ok=1
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_FLATUDP=2500` (the first lines; counts vary) and `UDPSCOPE_PROBE_FLATUDP_GAP=8000` (the last two)
|
||||
run as `--source flatudp` against `tools/mock_flatudp.py` with the options in `verification.md`. The second line is
|
||||
the source's own stderr message, printed once per distinct wrong size, not a probe line. The `continuity` line
|
||||
above is the scalar-list run (`Trigger,Ia,Cnt`, big endian); the mixed-list run with `Wave:int16[4]` reads
|
||||
`continuity: skipped (...)`. With no sender the stream lines read `connected=0 ... frames=0 ... ok=0`, `continuity`
|
||||
is skipped and the capture button reads `sensitive=0`: a reading, not a failure. A run without `--bad-every` reads
|
||||
`wrong size: count=0 ... ok=0` for the same reason.
|
||||
and, for the waveform-only list `Wave:int16[4]w`:
|
||||
|
||||
- `codec: ok=1`: the `signals` text round-trips, a hand-built 24-byte datagram decodes to the hand-computed
|
||||
values in both byte orders (`uint8` 254, `int16` -2, `float32` 1.5, `uint64[2]` 1 and 4294967298, `int8` -1)
|
||||
and the validity rules refuse what they should. It needs no source, so `--source probe` shows it too.
|
||||
`ok=0` is a codec defect: a swapped byte order, a wrong width or a rule that does not fire.
|
||||
- `stream: ... ok=1`: the model's signals are the list's rows in order, at the declared rate. `names` out of
|
||||
order is a decode that mixed rows; `rate` of 1000 against a different declared rate is a `sampling_rate`
|
||||
that did not reach the model (the ring and the decimation are then sized for the wrong cadence).
|
||||
- `channels: count=4`: one channel per signal. A vector signal is **not** one channel per element: the model
|
||||
reads its `count` values as consecutive samples of one channel, as for a UDPStreamer block signal.
|
||||
- `continuity: ch0_breaks=0 ch1_breaks=0`: consecutive samples of the mock's first two signals step by 1 and
|
||||
by 2 modulo 100. A break is a lost or reordered datagram, a batch flushed with the wrong number of samples,
|
||||
a wrong byte order or ingest decimation that is not 1:1. It is skipped for a list with a vector row: the
|
||||
model spans each frame by its largest element block, so the scalar channels of a mixed list are padded
|
||||
with zeros (`Frame::value` reads 0 out of range) and a continuity check on them would read nearly every
|
||||
step as a break.
|
||||
```
|
||||
[flatudp] stream: connected=1 signals=1 names=Wave expect=Wave frames=3737 rate=4000.000 ok=1
|
||||
[flatudp] channels: count=1 expect=1 ok=1
|
||||
[flatudp] continuity: wave_breaks=0 ok=1
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_FLATUDP=2500` (the first lines; counts vary) and `UDPSCOPE_PROBE_FLATUDP_GAP=1500` (the last two)
|
||||
run as `--source flatudp` against `tools/mock_flatudp.py` with the options in `verification.md`. The second line is
|
||||
the source's own stderr message, printed once per distinct wrong size, not a probe line. With no sender the stream
|
||||
lines read `connected=0 ... frames=0 ... ok=0`, `continuity` is skipped and the capture button reads
|
||||
`sensitive=0`: a reading, not a failure. A run without `--bad-every` reads `wrong size: count=0 ... ok=0` for the
|
||||
same reason.
|
||||
|
||||
- `codec: ok=1`: the `signals` text round-trips (including a `w` row and a bracketed count of 1 on a waveform), a
|
||||
hand-built 24-byte datagram decodes to the hand-computed values in both byte orders (`uint8` 254, `int16` -2,
|
||||
`float32` 1.5, `uint64[2]` 1 and 4294967298, `int8` -1), so do a `float64`, `uint16`, `int32`, `uint32` and
|
||||
`int64`; `decode_channels` puts a `channels` vector in `count` columns and a waveform in one; and the validity
|
||||
rules refuse what they should (a waveform of count 1, a waveform mixed with another row or of another length,
|
||||
257 channels). It needs no source, so `--source probe` shows it too. `ok=0` is a codec defect: a swapped byte
|
||||
order, a wrong width or a rule that does not fire.
|
||||
- `stream: ... ok=1`: the model's signals are the list's channels in order, each at its declared rate: `rate`
|
||||
for a scalar or a `channels` element (named `Name[k]`), `rate * count` for a waveform. `names` out of order is a
|
||||
decode that mixed rows; a rate that is not the expected one is a `sampling_rate` that did not reach the model
|
||||
(the ring and the decimation are then sized for the wrong cadence; a waveform at `rate` instead of `rate * 4`
|
||||
has a time axis 4 times too long).
|
||||
- `channels: count=7`: one channel per `SignalInfo`, which is `channel_count(rows)`: a scalar 1, a `channels`
|
||||
vector `count`, a waveform 1 (its `count` values are consecutive samples of that one channel).
|
||||
- `continuity: <name>_breaks=0 ...`: the mock's row k carries `(i * (k + 1) + e) % 100`, so consecutive samples
|
||||
of a channel from row k step by k + 1 modulo 100 (`Trigger` 1, `Ia` 2, `Cnt` 3, `Wave[0]` 4), and `Wave[1]-[0]`
|
||||
is the same-sample difference of two elements, which must be 1. A break is a lost or reordered datagram, a
|
||||
batch flushed with the wrong number of samples, an element mapped to the wrong channel, a wrong byte order
|
||||
or ingest decimation that is not 1:1. A waveform list reads `wave_breaks`: samples step by 1 inside a
|
||||
datagram and by `2 - count` into the next, at a phase the probe finds from the first such step and then holds;
|
||||
a non-zero count is a block of the wrong length or one that dropped samples. (Before the `channels` form, a
|
||||
mixed list showed ~400 breaks here: a scalar next to a vector was zero-padded to the vector's length.)
|
||||
- `wrong size: ok=1`: wrong-size datagrams were counted and the message names the received and expected size.
|
||||
The message must also be the only stderr line of its kind: one per distinct size, however many arrive.
|
||||
- `capture button: present=1 sensitive=1`: the kind reports datagrams, so the capture UI is live (contrast
|
||||
`[source-kinds]`, where the probe kind leaves it disabled). With no stream it reads `sensitive=0`.
|
||||
- `silence: connected=0 model_connected=0`: 2 s without a valid datagram raised `Disconnected`. `connected=1`
|
||||
is the timeout not firing, wrong-size datagrams refreshing the silence clock (they must not), or the mock's
|
||||
gap not covering the probe (see the timing note in `verification.md`).
|
||||
- `silence: connected=0 model_connected=0 after_ms=...`: 2 s without a valid datagram raised `Disconnected`
|
||||
(`after_ms` is the wait from the stream coming up to the drop; it only reads the gap's start in the app's
|
||||
clock). `connected=1` is the timeout not firing or wrong-size datagrams refreshing the silence clock (they
|
||||
must not); `never connected` is a stream that never came up before the probe gave up after 20 s.
|
||||
- `resumed: ok=1`: `Connected` was raised again and frames resumed. `connected=0` here means the source never
|
||||
recovered after the sender came back.
|
||||
|
||||
|
||||
+21
-15
@@ -443,22 +443,28 @@ precedent, not an exception.
|
||||
later checks the frame count moved on. The reading's own entry is in `probe-readings.md`.
|
||||
- `UDPSCOPE_PROBE_FLATUDP=2500` with `UDPSCOPE_EXIT_AFTER_MS`, run as `--source flatudp` against
|
||||
`tools/mock_flatudp.py` -- the flat UDP source end to end: a hand-computed codec and decode check (it
|
||||
needs no source), the stream's signal names and declared rate, the channel count (one per signal), value
|
||||
continuity across the first two signals (all-scalar lists only), the wrong-size accounting and the capture
|
||||
button. It enables the signals itself, so do **not** add `UDPSCOPE_ENABLE_ALL_MS`. Runs, each short and
|
||||
serial, with the mock killed afterwards (quote the `signals` value: zsh globs the `[4]`):
|
||||
- **mixed list, gap and bad datagrams:** mock `--rate 1000 --signals Trigger:uint8,Ia:float32,Cnt:uint64,Wave:int16[4]
|
||||
--gap 9,5 --bad-every 1 --log /tmp/flatudp-mock.log` started 1 s before the app, the same `signals` as a
|
||||
`--source-opt`, with `UDPSCOPE_PROBE_FLATUDP=2500 UDPSCOPE_PROBE_FLATUDP_GAP=8000 UDPSCOPE_EXIT_AFTER_MS=14500`;
|
||||
stderr carries exactly one `datagram of 22 bytes, expected 21` line. The mock's clock starts about 4 s
|
||||
before the app's probe clock (`xvfb-run` and the app's own startup), so a gap of 9 s to 14 s on the mock's
|
||||
clock covers the app's 8 s probe (silence) and ends before its 12 s one (resumed); a mock gap measured in
|
||||
the app's own seconds would miss both;
|
||||
- **scalar list, big endian (batching, continuity):** mock `--signals Trigger:uint8,Ia:float32,Cnt:uint64 --big-endian`,
|
||||
app `--source-opt endian=big`, `UDPSCOPE_PROBE_FLATUDP=2500 UDPSCOPE_EXIT_AFTER_MS=5000`;
|
||||
needs no source; it covers the `w` suffix, the waveform and 256-channel rules, the channel decode and the
|
||||
float64/uint16/int32/uint32/int64 types in both byte orders), the stream's channel names (`Wave[0]`..
|
||||
for a `channels` vector) and declared rates (`rate * count` for a waveform), the channel count, value
|
||||
continuity per row (and the offset between a vector's elements, or a waveform's block phase), the
|
||||
wrong-size accounting and the capture button. It enables the signals itself, so do **not** add
|
||||
`UDPSCOPE_ENABLE_ALL_MS`. Runs, each short and serial, with the mock killed afterwards (quote the `signals`
|
||||
value: zsh globs the `[4]`):
|
||||
- **mixed list, gap and bad datagrams:** mock `--rate 1000 --signals 'Trigger:uint8,Ia:float32,Cnt:uint64,Wave:int16[4]'
|
||||
--gap 8,4 --bad-every 1` started 1 s before the app, the same `signals` as a `--source-opt`, with
|
||||
`UDPSCOPE_PROBE_FLATUDP=2500 UDPSCOPE_PROBE_FLATUDP_GAP=1500 UDPSCOPE_EXIT_AFTER_MS=14500`; stderr carries
|
||||
exactly one `datagram of 22 bytes, expected 21` line. The silence probe is a 100 ms poll that waits for the
|
||||
stream, then for the 2 s of silence to take the source down, then for it to come back, so it does not
|
||||
depend on when `xvfb-run` finished starting; the only requirement is that the mock's gap begins after
|
||||
the app connected (the mock's clock starts about 4 s before the app's) and ends in time to read the return
|
||||
(`--gap 8,4` with a 14.5 s run does both with several seconds of margin);
|
||||
- **waveform-only list:** mock `--signals 'Wave:int16[4]w'`, app `--source-opt 'signals=Wave:int16[4]w'`,
|
||||
`UDPSCOPE_PROBE_FLATUDP=2500 UDPSCOPE_EXIT_AFTER_MS=5000`: one channel at `rate=4000`;
|
||||
- **big endian (decode of every width through the stream):** mock `--big-endian` on the mixed list, app
|
||||
`--source-opt endian=big`, `UDPSCOPE_PROBE_FLATUDP=2500 UDPSCOPE_EXIT_AFTER_MS=5000`;
|
||||
- **no sender:** `connected=0` throughout, a reading and not a failure.
|
||||
- `UDPSCOPE_PROBE_FLATUDP_GAP=8000` -- reads the source inside the mock's silence (2 s after its last datagram
|
||||
the source is disconnected) and 4 s later checks it reconnected and the stream moved on.
|
||||
- `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_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=`.
|
||||
|
||||
Reference in new issue
Block a user