Fixed triggering mode change when acquiring
This commit is contained in:
1 parent
3d4085898e
commit
0d741c401b
10 files changed
+504
-106
No files matched your search
@@ -42,7 +42,13 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||||
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_TRIG_TRACE_MS` — schedule individual probes. `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_FIELDS_MS` — change the trigger panel's edge and Normal/Single
|
||||
dropdowns. As much a crash test as a state check (see the GTK signal-signature invariant
|
||||
below), so time it inside a capture: `[fields-probe] ... capturing=1` followed by no
|
||||
SIGSEGV *is* the assertion.
|
||||
- `UDPSCOPE_PROBE_PAUSE_MS` — pause acquisition mid-run so layout numbers are
|
||||
measured on a still picture.
|
||||
- `UDPSCOPE_EXPORT_MS` (+ optional `UDPSCOPE_EXPORT_PATH`, default
|
||||
@@ -75,11 +81,20 @@ Two lines of the self-check output are assertions in disguise:
|
||||
the documented probes have shown one ~320 px step — the side panel's width — that
|
||||
reverts; it is intermittent and unexplained, so read `changes` before calling a range
|
||||
the twitch bug.
|
||||
- `[trig-probe] ... trigger=on ... display=...` (stderr, `UDPSCOPE_TRIG_TRACE_MS`) —
|
||||
with `trigger=on`, `display` must read `event(frozen)` or `nothing`, **never**
|
||||
`live`, except for the single interval that contains the toggle itself — recognise
|
||||
it by `latches=0` *and* the next probe reading `nothing` with the same frozen
|
||||
`axis=`. And `snap=[a..b]` must equal `axis=[a..b]`, with `b` positive by roughly
|
||||
- `[trig-probe] ... trigger=on ... display=... epoch=...` (stderr,
|
||||
`UDPSCOPE_TRIG_TRACE_MS`) — with `trigger=on`, `display` must read `event(frozen)`,
|
||||
`live(capturing)` or `nothing`, **never plain `live`**, except for the single interval
|
||||
that contains the toggle itself — recognise it by `latches=0` *and* the next probe
|
||||
reading `nothing` or `live(capturing)` with a frozen `axis=`. Plain `live` with the
|
||||
trigger on is the old "the plot keeps scrolling in trigger mode" defect; the named
|
||||
`live(capturing)` is the feature. In that state `epoch=` must equal minus the capture's
|
||||
edge and `axis=` must stay at `[-pre..+post]` across consecutive probes while
|
||||
`data=`'s upper bound climbs towards `+post` — an axis that moves instead means the
|
||||
capture is being treated as a live view (`clamp_to_data()` reaching a pinned window).
|
||||
`epoch=` must be back to `0.0000` in every other state. Note the probe reads
|
||||
`pending=` fresher than `display=` (which is last tick's decision), so a single
|
||||
`pending=0 display=live(capturing)` line is one frame of sampling skew, not a defect.
|
||||
And `snap=[a..b]` must equal `axis=[a..b]`, with `b` positive by roughly
|
||||
the post-trigger share: that is the "post window is empty" defect's observable.
|
||||
**`drawn=[a..b]` must equal `data=[a..b]`** in both modes: `data` is `x_at()` (what
|
||||
the crosshair reads) and `drawn` is `x0() + gather()` (what the renderer maps), so a
|
||||
@@ -109,6 +124,15 @@ Two lines of the self-check output are assertions in disguise:
|
||||
is the same class of bug, not rounding: `publish_event()` clamps the slice to every
|
||||
value ring's `produced()`, so publishing before each channel of the latching sample
|
||||
has been pushed silently drops it.
|
||||
- `[disarm-probe] was_capturing=1 display=.. seq=A->B events=C->D snap=[a..b] n=N
|
||||
axis=[..]` (stderr, `UDPSCOPE_PROBE_DISARM_MS`) — with `was_capturing=1`, **`display`
|
||||
must read `event(frozen)`** and `snap`/`axis` must be `[-pre..e]` where `e` is how far
|
||||
the post window had filled, with `n` that span times the stream's real rate plus one.
|
||||
`display=nothing` (or a `snap` from the *previous* event) is the "disarming mid-capture
|
||||
makes the event completely disappear" defect. **`seq` must advance by exactly one while
|
||||
`events` does not move**: no event fired, so the partial publish deliberately leaves
|
||||
`trigger_latches()` alone — which is also what keeps the `trigger events: N total, M at
|
||||
the disarm` line in the dump meaning what it says.
|
||||
- `[export-probe] mode=.. axis=[a..b] rows=N cols=M time=[c..d] stream=[..]` (stderr,
|
||||
`UDPSCOPE_EXPORT_MS`) — **`time=` must equal `axis=`** and **`rows=` must be the
|
||||
span times the stream's real rate, plus one** (2 s at 10 kS/s is 20001). The export
|
||||
@@ -116,7 +140,13 @@ Two lines of the self-check output are assertions in disguise:
|
||||
cut from the LOD pyramid, and a `time=` narrower than `axis=` is a window searched on
|
||||
the wrong clock (the live branch subtracts `display_epoch()` before searching the
|
||||
ring). In `mode=event`, `stream=` must be the edge ± the window; in `mode=live` it
|
||||
equals `time=`. The two `bytes=` lines are the writers: the `.npy` is
|
||||
equals `time=`.
|
||||
The one exception to both: an export taken **mid-capture** reports `mode=live` with
|
||||
`time=[-pre..e]` where `e` is how far the post window has filled, and `stream=` offset
|
||||
from it by the edge (so `stream - time` reads the edge exactly) — the export is what is
|
||||
on screen, and during a capture that is a partly filled window. Pin the probe outside a
|
||||
capture when checking the `rows=` count.
|
||||
The two `bytes=` lines are the writers: the `.npy` is
|
||||
`64·⌈(10+len(header)+1)/64⌉ + rows·(16 + 4·cols)`, so a size that is not that is a
|
||||
mispacked record. Read it back with `pd.DataFrame(np.load(path))` after changing
|
||||
either writer.
|
||||
@@ -146,7 +176,9 @@ once per frame by the UI (`tick()` in `src/ui.cpp`) around `RowSet::bind()`/`acq
|
||||
Samples themselves cross via `plot::data::RingBuffer` (single producer, single
|
||||
consumer) and never through the lock. Cross-thread scalars are atomics on `ScopeModel`
|
||||
(`trigger_enabled_`, `trigger_armed_`, `trigger_latches_`, `trigger_time_`,
|
||||
`event_`, `generation_`).
|
||||
`event_`, `generation_`). Two of them are a pair: the producer writes `capture_edge_`
|
||||
before `trigger_pending_` and those two carry release/acquire, so a consumer that sees a
|
||||
capture open sees which edge it belongs to.
|
||||
|
||||
### Data path
|
||||
|
||||
@@ -157,10 +189,37 @@ that is what guarantees a full pre-trigger history on the *first* latch after ar
|
||||
The spec's second buffer (item 1) is the latched `EventSnapshot`, which is already an
|
||||
immutable copy of the captured window; see README for why two live rings were dropped.
|
||||
|
||||
In trigger mode `RowSet::bind()` binds each trace to the snapshot and to nothing else
|
||||
(the last event, or nothing before the first). **Never the live ring**, not even while
|
||||
the post window is filling — SPECS:37 says the view must not refresh until the next
|
||||
event, and `trigger_pending()` is informational only, for the status line's wording.
|
||||
What a trace binds to is `ScopeModel::display_mode()`, one of four states, and
|
||||
`RowSet::bind()` takes it as an argument rather than recomputing it:
|
||||
|
||||
- `Live` — no trigger: the live ring on the stream clock.
|
||||
- `Capturing` — an edge has been accepted (so the pre window was full) and the post
|
||||
window is still filling: the live ring, on the coming event's own clock
|
||||
(`display_epoch() == -edge`), inside an axis already pinned to `[-pre, +post]`. The
|
||||
post side fills in view and the marker sits at x = 0 throughout.
|
||||
- `Held` — the frozen `EventSnapshot` of the last event.
|
||||
- `Waiting` — armed, nothing captured yet: blank.
|
||||
|
||||
**Disarm during a capture publishes what the capture got to**, then disarms: the producer
|
||||
cuts the window at the newest sample and that partial snapshot becomes the held event, so
|
||||
the picture on screen at the moment of the Disarm stays on screen. `engine.disarm()` clears
|
||||
`pending_`, which drops the display out of `Capturing`, and with nothing published the view
|
||||
fell back to the *previous* event — or to blank on a first capture. No event fired, so
|
||||
`trigger_latches()` is deliberately not bumped; `ScopeModel::event_seq()` counts published
|
||||
snapshots instead, and the frame tick keys its event framing on *that*. Keep the two
|
||||
distinct: the latch count is what the probes assert "how many events fired" with.
|
||||
|
||||
`Capturing` is a **deliberate departure from SPECS:37** ("data visualisation should not
|
||||
be refreshed till next event"), asked for explicitly; that reading is what the old "never
|
||||
the live ring mid-capture" rule enforced. What survives of the rule is that `Held` binds
|
||||
the snapshot *only* — a held event is never quietly replaced by live data — and that the
|
||||
*axis* does not move during a capture: it is the event's window from the edge onwards,
|
||||
never a sliding live view.
|
||||
|
||||
`display_mode()` must be read **once per frame** and passed around (the tick does this,
|
||||
and hands it to `bind()`). The producer can open or close a capture between any two
|
||||
reads, and two reads in one frame put the bind, the epoch, the pin and the status line on
|
||||
different clocks.
|
||||
|
||||
Layers to keep straight:
|
||||
|
||||
@@ -265,6 +324,15 @@ far the largest file.
|
||||
capture may legitimately be shorter than asked for — the ring need not reach back a
|
||||
full pre-trigger share on the first latch after arming, so `snap=[-0.339..1.000]`
|
||||
for a ±1 s request is correct there rather than a defect.
|
||||
- **A capture in progress is the one place the axis is pinned from the *request*.** There
|
||||
is no snapshot to measure yet, so `DisplayMode::Capturing` frames
|
||||
`[-(span - post_span_s(span)), +post_span_s(span)]` — computed with the producer's own
|
||||
`TriggerConfig::post_span_s()`, not a second expression that rounds differently, because
|
||||
the marker at x = 0 has to land where the coming cut will put it. The latch then re-pins
|
||||
from the snapshot, and the two agreeing is why the picture does not jump at the latch.
|
||||
- **`clamp_to_data()` must be skipped while `capturing` as well as while `event_held`.**
|
||||
A capture is live data on a pinned axis — the exact combination the clamp was written
|
||||
for and against: it would follow the live edge and drag the frozen window off the event.
|
||||
- **`publish_event()` cuts one absolute index range and uses it for the time ring and
|
||||
every value ring.** That is what makes `stamps` and each `values[c]` equal-length
|
||||
and index-aligned by construction instead of by two truncations that can disagree —
|
||||
@@ -288,6 +356,15 @@ far the largest file.
|
||||
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.
|
||||
- **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
|
||||
serve both: on the `notify::` connection the `GParamSpec*` lands in the `user_data` slot
|
||||
and the handler dereferences it as its `App*`. `G_CALLBACK` is a cast, so nothing warns.
|
||||
That is why the trigger panel's spin buttons and its dropdowns have separate thin entry
|
||||
points (`on_trigger_field_changed`, `on_trigger_drop_changed`) onto one
|
||||
`publish_trigger_fields()`: changing the edge or Normal/Single dropdown used to segfault
|
||||
outright. `UDPSCOPE_PROBE_FIELDS_MS` is the guard.
|
||||
- **The header bar's buttons are icon-only**, so headless probes must reach them
|
||||
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
|
||||
`find_labelled_button()`. The Arm/Disarm button in the trigger panel still has text.
|
||||
|
||||
Reference in new issue
Block a user