Files
udpscope/CLAUDE.md
T
Martino Ferrari 22e7a8efcc measure: the split's own means are back as above/below/between
`top`, `bottom` and `amplitude` became the *flat levels* in e83ad86 -- the level the
signal dwells at, read from the density of the window's buckets -- and that change kept
the split's own two means as its internal fallback for a window the pyramid cannot
summarize. Those means are now three types of their own, so the two different questions
about one split can be asked side by side: `above %` and `below %` are the plain mean of
the samples on one side of a share -- the reading the old `top`/`bottom` gave -- and
`between % %` is the mean of the band between two shares, which no single split can
answer, because a band's two edges are two thresholds.

Nothing about the arithmetic is new, and nothing is a new pass. `Above`/`Below` are the
`sum[]`/`count[]` split `measure_accumulate()` already folds -- the very figures
`Top`/`Bottom` fall back to -- and `Between` is the **difference of two levels' upper
sides**, both of which come out of one pass two over one window. It rides the bucket
fast path unchanged: a bucket wholly inside the band is strictly above the lower level
and strictly below the upper one, so both accumulators fold it whole, and only the
buckets that straddle either edge are opened. `measure_level_pcts()` is the one place the
shares are read, and it clamps both and puts `between`'s pair **in order**, so "between
80 % and 20 %" is the band between 20 % and 80 % -- normalized rather than refused,
because the shares name two edges rather than a direction. Nothing to measure reads `--`,
as for every other type: a share at 100 % has no sample above it, and a band can hold
none at all.

Measured, `[bandavg-probe]`, fast mock at a 2 s window (`--rate 1300000 --batch 1000
--freq 1`):

  sine   `above 50` 0.636620 (2/pi), `below 50` -0.636620, `between 50 75` 0.255873
  square `above 50` 0.986496 (30 harmonics) / 1.000000 (--square-harmonics 0),
         `between 50 75` 0.302744 / --
  ramp   `between 50 75` 0.250000 and `above 50` 0.500000, `above 100` --

The first three are the figures the old `top`/`bottom` documented; the ramp's are what a
uniform density predicts (a band's mean is its own centre); the ideal square's empty band
is the nothing-to-measure rule. The pane offers the types and edits their percentages
through the dialog's second share -- which only `between` shows -- and a measurement's
pair survives add/edit/delete and the session file (`threshold2`), where a file written
before it existed loads at the default.

No raw pass: with the measurements probe adding one of every type at a 2 s window, `raw`
went from 113966 of 240719031 covered (0.0%) to 134978 of 255372033 (0.1%), 0.8 -> 1.1
ns a covered sample.

Docs: the band-average invariant (and the two assertions the flat-level change moved),
the probe readings for the new probe, the verification recipe, the manual's measurements
page and the README.
2026-10-15 07:56:37 +02:00

5.5 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

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 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, user-defined math channels built in a node editor, and an export of the visible window to .npy/.csv.

Two documents bound the work and are not interchangeable with this one:

  • SPECS.md is the requirement document — what was asked for.
  • README.md documents what was actually built, including deliberate deviations from the spec and known limitations. Read it before concluding something is a bug.

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 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 self-check, driven by environment variables under xvfb-run against a mock source. Extend the probe/dump in src/ui.cpp rather than adding a separate harness.
  • Every source file carries an SPDX MIT header; match it in new files.

Table of contents

The detail lives in docs/claude/. Read the files that touch the area you are editing before you edit it — most of their contents are rules that cost a defect to learn.

Working in the repository

File What it covers
build-and-run.md Configuring and building, the desktop-entry target, the command-line options, and the two vendored dependencies (deps/plotter, deps/udpsclient) including what was changed upstream and why.
verification.md How to run the headless self-check, the two baselines every run is read against (exactly three build warnings; no Gtk-CRITICAL), and the environment variable that schedules each probe.
probe-readings.md The per-line reference for the self-check's output: what every probe and dump line means, the value it must read, and the defect a wrong reading names.

How the application is put together

File What it covers
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).

Invariants worth knowing before editing

File What it covers
invariants-plot-and-layout.md What plot::Figure/plot::Plot can and cannot be asked to do, what a row's contents belong to, the layout editor's overlay children, and the rule that layout must not depend on content.
invariants-readouts-and-cursors.md A readout quotes a stored sample and never a value between two; how the cursors are read back from the plots; the one formatter both panes share.
invariants-data-path.md What rebuilds the buffer and drops history, why decimation at ingest is an average and never a stride, and the two rules the f64 time ring and its cached first stamp impose.
invariants-rendering.md How the renderer maps a visible range onto samples (never the dx() grid), what gather() emits and relative to which origin, why every series needs a real MinMaxPyramid, and why a dense trace is drawn one pixel column at a time.
invariants-trigger-and-pinning.md The event window counted in stream seconds end to end, what pins an axis in each mode, and what each pin is measured from (the snapshot's own stamps, not the request).
invariants-time-axis-and-window.md No plot interaction changes the time scale; apply_time_window() is the only path that does; how the x labels pick their form; what a fit restores.
invariants-measurements.md Why the pane is refreshed on a cadence, why neither pass may read its window raw, how a crossing is counted with hysteresis, and which mean over a split is which.
invariants-math-channels.md A math channel is a channel; the engine runs on the producer thread, so a regression there costs packets and not frames; the node editor's own rules.
invariants-session-and-export.md What a session file persists and in what order, when a save is refused, and when an export is collected.
invariants-gtk-and-chrome.md The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the six bundled icons and their authoring rules, and how the Mallard manual is opened.