trigger: a phosphor display keeps the last N acquisitions; and a Force button
Two changes in the trigger pane, landed together. **Persistence.** Trigger mode keeps the last N published acquisitions and draws them fading toward the background, N being a panel control (1..8, default 1) with N=1 meaning today's behaviour exactly, so the knob is the switch. The layers are the accumulate path's own -- one layer per (signal, age) in the row's free trace slots, named with the accumulate vocabulary so `layer_base_name()` strips the suffix and `signal_color()` resolves the base signal -- so a layer is not a signal: the signal list, the colours and the measurements see one row per signal. `ScopeModel` retains the outgoing snapshot in seven atomic `shared_ptr` slots (no lock, no copy; the snapshot already exists) and a layer's channel is resolved **by name inside the snapshot**, because the channel list is renumbered by any structural change since that acquisition was cut. `RowSet::sync_layers()` re-plans from the rows and the depth alone, so a latch costs no rebuild, and `event_data_window()` skips layer slots so an older acquisition can never re-pin the axis. The schedule is 0.5^age -- the acquisition on screen at full brightness and each older one half the previous, independent of N, so raising the depth adds dimmer acquisitions rather than rescaling the ones already on screen. Memory is bounded by the ring's own `--buffer-mb` rather than by the depth, which is the bound that cannot be driven into an OOM, and the panel says when either bound bites. This also wired up `set_trace_alpha()`, which the accumulate path was setting and nothing was reading -- so the documented 0.35 translucency of an accumulation was opaque in fact. **Force.** In the trigger pane beside Arm/Disarm, shown only while trigger mode is on: one acquisition now, with the configured window. It is a synthetic edge rather than a second publisher -- the engine opens one capture at the present moment and everything else is the ordinary latch, so a forced acquisition is cut, pinned, framed, recorded and cached exactly like a latched one, and the button owns no window and no publish. The pre-trigger gate is deliberately skipped (capture now is a request for the history that exists, and the snapshot reports its own shorter pre_s), a capture already filling is refused rather than restarted, and the state afterwards is the mode's: Normal stays armed, Single disarms. The request is one atomic flag written under the model mutex with `armed` -- the producer reads `armed` at the top of a packet and the request at the bottom, so a request written between the two would be dropped -- and consumed at the end of a packet, after the channel loop and before the pending publish. **A defect fixed in passing, in the function Force was modelled on:** `on_arm_clicked()` passed `TriggerConfig::channel` (an index into `channels()`) to `set_signal_enabled()` (an index into `signals()`). With one signal acquired, arming switched on the stream's clock signal and left the watched signal untouched -- and since a signal the panel was not showing is a change, it rebuilt the buffer under the click, dropping the very pre-trigger window the arming, and now the Force, was about to capture. Measured before: `0.0100 s of history behind it`; after: `2.5299 s`. `trigger_signal_index()` is now the one resolution, used by both buttons.
This commit is contained in:
1 parent
fc4700d1c9
commit
e14e1ee069
22 files changed
+1652
-26
No files matched your search
@@ -57,6 +57,63 @@ precedent, not an exception.
|
||||
`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_FORCE_MS` — the trigger panel's **Force** button: one acquisition at the present
|
||||
moment, with the pre/post window the panel is set to, without waiting for the condition. It is a
|
||||
chain, not an instant — `MS` is when it *starts*: it forces trigger mode **off** through the toggle
|
||||
(a session file remembers the trigger, so the off state is stated rather than assumed) and reads the
|
||||
button's stated state 300 ms later; it then sets a level no mock signal reaches (9 V), a 2 s window
|
||||
at 50 % pre, acquires the mock's `sine` and turns the trigger on; it presses Force 2.5 s after that
|
||||
(the window write re-plans the buffer and clears the history, so the ring needs the time to fill
|
||||
past the 1 s leg A asks for); it presses a second time on a 100 s window at 99 % pre — a request no
|
||||
run this short can satisfy — 400 ms after the first latch. **Run it alone, against the plain mock**,
|
||||
with time for both legs:
|
||||
|
||||
```sh
|
||||
python3 tools/mock_udpstreamer.py --port 8114 --rate 10000 --batch 100 --duration 30 &
|
||||
sleep 1.5
|
||||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=14000 UDPSCOPE_PROBE_FORCE_MS=3000 \
|
||||
./build/udpscope --server 127.0.0.1 --port 8114 2>&1 | \
|
||||
grep -E 'force-probe|trigger:'
|
||||
```
|
||||
|
||||
Being alone is load-bearing twice: the level is chosen so the engine *cannot* latch on its own, so
|
||||
a latch count that moves is the force's and nothing else's — any other probe that arms a reachable
|
||||
condition adds latches of its own — and `UDPSCOPE_ENABLE_ALL_MS` before it would acquire four
|
||||
signals, which changes the level's default range and the plan. Two lines per pass are in
|
||||
[probe-readings.md](probe-readings.md); **`latches=P->Q` with `Q == P + 1`, `display=event(frozen)`,
|
||||
`matches_request=1` in leg A, `pin_is_snapshot=1` in both** are the assertions, and leg B's
|
||||
`matches_request=0` is *expected* — a ring that cannot reach back 99 s is the only state where
|
||||
"pinned to the snapshot, not to the request" is a reading rather than a coincidence.
|
||||
- `UDPSCOPE_PROBE_PERSIST_MS` (+ `UDPSCOPE_PROBE_PERSIST_DEPTH`, default 4, 1..8) — the **persistence
|
||||
display**: trigger mode keeps the last N acquisitions on screen, each older one drawn at half the
|
||||
previous one's brightness. It enables the signals itself (one call, as a restored session does) and
|
||||
arranges one signal per row — the arrangement the app gives an opened file, and the one with room
|
||||
for every age — sets the depth **through the panel's own spin**, arms the trigger on the mock's
|
||||
`square` and polls until the retention is full: an event on screen and as many older ones kept as
|
||||
the depth asks for. The depth is *left* where it put it, so the run's own tail is the display at
|
||||
that depth:
|
||||
|
||||
```sh
|
||||
python3 tools/mock_udpstreamer.py --port 9541 --rate 500000 --batch 1000 &
|
||||
PY=$!; sleep 0.5
|
||||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=16000 \
|
||||
UDPSCOPE_PROBE_PERSIST_MS=2500 UDPSCOPE_PROBE_PERSIST_DEPTH=4 \
|
||||
./build/udpscope --server 127.0.0.1 --port 9541 2>&1 | grep -E 'persist-probe|persistence:|layout:'
|
||||
kill $PY
|
||||
```
|
||||
|
||||
**Not with `UDPSCOPE_ENABLE_ALL_MS`**: that probe ends by switching a signal off two seconds later,
|
||||
which takes the row that plotted it — and its layers with it — out of the middle of the measurement.
|
||||
|
||||
**Read it as a pair of runs**: the same command at `..._PERSIST_DEPTH=1` is the display *without*
|
||||
the feature, so its `kept=0`, `layers=0`, `bytes=0` and its `layout:` line holding only the signals
|
||||
is the "N = 1 is exactly the old behaviour" reading, and it is also the run the exit dump's
|
||||
`frame cost:` line is compared against — the tick/draw means are means over the whole run, and the
|
||||
depth is raised at `MS`, so the difference is a *lower bound* on what the layers cost. **`ev_is_snap`
|
||||
and `pin_is_snap` must be 1 in both runs**: the older acquisitions are drawn and never pinned, so
|
||||
the axis, the framing and the measurements still describe the current event. `alphas=` is the decay
|
||||
schedule in use, oldest first — halves, and `1/2^age` at that. Both lines and the two exit-dump
|
||||
lines are in [probe-readings.md](probe-readings.md).
|
||||
- `UDPSCOPE_PROBE_TRIGRENUM_MS` — the trigger's capture must survive a **renumbering of its own
|
||||
channel**. It sets the reported geometry through the real widgets (1 s window, 10 % pre share,
|
||||
hold-off from `--holdoff`) on the mock's *square* alone, polls for a capture to open, and then
|
||||
|
||||
Reference in new issue
Block a user