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

+30 -1
View File
@@ -115,11 +115,40 @@ document describes everywhere else, and it is the default: `--server`, `--port`,
`--help` lists the registered kinds. A second kind, `probe` (a synthetic sine with one panel), is registered
only when `UDPSCOPE_PROBE_SOURCE_KINDS` is set, and exists only for the self-check.
The second built-in kind is the **MCC E-1608** Ethernet DAQ (8 single-ended or 4 differential 16-bit inputs, two
analog outputs, eight digital lines and a 32-bit counter). Its connection form takes an address, or a MAC address
to look up (optionally with the subnet to sweep), and groups the acquisition settings (scan rate, external clock,
trigger, restart after an overrun, factory calibration, the digital poll rate), a range per analog input, and the
direction and initial value of each digital line. The analog inputs are the plotted channels; each digital input
and the counter is an extra channel only if you ask for it. Two side-stack tabs belong to it: **Analog out**
(per DAC: a static level, or a sine, square, triangle or ramp from 0.1 to 10 Hz) and **Digital I/O** (a live LED per
input bit, a switch per output bit, the counter with a reset); they are insensitive while no board is connected.
Acquisition is hardware-paced only (the scan rate or an external clock), the channels times the rate must stay at
or below 250 kS/s, and the form refuses more. With the command-line options, a run that names no `aiN` key gets
`ai0=BIP10V`; naming any `aiN` key drops that default, so `ai1=BIP5V` alone is channel 1 alone.
**This kind has not been run against a board.** It is verified against `tools/mock_e1608.py`, a protocol mock written
from two independent implementations of the wire protocol. The following rest on a single reference or the two
disagree, and are the first thing to check when a board is attached (the mock's header lists the same items):
the counter reset command (0x31, from one reference only); the pacer value (the source sends the *aggregate*
rate, `round(80e6 / (scan_rate * channels) - 1)`, as one reference does; the other sends the per-channel
frequency); the scan-start trigger option bits (none 0, rising 4, falling 8, high 12, low 16, one reference only);
whether the handshake may answer status 2 ("code ignored"; one reference accepts it, the other refuses it, and the
source accepts it); the first frame id; and the maximum frame size. Two further things
are worth a look on a board even though the mock's header does not flag them: the calibration memory layout and
its coefficient type, and the status word's bits, in particular whether the "scan running" bit stays set while the
board waits for a trigger (the source restarts a scan that reads as stopped when auto-restart is on). The DIFF input
numbering (channel + 8) and the filler byte after scan start are agreed by both references. The scan overrun count
is approximate: the board does not say how many scans it dropped, so the frame after a restart reports one lost.
Errors (no device, a rejected connection code, a timeout, the scan socket closing) are written to stderr as
`[e1608] ...` once per distinct message; the window shows only that the source is disconnected (the status popover
reads `waiting`, and the panels go insensitive), not why.
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.
Planned, each as its own sub-project: a flat UDP kind, the MCC E-1608 and ABScope. ABScope has more than one
Planned, each as its own sub-project: a flat UDP kind and ABScope. ABScope has more than one
timebase, so it will change the frame and the data path together rather than fit the present single-timebase
frame.
+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=`.