From 3093568d11598e4ce9aae90d8cd75581b4db0cfb Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Sun, 1 Nov 2026 09:54:49 +0100 Subject: [PATCH] docs: the E-1608 source kind, its probe readings and its unverified commands Co-Authored-By: Claude Sonnet 4.6 --- README.md | 31 +++++++++++++++++++- docs/claude/architecture.md | 19 ++++++++++++ docs/claude/build-and-run.md | 20 +++++++++++++ docs/claude/probe-readings.md | 54 +++++++++++++++++++++++++++++++++-- docs/claude/verification.md | 19 +++++++++++- 5 files changed, 138 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 231e806..adf0e6c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/claude/architecture.md b/docs/claude/architecture.md index 84fe7e0..2106f7c 100644 --- a/docs/claude/architecture.md +++ b/docs/claude/architecture.md @@ -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_`. A UDPS file keeps `_` 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 diff --git a/docs/claude/build-and-run.md b/docs/claude/build-and-run.md index 31ca56d..c7d2a76 100644 --- a/docs/claude/build-and-run.md +++ b/docs/claude/build-and-run.md @@ -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 diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 0de0a95..465299c 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -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=`: **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]` ``` diff --git a/docs/claude/verification.md b/docs/claude/verification.md index 1c71ee1..43e11fb 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -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=`.