Feature/source plugins #1
No files matched your search
@@ -5,7 +5,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## What this project is
|
||||
|
||||
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
|
||||
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,
|
||||
@@ -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:
|
||||
|
||||
- **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
|
||||
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
|
||||
@@ -45,7 +46,7 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e
|
||||
|
||||
| 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
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb`
|
||||
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
|
||||
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:
|
||||
`cairo`, `gl` or `vulkan`; `cairo` is the software preset that exports both
|
||||
`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
|
||||
```
|
||||
|
||||
Threading: one producer thread owns the `udps_client_t` and calls
|
||||
`udps_client_poll()`; the GTK main thread owns every widget and the GL contexts.
|
||||
Threading: one producer thread owns the data source (the UDPStreamer client is the built-in
|
||||
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
|
||||
frame and the GUI thread takes once per frame around binding the traces.
|
||||
Samples cross the boundary through `plot::data::RingBuffer` (single producer,
|
||||
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
|
||||
|
||||
**A live axis is fixed at `[-window, 0]`, with the newest sample at 0.** That is what a bench
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:`
|
||||
|
||||
|
||||
@@ -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=`.
|
||||
|
||||
Reference in new issue
Block a user