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).
This commit is contained in:
1 parent
bfe13630f9
commit
bdf0ff7ddc
39 files changed
+2267
-136
No files matched your search
@@ -9,6 +9,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
tick as the channel dump; the status line only formats what is already there. The
|
||||
badge's markup is likewise rewritten on a change of phase (twice a second), never on
|
||||
all sixty frames.
|
||||
- **The trigger circle is the badge's sibling, in its alphabet and under its rules.** It sits
|
||||
before the badge on the status row (`main-window.ui` `trigger_status`) and says what the
|
||||
badge cannot: the *trigger's* state -- solid green while an event is held or a capture is
|
||||
filling, flashing green (the lit and dim halves) while armed and waiting, red while the
|
||||
trigger mode is on but disarmed, and **hidden while the trigger mode is off**, because a
|
||||
light for a mode the user is not in is noise and the mode text beside it shows no trigger
|
||||
words then either. Same width-locked glyph in every state, same half-second square for the
|
||||
flash, markup rewritten on a change of the state key only (`trigger_circle_state`), and the
|
||||
exit dump reads the tick's cached answer (`dbg_trigger_circle`) because it cannot ask the
|
||||
widget.
|
||||
- **A `notify::<prop>` handler takes three parameters, a `value-changed` handler two.**
|
||||
`GObject::notify` passes `(GObject*, GParamSpec*, gpointer)` while
|
||||
`GtkSpinButton::value-changed` passes `(GtkSpinButton*, gpointer)`, so one callback cannot
|
||||
@@ -722,3 +732,17 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
|
||||
box drawn from that is a box over the wrong thing. Its numbers depend on the *window's* size, which
|
||||
under the documented `xvfb-run` is the window's 756x894 minimum (a 640x480 screen), so a run on a
|
||||
larger screen gives different rectangles for the same panels.
|
||||
|
||||
- **GTK4 has no `GtkWidget::destroy` signal; the lifecycle hook is `close-request`, and
|
||||
`gtk_window_destroy()` never emits it.** A `g_signal_connect(window, "destroy", ...)` on a GTK4
|
||||
widget is a connect to a signal that does not exist: it never fires (and its "signal not found"
|
||||
warning is in the GObject domain, not GTK's, so the dump's `gtk warnings:` counter cannot see
|
||||
it). That is why the dialogs use `gtk_window_close()` when their *lifetime* matters (a close
|
||||
clears `App::measure_dialog`, which a destroy would not), and why the main window's own handler
|
||||
-- the scope windows' teardown, the debounced layout-save flush and the application quit -- is
|
||||
connected to **`close-request`** and returns `FALSE` so the default destroy still runs.
|
||||
Everything that ends the main window programmatically (the exit probe's `exit_probe_step3b`)
|
||||
closes it with `gtk_window_close()` for the same reason. The **scope windows go with it**:
|
||||
each is a `GtkWindow` added to the application, and one left behind keeps the GApplication's
|
||||
main loop alive around a window that is gone -- the hang where the exit run never reached its
|
||||
dump.
|
||||
@@ -70,6 +70,47 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
compares with a relative tolerance, because clamping to exactly `window_s_` can leave the
|
||||
group's span an ULP above it and a clamp that fires every frame is a re-frame loop, not a
|
||||
clamp.
|
||||
- **A row names its channel: number and signal, from one resolver.** The pane's row reads
|
||||
`N. signal · type`, the dock's and the strip's lines prefix the same number to the type,
|
||||
the signal's badge carries it, and the measurement dialog's signal dropdown numbers its
|
||||
entries with it -- all through `measure_channel_number()`, which is the position of the
|
||||
signal's channel in the model's channel list (the acquired channels in stream order), or 0
|
||||
when the signal is not acquired. The readouts rule cuts both ways here: every view shows the
|
||||
same number because every view asks the one function, and a view that derived a number by
|
||||
its own walk of the channel list would be the second spelling that drifts. 0 is a state, not
|
||||
a failure -- the unacquired signal's row is already grey -- so the readers *omit* the number
|
||||
rather than printing a 0. Renumbering on every enable, disable and CONFIG is inherent (the
|
||||
number answers "which active channel is this", which is what a rebuild renumbers), so a
|
||||
generation change rebuilds the pane as well as the signal list.
|
||||
- **The gate is a per-measurement option, and one band shared by everyone who uses it.**
|
||||
`MeasureOptions::gate` is set per measurement in its dialog; every gated measurement reads
|
||||
only inside the one shaded band -- a plotter `Region`, violet, its two bounds the gate's own
|
||||
pair of instruments, not the cursors, which stay what the cursors pane reads -- and the band
|
||||
is *shown exactly while some measurement is gated*, derived at `rebuild_measures_pane()`,
|
||||
because every change of the measure set (an add, an edit, a delete, a session restore)
|
||||
passes through there. Ungated measurements are keyed away from gated ones
|
||||
(`MeasureWindow`'s key is `(channel, fc, gated)`), so a gated measurement costs one more
|
||||
walk and changes no ungated number.
|
||||
The bounds live on the `RowSet` in **display x** (`gate_x0()`/`gate_x1()`), the cursors' own
|
||||
space, and that is the point: a live axis is newest-relative, so a band at `[-0.3, -0.1]`
|
||||
keeps meaning "300 ms to 100 ms ago" while the data flows under it, and a held event's
|
||||
stamps are edge-relative, so there the band names a fixed slice of the frozen frame. Either
|
||||
way it stays where it was put, which is what makes it a gate and not a second window.
|
||||
`update_measures()` clips the pane's window (viewport live, pin pinned) into the band once,
|
||||
and each gated window walks the clip: every statistic, mark and "it moved" booking below
|
||||
describes the gated slice through that one expression. A band entirely off the view clips to
|
||||
a zero span and the pane reads the same `--` any empty window gives. Because a band that
|
||||
moves can change what the *gated* measurements describe without the viewport or pin moving,
|
||||
the "it moved" test asks the band as well (`measure_gate_window`, booked every pass). While
|
||||
a region drag runs, the tick adopts the bounds out of the dragged row's plot
|
||||
(`sync_gate(drag_row)` -- the `sync_cursors()` contract: read only while a gesture holds the
|
||||
instrument, because outside one the read is our own echo) and mirrors them to every other
|
||||
row; the drag's *end* saves the session, because the bounds are session state
|
||||
(`gate_x0`/`gate_x1`, and each measure's `gate` flag in its `[[measure]]` section) and a
|
||||
drag that is not saved is a session that lies about where the gate is. `[gate-probe]` is the
|
||||
reading: the gated average differs from the ungated one on the same signal, the booked gate
|
||||
window is the band moved onto the stream's clock, the band is still where it was put a
|
||||
second later while `s0=` has moved under it, and a restored session brings both back.
|
||||
- **A measurement must not read its window raw.** Both passes go through
|
||||
`plot::data::MinMaxPyramid`, which the producer already maintains at amortised O(1) -- so the
|
||||
pane inherits the summary of the new samples rather than computing one, which is what the
|
||||
|
||||
@@ -119,8 +119,11 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
a split one -- never a flat index, which is 0-based and says nothing about where the plot is.
|
||||
What the cells share and what each keeps:
|
||||
- **One time window.** Every cell of every row is a member of the one x link group, so a pan or zoom
|
||||
in any cell moves them all, the crosshair is one x drawn in every cell (the hovered cell shows its
|
||||
y), and the **vertical cursor pair is one shared time drawn in every cell** (`cursors_aligned`).
|
||||
in any cell moves them all, the crosshair is one x drawn in every cell, and the **vertical cursor
|
||||
pair is one shared time drawn in every cell** (`cursors_aligned`). The horizontal crosshair line is
|
||||
the hovered cell's y **drawn in every cell of its figure row** -- the cells share that y, so the
|
||||
line is one value spanning the row rather than stopping at the divider; no other row gets one
|
||||
(`Figure::set_crosshair` spreads it, `[split-probe] cell-xhair` reads it).
|
||||
A split is an arrangement of more plots, not a second time scale: nothing about the time axis changes
|
||||
(see [invariants-time-axis-and-window.md](invariants-time-axis-and-window.md)).
|
||||
- **One y per figure row.** The cells of a figure row share one y range (the plotter's
|
||||
@@ -340,3 +343,33 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
relayout, the panels' chips, the layout save) are done once by the caller rather than per slot. Re-enabling
|
||||
does **not** put the traces back: the arrangement is the user's, and guessing where they were is worse than
|
||||
leaving them off.
|
||||
|
||||
- **The full-screen toggle and a scope window are *views*: neither edits the arrangement nor takes a second
|
||||
acquisition.** The per-row toggle (`RowSet::toggle_maximize_figure_row()`) saves every row's persistent
|
||||
state (`RowState` -- the fields `carry_row()` copies, because a `Row` is not copy-assignable: its
|
||||
`SlotSeries` owns a `unique_ptr`) plus the cells, the weights and the plot count, rebuilds the figure with
|
||||
the asked-for row's plots moved to the front, and puts every field back on the second click; it saves
|
||||
nothing, so a reopened session has the arrangement, not the view. Its overlay button is on screen
|
||||
*always* (`App::maximize_buttons`, a set of its own -- `layout_buttons` is the editor's and is cleared with
|
||||
it), placed from the same `FigureLayout` rects as the editor's buttons. A scope window
|
||||
(`App::ScopeWindow`, `open_scope_window()`) owns a `RowSet`, a `PlotArea` and the area's placeholder
|
||||
`Plot`, and is driven by the **main window's tick**: the same model mutex binds and acquires its rows
|
||||
(same display mode, same epoch), and its span follows the main window's window or pin every frame. It is
|
||||
therefore a second *view* -- the settle rule is that a scope must never acquire, plan or epoch anything of
|
||||
its own. Closing the main window destroys its scopes first: a leftover toplevel keeps the GApplication's
|
||||
loop alive around a window that is gone (the exit run's hang).
|
||||
|
||||
- **A figure row's full-screen is one click from two places, and never from one figure.** The
|
||||
toggle sits on the row's top-right corner, **out of hiding only while the pointer is near that
|
||||
corner**: its own placed rect inflated by `kMaximizeRevealPx`, per row, so approaching one
|
||||
row's corner reveals that row's toggle and no other. The pointer is read from a
|
||||
`GtkEventControllerMotion` on the **overlay**, not on the GL area -- a revealed toggle sits
|
||||
under the pointer, and a leave on the GL widget would fire the moment the button appeared and
|
||||
hide it again (the flicker the ancestor avoids); a leave on the overlay means the pointer left
|
||||
the plot, which is when the toggles go away. The plot menu offers the same action as *Full
|
||||
screen this figure* / *Restore the layout*;
|
||||
both end in the same `toggle_fullscreen()` -- the `RowState` save/restore, the area's figure,
|
||||
the toggles and the signal panels -- so the two cannot drift apart. A layout of a single
|
||||
figure row carries **no** toggle (and no menu entry): there is nothing to enlarge *to*, and a
|
||||
visible no-op is worse than none. The button reappears while a row *is* maximized, because
|
||||
there it is the way back.
|
||||
@@ -118,3 +118,15 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
marker says where the scope put the edge and a measurement mark says what a measurement measured;
|
||||
neither is something a user may move. The dump prints `readout instruments locked: N`, which must
|
||||
be 1 -- a drag is not something a probe can perform, so the state is what is asserted.
|
||||
|
||||
- **The crosshair's axis badges are painted *over* the tick labels, and that costs a flush.**
|
||||
A badge is a fill in the crosshair's colour framing the value where the line meets the axis
|
||||
it names — and it sits *in the label strip*, on top of the tick label at that row. The tick
|
||||
labels are staged into `glyphs_over_`, the over-text batch the frame flushes **after every
|
||||
line batch**, so a panel fill drawn at the ordinary moment lands *under* them: the label
|
||||
prints through the badge and the backplate reads as transparent (the defect this rule was
|
||||
learned from). `draw_crosshair_axis_badges()` therefore flushes the accumulated over-text
|
||||
*first* — those labels are already placed, so it is one batch — then draws both fills, then
|
||||
stages the badge texts, which leave with the rest of the over batch over everything. An
|
||||
assertion on badge pixels inside a label's rows is the regression test: with the fill under
|
||||
the text, the label's glyph pixels are still lit inside the badge's rect.
|
||||
@@ -192,3 +192,15 @@ of.
|
||||
|
||||
Nothing is lost by switching: the dialog saves the current session *before* loading the picked
|
||||
one, which is the only ordering in which a switch cannot lose what it switches from.
|
||||
|
||||
- **The event cache is keyed on the source *and its active configuration*, and the key moves
|
||||
with the switch.** An event's trigger, its channel set and its units are the configuration's,
|
||||
so the directory is `<base>/<host>-<port>/<configuration>/` (`EventStore`'s third constructor
|
||||
argument) and `retarget_event_store()` -- called next to every `App::config_name` write and
|
||||
resolving the name from `active_configuration()` when it is empty -- re-derives it, refreshing
|
||||
both views of the list only when the directory changed. A store built without a configuration
|
||||
keeps the legacy `<base>/<source>/` path, and the **first construction under a configuration
|
||||
adopts the loose event files it finds there** (`*.npy`/`*.txt`/`*.bmp`, moved into the
|
||||
configuration's folder): an upgrade keeps the events it had. The key is a *sanitized* path
|
||||
component, the same rule the configuration files use, so a name typed with a slash cannot
|
||||
leave the directory.
|
||||
+155
-23
@@ -305,8 +305,8 @@ Two lines of the self-check output are assertions in disguise:
|
||||
on screen. Both ranges are on the **stream's** clock, which is the space `measure_window` and
|
||||
the time marks are stored in -- a live axis is a fixed `[-window, 0]`, so comparing display x
|
||||
would read no drift whatever the data did. `win` is the window as of the last **tick**
|
||||
(`App::measure_view_window`) and is deliberately not read back here: the dump runs from the
|
||||
destroy handler, by which point the exit probe has switched the trigger on, and
|
||||
(`App::measure_view_window`) and is deliberately not read back here: the dump runs after
|
||||
`g_application_run` returns, by which point the exit probe has switched the trigger on, and
|
||||
`stream_at_x0()` then answers for a mode no frame was ever drawn in -- which read as a 45000 %
|
||||
drift on a run whose every frame was exact. `pin` is the same tick's pin (`App::measure_pin_window`,
|
||||
`off` when nothing was pinned) and it is the **target** while the view is pinned: a measurement
|
||||
@@ -1348,6 +1348,125 @@ mock's five signals and dies with `OSError: [Errno 90] Message too long`, so the
|
||||
two steps to it, which is visible in its `ges/x1`, `ges/x2` lines. A `gave up waiting after N
|
||||
polls` line means no latch (or no pin) was ever reached, and is a reading about the stream.
|
||||
|
||||
## `[gate-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_GATE_MS`. The reading for **what the per-measurement gate does**: the gated
|
||||
measurement reads the shared band, the ungated one the whole window, and the band is
|
||||
*relative* — fixed on screen while the data flows under it, live or held. Measured output
|
||||
(port 8140, `--rate 100000 --batch 100 --freq 1`):
|
||||
|
||||
```
|
||||
[gate-probe] on: gate=[-0.300000..-0.100000] view=[-2.000000..0.000000] s0=2.022990 for=[0.022990..2.022990] band=1 placed=1 plain=1 split=1 gwin=1 ungated=-1.36e-17 V gated=355.31 mV
|
||||
[gate-probe] still: gate=[-0.300000..-0.100000] s0=[2.022990->2.852990] still=1 s0_moved=1
|
||||
```
|
||||
|
||||
- `gate=` is the band, in **display** x; `view=` the axis; `s0=` the stream time of the
|
||||
display's x = 0 (`stream_at_x0()`); `for=` the pane's own window (the ungated one), in
|
||||
stream seconds. The probe measures only once the ring is older than the window (`s0 >= 2`
|
||||
here): the `plain=` assertion is exact zero because a whole-seconds window of the 1 Hz sine
|
||||
holds whole periods, and a young ring would clip it to a fraction of a cycle.
|
||||
- `band=1` — one gated measurement was enough to show the band, because the band is shared by
|
||||
every gated measurement and shown exactly while one exists. `placed=1` — the row's plot
|
||||
region holds exactly the bounds the `RowSet` does: the drawing and the state are one pair of
|
||||
numbers, not two.
|
||||
- `plain=1` — the ungated average is zero (`-1.36e-17 V`): it read the whole window.
|
||||
`split=1` — the gated average differs (`355.31 mV`, wherever in its period the stream
|
||||
happened to be): it read the band. The pair is the per-measurement split on one signal.
|
||||
- `gwin=1` — the pass booked `App::measure_gate_window` as the band moved onto the stream's
|
||||
clock, within 5 % of the band (the probe's `s0` read and the pass's can straddle one
|
||||
production interval). That is the phase-free statement of *which* slice the gated walk
|
||||
read; the slice's mean value itself depends on where in its period the stream is, which is
|
||||
why the assertion is on the window and not on the value.
|
||||
- The second line is the **relative** assertion, one second later: `still=1` — the band
|
||||
exactly where it was put — and `s0_moved=1` — the stream's clock moved under it.
|
||||
`still=0` is a gesture echo adopted outside a drag; `s0_moved=0` on a live stream means the
|
||||
display epoch stopped following, which is the unbound-trace state the probe's assignment
|
||||
exists to prevent.
|
||||
- The exit dump carries the session leg: `gate: on=1 bounds=[-0.300000..-0.100000]`, and a
|
||||
second run on the same config with the probe unset restores both measurements with their
|
||||
gate flags (`measurements: 2 1.sine average=-0.000005 1.sine average=-0.338823` on one
|
||||
restore run -- numbered with the signal's active-channel number, the same one the pane's
|
||||
rows carry) and the same band.
|
||||
|
||||
## `[crosshair-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_CROSSHAIR_MS`. The reading for **the crosshair's axis badges**: a real pointer
|
||||
hover — synthesised through the GTK adapter's own controller, a plain move with no button —
|
||||
must place the crosshair, and with it the two badges the plot writes in the label strips: the
|
||||
y value in the y strip, the x time in the x strip, each inside its strip and outside the axes.
|
||||
Measured output (viewport pixels; the absolute numbers are the headless window's own):
|
||||
|
||||
```
|
||||
[crosshair-probe] active=1 y=[74x47@31,588] x=[59x47@749,1529] y_ok=1 x_ok=1 y_centre=1
|
||||
[crosshair-probe] off: plot_flag=0
|
||||
```
|
||||
|
||||
- `active=1` is the hover having reached the plot (`Crosshair::active`) — the adapter's
|
||||
viewport conversion is what this probe exercises; the pixels themselves are the plotter
|
||||
suite's (`crosshair_ink_stays_inside_the_axes`, which asserts the badges' rects against the
|
||||
frame and pins the off leg too).
|
||||
- `y_ok=1 x_ok=1` are the two placements: the y badge right-aligned inside the y label strip
|
||||
(its right edge on the axis line, where the y ticks end), the x badge inside the x label
|
||||
strip below the axes. Zero-sized badges, or one outside its strip, is the defect.
|
||||
- `y_centre=1` is the y badge **centred on its line**: its middle row is the pointer's screen
|
||||
y (the probe injected one). `0` is the top-aligned badge — the line running along the
|
||||
badge's top edge, reading as if the number belonged to the tick below it — which is half a
|
||||
badge off, well beyond the one pixel of slack.
|
||||
- The second line flips the flag the preference drives (`set_crosshair_axis_labels(false)`)
|
||||
and reads the plot's own flag back: `plot_flag=0` — a preference that reaches the file but
|
||||
not the plots is the failure this leg names.
|
||||
- The pixel-level assertions live in the plotter's suite; this probe is the adapter's half.
|
||||
The preference itself is `[prefs-probe]`'s `xhair=` field, which reads the settings file
|
||||
back from disk after the dialog toggles it.
|
||||
|
||||
## `[maximize-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_MAXIMIZE_MS`. The one-click **full-screen row** toggle, exercised through the
|
||||
`RowSet` call the button's handler makes (a probe cannot click an overlay button the frame
|
||||
places), arranged as two single-cell rows with one signal each. The two reads after it are the
|
||||
**corner reveal**: no pointer over the area, then a pointer position in the first row's corner
|
||||
(set directly -- the probe owns the state the motion controller fills). Measured output:
|
||||
|
||||
```
|
||||
[maximize-probe] single_buttons=0 before: rows=2 frows=2 row0=sine row1=square buttons=2; max: rows=1 frows=1 flag=1 row0=sine; back: rows=2 row0=sine row1=square flag=0
|
||||
[maximize-probe] corner: hidden_when_off=1 shown_when_near=1 other_row=0
|
||||
```
|
||||
|
||||
- `single_buttons=` is the toggle count *before* the second row is added, while the layout is
|
||||
the default one figure: **0** — one figure has nothing to enlarge *to*, so it carries no
|
||||
button (and `buttons=` is what the same count reads once the second row exists).
|
||||
- `before:` is the arrangement as set: two plots, two figure rows, the signals on their rows,
|
||||
and `buttons=` one overlay toggle per figure row (the toggles are on screen whether or not
|
||||
the layout editor is up).
|
||||
- `max:` is the toggle: one plot, one figure row, `flag=1`, and the *maximized row's own*
|
||||
signal (`row0=sine`) — the row that was asked for, not whatever sat first.
|
||||
- `back:` is the toggle again: the arrangement, the plot count and both rows' signals exactly
|
||||
as before, `flag=0`. A signal that changed here is the state save/restore (`RowState` in
|
||||
`plot_host.*`) losing a field; the restore is a view change, never an edit.
|
||||
- `corner:` is the proximity reveal: `hidden_when_off=1` (no pointer in the area, so nothing
|
||||
shows), `shown_when_near=1` (a pointer in the first row's top-right corner brings *that*
|
||||
row's toggle out), and `other_row=0` -- the second row's toggle stays hidden, because the
|
||||
zone is per row. A `shown_when_near=0` names a dead reveal path (the motion hook or the zone);
|
||||
`other_row=1` a zone that leaked across rows.
|
||||
|
||||
## `[scope-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_SCOPE_MS`. The **second window**: `open_scope_window()` twice — once copying
|
||||
the main window's figure 0, once empty — and read back a few frames later (the main tick is the
|
||||
scope's bind/acquire/repaint pump). Measured output:
|
||||
|
||||
```
|
||||
[scope-probe] count=2 copied='sine' bound=1 empty_ok=1 windows=1
|
||||
```
|
||||
|
||||
- `count=2` is both windows having been created and retained; `windows=1` each being a visible
|
||||
top-level window.
|
||||
- `copied='sine'` is the copy's first slot holding the main window's figure-0 signal, and
|
||||
`bound=1` the tick having resolved it to a live channel — the scope is bound to the same
|
||||
model, not a snapshot taken at creation.
|
||||
- `empty_ok=1` is the `-1` call producing an empty row, which is the other entry point the
|
||||
overflow menu uses.
|
||||
|
||||
## `[search-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_SEARCH_MS`. The handler calls `condition_probe()` directly — the condition is
|
||||
@@ -1432,10 +1551,10 @@ popover row reads `failed: <strerror>` — one stderr line, no retry storm again
|
||||
```
|
||||
[defaults] calibrate: 1 0 model=1 0 | trigger: 0.001 0.002 0 0.001 0 50 0.01 0 0 1
|
||||
[defaults] calibrated: 2.5 -1.25 model=2.5 -1.25
|
||||
[configs] before=default active=default
|
||||
[configs] save-as=1 names=probe,default active=probe
|
||||
[configs] before=default active=default store=default
|
||||
[configs] save-as=1 names=probe,default active=probe store=probe
|
||||
[configs] round trip read=1 comment='written by the configs probe' buffer_samples=10000000 relative=0 capture=0 plots=1 signals=4 measures=0 math=0 noise=-1 hf=-1
|
||||
[configs] after forget names=default
|
||||
[configs] after forget names=default active=default store=default
|
||||
```
|
||||
`noise=` and `hf=` are the first measurement's band and corner as they came back from the file. The probe
|
||||
sets them to **7.5 and 250** before saving, so a key that never reached the file reads back as the defaults
|
||||
@@ -1450,16 +1569,17 @@ sets them to **7.5 and 250** before saving, so a key that never reached the file
|
||||
| `[configs] names` | gains `probe`, and loses it after `forget` | a save that writes somewhere the listing does not read, or a delete that leaves the file |
|
||||
| `[configs] active` | `probe` after the save-as | the `active` file not written, so the next start opens the wrong configuration |
|
||||
| `[configs] round trip` | `read=1`, the note verbatim, `buffer_samples` non-zero | the note or the startup settings not persisted — a configuration that carries a layout but not the buffer size it was tuned with |
|
||||
| `[configs] store` | the **event cache's configuration key**, following the active one: `default` → `probe` → `default` | a cache still keyed on the source alone (the key never moves), or a switch that renames the session but leaves the events list showing the configuration it left: `store=default` on the `save-as` line, or `store=probe` after `forget`, names it |
|
||||
|
||||
## `[rowmenu]`
|
||||
|
||||
```
|
||||
[rowmenu] main 4 'Remove signal' h=18 label_x=26 accel_right=-1/174 ok=1
|
||||
[rowmenu] main rows=5 bad=0 labels=[Y axis: auto scale,Y axis: fixed range...,Fit X axis,Fit all,Remove signal]
|
||||
[rowmenu] remove 0 'Back' h=18 label_x=52 accel_right=-1/178 ok=1
|
||||
[rowmenu] remove rows=6 bad=0 labels=[Back,sine,square,ramp,noise,All traces on this plot]
|
||||
[rowmenu] summary main_rows=5 main_bad=0 remove_rows=6 remove_bad=0 after_back_rows=5 after_back_bad=0 second_rows=6 second_bad=0 remove_row_in_main=1 remove_row_gone=1 can_open_again=1
|
||||
[rowmenu] remove-clicked 'sine' trace_freed=1
|
||||
[rowmenu] main 6 'Remove signal' h=18 label_x=26 accel_right=-1/248 ok=1
|
||||
[rowmenu] main rows=7 bad=0 labels=[Y axis: auto scale,Y axis: fixed range...,Fit X axis,Fit all,Open this plot in a new window,Edit the layout...,Remove signal]
|
||||
[rowmenu] remove 0 'Back' h=18 label_x=52 accel_right=-1/118 ok=1
|
||||
[rowmenu] remove rows=1 bad=0 labels=[Back]
|
||||
[rowmenu] summary main_rows=7 main_bad=0 remove_rows=1 remove_bad=0 after_back_rows=7 after_back_bad=0 second_rows=1 second_bad=0 remove_row_in_main=1 remove_row_gone=1 can_open_again=1
|
||||
[rowmenu] remove-clicked '' trace_freed=1
|
||||
```
|
||||
|
||||
| field | value it must read | the defect a wrong reading names |
|
||||
@@ -1467,6 +1587,7 @@ sets them to **7.5 and 250** before saving, so a key that never reached the file
|
||||
| `h` | 18 for every row of both views | the plot menu reverting to Adwaita's 34 px buttons — the row style not applied (`popover.udpscope-menu`) |
|
||||
| `label_x` | 26 for every row, 52 for **Back** | labels not sharing one column; the Back row's 52 is its leading icon, which every menu indents for |
|
||||
| `accel_right`/`W` | equal when the row has a shortcut (`Fit all` → `F`), `-1` when it has none | the shortcut not flush right, or shown where the action has none |
|
||||
| `main_rows` | 6 for a one-figure layout, **7** with `Full screen this figure` (a layout of two or more figures), plus 1 with `Restore the layout` while one is maximized | the full-screen entry missing where it would act (it is deliberately absent for a single figure: there is nothing to enlarge to), or the layout-editor entry missing: the plot menu is the third route into the editor beside `l` and the overflow menu |
|
||||
| `remove_rows` | 1 per occupied trace slot, plus `All traces on this plot` when there is more than one, plus the **Back** row | a remove view built from the wrong row, or missing its way out |
|
||||
| `remove_row_gone`, `remove_row_in_main` | 1 | a submenu that floats beside the menu instead of replacing it |
|
||||
| `can_open_again` | 1 | the second click not reopening: the `GtkMenuButton`-owned submenu that collapsed and left its row dead |
|
||||
@@ -1620,7 +1741,7 @@ run they report:
|
||||
[events] after events page opened: available=1 dock_sensitive=1 page_sensitive=1 page=events
|
||||
[events] open alone: page=events page_box=347
|
||||
[events] preview redrawn: signals=3 stored=480x132 row_drawn=1
|
||||
[events] dir=/home/u/.cache/udpscope/events/127.0.0.1-21023 n=3 newest: stem=20261013-184140-0003 dur=2.000000 pre=1.600000 post=0.400000 edge=5.660100 signals=3 samples=20001 rate=10000 seq=3 thumb_bytes=190134 recorded=1 page=1 plot_w_stable=1 plot_w=756/756
|
||||
[events] dir=/home/u/.cache/udpscope/events/127.0.0.1-21023/default n=3 newest: stem=20261013-184140-0003 dur=2.000000 pre=1.600000 post=0.400000 edge=5.660100 signals=3 samples=20001 rate=10000 seq=3 thumb_bytes=190134 recorded=1 page=1 plot_w_stable=1 plot_w=756/756
|
||||
[events] activated row0: replaying=1 page=events page_box=347 tick=0 window=2.000 axis=[-1.600,0.400] marker=0.000 thumbnail=1 thumb_size=138x46 thumb_visible=1 rows_kept=1 selected=2/2
|
||||
[events] dock events: rows=3 pane_rows=3 same_titles=1 sensitive=1 dock_w=268 on_screen=1 marked=0 members=0 max_ok=1 expected_marked=1 at=after pane replay
|
||||
[events] dock replay: clicked=1 replaying=1 waveform_changed=1
|
||||
@@ -1857,9 +1978,9 @@ These ride on the `[cursors-probe]` readings (every state line gets a `dock curs
|
||||
## `strip:` (the readouts strip)
|
||||
|
||||
```
|
||||
[cursors-probe] <what> strip: visible=1 expected=1 visible_ok=1 dock_shown=1 cursors=4 measures=0 same_text=1 h=26 sens_same=1 collapsed=1 window=756 groups=100 groups_ok=1
|
||||
[measures-probe] <what> strip: visible=1 expected=1 visible_ok=1 dock_shown=1 cursors=0 measures=12 same_text=1 h=26 sens_same=1 collapsed=1 window=756 groups=010 groups_ok=1
|
||||
[measures-probe] start strip: visible=0 expected=0 visible_ok=1 dock_shown=1 cursors=0 measures=0 same_text=1 h=0 sens_same=1 collapsed=1 window=756 groups=000 groups_ok=1
|
||||
[cursors-probe] <what> strip: visible=1 expected=1 visible_ok=1 dock_shown=1 cursors=4 measures=0 same_text=1 h=26 sens_same=1 collapsed=1 window=756 groups=100 groups_ok=1 add=1
|
||||
[measures-probe] <what> strip: visible=1 expected=1 visible_ok=1 dock_shown=1 cursors=0 measures=12 same_text=1 h=26 sens_same=1 collapsed=1 window=756 groups=010 groups_ok=1 add=1
|
||||
[measures-probe] start strip: visible=0 expected=0 visible_ok=1 dock_shown=1 cursors=0 measures=0 same_text=1 h=0 sens_same=1 collapsed=1 window=756 groups=000 groups_ok=1 add=1
|
||||
[cursors-probe] dock hidden strip: visible=1 expected=1 visible_ok=1 dock_shown=0 cursors=4 measures=0 same_text=1 h=26 sens_same=1 ...
|
||||
[cursors-probe] dock restored strip: visible=1 expected=1 visible_ok=1 dock_shown=1 ... collapsed=1 window=756
|
||||
```
|
||||
@@ -1868,7 +1989,7 @@ One `strip:` line follows every `dock cursors:` / `dock measures:` line, and eac
|
||||
|
||||
- **`visible` equals `expected`, and `expected = (!dock_shown || collapsed) && (cursors on || measures non-empty)`** (`dock_shown` is `show-sidebar`, `collapsed` the split view's, both read in the same frame as `visible`; "cursors on" is the cursors toggle, not the `cursors=` chip count, which is a rebuild's and is 1 even with them off). `visible_ok=1` is that equality; `visible_ok=0` is `sync_readouts_strip()` not run, or run before the reconciliation. A single such line right after the probe's own `update_side_visibility()` or on the very first line (the tick has not set `collapsed` yet) is the sub-frame before the next tick and not a defect.
|
||||
- **What each leg reads.** `[measures-probe] start` (before the probe adds anything, from a **fresh** config: a stored session brings its measurements back) is the "neither" state: `visible=0 expected=0 groups=000 h=0`. The measures probe's later lines have measurements and the cursors off: `groups=010`. The cursors probe alone has the cursors on and no measurements: `groups=100`; run after the measures probe in the same session it has both: `groups=111`. A line with the strip visible and `groups=000`, or hidden with a group on, is `sync_readouts_strip()` not called from that path.
|
||||
- **`groups=<cursors><measures><separator>`** is which of `strip_cursor_group`, `strip_measure_group` and `strip_sep` are visible; **`groups_ok=1`** is that they equal (cursors on, a measurement, both). `0` is a group left over from the previous state.
|
||||
- **`groups=<cursors><measures><separator>`** is which of `strip_cursor_group`, `strip_measure_group` and `strip_sep` are visible; **`groups_ok=1`** is that they equal (cursors on, a measurement, both). `0` is a group left over from the previous state. **`add=1`** is the strip's `[+]` sitting after the last measurement chip (a button, not the chip box) -- the strip adds a measurement itself rather than only opening the pane; it is structural because a probe that clicked it would open a dialog on every report.
|
||||
- **`h=0` with `visible=1` on the `cursors-probe ... on` and `measures-probe ... added` lines** is the strip shown in the same frame the probe reads it, before its first allocation; the next line reads 26. It is not a `visible_ok` miss: the toggle handler and `rebuild_measures_pane()` call `sync_readouts_strip()` themselves, so `visible` is already right.
|
||||
- **`visible=1 expected=1` on every line that has something to read, under `xvfb-run`**, the `dock restored` line and the lines with `dock_shown=1` included: the window is 756 px, below the 900 px `kSidebarRoom`, so the split view is collapsed, the dock is an overlay on the plot, and the strip stays. `visible=0` there is the old rule (hidden whenever the dock is shown) or `collapsed` not being read. The wide case (`collapsed=0`, `dock_shown=1`, `visible=0 expected=0`) is **not reachable from a probe**, because a window with no manager cannot be resized by `gtk_window_set_default_size()`.
|
||||
- **`h` is 26 with `visible=1`, 0 with `visible=0`.** Any other visible height is the chips setting the strip's height (the button trim in `ui_widgets.cpp` or the `height-request` lost).
|
||||
@@ -2118,15 +2239,22 @@ same reason.
|
||||
## `[prefs-probe]`
|
||||
|
||||
```
|
||||
[prefs-probe] opened=1 rows=1 dir=/tmp/k5/cache/udpscope/events store=/tmp/k5/cache/udpscope/events/127.0.0.1-8000
|
||||
[prefs-probe] dir=/tmp/udpscope-prefs-events keep=7 rel=1 theme=dark live=1
|
||||
[prefs-probe] opened=1 rows=1 dir=/tmp/k5/cache/udpscope/events store=/tmp/k5/cache/udpscope/events/127.0.0.1-8000/default cfg=1 cfg_switch=1
|
||||
[prefs-probe] dir=/tmp/udpscope-prefs-events keep=7 rel=1 theme=dark xhair=0 live=1
|
||||
[prefs-probe] default restored: file=''
|
||||
[prefs-probe] browse: cancelled=1 icon_ok=1 field='/tmp/k5/cache/udpscope/events'
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_PREFS_MS` opens the preferences, changes every row, and reads the settings file *back
|
||||
from disk* after each change -- not from `App::settings`, because a dialog that edits its own copy and
|
||||
never writes is exactly the failure this has to catch. **A change applies as it is made**: the rows are
|
||||
never writes is exactly the failure this has to catch.
|
||||
|
||||
`store=` is the directory the cache is **really** using, and the `cfg=`/`cfg_switch=` pair is that
|
||||
directory's key: `cfg=1` reads `store=` ends `/<source>/<active configuration>/` (the cache is per
|
||||
source *and* configuration, so two configurations of one source never list each other's events), and
|
||||
`cfg_switch=1` reads `retarget_event_store()` -- the one helper every configuration switch calls --
|
||||
moving the key to another configuration and back. A `cfg=0` names a store still keyed on the source
|
||||
alone; a `cfg_switch=0` a switch that leaves the cache pointing at the configuration it left. **A change applies as it is made**: the rows are
|
||||
libadwaita's (`AdwEntryRow`, `AdwSpinRow`, `AdwSwitchRow`, `AdwComboRow`), and the `preferences.ui`
|
||||
dialog has no Apply button for the probe to press.
|
||||
|
||||
@@ -2138,8 +2266,10 @@ dialog has no Apply button for the probe to press.
|
||||
plus the source's folder name**, which is the reading that says the row shows the path in force rather
|
||||
than a plausible string.
|
||||
- The second line must echo the probe's own input: directory `/tmp/udpscope-prefs-events`, keep `7`,
|
||||
relative time `1`, theme `dark`. Any field that differs means the row, the write or the parse dropped
|
||||
it. `live=1` is the theme having reached the running style manager rather than only the file.
|
||||
relative time `1`, theme `dark`, and `xhair=0` — the crosshair's axis-badge switch, which the
|
||||
probe drives off exactly as it drives the rest, and which reaches the running plots at once
|
||||
(`RowSet::set_crosshair_axis_labels()`). Any field that differs means the row, the write or the
|
||||
parse dropped it. `live=1` is the theme having reached the running style manager rather than only the file.
|
||||
- `default restored: file=''` closes the loop from the other side: the probe puts the default path back
|
||||
into the row, and the file must end up with **no** `event_cache_dir` at all. A path in the file is a
|
||||
user's choice, and leaving the default is not one -- pinning it there would stop the setting following
|
||||
@@ -2152,7 +2282,7 @@ dialog has no Apply button for the probe to press.
|
||||
|
||||
|
||||
```
|
||||
[prefs-probe] opened=1 apply=1 dir=/tmp/k5/cache/udpscope/events store=/tmp/k5/cache/udpscope/events/127.0.0.1-8000
|
||||
[prefs-probe] opened=1 apply=1 dir=/tmp/k5/cache/udpscope/events store=/tmp/k5/cache/udpscope/events/127.0.0.1-8000/default cfg=1 cfg_switch=1
|
||||
[prefs-probe] dir=/tmp/udpscope-prefs-events keep=7 rel=1 theme=dark live=1
|
||||
[prefs-probe] default restored: file=''
|
||||
[prefs-probe] browse: toplevels=3 extra=1 icon_ok=1 field='/tmp/k5/cache/udpscope/events'
|
||||
@@ -2862,9 +2992,10 @@ inside the panel does not close it), `click_outside_closes=1` (a click on the pl
|
||||
[split-probe] ruler-labels split=[row 1,row 2] mixed=[row 0,row 2]
|
||||
[split-probe] shared-y y_shared=1 y_gutter=1 y_fit=1 ruler_mirror=1 ranges=[A=-3.000000..7.000000 B=-3.000000..7.000000 | fit A=-1.04..1.04 B=-1.04..1.04 data=-1..1] axes_w=[671.599976,671.599976] auto=1
|
||||
[split-probe] x-axes row0_labelled=[1,0,1] bottom_labelled=[1,1,1] gap_ok=[1,1,1] gap_px=[3.0/3.0,3.0/3.0,3.0/3.0] col_gap_px=3.0 caption_ok=[1,1,1] last_plot_zoom=1
|
||||
[split-probe] cell-xhair left=1 right=1 centre_dy=0.0 sibling=0 (the horizontal line spans a split row's shared y, and no other row)
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_SPLIT_MS` (stderr; see [verification.md](verification.md)). 28 steps (0..27), 250 ms apart;
|
||||
`UDPSCOPE_PROBE_SPLIT_MS` (stderr; see [verification.md](verification.md)). 31 steps (0..30), 250 ms apart;
|
||||
each restructures and the *next* step reports the frame that was laid out with it, so a line's label
|
||||
names the arrangement the line **reads**, not the call that made it. The state lines share one format:
|
||||
`plots` (`row_count()`), `frows` (`figure_row_count()`), `cells=[..]` (per figure row), `widths=[..]`
|
||||
@@ -2887,6 +3018,7 @@ The other lines, in order:
|
||||
| `row+cell` | `cells=[2,2]`, `weights=[3,1\|-]` | row 0's cell weights lost or moved when a row and a cell were inserted below it (they belong to the figure row) |
|
||||
| `caps-calls` | `insert_cell=[1,1,0] refused_cell=1 insert_row=[1,1,0] refused_row=1 plots=8` | a cap that is not reached or not held: row 0 starts with two cells and takes two more, refusing the third call at 4; two rows then fit before the plot total reaches 8 and the third is refused; `plots` must stop at **8** |
|
||||
| `caps` | `plots=8 frows=4 cells=[4,2,1,1]`, `tile=1` | a ninth plot, or a fifth cell in a row |
|
||||
| `cell-xhair` | `left=1 right=1 centre_dy=0.0 sibling=0` | the crosshair's horizontal line, placed through `Figure::set_crosshair()` on the **second** cell of a two-cell row: it must also be drawn in the first cell (`left=1`) at the same screen row (`centre_dy`) — the cells share one y, so the line is one value — and must not appear in the figure's other (unsplit) row (`sibling=0`). A `left=0` names a row whose cells each keep their own line; a `sibling=1` a y spread across rows that do not share one |
|
||||
| `remove` | `plots=7 cells=[3,2,1,1]` | `remove_cell()` on a multi-cell row that removed the row, or that dropped a neighbour's traces |
|
||||
| `roundtrip-io` | `wrote=1 read=1 plots_in_file=3` | a save that was refused, or a file that lost a `[[plot]]` table |
|
||||
| `roundtrip` (state line) | `cells=[1,2] widths=[756\|524,229] weights=[-\|3,1]` and the three traces on `1`, `2a`, `2b` | the arrangement did not survive `collect_layout()` -> file -> `apply_layout()` |
|
||||
|
||||
@@ -60,7 +60,8 @@ see [probe-readings.md](probe-readings.md).
|
||||
|
||||
That second baseline used to be "exactly two, `gtk_widget_get_height` and `gtk_widget_get_width`,
|
||||
from the dump asking a destroyed widget for its size", and tolerating them was a mistake: the dump
|
||||
runs from the destroy handler, so the widget pointer is **freed memory**, and a critical is only
|
||||
runs **after `g_application_run` returns**, so the window and every widget in it are gone -- the pointer
|
||||
is **freed memory**, and a critical is only
|
||||
the lucky reading of it. A run that had built and destroyed other windows first —
|
||||
`UDPSCOPE_PROBE_HELP_MS`, which opens the About and shortcut dialogs and then closes them — has had
|
||||
that memory reused, and the same two calls
|
||||
@@ -74,7 +75,26 @@ precedent, not an exception.
|
||||
then dump the whole pipeline state to stdout (frame/sample counts, per-plot painted
|
||||
frames, trace bindings, panel sizes, status line in both states).
|
||||
- `UDPSCOPE_ENABLE_ALL_MS`, `UDPSCOPE_ARM_TRIGGER_MS`, `UDPSCOPE_PROBE_DISARM_MS`,
|
||||
`UDPSCOPE_TRIG_TRACE_MS` — schedule individual probes. `UDPSCOPE_PROBE_DISARM_MS` only
|
||||
`UDPSCOPE_TRIG_TRACE_MS` — schedule individual probes. `UDPSCOPE_ARM_TRIGGER_MS` clicks the
|
||||
trigger toggle and then samples the **trigger status circle** once a second for four
|
||||
seconds: an armed, waiting trigger reads `circle=flash-lit` / `circle=flash-dim` (the two
|
||||
halves of the 1 Hz flash, in the phase the sample lands in), a latched event `circle=green`,
|
||||
a disarm `circle=red`; the exit dump carries the last tick's state as `trigger circle:`
|
||||
(`off` under `UDPSCOPE_EXIT_AFTER_MS`, whose own probe goes live before it dumps).
|
||||
`UDPSCOPE_PROBE_CROSSHAIR_MS` hovers the pointer over row 0 through the adapter's own
|
||||
controller and asserts the crosshair's two **axis badges** landed in their label strips
|
||||
(`[crosshair-probe] active=1 ... y_ok=1 x_ok=1`), then flips the flag off and reads the
|
||||
plot's back; the pixel-level contract is the plotter suite's. The preference behind the
|
||||
flag is `[prefs-probe]`'s `xhair=` field. `UDPSCOPE_PROBE_MAXIMIZE_MS` arranges two
|
||||
single-cell rows (sine, square) and exercises the one-click **full-screen** toggle through
|
||||
the same `RowSet` call the button's handler makes — `[maximize-probe] before: rows=2 frows=2
|
||||
row0=sine row1=square buttons=2; max: rows=1 frows=1 flag=1 row0=sine; back: rows=2
|
||||
row0=sine row1=square flag=0` — where `buttons=` is the overlay's one toggle per figure row
|
||||
and the `back` leg is the proof that a maximize moves no signal. `UDPSCOPE_PROBE_SCOPE_MS`
|
||||
opens the **second window** twice, once copying figure 0 and once empty, and reads it back
|
||||
a few frames later: `[scope-probe] count=2 copied='sine' bound=1 empty_ok=1 windows=1` —
|
||||
`bound=1` is the main tick having bound the copy's rows to the same model, and `empty_ok=1`
|
||||
the empty scope staying empty. `UDPSCOPE_PROBE_DISARM_MS` only
|
||||
*starts* its sequence: it switches the trigger on and then polls for a capture to open,
|
||||
so the Disarm lands ~300 ms into one rather than wherever a fixed delay happened to fall.
|
||||
- `UDPSCOPE_PROBE_FORCE_MS` — the trigger panel's **Force** button: one acquisition at the present
|
||||
@@ -234,13 +254,16 @@ precedent, not an exception.
|
||||
window prices a 15 s one at nothing (four measurements over four channels read `35.69 ms` a
|
||||
recompute and a 4.58 ms tick mean at a 15 s window against `6.58 ms`/1.34 ms for one window at
|
||||
the same window).
|
||||
- `[measure-probe] N measurements on 'X'` (stderr, `UDPSCOPE_PROBE_MEASURE_MS`) — adds one
|
||||
- `[measure-probe] N measurements on 'X' (channel C)` (stderr, `UDPSCOPE_PROBE_MEASURE_MS`) — adds one
|
||||
measurement of each statistical type (average, top, peak to peak, amplitude) on the first
|
||||
*channel* and forces a recompute. It must name a **channel**, not `signals().front()`: the
|
||||
first signal of a stream is its timestamp, which is not acquired, and naming it adds four
|
||||
measurements that all read `--` *and* makes a performance run look free (each bails before
|
||||
collecting anything -- that is how the first baseline of the stall below read 0.4 ms).
|
||||
The values are in the exit dump as `measurements: <type>=<value>`; on the fast mock
|
||||
The values are in the exit dump as `measurements: N.signal <type>=<value>`, each numbered
|
||||
with the signal's active-channel number (the same one the pane's rows carry, and unnumbered
|
||||
when the signal is not acquired -- `enable_all` switches one off before the dump, so its
|
||||
measurements read unnumbered there); on the fast mock
|
||||
(`tools/mock_udpstreamer.py --rate 1300000 --batch 2000`) a unit sine must read
|
||||
`peak to peak=2` **exactly**, `average=0`, and the two flat-level types as their definition
|
||||
says: **`top=0.957988`** and `amplitude=1.915983`. `top` is no longer 2/pi -- it is the *flat*
|
||||
@@ -346,6 +369,42 @@ precedent, not an exception.
|
||||
[probe-readings.md](probe-readings.md); the window rule itself is in
|
||||
[invariants-measurements.md](invariants-measurements.md).
|
||||
|
||||
- `[gate-probe]` (stderr, `UDPSCOPE_PROBE_GATE_MS`) — the **per-measurement measurement gate**:
|
||||
a gated measurement must read the shared band and an ungated one the whole window, and the
|
||||
band must stay put while the data moves under it. It enables the sine, puts it on a plot
|
||||
(the way `enable_all_probe` does — an unbound trace leaves the display epoch at 0, and a
|
||||
still window is the state the `still=` leg asserts moves), adds **two** averages — index 0
|
||||
ungated, index 1 gated, which is what makes the pane rebuild *derive* the band from the
|
||||
measure set — and re-states the band's bounds to a fixed relative interval, 300 ms ago to
|
||||
100 ms ago, where samples exist whatever the run's age. It first waits for the ring to be
|
||||
older than the window, because the `plain=` assertion below is exact zero only while a
|
||||
whole-seconds window of the mock's 1 Hz sine holds whole periods; a young ring clips the
|
||||
window to a fraction of a cycle and the reading is merely small. Two lines:
|
||||
|
||||
```sh
|
||||
python3 tools/mock_udpstreamer.py --port 8140 --rate 100000 --batch 100 --freq 1 &
|
||||
sleep 1.5
|
||||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=14000 UDPSCOPE_PROBE_GATE_MS=3000 \
|
||||
./build/udpscope --server 127.0.0.1 --port 8140 2>&1 | grep -E 'gate-probe'
|
||||
```
|
||||
|
||||
The first line must read **`band=1 placed=1 plain=1 split=1 gwin=1`**: one gated
|
||||
measurement showed the band, the plot's region holds exactly the bounds the `RowSet` does,
|
||||
the ungated average is zero (it read the whole window — `-1.36e-17 V` on the reference run),
|
||||
the gated average differs (it read the band — `355.31 mV`, wherever in its period the
|
||||
stream happened to be), and the pass booked `measure_gate_window` = the band moved onto the
|
||||
stream's clock (`s0=`), which is the phase-free statement of *which* slice the gated walk
|
||||
read. The second line, a second later, must read **`still=1 s0_moved=1`**: the band exactly
|
||||
where it was put while the stream's clock moved under it — the *relative* behaviour the
|
||||
gate exists for. The probe saves the bounds the way a drag end does; the exit dump's
|
||||
**`gate:` line** is the round trip — a second run on the same config, with the probe unset,
|
||||
must restore both measurements (`measurements: 2 average≈0 average≠0`) and read
|
||||
`gate: on=1 bounds=[-0.300000..-0.100000]`. **Not in one run with the other measurement
|
||||
probes** (`UDPSCOPE_PROBE_MEASURE_MS`, `_MEASURES_MS`, `_BANDAVG_MS`, `_LEVELS_MS`,
|
||||
`_MEASUREPIN_MS`): they add measurements to one pane and these lines quote indexes 0 and 1.
|
||||
The lines are in [probe-readings.md](probe-readings.md); the rule is in
|
||||
[invariants-measurements.md](invariants-measurements.md).
|
||||
|
||||
- `[pan-probe] asked=[..] was=[..]`, `[pan-probe] after=[..] held=D asked=[..]`, then
|
||||
`[pan-probe] zoom asked=[..] after=[..] span=S window=W limited=L`
|
||||
(stderr, `UDPSCOPE_PROBE_PAN_MS`) — **`held` must be 1**: a gesture must not move a window
|
||||
|
||||
Reference in new issue
Block a user