docs: the E-1608 source kind, its probe readings and its unverified commands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-11-01 09:54:49 +01:00
1 parent 2e9f9664cf
commit 3093568d11
5 files changed
+138 -5

No files matched your search

+19
View File
@@ -42,6 +42,25 @@ What feeds `Acquisition` is a `DataSource`, one of the registered *kinds*.
`[A-Za-z0-9._-]` become `_`) on port **1** (0 is rejected by the session-file parser), the kind is
stored as `source_kind` and its settings as `source_opt_<key>`. A UDPS file keeps `<host>_<port>` and
has neither key, so a legacy session is read as UDPS and nothing existing moves.
- **The E-1608 kind has three layers, and only the top one knows the framework.** `e1608_link`
(discovery, handshake, framed TCP commands, the scan socket) and `e1608_device` (typed commands,
factory calibration, `decode_scans`) are plain POSIX C++ with no GTK or `DataSource` types;
`e1608_source` is the `DataSource`; `e1608_kind` and `e1608_panels` are the UI. A change to the wire
never reaches the UI files.
- The E-1608 source runs **everything inside `poll()`** on the producer thread: posted commands, the
connect state machine (Down, Scanning, Halted), the scan read, the 500 ms status check, the digital
input/counter poll and the 20 ms waveform tick. There is no second thread. A connect attempt (discovery,
handshake, calibration read, scan start) can block `poll()` for as long as the command timeout allows each
step; that delays only the command queue. Every exception is caught inside `poll()`: an escape would end
the producer thread, and `drain_commands()` runs inside the same `try` for that reason.
- Its frames are **fixed-size blocks** of about 10 ms (`round(scan_rate / 100)` scans, 1 to 4096), not
whatever one `recv` returned: `build_stamps` warns once when a frame's sample count differs from the
first one's. Digital lines and the counter are held across a block (a step trace).
- The E-1608 panels read the board's state from `E1608Readouts`, a set of atomics the producer writes
and a tick callback throttled to 100 ms reads; a panel never touches the source's internals, and writes
only through `post()`. `Acquisition::on_event` ignores the detail string and does nothing for `Error`,
so the source also writes each new error to stderr as `[e1608] ...` (once per distinct message, not per
retry); that line is where a user sees why a connection failed.
### Data path
+20
View File
@@ -115,6 +115,26 @@ and also exports `LIBGL_ALWAYS_SOFTWARE=1`: the plot is a `GtkGLArea`, so a disp
usable GPU fails twice, and this is the pair `tools/make-help-shots.sh` already sets. Nothing
is exported when the option is absent, so GTK's own choice stands.
`--source e1608` selects the MCC E-1608 Ethernet DAQ; its settings are `--source-opt` keys, all optional
(`address` or `mac`+`subnet`, `port` 54211, `code` 0, `timeout_ms` 2000, `scan_rate` 1000 per channel,
`ext_clock`, `trigger` `none|rising|falling|high|low`, `auto_restart` 1, `calibrate` 1, `poll_rate` 10,
`mode` `se|diff`, `ai0`..`ai7` `BIP10V|BIP5V|BIP2V|BIP1V` (an absent key = off, **and any `aiN` key at all
drops the default**: only a run with no `aiN` key gets `ai0=BIP10V`, so `ai1=BIP5V` alone is channel 1
alone, and a value that is not a range name is a channel that is off), `dir0`..`dir7` `i|o`,
`plot0`..`plot7`, `plot_counter`, `do_init`, `ao0_init`, `ao1_init`). Either `address` or `mac` is
required; a MAC with no `address` is looked up by broadcast, or by a sweep of `subnet` (`a.b.c`).
The enabled channels times `scan_rate` must not exceed 250 kS/s. `tools/mock_e1608.py` is a protocol
mock of the board for development (UDP discovery and handshake, TCP commands, a scan stream with a sine
per channel, calibration memory, `--port`, `--mac`, `--log FILE`, `--drop-after S` and `--overrun-after S`
switches, `--code N`); there is no real board in the loop, and the mock's header lists the items whose
layout is unverified. Errors are written to stderr as `[e1608] ...`, once per distinct message.
```sh
python3 tools/mock_e1608.py --port 54711 &
./build/udpscope --source e1608 --source-opt address=127.0.0.1 --source-opt port=54711 \
--source-opt ai0=BIP10V --source-opt ai1=BIP5V --source-opt plot0=1
```
`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
+51 -3
View File
@@ -1827,8 +1827,9 @@ are unrealized and measure 0x0.
## `[source-kinds]`
```
[source-kinds] kinds=udps,probe
[source-kinds] kinds=udps,e1608,probe
[source-kinds] form udps: round_trip=1 valid=1
[source-kinds] form e1608: round_trip=1 valid=1
[source-kinds] form probe: round_trip=1 valid=1
[source-kinds] panel disconnected: panels=1 present=1 sensitive=0
[source-kinds] panel connected: panels=1 present=1 sensitive=1
@@ -1840,10 +1841,10 @@ are unrealized and measure 0x0.
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,probe`: a missing kind means `register_builtin_sources()` was not called before `parse_args()`,
- `kinds=udps,e1608,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.
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 and plot0; a mismatch names one of those.
- `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
@@ -1856,6 +1857,53 @@ kind, so a run without it never shows that kind. The `form` line appears once pe
The UDPS run has no `source-` page in `side_stack`, because the UDPS kind has no panels.
## `[e1608]`
```
[e1608] panel ao: present=1 sensitive=1 connected=1
[e1608] panel dio: present=1 sensitive=1 connected=1
[e1608] actions: ao0_level=1 do1=1 counter_reset=1
[e1608] stream: connected=1 signals=4 expect=4 names=AI0,AI1,DI0,Counter frames=274 rate=1000.000 ok=1
[e1608] calibration: ai0_mean=0.5000 expect=0.5000 ok=1
[e1608] capture button: present=1 sensitive=0
[e1608] state: connects=1 message=''
[e1608] decode: ok=1
[e1608] scan socket closed by the board
[e1608] reconnect: connects=2 connected=1 ok=1
[e1608] resumed: frames_before=612 frames_after=662 ok=1
```
`UDPSCOPE_PROBE_E1608=1500` (the first eight lines; `frames=` and the exact mean vary) and
`UDPSCOPE_PROBE_E1608_RECONNECT=8000` (the last two) run as `--source e1608` against `tools/mock_e1608.py`
with the options in `verification.md`. The ninth line is the source's own stderr message about the drop, not a
probe line (its text is the error the source met, once per distinct message). `connected=0` throughout, with
`actions: skipped (not connected)`, is the no-board run; it is a reading and not a failure, and it also prints
the one `[e1608] handshake with ...: Connection refused` line, `stream: ... signals=0 expect=1 ... ok=0`,
`calibration: ai0_mean=nan ... ok=0` and `state: connects=0 message='handshake with ...'`.
- `panel ...: present=1 sensitive=<connected>`: **disable, don't hide.** `present=0` is a panel that was never
added; `sensitive` that differs from `connected` is `apply_source_panels` acting on the wrong widget.
- `actions: ao0_level=1 do1=1 counter_reset=1`: each control was found by its widget name (`e1608-ao0-level`,
`e1608-do1`, `e1608-counter-reset`). A `0` is a renamed widget, or (`do1`) `dir1=o` missing from the run. The
effects are read off the **mock's log**, not here: a DAC write near 2.5 V (within 0.01), `DOUT 0x02`, `COUNTER_RESET`.
A control that was found but whose effect is missing is a command that `post()` did not deliver or `poll()`
did not run.
- `stream: signals=4 expect=4 rate=1000.000 ok=1`: the enabled channels plus the plotted digital lines plus the
counter, at the scan rate. `signals` below `expect` is a signal the source did not declare; `rate` off by a
factor is the pacer computed from the wrong aggregate.
- `calibration: ai0_mean=0.50xx ok=1`: the mock's AI0 is 0.5 V under a 5 Hz sine of 0.1 V, read through
non-trivial factory coefficients; the mean over two periods lands on 0.5 V only when `decode()` applies them.
About `0.2043` is the **uncalibrated** reading (the calibration read failed, or `calibrate=0`); `nan` is a
run that never got AI0 (not connected, or AI0 not enabled).
- `capture button: present=1 sensitive=0`: a source with no datagrams leaves the capture UI disabled, not hidden.
- `state: connects=1 message=''`: a non-empty message at this point is a refusal that happened (for example
`calibration memory refused; using nominal values`), which the stream then ran through.
- `decode: ok=1`: a fixed raw buffer against hand-computed volts (intercept 100, BIP10V and BIP5V, the code
clamp at both ends). It needs no board; `ok=0` is a change to the calibration arithmetic.
- `reconnect: connects=2 connected=1`, `resumed: ok=1`: the mock dropped both links at about 3 s; the source
reported it, retried after 2 s and the frame count moved on. `connects=1` is a source that never noticed the
drop (neither the scan read nor the status check raised it), `resumed: ok=0` is a reconnect that did not restart the scan.
## `[prefs-probe]`
```
+18 -1
View File
@@ -423,7 +423,24 @@ 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.
Seven `[source-kinds]` lines; the reading's own entry is in `probe-readings.md`.
Eight `[source-kinds]` lines (one `form` line per registered kind: `udps`, `e1608`, `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
digital-output toggle and a counter reset each reaching the mock, the stream's channels and rate, the
factory calibration applied, the capture button disabled, and a hand-computed decode. It enables the
signals itself, so do **not** add `UDPSCOPE_ENABLE_ALL_MS` (that probe switches the first signal off
after two seconds). Three runs, each short and serial, with the mock killed afterwards:
- **no board:** panels `sensitive=0`, `connected=0`, no retry flood on stderr (the one
`[e1608] handshake with ...: Connection refused` line is printed once, not per retry);
- **the mock:** `--drop-after 3 --log /tmp/e1608-mock.log`, options `scan_rate=1000 ai0=BIP10V
ai1=BIP5V plot0=1 plot_counter=1 dir1=o ao0_init=1.5`, with `UDPSCOPE_PROBE_E1608=1500
UDPSCOPE_PROBE_E1608_RECONNECT=8000 UDPSCOPE_EXIT_AFTER_MS=10500`; then read the mock's log: the AO
levels `1.5, 2.5, 1.5` (the log prints the quantised voltage, so `1.4999, 2.4999, 1.4999` is the
expected text), the DOUT bytes `0x00, 0x02, 0x00`, one `COUNTER_RESET`, one `DROP`;
- **a rejected code:** mock `--code 1234`, app `code=7`: one error line, the retries stop.
- `UDPSCOPE_PROBE_E1608_RECONNECT=8000` -- reads the source after the mock dropped the link and 500 ms
later checks the frame count moved on. The reading's own entry is in `probe-readings.md`.
- `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=`.