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

+4 -3
View File
@@ -5,7 +5,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What this project is ## What this project is
udpscope is a **GTK4 oscilloscope-style viewer for a UDPStreamer source**. It receives one udpscope is a **GTK4 oscilloscope-style viewer for a UDPStreamer source**. It receives one
UDP stream with `deps/udpsclient`, buffers it in a large ring (default 200 MB), and draws it data source -- a UDPStreamer stream through `deps/udpsclient` by default, or another registered
source kind (see `docs/claude/architecture.md`) -- buffers it in a large ring (default 200 MB), and draws it
with `deps/plotter` — a figure of up to eight stacked rows sharing one x window. On top of with `deps/plotter` — a figure of up to eight stacked rows sharing one x window. On top of
the live view it provides the things a bench scope provides: a trigger with a pre/post event the live view it provides the things a bench scope provides: a trigger with a pre/post event
window, a pause that freezes the visible window, cursors, statistical measurements, window, a pause that freezes the visible window, cursors, statistical measurements,
@@ -20,7 +21,7 @@ Two documents bound the work and are **not** interchangeable with this one:
Three things shape almost every change here, and the linked files expand on each: Three things shape almost every change here, and the linked files expand on each:
- **There are exactly two threads** — a producer that owns the UDP client and the GTK main - **There are exactly two threads** — a producer that owns the data source and the GTK main
thread that owns every widget and the GL context — and they share `ScopeModel` under one thread that owns every widget and the GL context — and they share `ScopeModel` under one
mutex. Samples cross through lock-free rings, never through the lock. mutex. Samples cross through lock-free rings, never through the lock.
- **There is no unit test suite.** The only automated check is the application's own headless - **There is no unit test suite.** The only automated check is the application's own headless
@@ -45,7 +46,7 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e
| File | What it covers | | File | What it covers |
| --- | --- | | --- | --- |
| [architecture.md](docs/claude/architecture.md) | The two threads and what they share; the one `BufferStore` and the four display modes (`Live`, `Capturing`, `Held`, `Waiting`); the display clock and `stream_at_x0()`; the layers to keep straight (`SignalInfo` → `ChannelBuffer` → `SlotSeries` → `RowSet`). | | [architecture.md](docs/claude/architecture.md) | The two threads and what they share; the one `BufferStore` and the four display modes (`Live`, `Capturing`, `Held`, `Waiting`); the display clock and `stream_at_x0()`; the layers to keep straight (`SignalInfo` → `ChannelBuffer` → `SlotSeries` → `RowSet`); the source plugins. |
### Invariants worth knowing before editing ### Invariants worth knowing before editing
+20 -2
View File
@@ -27,6 +27,7 @@ Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb`
window the toolbar offers, default 600), `--holdoff` (default trigger hold-off), window the toolbar offers, default 600), `--holdoff` (default trigger hold-off),
`--recv-buffer`, `--plots` (the number of stacked plots to start with, 1..8; rows are `--recv-buffer`, `--plots` (the number of stacked plots to start with, 1..8; rows are
added and removed from the **Layout editor** afterwards), `--relative-time` (see below), added and removed from the **Layout editor** afterwards), `--relative-time` (see below),
`--source NAME` and the repeatable `--source-opt k=v` (the source kind and its settings; see "Data sources" below),
`--capture-packets N` (see "Packet capture" below), `--renderer NAME` (GTK's renderer: `--capture-packets N` (see "Packet capture" below), `--renderer NAME` (GTK's renderer:
`cairo`, `gl` or `vulkan`; `cairo` is the software preset that exports both `cairo`, `gl` or `vulkan`; `cairo` is the software preset that exports both
`GSK_RENDERER` and `LIBGL_ALWAYS_SOFTWARE` before GTK initialises — see the SSH `GSK_RENDERER` and `LIBGL_ALWAYS_SOFTWARE` before GTK initialises — see the SSH
@@ -98,13 +99,30 @@ src/event_series.hpp an immutable captured event as a plot::data::Series
src/ui.* the GTK4 window: toolbar, trigger panel, signal panel, plot box src/ui.* the GTK4 window: toolbar, trigger panel, signal panel, plot box
``` ```
Threading: one producer thread owns the `udps_client_t` and calls Threading: one producer thread owns the data source (the UDPStreamer client is the built-in
`udps_client_poll()`; the GTK main thread owns every widget and the GL contexts. one) and calls `poll()`; the GTK main thread owns every widget and the GL contexts.
They share `ScopeModel` under one mutex, which the producer takes once per DATA They share `ScopeModel` under one mutex, which the producer takes once per DATA
frame and the GUI thread takes once per frame around binding the traces. frame and the GUI thread takes once per frame around binding the traces.
Samples cross the boundary through `plot::data::RingBuffer` (single producer, Samples cross the boundary through `plot::data::RingBuffer` (single producer,
single consumer), never through the lock. single consumer), never through the lock.
## Data sources
What udpscope shows is fed by a *source kind*; the connection chooser's **Source type** row picks one and
swaps in that kind's own connection form. The built-in kind is **UDPS**, the UDPStreamer client this
document describes everywhere else, and it is the default: `--server`, `--port`, `--multicast` and
`--interface` populate its entries, and the same thing is spelled `--source udps --source-opt server=...`.
`--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.
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
timebase, so it will change the frame and the data path together rather than fit the present single-timebase
frame.
## The x axis is relative to now ## The x axis is relative to now
**A live axis is fixed at `[-window, 0]`, with the newest sample at 0.** That is what a bench **A live axis is fixed at `[-window, 0]`, with the newest sample at 0.** That is what a bench
+24 -2
View File
@@ -10,8 +10,8 @@ concluding something is a bug.
### Threads ### Threads
Exactly two: a producer thread in `Acquisition` owns `udps_client_t` and calls Exactly two: a producer thread in `Acquisition` owns a `DataSource` and calls
`udps_client_poll()`; the GTK main thread owns every widget and GL context. They share `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 `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()`. 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 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 before `trigger_pending_` and those two carry release/acquire, so a consumer that sees a
capture open sees which edge it belongs to. 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 ### Data path
`ScopeModel` holds **one `BufferStore`** (`live()`): one shared time-base ring plus one `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 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 --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: The app can be launched from the shell rather than only from a terminal, but only on request:
```sh ```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 (`cmake --install --component Help`) — and the ordinary `cmake --install` still installs every
component, so its behaviour is unchanged. 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 buffer, default 200), `--max-window`, `--holdoff`, `--recv-buffer`, `--plots` (the row
count to *start* with, 1..`RowSet::kMaxRows`; the layout editor owns it afterwards), 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, `--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 - `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. 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]` ## `[prefs-probe]`
``` ```
@@ -1901,14 +1933,15 @@ copy and never writes is exactly the failure this has to catch.
``` ```
[prefs-probe] mnemonics=4/4 [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 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 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 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 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:` ## `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 ./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' `-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 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 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 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 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`. 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 - `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 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=`. hit 0. Reports the resulting `mode=`, `axis=` and `marks placed=`.