docs: the source-plugin framework, the --source options and the source-kinds probe

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-31 20:22:22 +01:00
1 parent 5cee00aaee
commit 4d859d6a37
6 files changed
+96 -11

No files matched your search

+24 -2
View File
@@ -10,8 +10,8 @@ concluding something is a bug.
### Threads
Exactly two: a producer thread in `Acquisition` owns `udps_client_t` and calls
`udps_client_poll()`; the GTK main thread owns every widget and GL context. They share
Exactly two: a producer thread in `Acquisition` owns a `DataSource` and calls
`source->poll(50)`; the GTK main thread owns every widget and GL context. They share
`ScopeModel` under `ScopeModel::mutex()`, taken once per DATA frame by the producer and
once per frame by the UI (`tick()` in `src/ui.cpp`) around `RowSet::bind()`/`acquire()`.
Samples themselves cross via `plot::data::RingBuffer` (single producer, single
@@ -21,6 +21,28 @@ consumer) and never through the lock. Cross-thread scalars are atomics on `Scope
before `trigger_pending_` and those two carry release/acquire, so a consumer that sees a
capture open sees which edge it belongs to.
### Sources
What feeds `Acquisition` is a `DataSource`, one of the registered *kinds*.
- The kinds live in `src/source/`. `register_builtin_sources()` fills the registry explicitly --
there is no static-initialiser registration, which a linker may drop -- and a future `dlopen`
loader would only call `register_kind()`.
- A `DataSource` delivers into `Acquisition` (a `SourceSink`) from inside `poll()`. `Frame` is
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
`src/source/udps_*.cpp` (`grep -rn "udps_" src` outside those files is a leak of the old coupling).
- 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 `side_stack`, named `source-<id>`. A disconnected source
leaves them **insensitive, not hidden** (`apply_source_panels` in `src/ui_source.cpp`).
- Session file naming for a non-UDPS kind: the pseudo-host is `<kind>-<identity>` (characters outside
`[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.
### Data path
`ScopeModel` holds **one `BufferStore`** (`live()`): one shared time-base ring plus one
+6 -1
View File
@@ -13,8 +13,11 @@ cmake --build build
./build/udpscope --server 127.0.0.1 --port 8000
./build/udpscope --server 10.0.0.5 --port 8000 --multicast 239.1.2.3 --interface 10.0.0.9
./build/udpscope --source udps --source-opt server=127.0.0.1 --source-opt port=8000 # same as --server/--port
```
`--help` lists the registered source kinds.
The app can be launched from the shell rather than only from a terminal, but only on request:
```sh
@@ -96,7 +99,9 @@ prefix-wide target *is* that rule, reached through the `Help` component
(`cmake --install --component Help`) — and the ordinary `cmake --install` still installs every
component, so its behaviour is unchanged.
Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb` (per
Options: `--source NAME` and the repeatable `--source-opt k=v` (the source kind and its settings;
`--server`, `--port`, `--multicast` and `--interface` populate the UDPS entries of the same options),
`--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb` (per
buffer, default 200), `--max-window`, `--holdoff`, `--recv-buffer`, `--plots` (the row
count to *start* with, 1..`RowSet::kMaxRows`; the layout editor owns it afterwards),
`--capture-packets N` (the raw-datagram ring of README's "Packet capture"; 1..100000,
+35 -2
View File
@@ -1824,6 +1824,38 @@ are unrealized and measure 0x0.
- `rows` is the measurement count and `accums` the accumulator count; the two must agree, because one
accumulator per measurement row is what makes `advanced` a statement about every row.
## `[source-kinds]`
```
[source-kinds] kinds=udps,probe
[source-kinds] form udps: 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
[source-kinds] capture button: present=1 sensitive=0
[source-kinds] command: before=0 after=1 drained=1
```
`UDPSCOPE_PROBE_SOURCE_KINDS=2000`, run as `--source probe --source-opt rate=500`, is the one reading for the
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()`,
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.
- `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
act on the kind's own widget, not the scroller around it).
- `capture button: present=1 sensitive=0`: a source with no datagrams leaves the capture UI **disabled, not
hidden.** `sensitive=1` is the button live for a source that has nothing to capture, `present=0` a hidden one.
- `command: before=0 after=1 drained=1`: the command reached the producer thread through `post()`.
`drained=0` is a queue that `poll()` does not drain on some path; `after` greater than `before + 1` is a
command run twice.
The UDPS run has no `source-` page in `side_stack`, because the UDPS kind has no panels.
## `[prefs-probe]`
```
@@ -1901,14 +1933,15 @@ copy and never writes is exactly the failure this has to catch.
```
[prefs-probe] mnemonics=4/4
[sources] form mnemonics=8/8
[sources] form mnemonics=9/9
```
How many of the dialog's rows have `use-underline` set -- the property that makes a `_` in a row's title a
mnemonic. It is FALSE on `AdwPreferencesRow` by default, which is why these titles were drawn with a
literal underscore; anything below the full count is that bug back. Read by property name rather than
through one row type, for the reason in `invariants-gtk-and-chrome.md`. The form's line appears only when
`UDPSCOPE_PROBE_NEWHOST` drives the form.
`UDPSCOPE_PROBE_NEWHOST` drives the form. It reads 9/9 since the chooser gained its Source type row, which has a
mnemonic of its own (a non-UDPS kind's form has fewer rows: the `probe` kind reads 6/6).
## `trigger panel:`
+7 -1
View File
@@ -13,7 +13,9 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
./build/udpscope --server 127.0.0.1 --port 8000
```
Two baselines every run is read against. A clean build emits **three** warnings and no more: two
Two baselines every run is read against. The UDPS runs must be **unchanged by the source-plugin work**:
a UDPS self-check, number-normalised (`sed 's/[0-9.]\+/N/g'`), diffs empty against one taken before it.
A clean build emits **three** warnings and no more: two
`-Wmissing-field-initializers` on `MathNode::source` and one `-Woverlength-strings` on the icons'
generated GResource `.c`, whose blob is **12352** against C99's 4095-byte string-literal limit with
the seven action icons and the two app icons that remain (a note about a generated file, which GCC
@@ -418,6 +420,10 @@ precedent, not an exception.
defect where the accumulation is gated on the visible tab, which would leave a gap in min/max/avg for
exactly as long as the trigger was on screen. It enables *and plots* its own signal, so it needs no other
probe ahead of it. The reading's own entry is in `probe-readings.md`.
- `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`.
- `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=`.