working on todo.md features
This commit is contained in:
1 parent
210d124814
commit
8f54841c2c
110 files changed
+15932
-6576
No files matched your search
@@ -144,3 +144,134 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
`process()` uses that for `last_trigger_s_`, `pending_start_s_` **and** the pre-window gate —
|
||||
all three, or the marker, the cut and the gate would disagree with each other by the width of
|
||||
a pulse.
|
||||
|
||||
## 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.
|
||||
|
||||
## A comparison is two plots of one model
|
||||
|
||||
*Compare* and *accumulate* are still one model -- the buffers cannot hold two independent loads -- but a
|
||||
comparison is **two plots** over it, and which plot draws what is only a matter of which channel *names*
|
||||
its rows bind. `App::rows` is the primary (`RowSet`, area, gestures); `App::rows_b` is the second, with
|
||||
its own `RowSet` and its own hidden `PlotArea` in a paned, deliberately without the drop target, the
|
||||
right-click menu or the frame counter: those act on the primary's rows, and a menu on the second plot
|
||||
that rearranged the first plot's rows would be a menu lying about its subject.
|
||||
|
||||
Rules that cost a defect each:
|
||||
|
||||
- **The layout is the user's and is mirrored, never reinvented.** The group build must not call
|
||||
`set_row_count()`; it reads the rows' slot names and re-names the same slots for each layer
|
||||
(`… (A)`, `… (B)` for a comparison; a bare name for layer 0 and `… #2`, `… #3` for the extra layers of
|
||||
an accumulation). An *accumulation* puts its extra layers into free slots -- the row that holds the
|
||||
signal first, then any row with room -- so the user's traces never move. The default layout is four
|
||||
signals in one row, which is why `kSlotsPerRow` is 8: with four, "accumulate these two events" had
|
||||
nowhere to put the second copy and silently did nothing.
|
||||
- **A layer is not a signal.** A comparison's model holds one channel per (signal, layer), and the
|
||||
*panel* must not treat the copies as signals of their own: the signal list shows one row per signal
|
||||
(`layer_base_name()`, the shared rule that takes `(A)`, `(B)` and `#N` back off), `signal_color_index()`
|
||||
counts *signals* and not channels -- counting channels spread one signal's colour across the palette
|
||||
-- and both plots of a comparison bind their signal's layers to the *same* colour index, because two
|
||||
colours would say they were different signals. The reading is `list=4` in `[events] compare plots`
|
||||
for a two-event comparison of four signals.
|
||||
- **A slot name carries the last group's suffix, so it must be stripped before it is re-derived**:
|
||||
after a comparison the rows are named `sine (A)`, and a following accumulation that mapped that name
|
||||
again bound to no channel at all (`traces0=0`, a plot with nothing in it). `base_name()` takes
|
||||
`(A)`, `(B)` and `#N` back off.
|
||||
- **The axes are mirrored from the ranges, once per frame.** The plotter owns its window and a gesture
|
||||
reaches it through the plot's own setter with nothing announcing it, so the frame compares each row's
|
||||
signature with the previous frame's: whichever side moved is the side the user touched, and the other
|
||||
is set to match. The signature is `{span, y.min, y.max}` when the x axis has a **live edge**
|
||||
(`x_live_edge()`, not merely an armed follow: an armed axis with nothing streaming keeps a *stored*
|
||||
window, whose edges a pan moves and whose span never changes) and `{x.min, x.max, y.min, y.max}`
|
||||
otherwise. Comparing the edges under a live edge would read as "both sides moved" on every frame --
|
||||
the window is recomputed as a distance behind the newest sample -- and a zoom in one plot would be
|
||||
overwritten by the other's drift. When both moved the first plot is the reference, which is also what
|
||||
makes the frame after a build converge.
|
||||
- **`set_y_range()` clears that axis' auto-scale**, by design: a mirrored zoom is a user decision, and
|
||||
*Fit* re-arms it.
|
||||
|
||||
## A group of events is one model, built once
|
||||
|
||||
*Compare* and *accumulate* are the same machinery: several event files in **one** model, because the
|
||||
buffers cannot hold two independent loads -- `load_waveform_into_model` calls `set_signals()`, which
|
||||
rebuilds them, so a second load takes the first with it. So the group is built by hand: one channel per
|
||||
(signal, layer) pair, `set_target_span()` to cover the whole group *before* `set_signals()` (or the
|
||||
oldest samples are written past the ring's end), then all of the events pushed in one pass. A row is
|
||||
one **signal** and its slots are the events, which is what makes a comparison like for like and an
|
||||
accumulation a density; the trace alpha (0.35 while accumulating, 1 otherwise) is what turns agreement
|
||||
into brightness.
|
||||
|
||||
Two things every group must do, both of which cost a defect to learn:
|
||||
|
||||
- **Stop the acquisition first.** A live producer keeps pushing into the model it was started with --
|
||||
and it pushes *four* stream signals into the group's *eight* channels, so the group's own samples are
|
||||
the ones that go missing. The reading that catches it is the samples the busy channel holds
|
||||
(`samples0` in the `[events] compare` line): it must be exactly the number of stamps pushed, not
|
||||
that plus whatever the stream added.
|
||||
- **Free the slots the group does not fill.** A row carries the previous view's binding, and a trace
|
||||
left in slot 2 of a two-layer row is drawn as if it belonged to the group.
|
||||
|
||||
*Compare* shifts the first event so its span *ends* where the second's *begins*, and fills the half a
|
||||
sample does not belong to with **NaN** -- the plotter excludes non-finite samples from every series and
|
||||
from every pyramid bucket, so a NaN is a gap and not a repeat, and no line is drawn across the join.
|
||||
|
||||
**A replay changes the data and frames it once; it never rearranges the layout.** A row plots a signal
|
||||
*name*, and an event's export carries the same names the stream does, so loading the event into the
|
||||
model leaves every row, slot, colour and the row count pointing at the right signal by itself. So
|
||||
`replay_event()` must **not** rebind the rows (`bind_channels_to_rows()`, which is what a freshly opened
|
||||
*file* needs) -- that is the user's work, and a click on a list is not a request to rearrange it. The
|
||||
window is restated **once, on open** (`apply_time_window(span)`, the only path allowed to change the
|
||||
scale), so the whole event is framed the moment it is opened and the user's own zooms are left alone
|
||||
afterwards; never per frame. The measurements, the cursors and the readouts then report the stored
|
||||
event, because they read the model. What a replay *does* change is the model's contents and the trigger:
|
||||
it leaves trigger mode, without which the display stays `Held` and draws the old snapshot.
|
||||
|
||||
**A file view has nothing to acquire.** The signal list's acquire tick is *hidden* -- not disabled --
|
||||
whenever a file is on screen (`App::waveform_name` non-empty: a replay, a comparison, a `--open`run).
|
||||
The samples are stored, "acquire this signal" is a stream's question, and a greyed tick invites the user
|
||||
to look for a reason there is none for. Going live rebuilds the panel, so the ticks come back.
|
||||
|
||||
**"The same length" is the same count at the same rate, not the same span to the sample.** Two events
|
||||
this app recorded itself differ in span by the fraction of a sample the acquisition's window happened to
|
||||
fill to (0.9999 s of a 1 s pre-window is normal). Demanding one sample interval made a comparison of two
|
||||
just-recorded events refuse to open; the check allows four intervals or a millisecond, whichever is
|
||||
larger, and the merged stamp grid carries the difference as a gap.
|
||||
|
||||
**A replay must leave trigger mode.** `display_mode()` answers `Held` whenever the model holds a
|
||||
snapshot, and a held display draws *the snapshot*, not the live channels -- so replaying an event with
|
||||
trigger mode still armed loaded the file into the model and left the old capture on screen: a click
|
||||
that looks like it did nothing, with the event sitting in the ring unread. The replay also frames the
|
||||
view with `apply_time_window(report.span_s)`, so the click shows the whole event rather than the part
|
||||
of it that fell inside the live window.
|
||||
|
||||
The switch that decides recording belongs to the **trigger panel**, not to the pane it empties: it
|
||||
is a trigger option (it decides whether a latch is written at all), and a pane must not be the only
|
||||
place to reach the control that stops it.
|
||||
|
||||
|
||||
Every published snapshot is written to the event cache while recording is on: its table (the *export*
|
||||
format, so `--open` reads it back), a sidecar of what the list shows, and a preview drawn from the
|
||||
samples. Three rules keep that honest:
|
||||
|
||||
- **The record happens on the GTK thread, at the end of the tick that framed the snapshot**, not on
|
||||
the producer. The snapshot is taken by `shared_ptr` (a latch landing mid-write cannot pull the data
|
||||
away), and a `record_pending` flag defers the write to the end of the frame so the rows are already
|
||||
bound to the event when the picture is drawn. Recording from the producer would put file I/O
|
||||
between two `recv()` calls, which is the one thing the packet capture's design also refuses.
|
||||
- **Nothing a latch does may be expensive.** The write is a `write_npy` of the event's own window and
|
||||
a 480x160 raster; a 2 s, four-channel event is ~230 kB of picture and ~600 kB of samples. The pane
|
||||
is refreshed, not rebuilt, and a failure to write is a line on stderr -- a cache that cannot be
|
||||
written must not stop the scope.
|
||||
- **The preview is drawn from the data, never captured from the window.** The plot is a `GtkGLArea`:
|
||||
its ink is not in the widget's paint tree, so `gtk_snapshot_free_to_node()` on a widget paintable of
|
||||
it returns NULL and the "screenshot" would be an empty file. Drawing it in the store also keeps that
|
||||
class GTK-free, which is what makes the cache testable without a window.
|
||||
Reference in new issue
Block a user