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
|
then dump the whole pipeline state to stdout (frame/sample counts, per-plot painted
|
||||||
frames, trace bindings, panel sizes, status line in both states).
|
frames, trace bindings, panel sizes, status line in both states).
|
||||||
- `UDPSCOPE_ENABLE_ALL_MS`, `UDPSCOPE_ARM_TRIGGER_MS`, `UDPSCOPE_PROBE_DISARM_MS`,
|
- `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
|
- `UDPSCOPE_PROBE_PAUSE_MS` — pause acquisition mid-run so layout numbers are
|
||||||
measured on a still picture.
|
measured on a still picture.
|
||||||
- `UDPSCOPE_EXPORT_MS` (+ optional `UDPSCOPE_EXPORT_PATH`, default
|
- `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
|
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
|
reverts; it is intermittent and unexplained, so read `changes` before calling a range
|
||||||
the twitch bug.
|
the twitch bug.
|
||||||
- `[trig-probe] ... trigger=on ... display=...` (stderr, `UDPSCOPE_TRIG_TRACE_MS`) —
|
- `[trig-probe] ... trigger=on ... display=... epoch=...` (stderr,
|
||||||
with `trigger=on`, `display` must read `event(frozen)` or `nothing`, **never**
|
`UDPSCOPE_TRIG_TRACE_MS`) — with `trigger=on`, `display` must read `event(frozen)`,
|
||||||
`live`, except for the single interval that contains the toggle itself — recognise
|
`live(capturing)` or `nothing`, **never plain `live`**, except for the single interval
|
||||||
it by `latches=0` *and* the next probe reading `nothing` with the same frozen
|
that contains the toggle itself — recognise it by `latches=0` *and* the next probe
|
||||||
`axis=`. And `snap=[a..b]` must equal `axis=[a..b]`, with `b` positive by roughly
|
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.
|
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
|
**`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
|
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
|
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
|
value ring's `produced()`, so publishing before each channel of the latching sample
|
||||||
has been pushed silently drops it.
|
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,
|
- `[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
|
`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
|
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
|
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
|
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
|
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
|
`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
|
mispacked record. Read it back with `pd.DataFrame(np.load(path))` after changing
|
||||||
either writer.
|
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
|
Samples themselves cross via `plot::data::RingBuffer` (single producer, single
|
||||||
consumer) and never through the lock. Cross-thread scalars are atomics on `ScopeModel`
|
consumer) and never through the lock. Cross-thread scalars are atomics on `ScopeModel`
|
||||||
(`trigger_enabled_`, `trigger_armed_`, `trigger_latches_`, `trigger_time_`,
|
(`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
|
### 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
|
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.
|
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
|
What a trace binds to is `ScopeModel::display_mode()`, one of four states, and
|
||||||
(the last event, or nothing before the first). **Never the live ring**, not even while
|
`RowSet::bind()` takes it as an argument rather than recomputing it:
|
||||||
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.
|
- `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:
|
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
|
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]`
|
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.
|
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
|
- **`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
|
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 —
|
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
|
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
|
badge's markup is likewise rewritten on a change of phase (twice a second), never on
|
||||||
all sixty frames.
|
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
|
- **The header bar's buttons are icon-only**, so headless probes must reach them
|
||||||
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
|
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
|
||||||
`find_labelled_button()`. The Arm/Disarm button in the trigger panel still has text.
|
`find_labelled_button()`. The Arm/Disarm button in the trigger panel still has text.
|
||||||
|
|||||||
@@ -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
|
Pre-trigger history is what the ring already holds, so the percentage is a share of
|
||||||
the event window and not of the buffer.
|
the event window and not of the buffer.
|
||||||
|
|
||||||
In trigger mode a trace is bound to the snapshot and to nothing else: the last
|
**A capture fills in view** (`DisplayMode::Capturing`). The moment an edge is accepted —
|
||||||
event latched, or nothing at all before the first one. Never the live ring, not
|
which, by the gate below, means the pre-trigger history is already complete — the axis is
|
||||||
even while the post window is filling — a picture that changes only on an event is
|
pinned to the whole event window `[-pre, +post]` with the edge at x = 0, and the trace
|
||||||
the whole point of trigger mode (SPECS: "data visualisation should not be refreshed
|
follows the live ring inside it until the snapshot lands. So the pre side is there
|
||||||
till next event"), and binding the live ring mid-capture made the plot scroll for
|
immediately, the post side sweeps in from x = 0 to the right, and at the latch the
|
||||||
most of every event cycle.
|
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
|
**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
|
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 |
|
| `[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: 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: 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 |
|
| `[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
|
While an event is on screen — being captured or held — a **red vertical line marks the
|
||||||
every row, at the edge's own stream time — the latch may complete a post-window
|
trigger instant** on every row, at x = 0. The latch completes a post-window later, but
|
||||||
later, but the line marks the edge, so the pre/post split can be read straight off
|
the line marks the edge, so the pre/post split can be read straight off the plot. It is
|
||||||
the plot. It is not one of the user's cursors: it has its own colour, ignores the
|
not one of the user's cursors: it has its own colour, ignores the cursor show/hide
|
||||||
cursor show/hide toggle, and is parked (invisible) when no event is held.
|
toggle, and is parked (invisible) when there is no event.
|
||||||
|
|
||||||
**The status line.** It starts with an activity badge and then the mode, the
|
**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
|
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
|
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
|
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
|
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
|
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
|
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()`
|
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.
|
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
|
## Review findings not (yet) implemented
|
||||||
|
|
||||||
These are known deviations, found by a spec/code review, that are deliberate for
|
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 —
|
- **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
|
see the trigger section above for why (the spec's sentence predates the event
|
||||||
window being a separate control).
|
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`
|
- **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.
|
cannot give up a trace and the slots are therefore created up front.
|
||||||
- **One toggle drives both cursor orientations.** The vertical markers and the
|
- **One toggle drives both cursor orientations.** The vertical markers and the
|
||||||
|
|||||||
@@ -349,6 +349,32 @@ void Acquisition::on_data(const udps_frame_t* frame) {
|
|||||||
model_.trigger_armed().store(true, std::memory_order_relaxed);
|
model_.trigger_armed().store(true, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
} else if (engine.armed()) {
|
} else if (engine.armed()) {
|
||||||
|
// Disarm cuts a capture in progress where it got to and keeps it, instead of
|
||||||
|
// discarding it. `engine.disarm()` clears `pending_`, which drops the display out of
|
||||||
|
// `Capturing`; with nothing published the view fell back to the *previous* event, or
|
||||||
|
// to blank if this was the first -- the capture the user was watching simply
|
||||||
|
// vanished. Publishing first means the same snapshot path freezes it, so what is on
|
||||||
|
// screen at the moment of the Disarm stays on screen.
|
||||||
|
//
|
||||||
|
// The full post window is requested and `live_window_locked()` clips it to what the
|
||||||
|
// rings hold, so the snapshot ends at the newest sample and reports its own shorter
|
||||||
|
// `post_s` -- the same way a first latch whose pre window was short reports its own
|
||||||
|
// `pre_s`. No event fired, so `trigger_latches()` is deliberately *not* bumped; the
|
||||||
|
// UI notices the new picture through `event_seq()`.
|
||||||
|
if (engine.pending()) {
|
||||||
|
const double edge = engine.last_trigger_time();
|
||||||
|
model_.publish_event(event_span - post_want, post_want, edge);
|
||||||
|
model_.trigger_time().store(edge, std::memory_order_relaxed);
|
||||||
|
model_.trigger_time_valid().store(true, std::memory_order_relaxed);
|
||||||
|
auto snap = model_.event();
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[udpscope] trigger: disarmed mid-capture, edge at %.3f s kept as "
|
||||||
|
"[-%.4f..%.4f] n=%llu (post window asked %.4f s, filled %.4f s)\n",
|
||||||
|
edge, snap ? snap->pre_s : 0.0, snap ? snap->post_s : 0.0,
|
||||||
|
static_cast<unsigned long long>(snap ? snap->stamps.size() : 0), post_want,
|
||||||
|
engine.post_elapsed());
|
||||||
|
std::fflush(stderr);
|
||||||
|
}
|
||||||
engine.disarm();
|
engine.disarm();
|
||||||
model_.trigger_armed().store(false, std::memory_order_relaxed);
|
model_.trigger_armed().store(false, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
@@ -437,6 +463,11 @@ void Acquisition::on_data(const udps_frame_t* frame) {
|
|||||||
last_stamp_ = last_stamp_used;
|
last_stamp_ = last_stamp_used;
|
||||||
|
|
||||||
model_.trigger_armed().store(engine.armed(), std::memory_order_relaxed);
|
model_.trigger_armed().store(engine.armed(), std::memory_order_relaxed);
|
||||||
|
// The edge goes out before the flag that makes the UI read it (release/acquire on
|
||||||
|
// `trigger_pending`), so a tick that sees a capture open sees which edge it is for. A
|
||||||
|
// capture that opens and latches inside this one frame never reports pending, and that
|
||||||
|
// is right: the display goes straight to the frozen event.
|
||||||
|
if (engine.pending()) model_.set_capture_edge(engine.last_trigger_time());
|
||||||
model_.set_trigger_pending(engine.pending());
|
model_.set_trigger_pending(engine.pending());
|
||||||
static std::int64_t next_engine_log = 0;
|
static std::int64_t next_engine_log = 0;
|
||||||
const std::int64_t now_e = static_cast<std::int64_t>(frame->recv_time * 1.0e6);
|
const std::int64_t now_e = static_cast<std::int64_t>(frame->recv_time * 1.0e6);
|
||||||
|
|||||||
+6
-4
@@ -98,11 +98,13 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
|
|||||||
: std::string("ch") + std::to_string(c);
|
: std::string("ch") + std::to_string(c);
|
||||||
};
|
};
|
||||||
|
|
||||||
// What the rows bind to is what gets exported, which is the same branch `RowSet::bind()`
|
// What the rows bind to is what gets exported, so the split is the same `DisplayMode`
|
||||||
// takes: the frozen event in trigger mode, the live ring otherwise. Reading the ring
|
// `RowSet::bind()` switches on: the frozen event only while one is held, the live ring
|
||||||
// while an event is on screen would export data the user cannot see.
|
// otherwise -- including mid-capture, where the rows follow the live ring too. Reading
|
||||||
|
// the ring while an event is on screen would export data the user cannot see, and
|
||||||
|
// reading the stale snapshot mid-capture would export the *previous* event.
|
||||||
std::shared_ptr<const EventSnapshot> snap = model.event();
|
std::shared_ptr<const EventSnapshot> snap = model.event();
|
||||||
if (model.trigger_enabled() && snap != nullptr) {
|
if (model.display_mode() == DisplayMode::Held && snap != nullptr) {
|
||||||
// The snapshot is immutable and already index-aligned, so the visible window is two
|
// The snapshot is immutable and already index-aligned, so the visible window is two
|
||||||
// binary searches over its stamps and the same index range for every channel.
|
// binary searches over its stamps and the same index range for every channel.
|
||||||
const std::vector<f32>& st = snap->stamps;
|
const std::vector<f32>& st = snap->stamps;
|
||||||
|
|||||||
@@ -350,6 +350,9 @@ void ScopeModel::publish_event(double pre_s, double post_s, double edge) {
|
|||||||
snap->pre_s = snap->stamps.empty() ? 0.0 : -static_cast<double>(snap->stamps.front());
|
snap->pre_s = snap->stamps.empty() ? 0.0 : -static_cast<double>(snap->stamps.front());
|
||||||
snap->post_s = snap->stamps.empty() ? 0.0 : static_cast<double>(snap->stamps.back());
|
snap->post_s = snap->stamps.empty() ? 0.0 : static_cast<double>(snap->stamps.back());
|
||||||
event_.store(std::move(snap), std::memory_order_release);
|
event_.store(std::move(snap), std::memory_order_release);
|
||||||
|
// Bumped after the snapshot is in place, so a consumer that sees the new sequence
|
||||||
|
// number sees the snapshot it counts.
|
||||||
|
event_seq_.fetch_add(1, std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferStore::set_epoch(f64 epoch) noexcept {
|
void BufferStore::set_epoch(f64 epoch) noexcept {
|
||||||
|
|||||||
+48
-7
@@ -127,6 +127,21 @@ struct StreamStats {
|
|||||||
std::atomic<bool> have_config{false};
|
std::atomic<bool> have_config{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// What the plot is showing, which is one decision made once per frame: the bind, the
|
||||||
|
/// export and the status wording all have to agree, and the producer can latch between
|
||||||
|
/// any two of them.
|
||||||
|
///
|
||||||
|
/// `Capturing` is the live-updating capture: an edge has been accepted (so the pre
|
||||||
|
/// window was full) and the post window is still filling, and the plot follows the live
|
||||||
|
/// ring on an edge-relative axis until the snapshot lands. It is a deliberate departure
|
||||||
|
/// from SPECS:37 ("not refreshed till next event") -- see README.
|
||||||
|
enum class DisplayMode {
|
||||||
|
Live, ///< no trigger: the live ring, stream clock
|
||||||
|
Capturing, ///< an edge was accepted: the live ring, edge-relative clock
|
||||||
|
Held, ///< the frozen snapshot of the last event
|
||||||
|
Waiting, ///< armed, nothing captured yet: blank
|
||||||
|
};
|
||||||
|
|
||||||
/// The application's data model: the stream description, the two sample buffers,
|
/// The application's data model: the stream description, the two sample buffers,
|
||||||
/// the trigger configuration and the counters. Structural changes (a CONFIG
|
/// the trigger configuration and the counters. Structural changes (a CONFIG
|
||||||
/// update, an enable/disable, a calibration) rebuild the buffers, so all mutating
|
/// update, an enable/disable, a calibration) rebuild the buffers, so all mutating
|
||||||
@@ -191,6 +206,11 @@ class ScopeModel {
|
|||||||
std::shared_ptr<const EventSnapshot> event() const noexcept {
|
std::shared_ptr<const EventSnapshot> event() const noexcept {
|
||||||
return event_.load(std::memory_order_acquire);
|
return event_.load(std::memory_order_acquire);
|
||||||
}
|
}
|
||||||
|
/// How many snapshots `publish_event()` has produced. Not interchangeable with
|
||||||
|
/// `trigger_latches()`: a Disarm during a capture publishes what the capture got to
|
||||||
|
/// *without* an event having fired, so this counts "there is a new picture to frame"
|
||||||
|
/// while the latch count keeps answering "how many events fired".
|
||||||
|
std::uint64_t event_seq() const noexcept { return event_seq_.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
/// Publishes the live buffer's new samples to its `ChannelBuffer`s.
|
/// Publishes the live buffer's new samples to its `ChannelBuffer`s.
|
||||||
void acquire_all();
|
void acquire_all();
|
||||||
@@ -211,16 +231,35 @@ class ScopeModel {
|
|||||||
}
|
}
|
||||||
void set_trigger_enabled(bool on);
|
void set_trigger_enabled(bool on);
|
||||||
|
|
||||||
/// Whether the engine is mid-capture (an edge was seen and the post-trigger
|
/// Whether the engine is mid-capture (an edge was seen -- so the pre window was
|
||||||
/// window is still filling). Purely informational: it drives the status line's
|
/// full -- and the post-trigger window is still filling). This is what puts the
|
||||||
/// wording. The plot deliberately does *not* react to it -- in trigger mode the
|
/// display in `DisplayMode::Capturing`: the plot follows the live ring, on an axis
|
||||||
/// picture changes only on a latch (SPECS: "data visualisation should not be
|
/// pinned to the coming event's window, until the snapshot replaces it.
|
||||||
/// refreshed till next event").
|
///
|
||||||
|
/// Release/acquire, not relaxed: `capture_edge()` is written before this is set and
|
||||||
|
/// has to be visible to a consumer that sees the flag.
|
||||||
bool trigger_pending() const noexcept {
|
bool trigger_pending() const noexcept {
|
||||||
return trigger_pending_.load(std::memory_order_relaxed);
|
return trigger_pending_.load(std::memory_order_acquire);
|
||||||
}
|
}
|
||||||
void set_trigger_pending(bool on) noexcept {
|
void set_trigger_pending(bool on) noexcept {
|
||||||
trigger_pending_.store(on, std::memory_order_relaxed);
|
trigger_pending_.store(on, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stream time of the edge of the capture `trigger_pending()` refers to. Written
|
||||||
|
/// by the producer before it raises the flag, so a consumer that sees the flag sees
|
||||||
|
/// this too; stale once the flag clears.
|
||||||
|
f64 capture_edge() const noexcept { return capture_edge_.load(std::memory_order_relaxed); }
|
||||||
|
void set_capture_edge(f64 e) noexcept {
|
||||||
|
capture_edge_.store(e, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the plot should bind to this frame. Read it *once* per frame and pass the
|
||||||
|
/// answer around: calling it twice can straddle a latch, and then the bind and the
|
||||||
|
/// axis disagree about which clock they are on.
|
||||||
|
DisplayMode display_mode() const noexcept {
|
||||||
|
if (!trigger_enabled()) return DisplayMode::Live;
|
||||||
|
if (trigger_pending()) return DisplayMode::Capturing;
|
||||||
|
return event() != nullptr ? DisplayMode::Held : DisplayMode::Waiting;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The x origin the plot uses: 0 for "seconds since the stream started", or minus
|
/// The x origin the plot uses: 0 for "seconds since the stream started", or minus
|
||||||
@@ -283,7 +322,9 @@ class ScopeModel {
|
|||||||
f64 epoch_ = 0.0;
|
f64 epoch_ = 0.0;
|
||||||
std::atomic<double> event_span_{1.0};
|
std::atomic<double> event_span_{1.0};
|
||||||
std::atomic<bool> trigger_pending_{false};
|
std::atomic<bool> trigger_pending_{false};
|
||||||
|
std::atomic<double> capture_edge_{0.0};
|
||||||
std::atomic<std::shared_ptr<const EventSnapshot>> event_; ///< the event on screen
|
std::atomic<std::shared_ptr<const EventSnapshot>> event_; ///< the event on screen
|
||||||
|
std::atomic<std::uint64_t> event_seq_{0};
|
||||||
std::atomic<double> display_epoch_{0.0};
|
std::atomic<double> display_epoch_{0.0};
|
||||||
std::atomic<bool> trigger_armed_{false};
|
std::atomic<bool> trigger_armed_{false};
|
||||||
std::atomic<bool> trigger_enabled_{false};
|
std::atomic<bool> trigger_enabled_{false};
|
||||||
|
|||||||
+20
-14
@@ -143,7 +143,7 @@ int RowSet::channel_of(int row_index, int slot) const {
|
|||||||
return row(row_index).channel[static_cast<std::size_t>(slot)];
|
return row(row_index).channel[static_cast<std::size_t>(slot)];
|
||||||
}
|
}
|
||||||
|
|
||||||
void RowSet::bind(ScopeModel& model) {
|
void RowSet::bind(ScopeModel& model, DisplayMode mode) {
|
||||||
const std::vector<SignalInfo>& signals = model.signals();
|
const std::vector<SignalInfo>& signals = model.signals();
|
||||||
const std::vector<ChannelInfo>& channels = model.channels();
|
const std::vector<ChannelInfo>& channels = model.channels();
|
||||||
for (int i = 0; i < kMaxRows; ++i) {
|
for (int i = 0; i < kMaxRows; ++i) {
|
||||||
@@ -181,20 +181,26 @@ void RowSet::bind(ScopeModel& model) {
|
|||||||
r.slots[si].bind(nullptr);
|
r.slots[si].bind(nullptr);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// In trigger mode the trace shows the captured event and nothing else: the
|
// The source is the frame's one `DisplayMode`, decided by the caller: `Held` is
|
||||||
// last one latched, or nothing at all before the first. Never the live ring,
|
// the frozen snapshot, `Waiting` is blank (armed, nothing captured yet), and both
|
||||||
// not even while the post window is filling -- a frozen picture that only
|
// `Live` and `Capturing` are the live ring -- what differs between those two is
|
||||||
// changes on an event is the whole point of trigger mode, and binding the
|
// the *clock*, which the display epoch carries, not the binding.
|
||||||
// live ring mid-capture made the plot scroll for most of every event cycle.
|
|
||||||
const std::size_t ci = static_cast<std::size_t>(channel);
|
const std::size_t ci = static_cast<std::size_t>(channel);
|
||||||
if (model.trigger_enabled()) {
|
switch (mode) {
|
||||||
if (auto snap = model.event()) {
|
case DisplayMode::Held:
|
||||||
r.slots[si].bind_event(std::move(snap), ci);
|
if (auto snap = model.event()) {
|
||||||
} else {
|
r.slots[si].bind_event(std::move(snap), ci);
|
||||||
r.slots[si].bind(nullptr); // armed, waiting for the first event: blank
|
} else {
|
||||||
}
|
r.slots[si].bind(nullptr);
|
||||||
} else {
|
}
|
||||||
r.slots[si].bind(model.live().shared_channel(ci));
|
break;
|
||||||
|
case DisplayMode::Waiting:
|
||||||
|
r.slots[si].bind(nullptr);
|
||||||
|
break;
|
||||||
|
case DisplayMode::Live:
|
||||||
|
case DisplayMode::Capturing:
|
||||||
|
r.slots[si].bind(model.live().shared_channel(ci));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-4
@@ -117,10 +117,13 @@ class RowSet {
|
|||||||
/// The channel index a row/slot holds, or -1.
|
/// The channel index a row/slot holds, or -1.
|
||||||
int channel_of(int row, int slot) const;
|
int channel_of(int row, int slot) const;
|
||||||
|
|
||||||
/// Rebinds every slot to what it should show this frame: the model's live buffer,
|
/// Rebinds every slot to what `mode` says it should show this frame. Called every
|
||||||
/// or -- in trigger mode -- the captured event. Called every frame, because the
|
/// frame, because the channel list is renumbered by any structural change.
|
||||||
/// channel list is renumbered by any structural change.
|
///
|
||||||
void bind(ScopeModel& model);
|
/// `mode` is passed in rather than read from the model, so that one frame's bind, axis
|
||||||
|
/// and status line all describe the same display: the producer can open or close a
|
||||||
|
/// capture between any two reads of `ScopeModel::display_mode()`.
|
||||||
|
void bind(ScopeModel& model, DisplayMode mode);
|
||||||
/// Publishes the live buffer's new samples.
|
/// Publishes the live buffer's new samples.
|
||||||
void acquire(ScopeModel& model) { model.acquire_all(); }
|
void acquire(ScopeModel& model) { model.acquire_all(); }
|
||||||
|
|
||||||
|
|||||||
+231
-53
@@ -99,12 +99,26 @@ struct App {
|
|||||||
/// Trigger-mode state for the status line: how many latches were on screen last
|
/// Trigger-mode state for the status line: how many latches were on screen last
|
||||||
/// frame, and whether an event is held (which is what "frozen" means).
|
/// frame, and whether an event is held (which is what "frozen" means).
|
||||||
std::uint64_t seen_latches = 0;
|
std::uint64_t seen_latches = 0;
|
||||||
|
/// Which snapshot was framed last. Separate from `seen_latches` because a Disarm during
|
||||||
|
/// a capture publishes a snapshot without an event having fired.
|
||||||
|
std::uint64_t seen_event_seq = 0;
|
||||||
bool event_held = false;
|
bool event_held = false;
|
||||||
/// Monotonic time of the latch `seen_latches` counts, so the status line can age it.
|
/// Whether a capture is open on screen: the edge has been accepted and the post window
|
||||||
|
/// is filling in view, on the coming event's own clock. It is what keeps
|
||||||
|
/// `clamp_to_data()` off the pinned axis and tells the status line apart from "armed".
|
||||||
|
bool capturing = false;
|
||||||
|
/// The edge of the capture `capturing` refers to, so a second edge accepted before the
|
||||||
|
/// first capture closed re-frames instead of being mistaken for the same one.
|
||||||
|
double capture_edge = 0.0;
|
||||||
|
/// Monotonic time of the last event, and the edge that started the clock. The clock
|
||||||
|
/// starts when the *edge* is accepted, not when the capture closes -- that is the
|
||||||
|
/// instant the user saw -- and the edge is kept so the latch that ends a live-updated
|
||||||
|
/// capture does not restart it.
|
||||||
/// Wall time, not stream time: "how long since the last event" is a question about the
|
/// Wall time, not stream time: "how long since the last event" is a question about the
|
||||||
/// user's wait, and a stream whose clock runs at any other speed would answer it wrong.
|
/// user's wait, and a stream whose clock runs at any other speed would answer it wrong.
|
||||||
/// 0 until the first latch of the session.
|
/// 0 until the first event of the session.
|
||||||
gint64 last_latch_us = 0;
|
gint64 last_latch_us = 0;
|
||||||
|
double timed_edge = 0.0;
|
||||||
double window_choices_limit = -1.0; ///< the cap the dropdown was last built with
|
double window_choices_limit = -1.0; ///< the cap the dropdown was last built with
|
||||||
/// The rate each channel actually *stored* over the last second, and the declared rate
|
/// The rate each channel actually *stored* over the last second, and the declared rate
|
||||||
/// beside it. The app does no decimation, so the stored rate is the datasource's own
|
/// beside it. The app does no decimation, so the stored rate is the datasource's own
|
||||||
@@ -133,6 +147,10 @@ struct App {
|
|||||||
int dbg_reset_visible = -1;
|
int dbg_reset_visible = -1;
|
||||||
std::vector<std::uint64_t> last_stored;
|
std::vector<std::uint64_t> last_stored;
|
||||||
std::uint64_t dbg_latches_at_disarm = 0;
|
std::uint64_t dbg_latches_at_disarm = 0;
|
||||||
|
std::uint64_t dbg_seq_at_disarm = 0;
|
||||||
|
bool dbg_capturing_at_disarm = false;
|
||||||
|
int dbg_disarm_polls = 0;
|
||||||
|
int dbg_disarm_in_capture = 0;
|
||||||
int dbg_swaps = 0;
|
int dbg_swaps = 0;
|
||||||
int dbg_axes_w_min = 1 << 20;
|
int dbg_axes_w_min = 1 << 20;
|
||||||
int dbg_axes_w_max = -(1 << 20);
|
int dbg_axes_w_max = -(1 << 20);
|
||||||
@@ -448,6 +466,7 @@ void on_arm_clicked(GtkButton* button, gpointer user) {
|
|||||||
}
|
}
|
||||||
if (armed) {
|
if (armed) {
|
||||||
app->event_held = false;
|
app->event_held = false;
|
||||||
|
app->capturing = false;
|
||||||
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
|
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
// Arming is a statement that this signal should be watched, so its acquisition
|
// Arming is a statement that this signal should be watched, so its acquisition
|
||||||
@@ -457,8 +476,7 @@ void on_arm_clicked(GtkButton* button, gpointer user) {
|
|||||||
gtk_button_set_label(GTK_BUTTON(app->arm_button), armed ? "Disarm" : "Arm");
|
gtk_button_set_label(GTK_BUTTON(app->arm_button), armed ? "Disarm" : "Arm");
|
||||||
}
|
}
|
||||||
|
|
||||||
void on_trigger_field_changed(GtkWidget*, gpointer user) {
|
void publish_trigger_fields(App* app) {
|
||||||
App* app = static_cast<App*>(user);
|
|
||||||
if (app->updating_panels) return;
|
if (app->updating_panels) return;
|
||||||
// Read the widgets first (they may be touched again by a re-entrant handler if a
|
// Read the widgets first (they may be touched again by a re-entrant handler if a
|
||||||
// setter ran while the lock was held), then publish the values under the lock the
|
// setter ran while the lock was held), then publish the values under the lock the
|
||||||
@@ -481,6 +499,18 @@ void on_trigger_field_changed(GtkWidget*, gpointer user) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void on_trigger_field_changed(GtkWidget*, gpointer user) {
|
||||||
|
publish_trigger_fields(static_cast<App*>(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same publish, for the two dropdowns. It needs its own entry point because
|
||||||
|
/// `notify::<prop>` passes a `GParamSpec*` ahead of the user data while
|
||||||
|
/// `GtkSpinButton::value-changed` does not: a two-parameter callback on a `notify::`
|
||||||
|
/// signal reads the pspec as its `App*` and crashes on the first field it touches.
|
||||||
|
void on_trigger_drop_changed(GObject*, GParamSpec*, gpointer user) {
|
||||||
|
publish_trigger_fields(static_cast<App*>(user));
|
||||||
|
}
|
||||||
|
|
||||||
void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user) {
|
void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user) {
|
||||||
App* app = static_cast<App*>(user);
|
App* app = static_cast<App*>(user);
|
||||||
if (app->updating_panels) return;
|
if (app->updating_panels) return;
|
||||||
@@ -897,6 +927,7 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
|
|||||||
signal = tc.channel;
|
signal = tc.channel;
|
||||||
}
|
}
|
||||||
app->event_held = false;
|
app->event_held = false;
|
||||||
|
app->capturing = false;
|
||||||
if (!on) {
|
if (!on) {
|
||||||
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
|
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
|
||||||
app->model->set_display_epoch(0.0); // back to seconds since the stream started
|
app->model->set_display_epoch(0.0); // back to seconds since the stream started
|
||||||
@@ -1317,12 +1348,30 @@ gboolean tick(gpointer user) {
|
|||||||
}
|
}
|
||||||
if (model.generation() != app->signal_generation) rebuild_signal_panels(*app);
|
if (model.generation() != app->signal_generation) rebuild_signal_panels(*app);
|
||||||
|
|
||||||
|
// What the display shows, decided once for the whole frame: the producer can open or
|
||||||
|
// close a capture between any two reads of the model, and the bind, the epoch, the pin
|
||||||
|
// and the status line all have to describe the same picture.
|
||||||
|
const DisplayMode display = model.display_mode();
|
||||||
|
const double cap_edge = model.capture_edge();
|
||||||
|
const bool new_capture =
|
||||||
|
display == DisplayMode::Capturing && (!app->capturing || cap_edge != app->capture_edge);
|
||||||
|
app->capturing = display == DisplayMode::Capturing;
|
||||||
|
if (new_capture) app->capture_edge = cap_edge;
|
||||||
|
|
||||||
{
|
{
|
||||||
// Serialise against the acquisition thread's CONFIG handling and against the
|
// Serialise against the acquisition thread's CONFIG handling and against the
|
||||||
// UI's own structural changes: the bind takes shared handles into the live
|
// UI's own structural changes: the bind takes shared handles into the live
|
||||||
// store, and acquire() reads it.
|
// store, and acquire() reads it.
|
||||||
std::lock_guard<std::mutex> lock(model.mutex());
|
std::lock_guard<std::mutex> lock(model.mutex());
|
||||||
app->rows->bind(model);
|
// Mid-capture the live ring is drawn on the coming event's clock, so its edge sits at
|
||||||
|
// x = 0 exactly as a snapshot's does and the pinned [-pre, +post] axis means the same
|
||||||
|
// thing in both. Every other mode reads stream seconds. The epoch is a consumer-side
|
||||||
|
// offset on the live channels, so it has to be in place before the bind that hands
|
||||||
|
// them to the traces -- and it is written here, under the lock, because a rebuild can
|
||||||
|
// replace the store.
|
||||||
|
const double want_epoch = display == DisplayMode::Capturing ? -cap_edge : 0.0;
|
||||||
|
if (model.display_epoch() != want_epoch) model.set_display_epoch(want_epoch);
|
||||||
|
app->rows->bind(model, display);
|
||||||
app->rows->acquire(model);
|
app->rows->acquire(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1331,11 +1380,12 @@ gboolean tick(gpointer user) {
|
|||||||
// the group and pushes it to the others, so the "which row acted" search this replaced
|
// the group and pushes it to the others, so the "which row acted" search this replaced
|
||||||
// (and the per-row pan/zoom propagation behind it) has no subject any more.
|
// (and the per-row pan/zoom propagation behind it) has no subject any more.
|
||||||
|
|
||||||
// While a captured event is on screen the window is pinned to [edge-pre,
|
// While an event is on screen -- held, or still being captured -- the window is pinned
|
||||||
// edge+post]; clamp_to_data would fight that pin whenever the bound series
|
// to [-pre, +post]; clamp_to_data would fight that pin whenever the bound series still
|
||||||
// still reports the acquisition buffer's raw coordinates, dragging the view
|
// reports the acquisition buffer's raw coordinates, dragging the view along the ring.
|
||||||
// along the ring. Clamping is a live-view concern only.
|
// Clamping is a live-view concern only, and a capture is live data on a pinned axis,
|
||||||
if (!app->event_held) app->rows->clamp_to_data();
|
// which is exactly the case it must not touch.
|
||||||
|
if (!app->event_held && !app->capturing) app->rows->clamp_to_data();
|
||||||
|
|
||||||
// Curator: the trigger's arm state can change under us (single mode disarms
|
// Curator: the trigger's arm state can change under us (single mode disarms
|
||||||
// itself), so the button and the status line read the published atomics.
|
// itself), so the button and the status line read the published atomics.
|
||||||
@@ -1345,48 +1395,88 @@ gboolean tick(gpointer user) {
|
|||||||
}
|
}
|
||||||
const bool armed = model.trigger_armed().load(std::memory_order_relaxed);
|
const bool armed = model.trigger_armed().load(std::memory_order_relaxed);
|
||||||
gtk_button_set_label(GTK_BUTTON(app->arm_button), armed ? "Disarm" : "Arm");
|
gtk_button_set_label(GTK_BUTTON(app->arm_button), armed ? "Disarm" : "Arm");
|
||||||
|
if (new_capture) {
|
||||||
|
// The edge has been accepted, which means the pre window was already full, so the
|
||||||
|
// whole event window can be framed now and the post side filled in view.
|
||||||
|
//
|
||||||
|
// The split is the *requested* one -- the same `post_span_s()` the producer will cut
|
||||||
|
// with -- because there is no snapshot to read it off yet, and the marker has to sit
|
||||||
|
// where that cut will put it rather than where a different rounding would.
|
||||||
|
double span = 0.0;
|
||||||
|
double post = 0.0;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(model.mutex());
|
||||||
|
span = std::max(1.0e-3, model.event_span());
|
||||||
|
post = model.trigger().post_span_s(span);
|
||||||
|
}
|
||||||
|
app->event_held = false;
|
||||||
|
app->rows->pin_event_window(span - post, post);
|
||||||
|
app->rows->set_trigger_marker(0.0, true);
|
||||||
|
select_window_choice(*app, span);
|
||||||
|
// The user saw the event at its edge, so that is when the counter starts.
|
||||||
|
app->timed_edge = cap_edge;
|
||||||
|
app->last_latch_us = g_get_monotonic_time();
|
||||||
|
}
|
||||||
|
|
||||||
const std::uint64_t latches = model.trigger_latches().load(std::memory_order_relaxed);
|
const std::uint64_t latches = model.trigger_latches().load(std::memory_order_relaxed);
|
||||||
if (latches != app->seen_latches) {
|
if (latches != app->seen_latches) {
|
||||||
app->seen_latches = latches;
|
app->seen_latches = latches;
|
||||||
app->event_held = true;
|
const double edge = model.trigger_time().load(std::memory_order_relaxed);
|
||||||
app->last_latch_us = g_get_monotonic_time();
|
// A capture the UI watched fill already started this clock at its edge; only one that
|
||||||
// The window is pinned to exactly what the snapshot captured, read off the
|
// opened and closed inside a single frame -- never reported pending, so never
|
||||||
// snapshot itself. Not off `event_span`/`pretrigger_pct`: those say what was
|
// displayed as a capture -- starts it here.
|
||||||
// *asked for*, in stream seconds, while how much the capture actually covers
|
if (edge != app->timed_edge) {
|
||||||
// depends on the cadence the trigger channel was running at. Pinning to the
|
app->timed_edge = edge;
|
||||||
// request collapsed the waveform into a sliver at x = 0 and left the rest of
|
app->last_latch_us = g_get_monotonic_time();
|
||||||
// the axis -- the whole post side included -- blank.
|
|
||||||
//
|
|
||||||
// The snapshot is edge-relative (the edge at x = 0), so the axis needs no epoch
|
|
||||||
// shift: snapshot, pin and marker all speak the same coordinates. Rebind first
|
|
||||||
// so the slots hold the snapshot before the pin is applied.
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(model.mutex());
|
|
||||||
app->rows->bind(model);
|
|
||||||
}
|
}
|
||||||
double pre = 0.0;
|
|
||||||
double post = 0.0;
|
|
||||||
if (auto snap = model.event()) {
|
|
||||||
pre = snap->pre_s;
|
|
||||||
post = snap->post_s;
|
|
||||||
}
|
|
||||||
if (!(pre + post > 0.0)) {
|
|
||||||
// Nothing was captured (no samples in the ring yet): fall back to the
|
|
||||||
// requested window so the axis is at least readable.
|
|
||||||
const double span = std::max(1.0e-3, model.event_span());
|
|
||||||
pre = std::min(0.99, model.trigger().pretrigger_pct / 100.0) * span;
|
|
||||||
post = span - pre;
|
|
||||||
}
|
|
||||||
app->rows->pin_event_window(pre, post);
|
|
||||||
app->rows->set_trigger_marker(0.0, true); // the edge sits at x = 0
|
|
||||||
select_window_choice(*app, pre + post);
|
|
||||||
}
|
}
|
||||||
// The trigger instant is marked on every row while its event is the thing on
|
|
||||||
// screen, so the pre/post split around it can be read off the plot.
|
// Framing the held event is keyed on *which snapshot was published*, not on the latch
|
||||||
if (trigger_on && model.trigger_time_valid().load(std::memory_order_relaxed)) {
|
// count: a Disarm during a capture publishes what the capture got to without an event
|
||||||
// x is seconds from the event: the display epoch is minus the edge time, so the
|
// having fired, and that partial has to be framed and held exactly like one that ran
|
||||||
// edge sits at exactly x = 0 wherever the user has zoomed.
|
// to term. The latch count stays the answer to "how many events fired", which is what
|
||||||
} else {
|
// the probes assert on.
|
||||||
|
const std::uint64_t seq = model.event_seq();
|
||||||
|
if (seq != app->seen_event_seq) {
|
||||||
|
app->seen_event_seq = seq;
|
||||||
|
// A publication and the *next* edge can land in the same frame. The display follows
|
||||||
|
// the newer of the two, so re-framing on the snapshot is only right when the snapshot
|
||||||
|
// is what is bound; otherwise the capture above has already framed its own window.
|
||||||
|
if (display == DisplayMode::Held) {
|
||||||
|
app->event_held = true;
|
||||||
|
// The window is pinned to exactly what the snapshot captured, read off the
|
||||||
|
// snapshot itself. Not off `event_span`/`pretrigger_pct`: those say what was
|
||||||
|
// *asked for*, in stream seconds, while how much the capture actually covers
|
||||||
|
// depends on the cadence the trigger channel was running at. Pinning to the
|
||||||
|
// request collapsed the waveform into a sliver at x = 0 and left the rest of
|
||||||
|
// the axis -- the whole post side included -- blank.
|
||||||
|
//
|
||||||
|
// The snapshot is edge-relative (the edge at x = 0) and the epoch is back to 0, so
|
||||||
|
// the bind at the top of this frame already put the slots and the axis on the same
|
||||||
|
// coordinates the pin is about to state.
|
||||||
|
double pre = 0.0;
|
||||||
|
double post = 0.0;
|
||||||
|
if (auto snap = model.event()) {
|
||||||
|
pre = snap->pre_s;
|
||||||
|
post = snap->post_s;
|
||||||
|
}
|
||||||
|
if (!(pre + post > 0.0)) {
|
||||||
|
// Nothing was captured (no samples in the ring yet): fall back to the
|
||||||
|
// requested window so the axis is at least readable.
|
||||||
|
const double span = std::max(1.0e-3, model.event_span());
|
||||||
|
post = model.trigger().post_span_s(span);
|
||||||
|
pre = span - post;
|
||||||
|
}
|
||||||
|
app->rows->pin_event_window(pre, post);
|
||||||
|
app->rows->set_trigger_marker(0.0, true); // the edge sits at x = 0
|
||||||
|
select_window_choice(*app, pre + post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The trigger instant is marked on every row while its event is the thing on screen --
|
||||||
|
// being captured or held. x is seconds from the event in both: the display epoch is
|
||||||
|
// minus the edge while the live ring is on a pinned axis, and the snapshot's own stamps
|
||||||
|
// are edge-relative, so the edge sits at exactly x = 0 wherever the user has zoomed.
|
||||||
|
if (!app->capturing && !(trigger_on && model.trigger_time_valid().load(std::memory_order_relaxed))) {
|
||||||
app->rows->set_trigger_marker(0.0, false);
|
app->rows->set_trigger_marker(0.0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1568,7 +1658,9 @@ gboolean tick(gpointer user) {
|
|||||||
StreamStats& st = model.stats();
|
StreamStats& st = model.stats();
|
||||||
const char* mode = "live";
|
const char* mode = "live";
|
||||||
if (trigger_on) {
|
if (trigger_on) {
|
||||||
if (app->event_held) {
|
if (app->capturing) {
|
||||||
|
mode = "trigger: CAPTURING (post-trigger window filling)";
|
||||||
|
} else if (app->event_held) {
|
||||||
mode = "trigger: EVENT HELD (Arm to re-arm, or 'Go live' for streaming)";
|
mode = "trigger: EVENT HELD (Arm to re-arm, or 'Go live' for streaming)";
|
||||||
} else if (armed && have_trigger_value) {
|
} else if (armed && have_trigger_value) {
|
||||||
static char waiting[192];
|
static char waiting[192];
|
||||||
@@ -1724,9 +1816,9 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
|
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
|
||||||
}
|
}
|
||||||
g_signal_connect(app.trigger_edge_drop, "notify::selected",
|
g_signal_connect(app.trigger_edge_drop, "notify::selected",
|
||||||
G_CALLBACK(on_trigger_field_changed), &app);
|
G_CALLBACK(on_trigger_drop_changed), &app);
|
||||||
g_signal_connect(app.trigger_mode_drop, "notify::selected",
|
g_signal_connect(app.trigger_mode_drop, "notify::selected",
|
||||||
G_CALLBACK(on_trigger_field_changed), &app);
|
G_CALLBACK(on_trigger_drop_changed), &app);
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2068,8 +2160,11 @@ gboolean trig_trace_probe(gpointer user) {
|
|||||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||||
const std::size_t si = static_cast<std::size_t>(s);
|
const std::size_t si = static_cast<std::size_t>(s);
|
||||||
if (r.channel[si] < 0) continue;
|
if (r.channel[si] < 0) continue;
|
||||||
|
// A live binding in trigger mode is only legitimate as a capture in progress, so
|
||||||
|
// the two are named apart: `live` with `trigger=on` is the defect, `live(capturing)`
|
||||||
|
// is the feature.
|
||||||
if (r.slots[si].bound_event() != nullptr) display = "event(frozen)";
|
if (r.slots[si].bound_event() != nullptr) display = "event(frozen)";
|
||||||
else if (r.slots[si].bound() != nullptr) display = "live";
|
else if (r.slots[si].bound() != nullptr) display = a->capturing ? "live(capturing)" : "live";
|
||||||
shown = &r.slots[si];
|
shown = &r.slots[si];
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
@@ -2160,10 +2255,11 @@ gboolean trig_trace_probe(gpointer user) {
|
|||||||
bound_hi, dx_nominal, dx_real,
|
bound_hi, dx_nominal, dx_real,
|
||||||
dx_real > 0.0 ? dx_nominal / dx_real : 0.0, got_lo, got_hi, want_lo, want_hi);
|
dx_real > 0.0 ? dx_nominal / dx_real : 0.0, got_lo, got_hi, want_lo, want_hi);
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[trig-probe] trigger=%s pending=%d display=%s latches=%llu armed=%d "
|
"[trig-probe] trigger=%s pending=%d display=%s epoch=%.4f latches=%llu armed=%d "
|
||||||
"live_pub=%llu snap=[%.4f..%.4f] n=%zu axis=[%.4f..%.4f] "
|
"live_pub=%llu snap=[%.4f..%.4f] n=%zu axis=[%.4f..%.4f] "
|
||||||
"data=[%.4f..%.4f] drawn=[%.4f..%.4f]\n",
|
"data=[%.4f..%.4f] drawn=[%.4f..%.4f]\n",
|
||||||
m.trigger_enabled() ? "on" : "off", int(m.trigger_pending()), display,
|
m.trigger_enabled() ? "on" : "off", int(m.trigger_pending()), display,
|
||||||
|
m.display_epoch(),
|
||||||
static_cast<unsigned long long>(m.trigger_latches().load()),
|
static_cast<unsigned long long>(m.trigger_latches().load()),
|
||||||
int(m.trigger_armed().load()),
|
int(m.trigger_armed().load()),
|
||||||
static_cast<unsigned long long>(m.live().values(0).produced()), snap_lo, snap_hi,
|
static_cast<unsigned long long>(m.live().values(0).produced()), snap_lo, snap_hi,
|
||||||
@@ -2185,6 +2281,28 @@ gboolean arm_probe(gpointer user) {
|
|||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Headless self-check: changes the trigger panel's two dropdowns, which is what a user
|
||||||
|
/// does to switch Normal/Single or the edge -- mid-capture, if the probe is timed into
|
||||||
|
/// one. Reaching the dropdowns directly is the point: their `notify::selected` handler
|
||||||
|
/// has a different signature from the spin buttons' `value-changed`, and connecting the
|
||||||
|
/// wrong one crashed on the first field read with no way to tell from the state dump.
|
||||||
|
/// A line here with no SIGSEGV after it is the assertion.
|
||||||
|
gboolean fields_probe(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
GtkDropDown* mode = GTK_DROP_DOWN(a->trigger_mode_drop);
|
||||||
|
GtkDropDown* edge = GTK_DROP_DOWN(a->trigger_edge_drop);
|
||||||
|
gtk_drop_down_set_selected(mode, gtk_drop_down_get_selected(mode) == 0 ? 1 : 0);
|
||||||
|
gtk_drop_down_set_selected(edge, (gtk_drop_down_get_selected(edge) + 1) % 3);
|
||||||
|
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||||
|
std::fprintf(stderr, "[fields-probe] mode=%u edge=%u published mode=%d edge=%d capturing=%d\n",
|
||||||
|
gtk_drop_down_get_selected(mode), gtk_drop_down_get_selected(edge),
|
||||||
|
int(a->model->trigger().mode), int(a->model->trigger().edge),
|
||||||
|
int(a->model->trigger_pending()));
|
||||||
|
std::fflush(stderr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
/// Headless self-check: acquire every signal the CONFIG described, the way the
|
/// Headless self-check: acquire every signal the CONFIG described, the way the
|
||||||
/// signal panel's switches would, and put each resulting channel on a plot, the way
|
/// signal panel's switches would, and put each resulting channel on a plot, the way
|
||||||
/// a drag from the panel onto a row would. Without the second half nothing is bound
|
/// a drag from the panel onto a row would. Without the second half nothing is bound
|
||||||
@@ -2214,6 +2332,8 @@ gboolean enable_all_probe(gpointer user) {
|
|||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gboolean disarm_probe_step3(gpointer user);
|
||||||
|
|
||||||
gboolean disarm_probe_step2(gpointer user) {
|
gboolean disarm_probe_step2(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
@@ -2226,15 +2346,70 @@ gboolean disarm_probe_step2(gpointer user) {
|
|||||||
a->dbg_latches_at_disarm = a->model->trigger_latches().load();
|
a->dbg_latches_at_disarm = a->model->trigger_latches().load();
|
||||||
std::fprintf(stderr, "[disarm-probe] %llu events so far, armed now=%d\n",
|
std::fprintf(stderr, "[disarm-probe] %llu events so far, armed now=%d\n",
|
||||||
(unsigned long long)a->dbg_latches_at_disarm, int(a->model->trigger().armed));
|
(unsigned long long)a->dbg_latches_at_disarm, int(a->model->trigger().armed));
|
||||||
|
a->dbg_capturing_at_disarm = a->model->trigger_pending();
|
||||||
|
a->dbg_seq_at_disarm = a->model->event_seq();
|
||||||
|
g_timeout_add(600, disarm_probe_step3, a);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Headless self-check, the half that reads the *outcome* of the Disarm. A disarm that
|
||||||
|
/// landed mid-capture (`was_capturing=1`) must leave an event on screen with the
|
||||||
|
/// capture's own span: `display` reading `nothing` there is the "the event completely
|
||||||
|
/// disappeared" defect, and `seq` must have advanced by one while `events` has not.
|
||||||
|
gboolean disarm_probe_step3(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
const DisplayMode d = a->model->display_mode();
|
||||||
|
auto snap = a->model->event();
|
||||||
|
RowSet::Row& r = a->rows->row(0);
|
||||||
|
const plot::Range axis = r.plot != nullptr ? r.plot->x_range(r.axis_x) : plot::Range{0.0, 0.0};
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[disarm-probe] was_capturing=%d display=%s seq=%llu->%llu events=%llu->%llu "
|
||||||
|
"snap=[%.4f..%.4f] n=%llu axis=[%.4f..%.4f]\n",
|
||||||
|
int(a->dbg_capturing_at_disarm),
|
||||||
|
d == DisplayMode::Held ? "event(frozen)"
|
||||||
|
: d == DisplayMode::Live ? "live"
|
||||||
|
: d == DisplayMode::Capturing ? "live(capturing)"
|
||||||
|
: "nothing",
|
||||||
|
(unsigned long long)a->dbg_seq_at_disarm,
|
||||||
|
(unsigned long long)a->model->event_seq(),
|
||||||
|
(unsigned long long)a->dbg_latches_at_disarm,
|
||||||
|
(unsigned long long)a->model->trigger_latches().load(),
|
||||||
|
snap ? -snap->pre_s : 0.0, snap ? snap->post_s : 0.0,
|
||||||
|
(unsigned long long)(snap ? snap->stamps.size() : 0), double(axis.min),
|
||||||
|
double(axis.max));
|
||||||
|
std::fflush(stderr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for a capture to actually be open before clicking Disarm. The interesting case
|
||||||
|
/// is the Disarm that lands *mid-capture*, and a fixed delay catches it only by luck --
|
||||||
|
/// the capture opens as soon as the ring holds a pre window and closes one post window
|
||||||
|
/// later. Polling for `trigger_pending()` makes the probe deterministic.
|
||||||
|
gboolean disarm_probe_wait(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
++a->dbg_disarm_polls;
|
||||||
|
if (a->model->trigger_pending()) ++a->dbg_disarm_in_capture;
|
||||||
|
// 300 ms into the capture: post-trigger data worth keeping, well short of the latch.
|
||||||
|
// The 10 s cap is for a stream that never triggers -- the probe still has to report.
|
||||||
|
if (a->dbg_disarm_in_capture >= 6 || a->dbg_disarm_polls > 200) {
|
||||||
|
if (a->dbg_disarm_in_capture == 0) {
|
||||||
|
std::fprintf(stderr, "[disarm-probe] no capture opened in 10 s; disarming anyway\n");
|
||||||
|
std::fflush(stderr);
|
||||||
|
}
|
||||||
|
disarm_probe_step2(a);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
gboolean disarm_probe_step1(gpointer user) {
|
gboolean disarm_probe_step1(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
if (a->trigger_toggle != nullptr)
|
if (a->trigger_toggle != nullptr)
|
||||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
|
||||||
g_timeout_add(2000, disarm_probe_step2, a);
|
g_timeout_add(50, disarm_probe_wait, a);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2350,6 +2525,9 @@ int run_ui(int argc, char** argv, ScopeModel& model, Acquisition& acq, RowSet& r
|
|||||||
if (const char* env = g_getenv("UDPSCOPE_ARM_TRIGGER_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_ARM_TRIGGER_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), arm_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), arm_probe, &app);
|
||||||
}
|
}
|
||||||
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_FIELDS_MS")) {
|
||||||
|
g_timeout_add(static_cast<guint>(std::atoi(env)), fields_probe, &app);
|
||||||
|
}
|
||||||
if (const char* env = g_getenv("UDPSCOPE_TRIG_TRACE_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_TRIG_TRACE_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), trig_trace_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), trig_trace_probe, &app);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
1. better time window management:
|
||||||
|
|
||||||
|
- instead of a drop down simple combo should be an advanced sub menu with text input + unit selector (us,ms,s,min) + two button to increase / decrease (factor 10 each time)
|
||||||
|
- when `timewindow x sample rate x sample size` is greater than `buffer size` apply downsampling of input signals with low pass filter to match the correct freq: e.g. downsample at 100kSps -> low pass filter at 50kHz
|
||||||
|
|
||||||
|
2. improve layout management: instead of a fix number of figure, in the menu add a layout editor option, user can add and remove rows ([x] close to the top left of the row to remove, [+] in the top center and bottom center to add new one [above] and [below])
|
||||||
|
3. improve plot font rendering and line rendering
|
||||||
|
4. improve plot performances with large data (e.g. at 10MSamples the plot start to be laggy)
|
||||||
Reference in new issue
Block a user