docs: events in the dock and on its page

The dock's Events block, the events slide-over page, the availability-rule deviation, the probe lines and the removal of the left pane are described; the false remembered-width claim is gone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-11-03 07:19:33 +01:00
1 parent 9db5e4a49c
commit c4b79da057
7 files changed
+206 -105

No files matched your search

+53 -26
View File
@@ -94,7 +94,7 @@ switches trigger mode on, waits for an event, then leaves trigger mode the way t
"Go live" button does, closes the window, and prints on stdout the status line in
both states, the frame and per-channel sample counts, each plot's painted-frame
count, which channel each trace slot holds, and the allocated size and reveal
state of the dock and the events pane.
state of the dock, and the events page's controls (present and nested in the page).
Every size it prints is read from a value the frame tick captured while the window was alive, not
from the widget: the dump runs from the destroy handler, where the widget pointer is already freed
@@ -146,8 +146,8 @@ panel laid over the plot's right edge, opaque, with its own title, a *Close* but
page. The Trigger badge opens the trigger form; the Source badge (when shown) opens the source kind's own panels
(the E-1608's *Analog out* and *Digital I/O*, behind a segmented switch at the top of the panel). It
closes with a second click on its badge, with `Esc`, with *Close*, or with a click on the plot; it never
resizes the plot. The right-hand **dock** (the sidebar, **280 px**, fixed) holds the **readouts**: a
compact, read-only copy of the cursors table (a header row `A B delta`, then one line per cursor
resizes the plot. The right-hand **dock** (the sidebar, **280 px**, fixed) holds the **readouts** and, under
them, the **Events** block (below): the readouts are a compact, read-only copy of the cursors table (a header row `A B delta`, then one line per cursor
line) and of the measurements (one line each: the signal's colour chip, the type, the value). It is
shown from the start and insensitive until the stream has signals. Each of the two blocks has an
**Edit** button, which opens the full panel as a slide-over page -- `cursors` or `measure` -- over the
@@ -159,8 +159,12 @@ cells are a fixed number of characters wide and ellipsized, so the dock's width
depend on what a value reads: the **who** cell of a cursor line is 5 characters (the full text is its
tooltip), a value cell 9 (its tooltip is the full text). The dock and the pages show **one string**:
`update_cursors_pane()` and `update_measures()` format each value once and hand that variable to the
page's label and to the dock's, so the two cannot disagree. The remaining part of the redesign is
listed under "Review findings not (yet) implemented".
page's label and to the dock's, so the two cannot disagree. The **Events** block is a heading with a
**Manage** button over a short list of the cached events, newest first -- one compact row each, never a
picture -- and one click on a row replays that event (see "Captured events"). The full list, with the
pictures and the comparison actions, is the `events` slide-over page that **Manage** opens. There is no
events pane beside the plot and no Events button in the header: events never take any of the plot's width.
The rest of the redesign is listed under "Review findings not (yet) implemented".
Threading: one producer thread owns the data source (the UDPStreamer client is the built-in
one) and calls `poll()`; the GTK main thread owns every widget and the GL contexts.
@@ -838,20 +842,36 @@ time to look at is still there:
20261009-123250-0001.bmp # a picture of it, drawn from the samples
```
The **left pane** is opened on demand from the toolbar's event-history button, is collapsed by
default, and its **divider can be dragged**: it starts at 240 pixels and remembers the width you last
chose, and while it is closed it takes no room at all. Both the button and the pane exist while **recording is on**, or while an event is on
screen: with the cache disabled and nothing being examined there is nothing to show, and a visible
empty column beside the plot is what *off* must not look like. Recording is switched in the **trigger
panel**, with the trigger's other options rather than in the pane whose contents it decides, and it is
**off by default** -- writing every latch to disk is the user's decision about their disk.
Events are read in two places. The **dock's Events block** lists them compactly -- one row per event, newest
first, at most **24** with a dim "... N more, see Manage" row after them: a marker (a filled circle on the
event on screen, a hollow one on each member of the comparison or accumulation on screen), the time and
duration, and the event's sequence number (`#7`). **One click on a row replays it.** The block's **Manage**
button opens the **`events` page**, a slide-over page over the plot with the full list (below), the *Live*
button and the two group actions. The page replaces the left pane that used to sit beside the plot behind a
header button and a draggable divider; both are gone, and so is the toolbar's Events button -- opening the
list no longer resizes the plot at all.
The block and the page exist all the time and are **insensitive, not hidden**, when there is nothing to show:
they are available while **recording is on**, while an event is on screen (a replay or a group), or while
the cache already holds events. **Deviation from the redesign spec,** which has the block insensitive
"outside trigger mode": a replay or a group *leaves* trigger mode (a file has no edge to wait for), so a block
that went grey outside trigger mode would grey itself under the very click that started the replay. Trigger
mode is therefore not one of the conditions. Recording is switched in the **trigger panel**, with the
trigger's other options rather than in the list whose contents it decides, and it is **off by default** --
writing every latch to disk is the user's decision about their disk. (The session file keeps nothing about
this list: there is no remembered width, because there is no pane to size.)
The dock follows every latch; the page's list is refreshed per latch only while it is open, and rebuilt when
**Manage** opens it. **The dock list has no thumbnails** -- its rows are fixed-width, ellipsized cells, so the
dock's size does not depend on what an event is called -- the page has them, and its rows are replayed with a
**double** click, because on the page a single click builds the selection that *Compare* and *Accumulate* read.
Each row carries **its own picture** beside the numbers -- time, duration, signal count, rate -- 138
pixels wide whatever the pane's width, and as tall as that event's own proportions make it: the
pixels wide whatever the page's width, and as tall as that event's own proportions make it: the
preview is one 44-pixel strip per signal, so a one-signal event is a short row and a six-signal one a
tall one (capped, so that a row stays a row). **Hovering it shows the whole preview at full size**,
through a tooltip -- the small picture says *which* event this is, the zoom says what is in it.
Clicking a row **replays** it: the same loader `--open` uses, on the event's file. The click **leaves
Double-clicking a row on the page (or clicking one in the dock) **replays** it: the same loader `--open` uses, on the event's file. The click **leaves
the list alone** -- your scroll position and the pair you had selected for a comparison both survive
it, because marking which event is on screen rewrites the rows' titles in place instead of rebuilding
the list. **The layout does
@@ -862,7 +882,7 @@ rearrange the user's work under a click. What changes is the data, and the trigg
file has no edge to wait for. Cursors, measurements, math channels and another export all work on a
stored event. **Live** takes you back to the stream.
Two buttons under the pane's header read the **selection**:
Two buttons under the page's header read the **selection**:
- *Compare* puts the two selected events in **two plots side by side**, each with **the layout you
already have** -- the same rows, the same signals in the same slots, the same colours -- one showing
@@ -880,8 +900,8 @@ model whose channel count no longer matches the stream), the trigger is left, an
A replay leaves trigger mode, because a file has no edge to wait for -- an armed trigger keeps a
*frozen capture* on screen (`display_mode()` stays `Held` while the model holds a snapshot), and a
held display draws that snapshot rather than the file that was just loaded. The events pane stays
open through it: it is available while a stored event is on screen.
held display draws that snapshot rather than the file that was just loaded. The dock's Events block and
the `events` page stay available through it: they are available while a stored event is on screen.
**Going back to *Live* puts the live view back as it was.** Opening a file changes two things that
are not the user's choice -- the time window becomes the event's own span, because that is what
@@ -912,7 +932,7 @@ Three things worth knowing:
- **The picture is decoded synchronously** (`gdk_texture_new_from_filename()` into
`gtk_picture_set_paintable()`), not through `gtk_picture_set_filename()`: that path uses the image
loader's sandboxed, asynchronous decoder, which here fails silently -- the picture keeps a
paint-able and an allocation, so a probe reports success while the pane shows an empty rectangle.
paint-able and an allocation, so a probe reports success while the page shows an empty rectangle.
It is then **scaled once to the row's size** and cached by file: a `GtkPicture`'s natural size is
its content, so the stored 480-pixel preview given straight to a row claims the row's whole width
and squeezes the text out of it -- a size request on the picture is a floor, not a cap.
@@ -929,8 +949,8 @@ Three things worth knowing:
one at a time was eight of those, which is most of what made the switch feel slow. They are now
enabled in a single call, and a rebuild whose plan is unchanged clears the buffer instead of
allocating a new one.
- The switch in the pane's header stops the recording; the cache is only useful if it was already
on when the event you wanted happened, which is why it defaults to on.
- The switch in the trigger panel starts and stops the recording; the cache is only useful if it was
already on when the event you wanted happened, so switch it on before the event you are waiting for.
## Opening an exported waveform
@@ -1241,8 +1261,10 @@ reveals it, while one whose *pane is collapsed* (the dock, in a window too narro
plot) is dropped, because it is unmapped with its pane (a hidden page keeps its own *visible* flag
true -- the widget's own flag, which is all `is_visible()` walks -- so the old test walked over
surfaces that were not on screen); a narrow window therefore walks five steps. The dock's stop
describes the readouts and the **Edit** buttons; it names no slide-over page, so it closes the panel
like the plot's. The cursors and measurements pages have no stop of their own, and their contextual
describes the readouts and their **Edit** buttons and the Events block under them (one click on a row
replays it, **Manage** opens the list); it names no slide-over page, so it closes the panel like the plot's.
There is no stop for the `events` page, so nothing in the tour opens it; the contextual help's
event-history hint is attached to the page's heading the way the cursors' and measurements' are. The cursors and measurements pages have no stop of their own, and their contextual
help "(i)" hints are attached to the pages' headings the way the trigger's and the signals' are: they
are drawn while the page is open, and the dock has none. The chooser's
tour works the same way over its own two pages: its tabs step shows the *Waveforms* page and
@@ -3021,11 +3043,16 @@ now — they are listed so they are not mistaken for anything else:
**who** cell ellipsizes at **5 characters** -- the width the 280 px pin allows; with 8 the readouts'
minimum width was 304 px and widened the sidebar -- and its tooltip carries the full text. (6) The dock and
the pages **share one string** for every value (see `docs/claude/invariants-readouts-and-cursors.md`).
(7) The **Events block and the `events` page are available** while recording is on, a replay is on screen, or
the cache holds events -- **not** "outside trigger mode" as the spec has it, because a replay or a group leaves
trigger mode and the block would grey itself under the click that started it. (8) The **dock's event rows carry no
thumbnails** (the page's do) and show a **hollow marker on the members of the group on screen**, which the page's
rows do not. (9) The dock lists at most 24 events; the page lists them all.
The signal badges, the signal pages and the drag (step 2) are done. Still to come, in the spec's
order: the events section in the dock, the deletion of the left pane and the Events toggle, and
`USER-GUIDE.md` and the manual's figures (4). Until then the events pane, the header's Events toggle
and the manual (which still describes the side panel's tabs, and the cursors and measurements as
panes of it) still describe the previous layout.
order: `USER-GUIDE.md` and the manual's figures (4). The events section in the dock and the `events`
page are done, and the left events pane, its divider and the header's Events toggle are deleted. Until
the manual is swept, it (which still describes the side panel's tabs, and the cursors, measurements
and events as panes of it) still describes the previous layout.
- **A plot holds at most eight signals** (`RowSet::kSlotsPerRow`), because a `Plot`
cannot give up a trace and the slots are therefore created up front.
- **One toggle drives both cursor orientations.** The vertical markers and the
+23 -5
View File
@@ -102,10 +102,12 @@ touching the data path.
own and registered under its id (`trigger`, `source-<id>`, `signals` (the signal list) and
`signal:<name>` (one signal's page, `src/ui_signal_page.cpp`, created on demand by `signal_page_ensure()`
and pruned by `signal_pages_sync()`, which `rebuild_signal_panels()` ends with), `cursors` (the cursors
panel, unchanged) and `measure` (the measurements panel, unchanged; `m` toggles it); `events` is reserved
for a later step). Both of those are registered whether or not they are open (a closed page's widgets
exist, unmapped), and both are insensitive until the stream has signals, by the same
`update_signal_availability()` call that sets the dock's sensitivity. One page is open at a time and `App::slideover.page`
panel, unchanged) and `measure` (the measurements panel, unchanged; `m` toggles it); `events` (the full events list, `events-pane.ui`'s `events_box`: the Live, Compare and Accumulate
controls and the multi-select list with its thumbnails; open it only through `open_events_page()`, which
refreshes the list first, because the slide-over has no on-open hook). They are registered whether or not
they are open (a closed page's widgets exist, unmapped); `cursors` and `measure` are insensitive until the
stream has signals, by the same `update_signal_availability()` call that sets the dock's sensitivity, and
`events` follows `sync_events_availability()` (below). One page is open at a time and `App::slideover.page`
is its id, empty when closed. The open page is **not** a session value. Esc, *Close*, a second click on the
badge and a click on the plot close it; the click-away is a capture-phase observer (see
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). With two or more `source-*` pages, an
@@ -120,7 +122,7 @@ touching the data path.
pick and focus from `slideover.source_pages` being non-empty every tick, so anything that indexes the badge
(the tour's `badges` step targets the whole bar, `[badges]`) must tolerate it being absent.
- **The dock** is the `AdwOverlaySplitView` sidebar (`side_box` > `dock_scroller` > `dock_page`), **280 px**
and fixed. It holds one thing, the **Readouts** box (`src/ui_dock.cpp`, `build_readouts_dock()`): a
and fixed. It holds two things, the **Readouts** box and, under a separator, the **Events** block (below). The Readouts box (`src/ui_dock.cpp`, `build_readouts_dock()`): a
**Cursors** block (heading, an **Edit** button that opens the `cursors` page, a `GtkGrid` of a header row
`A B delta` and one line per page line) and a **Measurements** block (heading, **Edit** opening the
`measure` page, one line per measurement: chip, type, value; a click on a line opens that measurement's
@@ -132,6 +134,22 @@ touching the data path.
kind's panels (those are slide-over pages). The window's measurement editor is a dialog
(`open_measure_dialog()`), not part of the `measure` page.
The **Events** block (`build_events_dock()`, also `ui_dock.cpp`) is a heading with a **Manage** button
(`open_events_page()`) over a short list (`dock_events_list`, 112 px, scrolled, no thumbnails): one compact
row per `EventRecord`, newest first, capped at `kDockEventsMax` (24) with a dim "... N more, see Manage"
row after them. A row's cells are a one-character marker (filled = the event on screen; hollow = a member
of the group on screen -- dock only, the page's rows do not mark members; blank otherwise), the title
(`event_row_title`, ellipsized, full text in the tooltip) and `#<seq>`; **one click replays** it
(`replay_event()`, the body of the page's double-click handler). Both views are rebuilt from one
`EventStore::list()` by `refresh_events_pane()`, but on different cadences: **the dock follows every
latch** (`record_event()` refreshes it unconditionally), while the page's list is refreshed per latch only
while the page is open and by `open_events_page()` otherwise. `mark_events_on_screen()` updates titles and
markers in place and never rebuilds either list. The block, and the page, are insensitive (never hidden)
unless `sync_events_availability()` says `event_recording || replaying || events_cached` -- a different
rule from the spec's (see [invariants-trigger-and-pinning.md](invariants-trigger-and-pinning.md)).
There is no events pane, divider or header toggle any more: the page is reached from Manage (or by a
caller of `open_events_page()`), and the plot is never squeezed by events.
**When each is built and when it is read.** `build_slideover()` runs in `on_activate` right after the tour
overlay is built (the tour's overlay wraps the whole window, so its dim rectangles cover the slide-over
too); the trigger form is registered as
+19 -12
View File
@@ -210,9 +210,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
entry points in `source_dialog.cpp` (`chooser_tour_begin`, `chooser_tour_autostart`), and *that* walk
is what a first run meets first -- see the tour bullets in the help-affordance section below.
- **A `GtkPaned` hands its start child at least its minimum unless the position says otherwise.**
Putting the events pane in a paned is what makes its width the user's to drag -- but with the
position left alone, `shrink-start-child` or not, the pane's minimum is reserved as an empty column
while it is *closed*, and the plot pays for a pane nobody opened. The position is the only knob that
(The events pane that taught this is gone -- its list is a slide-over page now, with no divider -- but
the rule holds for any paned.) With the
position left alone, `shrink-start-child` or not, a closed side child's minimum is reserved as an empty
column, and the plot pays for a pane nobody opened. The position is the only knob that
says "no room": store the width when it is closed (reading `gtk_paned_get_position()` *before*
zeroing it) and set 0, restoring it when the pane opens. `gtk_paned_set_shrink_start_child(TRUE)` is
still needed, or the minimum would clamp even a zero position.
@@ -220,7 +221,8 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
= FALSE` makes both the revealer's minimum and its natural size zero -- and its box then hands it
everything the expanding sibling beside it did not claim, so a *collapsed* pane reserved a
310-pixel empty column between the window edge and a plot drawn 630 pixels wide in a 1270-pixel
window. Every vertical revealer in the side panel hides this, because there the leftover goes down
window. (That pane was a revealer beside the plot; it is a slide-over page now, so no revealer sits in
a horizontal box any more.) Every vertical revealer in the side panel hid this, because there the leftover goes down
instead of across. `gtk_widget_set_visible()` is what a layout honours, so anything that must not
take room has to be hidden and not merely unrevealed; the revealer keeps the slide while the pane
*is* open. The allocated width is how this was found -- the revealer's own measure said 0.
@@ -278,8 +280,9 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
clicked — a switch that greyed out with its own pane could never turn it back on. That is why the
sensitive target is the pane's **body** (`trigger_body`, `cursors_body`) and not the pane.
- **A list box outlives the children of the pane it sits in, and the teardown order is not yours to
choose.** Tearing the window down finalizes the events pane's buttons, then the list box, and the
list's own `selected-rows-changed` then reaches a handler holding the freed pointers: four
choose.** Tearing the window down finalizes the events page's buttons, then its list box, and the
list's own `selected-rows-changed` then reaches a handler holding the freed pointers (`update_group_buttons()`
therefore keeps its `app.window == nullptr` guard): four
`Gtk-CRITICAL`s from `gtk_widget_set_sensitive` and a read of memory nobody owns. `close-request` is
*not* the hook -- `UDPSCOPE_EXIT_AFTER_MS` ends the self-check with `gtk_window_destroy()`, which does
not emit it. The hook that does work is `App::window`: every teardown path clears it *before* the
@@ -296,22 +299,26 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
- **A pointer to a widget inside a list survives the list being rebuilt.** `events_thumb` named a row's
picture across `refresh_events_pane()`, which removes every row first: the next reader of that
pointer got a `Gtk-CRITICAL` from `gtk_picture_get_paintable` on freed memory (the four criticals
read as "the pane is empty"). A rebuild has to null what it is about to destroy, not only rebuild it.
read as "the page is empty"). A rebuild has to null what it is about to destroy, not only rebuild it
(`rebuild_events_pane_list()`, the page half of `refresh_events_pane()`, nulls `events_thumb` before it removes the page list's children). A handler on a
row that rebuilds the list destroys the widget it was emitted from: `outlive_the_rebuild(widget)`. The dock's
rows are a second list built from the same records; the dock row's click handler is safe only because
`replay_event()` never rebuilds the dock list.
- **`gtk_picture_set_filename()` can render nothing, silently.** It hands the file to the image
loader's sandboxed, asynchronous decoder, which on this machine fails without a message: the
picture keeps a *paint-able* and takes an allocation, so a probe that checks
`gtk_picture_get_paintable()` reads success while the pane shows an empty rectangle (the event
pane's preview did exactly that). `gdk_texture_new_from_filename()` decodes the same file on this
`gtk_picture_get_paintable()` reads success while the page shows an empty rectangle (the event
page's preview did exactly that). `gdk_texture_new_from_filename()` decodes the same file on this
thread and `gtk_picture_set_paintable()` hands over the result -- and a NULL texture at least says
so. A picture that must be on screen is also a picture with a **height request**: `can_shrink`
alone asks for no height at all in a box.
- **A cache that is the *only* source of a widget's content must be created by the thing that reads
it.** The event pane's thumbnails come from an `EventThumbCache` hung off the list box with
it.** The events page's thumbnails (the dock's rows have none) come from an `EventThumbCache` hung off the page's list box with
`g_object_set_data_full`, and `event_preview_texture()` answers out of that cache or not at all.
Nothing ever created it — so for as long as the pane existed, **every** row came up without a
Nothing ever created it — so for as long as the list existed, **every** row came up without a
picture, which is what "event screenshot are unusable and sometime missing" was. A missing cache
there is not a slow path, it is a blank one; `event_thumb_cache()` therefore creates one on first
use. Owned by the **list**, because nothing else has the pane's lifetime: textures of a list that
use. Owned by the **list**, because nothing else has the page's lifetime: textures of a list that
no longer exists are so much held memory, and the destroy notify is where they are unref'd.
- **Hover magnification is a tooltip, not a popover of ours.** A row's thumbnail is 138 px wide and
the stored preview is 480: the small one says *which* event this is, and the zoom says what is in
+10 -2
View File
@@ -40,6 +40,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
moved the plot column by about 180 px, which the layout probe read as a different `done_box` and a
taller figure.
- **Events never take the plot's width.** The events list is a slide-over page (an overlay child over the plot) and
the dock's Events block lives in the fixed 280 px column, so there is no events divider, revealer or pane beside the
plot to squeeze it; the first latch must not move the plot area (`[events] ... plot_w_stable=1`, which compares the
allocated width before and after). A dock row is a fixed-width ellipsized label (title at 18 characters, `#seq`), the
list is a fixed 112 px scroller capped at `kDockEventsMax` rows plus one "N more" row, and it has no thumbnails: the
dock's width and height do not depend on how many events there are or what they say ("layout must not depend on
content"). The thumbnails are the *page's* (138 px wide).
- **`plot::Plot` cannot remove or retarget a trace.** Hence fixed trace slots created
up front and rebinding via `SlotSeries`; a *name* change (legend text) is the one
thing that forces a rebuild. Cursors are parked outside the axis range instead of
@@ -131,7 +139,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
the Source badge is on screen for the E-1608 and hidden for UDPS, and its text differs by kind). A change that makes it grow with the signal count has
taken the scroller out of the measurement.
- **The dock's width is fixed too, and it is the readouts' width that holds it there** (`side_column`
`width-request` 260, the split view's 280 px floor and cap). The dock holds only the compact readouts, so a
`width-request` 260, the split view's 280 px floor and cap). The dock holds only the compact readouts and the Events block (a fixed-height list, below), so a
section's visibility is never what makes it taller; the scroller absorbs the rest. **The readouts' minimum
width is the sum of their cells' character widths, and it must stay under 280** (the Readouts box is 268:
280 less its 6 px margins): a cursor line is the `who` cell (5 characters) plus three value cells (9 each),
@@ -139,7 +147,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
tooltip carries the text and the layout never depends on what a value reads. The `who` cell was 8
characters, which made the readouts' minimum width 304 px; that pushed the sidebar to 304 px
(`dock_w=304`) and defeated the 280 pin. Widening any cell is the same
trap -- read `[readouts] ... dock_w=` (must be 280) and the `dock cursors:` / `dock measures:` `fits=`.
trap -- read `[readouts] ... dock_w=` (must be 280) and the `dock cursors:` / `dock measures:` `fits=`, and for the Events block `dock events: ... dock_w=` (268).
- **Trace slots are identified by signal name, not channel index.** Enabling a signal
or a new CONFIG renumbers the channel list, so an index would silently re-point a
trace at another signal.
+18 -9
View File
@@ -440,15 +440,24 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
## A latch is also a file
The pane that lists the cache is **on demand**: the toolbar's event-history button and the pane are
collapsed until it is asked for, because a column of stale events beside a live plot is what a user
who has stopped looking at events does not want. What makes it *available* -- (recording is on) or
(an event is on screen) -- is decided in one place, `sync_events_availability()`, so the button and
the pane cannot disagree; a disabled cache with nothing being examined takes the button away *and*
collapses the pane, because a visible empty column is what "off" must not look like. Trigger mode is
deliberately **not** one of the conditions: a stored event being examined is a file, which has no
trigger, and tying the pane to the trigger would close the list under the click that opened an event.
The recording switch lives in the trigger panel, not in the pane it empties.
The cache is listed twice, from one `EventStore::list()`: the dock's compact **Events block** and the full list on the
`events` slide-over page (the left pane, its divider and the header toggle are gone, so events no longer take
any of the plot's width). What makes it *available* -- recording is on, **or** a replay is on screen, **or** the
cache already holds something (`event_recording || replaying || events_cached`) -- is decided in one place,
`sync_events_availability()`, and it sets the block's sensitivity (`dock_events_set_sensitive()`) and the page's
(`events_box`) from the one `available`, so they cannot disagree. Unavailable means **insensitive, never hidden**
("disable, do not hide"): the block stays on screen greyed, and turning recording off with the page open leaves it up
and greyed rather than stale and live.
**This deliberately differs from the redesign spec**, which has the block insensitive "outside trigger mode". Trigger
mode is *not* one of the conditions: a stored event being examined is a file, which has no trigger, and a replay or a
group *leaves* trigger mode, so a block that went grey outside trigger mode would grey itself under the very click
that started the replay -- the one-click replay in the dock would disable the dock it was clicked in. Do not "fix" this
to match the spec. The recording switch lives in the trigger panel, not in the block it empties.
The two views follow the latch on different cadences: the dock is rebuilt on **every** latch (`record_event()`), the
page's list only while the page is open, and `open_events_page()` refreshes it when it was closed; a caller that
opens the page any other way shows a list that is as stale as the last time it was open.
## Opening a file is a detour, so *Go live* is a restore
+49 -26
View File
@@ -491,7 +491,7 @@ Two lines of the self-check output are assertions in disguise:
column and says there is one scroller and no stack. These are captured on the tick or at the exit probe:
the dump runs after the window is destroyed. A source with extra panels adds *no* dock section: its panels
are slide-over pages (`source-<id>`), so the line reads the same on the E-1608 and the probe kind.
- `[readouts] pages cursors=1 measure=1 dock_children=1 dock_w=280 pages_sensitive=1 signals=1` (in the exit
- `[readouts] pages cursors=1 measure=1 dock_children=3 dock_w=280 pages_sensitive=1 signals=1` (in the exit
dump; `dock_children` counts the direct children of the dock page) -- the two pages and the dock against the
stream. **`cursors=1 measure=1`**: both slide-over pages are registered, always and from the start (not when
the stream has signals); `0` is a page that was never added, and then the dock's Edit buttons and the `m` key
@@ -500,9 +500,10 @@ Two lines of the self-check output are assertions in disguise:
`update_signal_availability()` writes them with the dock), so a run against a port with no sender reads
`pages_sensitive=0 signals=0`, a normal run `pages_sensitive=1 signals=1`. `pages_sensitive=1` with `signals=0`
is a page that can be opened and read with nothing to read; `pages_sensitive=0` with `signals=1` is a
stream whose pages never came back after the signal list arrived. **`dock_children=1`**: the dock
page holds the readouts box and nothing else (a second child is a section that crept back into the dock; 0 is a
dock that was built empty).
stream whose pages never came back after the signal list arrived. **`dock_children=3`**: the dock
page holds the readouts box, a separator and the Events block (`build_events_dock()`) and nothing else (a fourth
child is a section that crept back into the dock; 1 is a dock without its Events block, 0 a dock that was built
empty).
- `help layer: contextual=on hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} icon=dialog-information-symbolic(ok) hint=12x12; tour=idle steps=6 step=0 auto=1 requested=0` (in the exit
dump, beside `dock:`) — the help affordance layer as the last tick found it. **`hints=` must equal
the number of ids**, and the ids are the surfaces the contextual help is on: eleven of the window
@@ -1601,24 +1602,33 @@ run they report:
## `[events]`
```
[events] live, recording off: button_visible=0 pane=0 pane_w=0 tick=1 recording=0
[events] recording on, no trigger: button_visible=1 pane=0 pane_w=0 tick=1 recording=1
[events] after trigger on: button_visible=1 pane=0 pane_w=0 tick=1 recording=1
[events] live, recording off: available=0 dock_sensitive=0 page_sensitive=0 page=none
[events] recording on, no trigger: available=1 dock_sensitive=1 page_sensitive=1 page=none
[events] after trigger on: available=1 dock_sensitive=1 page_sensitive=1 page=none
[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 pane=1
[events] activated row0: replaying=1 pane=1 pane_w=240 pane_box=228 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] 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] 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
[events] dock events: rows=3 pane_rows=3 same_titles=1 sensitive=1 dock_w=268 on_screen=1 marked=1 members=0 max_ok=1 expected_marked=1 at=after dock click
[events] replayed: channels=3 samples=20001 replaying=1 waveform=1
[events] live again: replaying=0 waveform=0
[events] live restored (after replay): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=10/5
[events] recording off, trigger off: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
[events] recording off, trigger on: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
[events] recording off, trigger off: available=1 dock_sensitive=1 page_sensitive=1 page=events
[events] recording off, trigger on: available=1 dock_sensitive=1 page_sensitive=1 page=events
[events] compare buttons: cached=2 selected=2 compare=1 accumulate=1
[events] compare: kind=1 rows=1 channels=8 samples0=32964 traces0=4 traces1=-1 window=2.000 recording=1 pane=1
[events] dock events: rows=2 pane_rows=2 same_titles=1 sensitive=1 dock_w=268 on_screen=0 marked=-1 members=0 max_ok=1 expected_marked=-1 at=two events latched
[events] compare: kind=1 rows=1 channels=8 samples0=32964 traces0=4 traces1=-1 window=2.000 recording=1 page=events
[events] dock events: rows=2 pane_rows=2 same_titles=1 sensitive=1 dock_w=268 on_screen=0 marked=-1 members=2 max_ok=1 expected_marked=-1 at=comparison up
[events] compare plots: area_b=1 rows_a=1 rows_b=1 list=4 x_a=[-2.000,0.000] x_b=[-2.000,0.000] same_x=1
[events] compare sync: y_a=-3.500 y_b=-3.500 mirrored=1
[events] accumulate: kind=2 rows=1 channels=8 samples0=32964 selected=2 traces0=8 window=2.000
[events] group left: replaying=0 kind=0 waveform=0
[events] live restored (after a group): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=22/16
[events] dock replay: clicked=1 replaying=1 (second click, live before)
[events] dock events: rows=2 pane_rows=2 same_titles=1 sensitive=1 dock_w=268 on_screen=1 marked=1 members=0 max_ok=1 expected_marked=1 at=second dock click
```
`thumb_size` is the built size of the row's picture: `kEventThumbW` wide, and as tall as the *stored*
@@ -1628,8 +1638,18 @@ reads `138x75`) as well as with the CSS border and the display's scale factor. W
`0x0`, and a height pinned to the floor for *every* event says the aspect fit was dropped and tall
previews are being squashed again.
**The E-1608 never latches an event** (its stream carries no trigger edge to record): on `--source e1608` the probe reads
`n=0`, the `recording on` lines read `1 1 1` (recording is on, so the block is available), and the two
`recording off` lines read `available=0 dock_sensitive=0 page_sensitive=0 page=events` -- the page is open and
greyed, the "disabled, not hidden" path -- while `activated row0:` reads `replaying=0 ... marker=-1000000000000.000
thumbnail=0 rows_kept=0` and the `dock events:` / `dock replay:` lines read `rows=0`, `clicked=0 replaying=0`. The
`page=` of the first availability lines is whichever `source-*` page the kind opened at start-up, not `none`.
A UDPS run against a **fresh cache** reads the same `n=0` and `rows=0` on the opening `dir=` / `dock` lines (nothing
has latched when they are printed) and fills in from the group leg on; a UDPS run with a populated cache is the one
that proves `same_titles` and the dock click on real rows.
`[events] preview redrawn:` is the repair path, and the probe forces it: step 1 deletes the newest
`.bmp`, drops the thumbnail cache and refreshes the pane, so this line is `EventStore::redraw_preview()`
`.bmp`, drops the thumbnail cache and refreshes the page's list, so this line is `EventStore::redraw_preview()`
drawing the picture again from the `.npy`. `stored` is the file it wrote, read back out of the BMP
header — `480` wide and `44 * signals` tall — and `row_drawn=1` says the row got a paintable from it.
The probe therefore needs a cache a **previous** run populated: it fires ~4.5 s in, before this run's
@@ -1668,23 +1688,25 @@ session that then disagrees with it takes the pair apart.
| `signals`, `samples` | the event's channels and rows | a table built from the *rows* instead of the snapshot: only what happened to be plotted |
| `thumb_bytes` | a real 24-bit BMP, `54 + 480 * (44 * signals) * 3` rounded up per row to a multiple of four -- so it *grows with the signal count* (190134 at three signals, 380214 at six) | a preview that is empty or missing -- the widget snapshot of a `GtkGLArea`, which is NULL -- or a picture back on a fixed total height, which gave a six-signal event 26 px a signal and an eight-signal one 20: a smear, not a waveform |
| `recorded` | 1 while the trigger panel's switch is on | the switch not wired, or recording defaulting off (it must be on: the cache is for the event you did not expect) |
| `pane` | 1 once the history button has been clicked | the button's handler not revealing the pane |
| `live` | `button_visible=0 pane=0** | a pane or a button on screen with no trigger mode: a column of nothing beside a live plot |
| `after trigger on` | `button_visible=1 pane=0` | the pane revealed by trigger mode alone (it must be on demand from the toolbar button), or the button missing in trigger mode |
| `recording off, trigger off` and `recording off, trigger on` | `button_visible=1 pane=1 recording=0` in both **once the cache holds anything** (the probe's own run does by this point) | the button going away with the switch when events are already written -- an event on disk outlives the switch that wrote it -- or `pane_w` not being 240 with the pane open. With an *empty* cache the same two lines read `button_visible=0 pane=0`, which is the empty column the item exists to prevent |
| `page` (on the `dir=` line) | 1 once the `events` page is open (the probe opens it with `open_events_page()`) | a page that did not open, or one that opened before its list was refreshed |
| `plot_w_stable`, `plot_w=a/b` | 1, with the plot area's width before and after the first latch equal and both > 0 (756/756 on the documented window) | events squeezing the plot again (the old pane did), or two unrealised `-1` widths read as equal (the probe requires > 0) |
| `available`, `dock_sensitive`, `page_sensitive` (the lines headed `live, recording off`, `recording on, no trigger`, `after trigger on`, `after events page opened`, `recording off, trigger off/on`) | the three equal on every line, because both views follow `sync_events_availability()`: `0 0 0` live with recording off and an empty cache; `1 1 1` once recording is on, **and it stays `1 1 1` in trigger mode and with the page open** (a replay or a group leaves trigger mode, and a block that greyed itself under that click is the trap) | the dock block and the page disagreeing (one of the two not wired to the sync), or the block going `0` under trigger mode. After recording is switched off the lines read `1 1 1` **once the cache holds anything** (the probe's own run does by this point): an event on disk outlives the switch that wrote it. With an *empty* cache they read `0 0 0` -- insensitive, **never hidden** ("disable, do not hide") |
| `page` (on the availability lines) | `none` until the probe opens the page, then `events` | the page not registered (`open_events_page()` returning false), or a page left open by an earlier leg |
| `open alone` | `page=events page_box=` a width near the slide-over's (347 on the documented window) | the page's box measuring 0 (an unrevealed `GtkRevealer` as the content instead of `events_box`), or a page not opened at all |
| `dock events` | `same_titles=1`, `rows` equal `pane_rows` (both capped at 24: `pane_rows` is cut to the dock's `kDockEventsMax`), `sensitive=1`, **`dock_w` below 280** (268: the block fits the fixed dock; more means a row that no longer ellipsizes), `max_ok=1` (dock children <= 24 + the "N more" row). `on_screen` counts filled markers: **1 after a replay, 0 while live or in a group**; `marked` is the index of the first filled row (-1 when none), `expected_marked=1` when the filled row is the replayed record's file (-1 where no replay is named); `members` counts hollow markers: **2 with a comparison up**, 0 otherwise. The `at=` text names the moment | `same_titles=0` is the two lists rebuilt from different data or one of them stale (the dock follows every latch, the page only while open or on Manage -- a latch landing between the page's refresh and the read can show `rows=9 pane_rows=8` once, which is a probe-time race, not a defect, if it does not persist); `on_screen` above 1 is two filled markers (`event_on_screen` not shared with the title); `members=0` in a comparison is the group not marking its members; `max_ok=0` is the cap lost |
| `dock replay` | `clicked=1 replaying=1 waveform_changed=1`; the second line, from live, reads `clicked=1 replaying=1` | the dock row's click not reaching `replay_event()` (`clicked=0` with a non-empty list), or the click rebuilding the dock list under its own handler. With an empty cache (`rows=0`) it reads `clicked=0 replaying=0`, which is not a defect |
| `thumbnail`, `thumb_size`, `thumb_visible` | 1, a non-zero size, and 1 | the picture loaded through `gtk_picture_set_filename()`, which fails silently (the paint-able is there, the allocation is there, nothing is drawn), or a `GtkPicture` given a size request and no cap -- its natural size is its content, so the row's text is squeezed off. `thumb_size` is the reading that catches both. It also catches the cache never being *created*: `event_preview_texture()` answers from the cache or not at all, so a reader that does not create one leaves **every** row blank, which is what `thumbnail=0` meant for as long as it read 0 |
| `preview redrawn` | `stored=480x<44*signals>` and `row_drawn=1` | a preview that cannot be repaired. The `.npy` is the event and the `.bmp` is only a picture of it, so a write that failed, a truncated file or a cache from an older build must be redrawn rather than shown as a blank row -- `row_drawn=0` is a row the user reported as "sometimes missing" |
| `axis`, `marker`, `pane_box` (in `activated row0`) | `[-pre, +post]` of the event just opened, `marker=0.000`, and a `pane_box` close to `pane_w` | an opened event framed on something other than its own pin; a trigger line left **parked** (a large negative) rather than on the edge, which is a file opened without its marker; or a pane whose *rendered* width is 0 while the divider says 240 -- the position is what was asked for, `pane_box` is what the pane actually got, and a divider at 240 with a zero-width pane is still no pane |
| `axis`, `marker` (in `activated row0`) | `[-pre, +post]` of the event just opened, `marker=0.000` | an opened event framed on something other than its own pin; a trigger line left **parked** (a large negative) rather than on the edge, which is a file opened without its marker. `page=events` and `page_box` (the page's width, as in `open alone`) say the page is still up and has a width: a replay from the page must not close it |
| `rows_kept`, `selected` (in `activated row0`) | `rows_kept=1` and `selected=N/N` -- the counts either side of the activation equal | the list **rebuilt** by the activation. It is the row *object* that is compared, not its index: a rebuilt list has a row 0 too, and only identity tells a list that was left alone from one that was torn down under the user's scroll position and their comparison selection. `mark_events_on_screen()` must rewrite the titles in place |
| `replaying` (last line) | 0 | a replay with no way back: *Live* must start a fresh client and thread |
| `window`, `trigger`, `channel`, `level` (in `live restored`) | each equal to its `(want …)` | the way back out of a file view discarding what the live view was set to. One wrong half names which writer won: a `window` that reads the **session file's** value is the deferred `apply_layout()` beating the restore; `trigger=0` is the toggle the file view switched off and nobody switched back; an empty or shifted `channel` is an index restored instead of a name, after a rebuild renumbered (or a group renamed) the channels; and a `level` that moved on its own is `rebuild_trigger_signals()` re-defaulting it because `trigger_level_touched` was not restored with the configuration |
| `pending`, `gen` (in `live restored`) | `pending=0`, and the two generations different | the restore still waiting. It is gated on the stream's first CONFIG, so `pending=1` says the channel list the stored trigger name needs never arrived -- with `channels=0` behind it, a session file that enabled nothing |
| `pinned`, `held`, `mode` (in `live restored`) | `pinned=0`, **or** `pinned=1` with `held=1` | a file's pin surviving the way back, which is a live view that cannot be navigated. `apply_time_window()` drops it; the second reading is the legitimate case -- a restored trigger that was *on* can latch again before the probe reads, and a held capture is pinned, correctly |
| `pane_w` | the divider's position: 0 while the pane is closed, its remembered width (240 by default) while it is open | the pane reserving a column while closed (a paned hands its start child its minimum unless the position says otherwise), or a width that is not remembered |
| `tick` | 1 in live mode, **0** once a file is on screen | the signal list still offering "acquire this signal" for samples that are already stored |
| `window` (in `activated row0`) | the **event's** span -- the probe sets 0.5 s just before the click, so reading the event's span says opening it framed it | a replay that leaves the previous window alone, or one that restates it every frame instead of once |
| `compare buttons` | `selected=2` and both buttons sensitive | the buttons reading something other than the selection, or staying insensitive with a valid pair selected |
| `compare` | `kind=1 rows=1 channels=8 samples0=33045 traces0=4`, `pane=1` | `samples0` above the stamps pushed -- a live producer left running, pushing four stream signals into the group's eight channels; `rows=1` because the layout is the user's and must not be re-derived; `traces0=4` because the four slots the layout holds are renamed, not replaced |
| `compare` | `kind=1 rows=1 channels=8 samples0=33045 traces0=4`, `page=events` | `samples0` above the stamps pushed -- a live producer left running, pushing four stream signals into the group's eight channels; `rows=1` because the layout is the user's and must not be re-derived; `traces0=4` because the four slots the layout holds are renamed, not replaced |
| `compare plots` | `area_b=1 rows_a=1 rows_b=1 list=4 same_x=1` | no second plot on screen, a second plot with a different row count, two plots not on one x window, or `list` above the signal count -- the panel listing each event's copy of a signal as a signal of its own |
| `compare sync` | `mirrored=1`, with `y_a` and `y_b` equal to the range the probe set on the first plot | the mirror not running, or running the wrong way: a y (or x span) change in one plot that the other never sees |
| `accumulate` | `kind=2 rows=1 channels=8 samples0=33045 traces0=8 window=2.000` | the second event's layers not landing: `traces0` stays at the four signals the layout holds when the slots were not free, or the row named after a previous group's suffix so nothing binds |
@@ -2360,14 +2382,15 @@ is what the compact-row CSS rule is for.
## `events pane:`
```
[udpscope] events pane: compare=1 accumulate=1 list=1 revealer=1 nested=1
[udpscope] events pane: compare=1 accumulate=1 list=1 nested=1
```
The events pane's frame read from the window: the two group actions, the list, and the revealer the `.ui`
builds, plus `nested` — the list being *inside* that revealer, which is the one thing a lost id cannot show
(a list parented elsewhere reads 1 everywhere else). `[events]` is the other half, and it drives the pane end
to end: recording off, recording on, the trigger, the history button that reveals it, a row click and the
replay, then Compare and Accumulate — so the file's buttons are read by being used.
The `events` page's frame read from the window (the page's content is `events_box` of `events-pane.ui`; there is
no revealer, divider or header toggle any more, so none is read): the two group actions, the list, plus `nested`
— the list being *inside* `events_box`, which is the one thing a lost id cannot show (a list parented elsewhere
reads 1 everywhere else). **All four must read 1.** `[events]` is the other half, and it drives the page end
to end: recording off, recording on, the trigger, the page opened with `open_events_page()`, a row activation
and the replay, then Compare and Accumulate — so the file's buttons are read by being used.
## `[windowpop]`
+34 -25
View File
@@ -433,14 +433,14 @@ precedent, not an exception.
config that has seen the first-run tour, which would close the page it opens (see the next-but-one bullet).
The reading's own entry is in `probe-readings.md`. (Probe name `dock_section_probe`; the variable and the
`[side-tab-probe]` tag keep their old names so existing scripts keep working. The measurements used to be a
dock section the probe scrolled out of view, with padding; the dock now holds only readouts, so there is
dock section the probe scrolled out of view, with padding; the dock now holds the readouts and the Events block, so there is
no scroll and no padding.)
- `UDPSCOPE_PROBE_CURSORS_MS` and `UDPSCOPE_PROBE_MEASURES_MS` also print the dock's copy of what they read:
a `dock cursors:` line after every `[cursors-probe]` report and a `dock measures:` line at the
measures probe's added / settled / final stops. **`same_text=1` on every one** is the assertion that the
dock and the page show one string; `fits=1` that the Readouts box is no wider than the dock. Entries in
`probe-readings.md`. The exit chain also prints one `[readouts]` line (pages registered, the dock's one
child, the dock's width, the pages' sensitivity, whether the stream has signals) on every exit-dump run.
`probe-readings.md`. The exit chain also prints one `[readouts]` line (pages registered, the dock page's three
children, the dock's width, the pages' sensitivity, whether the stream has signals) on every exit-dump run.
- `UDPSCOPE_PROBE_DOCK_SESSION_MS=2500` with `UDPSCOPE_EXIT_AFTER_MS=8000` -- the session file's `dock` key,
end to end: the dock hidden, saved and reloaded; shown again; and a file from the previous version
(`side_tab = "measure"`, no `dock`) loaded without an error. Three `[dock-session]` lines, each `ok=1`;
@@ -721,41 +721,50 @@ as distinct from the fault injection below, which exercises ingest:
leg of it (`title='Opening <basename>' detail='reading the recording'`, before `waveform loaded:`).
A run that names its source with `--server` prints **no** `loading:` line at all: every other probe
here is on that path and must stay on it.
- **The event cache round-trips, and only where it is available.** `UDPSCOPE_PROBE_EVENTS_MS` reads
the pane's availability first, in the state the app starts in -- `[events] live: button_visible=0
pane=0 recording=1`, **no pane and no button without trigger mode** -- then turns trigger mode on
through the real toggle (`[events] after trigger on: button_visible=1 pane=0`: the button appears,
the pane stays collapsed) and clicks the button, waits for a latch, then reads the cache and replays
the newest event through the pane's own row:
- **The event cache round-trips, and only where it is available.** `UDPSCOPE_PROBE_EVENTS_MS` needs a
config that has seen the tour (it opens a slide-over page) and a throwaway `XDG_CACHE_HOME`. It reads the
availability first, in the state the app starts in -- `[events] live, recording off: available=0
dock_sensitive=0 page_sensitive=0 page=none` (the dock block and the page agree, both insensitive and neither
hidden) -- then switches recording on (`available=1 dock_sensitive=1 page_sensitive=1`; **trigger mode is not
a condition**, and the line after the trigger is turned on reads the same), opens the `events` page with
`open_events_page()` (`page=events`, `[events] open alone: page=events page_box=347`), waits for a latch, then
reads the cache and replays the newest event through the page's own row:
`[events] dir=… n=1 newest: dur=2.000000 pre=1.000000 post=1.000000 edge=… signals=4 samples=20001
rate=10000 seq=1 thumb_bytes=230454 recorded=1 pane=1`, then
`[events] activated row0: replaying=1 thumbnail=1 thumb_size=108x36 thumb_visible=1` (the size is
what says the picture is *on screen*: a paint-able alone is not), `[events] replayed: channels=4
samples=20001 replaying=1 waveform=1` and `[events] live again: replaying=0 waveform=0`. It ends on
the pane's availability, through the trigger panel's switch: `[events] recording off, trigger off` and
`[events] recording off, trigger on` -- once the cache holds events (the probe's own run has written
some by this point) both read `button_visible=1 pane=1 recording=0`, because a button that shows the
cache must not go away with the switch that wrote it; against an *empty* cache the same two lines read
`button_visible=0 pane=0`, which is the empty column the item exists to prevent. **`samples` must equal the
export's row count for the same window** (`20001` for a 2 s event at 10 kS/s), `thumb_bytes` must be
a real BMP (`54 + 480*160*3`-ish, i.e. hundreds of kB, not 0), `thumbnail` must be 1 (the row loaded
the picture) and the last line must read `replaying=0` -- a replay with no way back is a one-way
rate=10000 seq=1 thumb_bytes=230454 recorded=1 page=1 plot_w_stable=1 plot_w=756/756` (**`plot_w_stable=1`**: the
first latch did not move the plot, which the old pane did), then
`[events] activated row0: replaying=1 page=events … thumbnail=1 thumb_size=138x46 thumb_visible=1` (the size is
what says the picture is *on screen*: a paint-able alone is not), the dock's reading of the same cache --
`[events] dock events: rows=N pane_rows=N same_titles=1 sensitive=1 dock_w=268 on_screen=1 … at=after pane
replay` -- then **one click on a dock row**, `[events] dock replay: clicked=1 replaying=1 waveform_changed=1`,
and the dock read again (`marked=1`, `expected_marked=1`: exactly one filled marker, on the replayed record),
`[events] replayed: channels=4 samples=20001 replaying=1 waveform=1` and `[events] live again: replaying=0
waveform=0`. It ends on the availability, through the trigger panel's switch: `[events] recording off,
trigger off` and `[events] recording off, trigger on` -- once the cache holds events (the probe's own run has
written some by this point) both read `available=1 dock_sensitive=1 page_sensitive=1`, because a block that
shows the cache must not go away with the switch that wrote it; against an *empty* cache (and on the E-1608,
which never latches) the same two lines read `0 0 0` with the page still open -- insensitive, never hidden.
**`samples` must equal the export's row count for the same window** (`20001` for a 2 s event at 10 kS/s),
`thumb_bytes` must be a real BMP (`54 + 480*160*3`-ish, i.e. hundreds of kB, not 0), `thumbnail` must be 1 (the
row loaded the picture) and the last line must read `replaying=0` -- a replay with no way back is a one-way
door. It then **waits for a second latch** (bounded: twelve retries) and drives the two group layouts
through the buttons: `[events] compare buttons: cached=2 selected=2 compare=1 accumulate=1` (the
buttons read the selection), `[events] halve`… `[events] halves a_first=20001 a_second=0 b_first=0
b_second=20001` -- **the two halves of the ring, and the reading that says a comparison's gap is a
gap: the first event's channel is finite only in the first half and the second's only in the second**
-- `[events] compare: kind=1 rows=4 channels=8 samples0=40002 traces0=2 traces1=2 window=4.000
recording=1 pane=1` (`samples0` must be exactly the stamps pushed, **not more**: a live producer left
running pushes its own four signals into the group's eight channels), `[events] compare plots:
recording=1 page=events` (`samples0` must be exactly the stamps pushed, **not more**: a live producer left
running pushes its own four signals into the group's eight channels), the dock line `members=2 … at=comparison
up` (the hollow markers on the group's members, which only the dock shows), `[events] compare plots:
area_b=1 rows_a=1 rows_b=1 x_a=[-2.000,0.000] x_b=[-2.000,0.000] same_x=1` (the second plot exists, is
on screen, mirrors the row count, and both are on one x window), then the sync -- the probe sets a y
range nobody would arrive at by accident on the *first* plot and reads the second a few frames later:
`[events] compare sync: y_a=-3.500 y_b=-3.500 mirrored=1` -- and `[events] accumulate: kind=2 rows=1
channels=8 samples0=33045 selected=2 traces0=8 window=2.000` (`traces0=8`: the four signals the layout
holds plus one layer each for the second event, **in the same row**, which is what an accumulation
means). Run it with a throwaway
`XDG_CACHE_HOME` and `UDPSCOPE_EXIT_AFTER_MS` long enough for the second latch (20 s).
means). The exit dump adds `events pane: compare=1 accumulate=1 list=1 nested=1` and the readouts line's
`dock_children=3`. Run it with a throwaway
`XDG_CACHE_HOME` and `UDPSCOPE_EXIT_AFTER_MS` long enough for the second latch (20 s). The dock row cap
(24) is only exercised by a cache that has filled past it: `max_ok=1`.
- **A waveform round-trips.** Export from a live run (`UDPSCOPE_EXPORT_MS` with
`UDPSCOPE_EXPORT_PATH`, against the mock at 10 kS/s), then open that file with `--open` and an
exit dump. The load must print