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
|
## 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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:`
|
||||||
|
|
||||||
|
|||||||
@@ -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=`.
|
||||||
|
|||||||
Reference in new issue
Block a user