Files
udpscope/help/C/org.udpscope.app/measurements.page
Martino Ferrari bdf0ff7ddc scope: full-screen figures, second windows, gated measurements, per-configuration events
A batch of view and instrument work, with the probes and docs for each.

- **Full screen per figure row**: a toggle on each row's top-right corner, revealed by
  pointer proximity (`kMaximizeRevealPx` around its placed rect, per row, read from an
  overlay-level motion controller so a revealed button under the pointer cannot flicker);
  the plot menu offers the same as *Full screen this figure* / *Restore the layout*. The
  arrangement is saved and restored through `RowState`, so nothing plotted moves. A layout
  of one figure carries no toggle.
- **Second window**: `open_scope_window()` copies a figure (cells, width shares, signals in
  their colours) or opens an empty one, with its own `RowSet` and `PlotArea`; the main
  window's tick binds, acquires and repaints it, mirroring the time window and pin. Entries:
  the plot menu's *Open this plot in a new window* and the overflow menu's *New scope window*.
- **Layout editor from the plot**: the plot menu gains *Edit the layout...* / *Done editing
  the layout*, flipping the app's one toggle.
- **Measurements gate**: `MeasureOptions::gate` restricts a measurement to the held/pause
  snapshot (live falls back to the live ring), persisted with the session and shown in the
  measure dialog.
- **Crosshair axis badges**: a preference (`crosshair_axis_labels`, on by default) and the
  plotter's badges where the lines meet the axes, in the axes' own formats; the plot menu
  and the probe read them.
- **Event cache per configuration**: `EventStore` is keyed `<source>/<configuration>/`,
  `retarget_event_store()` follows every configuration switch, and a pre-configuration cache
  (loose files) is adopted into the configuration's folder on first start.
