Fixed triggering mode change when acquiring

This commit is contained in:
Martino Ferrari committed 2026-10-01 17:54:17 +02:00
1 parent 3d4085898e
commit 0d741c401b
10 files changed
+504 -106

No files matched your search

+61 -12
View File
@@ -95,12 +95,45 @@ of the live ring into an immutable `EventSnapshot` and publishes it atomically.
Pre-trigger history is what the ring already holds, so the percentage is a share of
the event window and not of the buffer.
In trigger mode a trace is bound to the snapshot and to nothing else: the last
event latched, or nothing at all before the first one. Never the live ring, not
even while the post window is filling — a picture that changes only on an event is
the whole point of trigger mode (SPECS: "data visualisation should not be refreshed
till next event"), and binding the live ring mid-capture made the plot scroll for
most of every event cycle.
**A capture fills in view** (`DisplayMode::Capturing`). The moment an edge is accepted —
which, by the gate below, means the pre-trigger history is already complete — the axis is
pinned to the whole event window `[-pre, +post]` with the edge at x = 0, and the trace
follows the live ring inside it until the snapshot lands. So the pre side is there
immediately, the post side sweeps in from x = 0 to the right, and at the latch the
snapshot replaces the live binding on an axis that does not move: the only visible change
is that the picture stops.
This is a **deliberate deviation from the spec**, requested explicitly. SPECS:37 says the
visualisation "should not be refreshed till next event", and an earlier iteration of this
app read that strictly — a trace was bound to the snapshot and to nothing else, never the
live ring, not even while the post window was filling. What that reading cost was the whole
of every capture: for as long as the post side took to fill, the plot showed the *previous*
event with no sign that anything was happening. The literal rule is gone; what it was
protecting is not. A **held** event is still bound to the snapshot alone
and is never quietly overwritten by live data, and the axis is still the event's own
window throughout — a capture is live data on a frozen frame, not a live view.
The four display states are `Live` (no trigger), `Capturing`, `Held` (the frozen
snapshot) and `Waiting` (armed, nothing captured yet: blank). `ScopeModel::display_mode()`
answers which, and the frame tick reads it once and hands it to the bind, the display
epoch, the axis pin and the status line — the producer can open or close a capture between
any two reads, and the four disagreeing about which clock they are on is how a trace ends
up drawn a window away from its axis.
**Disarm during a capture keeps what the capture got to.** It cuts the window at the
newest sample, publishes that as the held event and freezes it — the axis tightens to the
part that filled (`[-pre, +0.3 s]` for a 1 s post window disarmed 300 ms in) with the edge
still at x = 0. It used to lose the capture outright: clearing the engine's pending flag
dropped the display out of `Capturing` with nothing published behind it, so the view fell
back to the *previous* event, or to blank if this was the first. No event fired, so the
latch counter does not move; a separate `ScopeModel::event_seq()` counts published
snapshots, and that is what the frame tick keys its framing on.
Mid-capture the live ring is read on the *event's* clock: `display_epoch()` is set to
minus the edge, so `x = 0` is the edge for the live channels exactly as it is in a
snapshot's own stamps. That re-basing machinery already existed (it was written for the
frozen view and then never needed, since snapshots carry edge-relative stamps); this is
what finally uses it.
**The whole trigger path measures the window in stream seconds, and never in
samples.** `TriggerEngine` is fed each sample's timestamp and latches once that clock
@@ -222,14 +255,15 @@ line always names the state it is in:
|---|---|
| `[live]` | trigger mode off: the view follows the stream continuously |
| `[trigger: armed, waiting for an event]` | watching for an edge; the view shows the previous state |
| `[trigger: CAPTURING (post-trigger window filling)]` | an edge was accepted; the post side is filling in view |
| `[trigger: EVENT HELD …]` | an event was latched; the view is frozen on it |
| `[trigger: disarmed (view frozen)]` | armed and waiting are not the same thing — nothing will arrive until it is armed again |
While an event is on screen a **red vertical line marks the trigger instant** on
every row, at the edge's own stream time — the latch may complete a post-window
later, but the line marks the edge, so the pre/post split can be read straight off
the plot. It is not one of the user's cursors: it has its own colour, ignores the
cursor show/hide toggle, and is parked (invisible) when no event is held.
While an event is on screen — being captured or held — a **red vertical line marks the
trigger instant** on every row, at x = 0. The latch completes a post-window later, but
the line marks the edge, so the pre/post split can be read straight off the plot. It is
not one of the user's cursors: it has its own colour, ignores the cursor show/hide
toggle, and is parked (invisible) when there is no event.
**The status line.** It starts with an activity badge and then the mode, the
connection, the measured rate, the packet/frame/gap counters, what plot 1 holds and
@@ -253,7 +287,10 @@ the buffer's size and span. Three of those are worth calling out:
then minutes, then hours. A frozen view looks the same whether the last event was a
second or an hour ago, so without it there is no way to tell a working trigger from
a dead one. Measured on the wall clock, not the stream's: it is a question about how
long *you* have been waiting. Before the first latch it reads `no event yet`.
long *you* have been waiting. The clock starts at the **edge**, not at the latch — that
is the instant the user saw, and it is also when the capture appeared on screen — so a
long capture reads as an event that is already ageing rather than one that has not
happened. Before the first event it reads `no event yet`.
Switching the header bar's **trigger** button off returns to `[live]` and resumes
drawing the stream; the trigger panel carries the same action as a
@@ -403,6 +440,12 @@ answered: a live view keeps scrolling while the file chooser is up. The live cut
through `ScopeModel::live_window_locked()`, the same index range `publish_event()`
uses, so an export and a capture of the same window hold the same samples.
The export follows the display, not the trigger switch: it reads the snapshot only while
one is **held**, and the live ring otherwise — including mid-capture, where the rows are
on the live ring too. So exporting during a capture writes the partly filled window that
is on screen (`time` from `-pre` up to wherever the post side has reached), not the
previous event.
## Review findings not (yet) implemented
These are known deviations, found by a spec/code review, that are deliberate for
@@ -429,6 +472,12 @@ now — they are listed so they are not mistaken for anything else:
- **Pre-trigger % is a share of the *event* window**, not of the retained history —
see the trigger section above for why (the spec's sentence predates the event
window being a separate control).
- **A capture is drawn as it fills**, against SPECS:37's "should not be refreshed till
next event", which the app used to honour literally. Requested explicitly: a capture
whose post window takes any appreciable time showed the previous event and no sign of
progress. What the spec was protecting still holds — a *held* event is bound to its
snapshot alone, and the axis is the event's own window from the edge onwards rather
than a sliding live view.
- **A plot holds at most four 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