- **GTK4 lifecycle**: the main window's handlers move from the non-existent `"destroy"`
  signal to `close-request`; scope windows are closed with it and the application quits
  explicitly (the exit run's hang). The exit probe closes with `gtk_window_close()`.
- Probes: `[maximize-probe]` (single-figure rule and the corner reveal), `[scope-probe]`,
  `[split-probe]` cell crosshair leg, `[prefs-probe]` store keying, `[configs]` store key,
  `[rowmenu]` layout-editor entry; docs, help pages, README and AGENT.md updated.

Also fixes a probe-only use-after-free: `toggle_maximize_figure_row` replaces the figure
object, so the area must be re-pointed (the button's path does; the probe's raw call did not).
2026-11-05 17:58:59 +01:00

190 lines
13 KiB
Plaintext

<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="measurements">
<info>
<link type="guide" xref="index#analysis"/>
<link type="seealso" xref="cursors"/>
<desc>A measurement is a statistic of one signal over the window the pane is about — the
visible range, or the frozen frame while a view is pinned — with optional
on-plot marks and a per-measurement statistics line.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Measurements</title>
<p>A measurement is a statistic of one signal over the window the pane is about, recomputed as
that window moves. The pane lists them with their values; each can also draw its result on
the plot.</p>
<p>That window is the <em>visible range</em> — so on a live view the numbers follow what you
pan and zoom to. On a <em>still frame</em> — a held event, a paused stream, an opened file — it
is the frame's own extent instead, so panning or zooming inside it changes nothing: the data
being measured has not changed and you are only looking closer.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Press <key>m</key>, or <gui>Edit</gui> beside <gui>Measurements</gui> in the dock. Either opens the
<gui>Measurements</gui> page over the plot; the dock itself shows each measurement's value,
and a click on one opens its dialog.</p>
</item>
<item>
<p>The page's <gui>+</gui> adds one; clicking a row opens the dialog where the signal,
the type, the threshold share or shares and the on-plot marks are set, with
<gui>Save</gui> and <gui>Delete</gui>.</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The types are average, top, bottom, min, max, peak to peak, amplitude, above,
below, between, pulse duration, period and frequency. <gui>top</gui> and <gui>bottom</gui> are the waveform's
<em>flat</em> high and low levels: the level the signal dwells at, taken from the densest
band of the window on each side of the split. A band-limited edge overshoots and rings —
a nominal 2 V square reaches about ±1.18 at its edges — and a ripple or an overshoot is
therefore <em>not</em> what these read. <gui>amplitude</gui> is the delta between those two
levels, and <gui>peak to peak</gui> is the swing between the extremes (so on a rippled
square <gui>amplitude</gui> reads 2 V where <gui>peak to peak</gui> reads 2.36 V).</p></item>
<item><p><gui>above</gui>, <gui>below</gui> and <gui>between</gui> are the opposite kind of
number: the plain <em>mean of the samples</em> above a share, below it, or between two
shares. That is a different question from <gui>top</gui>/<gui>bottom</gui> even about the
same split, and it is the older one — on a unit sine <gui>above 50%</gui> reads 0.64 V
(2/π, the mean of a half cycle) where <gui>top</gui> reads 0.96 V (near the peak, which is
where a sine dwells most), and on a band-limited square <gui>above 50%</gui> reads 0.99 V
where <gui>top</gui> reads the plateau, 1.00 V: the edge's ring is inside the first and not
inside the second. <gui>between</gui> takes <em>two</em> shares — <gui>from %</gui> and
<gui>to %</gui> — and reports the mean of the samples inside that band, which no single
split can answer; the order you type them in does not matter, since a band between 80 % and
20 % is the band between 20 % and 80 %.</p></item>
<item><p>A mean over a side carries whatever is on that side, so these three are the right
numbers when you want the <em>energy</em> or the <em>offset</em> of part of the waveform,
and the wrong ones when you want the level a plateau sits at. Like every other measurement,
one with nothing to measure reads <code>--</code> rather than a made-up number: a share at
100 % has no sample above it, and a band can hold none at all — on an ideal
(non-ringing) square every sample is exactly ±1, so a band between 50 % and 75 % is
empty.</p></item>
<item><p><gui>top</gui> and <gui>bottom</gui> also read <em>near the peak</em> on a
waveform that has no flat part, which is the honest reading rather than a failure: a unit
sine has no plateau, so the densest band is at its peak and these read about ±0.96 — not
the 0.64 a mean over each half would give, and not ±1 either. Only a level the signal
actually dwells at is a level.</p></item>
<item><p>The <em>split</em> says which side a level is read from, so any share between the
two levels reads the same number; it is not a boundary the value depends on. The one case
where it is read directly is a window the scope cannot summarize by level — a window
shorter than a level's own resolution, or one read through <gui>hf</gui> — where
<gui>top</gui>/<gui>bottom</gui> fall back to the mean of each side of the split and
<gui>amplitude</gui> to the window's peak to peak.</p></item>
<item><p>Each row names the <em>channel</em> it measures: its number among the active
channels (1., 2., … the same number the signal's badge in the bar carries), its name, and
the type — <code>1. sine · average</code>. The measurement dialog's signal dropdown
numbers its entries the same way. A channel list that changes (a signal switched off, a
new one enabled) renumbers, because the number is the answer to "which active channel is
this".</p></item>
<item><p>The on-plot marks come in two kinds and the mix is the reading: a <em>level</em>
mark is a ruler at a value — its value written in a small box on the ruler, the way the
axis writes its own ticks — and a <em>time</em> mark is a marker at a position, with the
position's own badge beside it. A level ruler's badge carries its number; a time
marker's carries its time. Marks whose badges meet share one panel with one line each,
so a column of close readings is one boxed readout rather than a smear of texts; and a
box over a noisy trace is what keeps the number readable. An average
draws one level mark; a max draws a level mark <em>and</em> a time mark at the first
sample that reached it; <gui>peak to peak</gui> draws a ruler at each extreme and
<gui>amplitude</gui> a ruler at each of its two flat levels — the pair is the span, so
the two rulers <em>are</em> the measurement.</p></item>
<item><p>Marks cannot be dragged. A measurement mark says what the measurement measured,
so it is created unselectable — as is the trigger marker.</p></item>
<item><p>A pass is throttled to a cadence rather than run every frame, so a ruler over a
live view is <em>stepped</em>. The number and the ruler come from the same pass, so they
always agree.</p></item>
<item><p>An invalid pass keeps its <em>level</em> marks and drops its <em>time</em>
marks. A level is a property of the signal and outlives the window; a position inside
the window does not. That is what stops the rulers blinking off at a mode change.</p></item>
<item><p>A crossing is counted with <em>hysteresis</em>, not on a bare level: the
thresholds are a tenth of the window's peak to peak either side of the share, and an
edge is counted only once the signal has passed the far one. Period, frequency and pulse
duration all depend on it. The mid level is exactly where a waveform moves fastest, so
any interferer faster than the fundamental recrosses a single level several times per
real crossing — a 5 Hz sine with a 500 Hz tone at a tenth of the amplitude reads 39 Hz
without the band and 5 Hz with it. The band holds to an interferer a third of the
waveform's own size.</p></item>
<item><p>The band is clamped to stay inside the data, which is what keeps a threshold set
near an extreme usable: a fixed band there would sit above every sample and the
measurement would read nothing. A share near an extreme still leaves the band little
room, so it rejects less — which is one reason period and frequency offer no share to
set.</p></item>
<item><p>A window with too few crossings reads <code>invalid</code> / <code>--</code>
rather than a plausible invented number. Frequency over a window holding one edge is the
usual case.</p></item>
<item><p><gui>frequency</gui> is reported in Hz and <gui>period</gui>/<gui>pulse
duration</gui> in seconds; every other type carries the channel's own unit. A value is
written with a <em>metric prefix</em> so its digits sit in <code>1..999</code> —
<code>636.62 mV</code>, not <code>0.64 V</code> — including when the stream declares no
unit at all, which is always the case for a math channel: there the prefix goes tight
against the number (<code>636.62m</code>), since two decimals of an unprefixed small
value is <code>0.00</code>.</p></item>
<item><p>Cost is bounded by reading the summary pyramid the producer already maintains,
not the raw window: a bucket's sum is a sum, so the mean, the extremes and a level's own
mean are exact. <gui>top</gui>, <gui>bottom</gui> and <gui>amplitude</gui> read the
<em>density</em> of the window out of that same summary — which is what tells a flat level
from a ripple — so those three cost a second read of the summary and never of a sample, and
<gui>between</gui>'s second share is one more level in the same summary walk rather than a
walk of its own.</p></item>
<item><p><gui>Statistics</gui> adds a second line to every measurement showing
<code>n</code>, the mean, the standard deviation and the range. Each observation is one
<em>recompute</em> of the pane, so <code>n</code> counts windows measured, not samples —
a mean frequency over many windows is what that claims to be. <gui>Reset</gui> clears
every accumulator and starts fresh. A change of display window, a CONFIG or a
measurement edit also resets; a pause, a resume, a zoom, a pan and the toggle itself do
not, so turning the line off to read the plot and back on again keeps the history. On a
still frame a pan or a zoom changes nothing for the history to record in the first place —
the values do not move there at all.</p></item>
</list>
</section>
<section id="gate">
<title>The gate</title>
<p>A measurement's dialog carries <gui>Gate enabled</gui>. Every measurement
with it set reads only the samples inside one shared band — violet, labelled <em>gate</em>,
carried by its own two draggable edges on the plots, which are not the cursors (those keep
meaning what the cursors pane reads). Measurements without it are untouched: they read the
whole window and share the walks they always shared.</p>
<p>The band is shown exactly while some measurement is gated — gate one and it appears,
gate or delete the last one and it goes — because it is one area that all of the gated
measurements share, not a property of any one of them. Drag an edge to resize it, grab the
middle to move it.</p>
<p>The band lives in the same relative time the axis labels use, so it stays where it was
put in both modes: live, a band at 300 ms to 100 ms ago stays put on screen while the data
streams beneath it — the gated read of the newest data a bench scope gives — and on a held
event it names a fixed slice of the frozen frame. A band with no samples in it — dragged
outside the view, or over a gap — reads <code>--</code> like any empty window. The band's
position and each measurement's gate flag are saved with the session.</p>
</section>
<section id="example">
<title>Example</title>
<p>On a 1 Hz ±1 sine: <code>average=0</code>, <code>top=0.957988</code>,
<code>bottom=-0.957988</code> (the flat levels, near the peaks — a sine has no plateau),
<code>max=1</code>, <code>min=-1</code>,
<code>peak to peak=2</code>, <code>amplitude=1.915983</code>, <code>pulse duration=0.5 s</code>
(duty 0.5), <code>period=1 s</code>, <code>frequency=1 Hz</code>. On the same stream's
band-limited <code>square</code>, whose edges ring out to ±1.179: <code>top=0.999983</code>,
<code>bottom=-0.999983</code>, <code>amplitude=1.999966</code> against
<code>peak to peak=2.358701</code> — the levels are the plateau's and the extremes are the
edges'. Over a 2 M-sample window those passes read about 0.1% of the samples.</p>
<p>The mean-over-a-side types on the same two windows: on the sine
<code>above 50=0.636620</code>, <code>below 50=-0.636620</code> and
<code>between 50 75=0.255873</code>; on the rippled square <code>above 50=0.986496</code> and
<code>between 50 75=0.302744</code>, against <code>1.000000</code> for the same
<code>above 50</code> on an ideal (non-ringing) square. A <code>ramp</code>, whose density is
uniform, is the shape that names what a band mean <em>is</em>: <code>above 50=0.500000</code>
and <code>between 50 75=0.250000</code>, each band's own centre.</p>
</section>
</page>