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
@@ -112,6 +112,16 @@ class RowSet {
|
||||
/// `add_trace()`, which is why this is stored rather than resolved per frame --
|
||||
/// changing it needs a rebuild, and `assign()` already forces one.
|
||||
std::array<int, kSlotsPerRow> color_index = {0, 1, 2, 3};
|
||||
/// The *persistence layer* this slot draws: 0 for a signal's own trace (whatever the frame's
|
||||
/// display mode binds it to), `a` > 0 for the acquisition `a` latches back, drawn at
|
||||
/// `slot_alpha`. A layer slot holds a suffixed name (`sine (A)`) and no channel at all -- it is
|
||||
/// not a signal, and `channel[]` stays -1 for it (see `assign_layer()`).
|
||||
std::array<int, kSlotsPerRow> layer = {0, 0, 0, 0};
|
||||
/// How bright the slot's trace is built: 1 for a signal's own trace, `persist_alpha(layer)` for
|
||||
/// a persistence layer, multiplied into `trace_alpha_`. It lives on the slot because a `Plot`
|
||||
/// fixes a trace's colour -- alpha included -- at `add_trace()`, so a per-layer brightness is a
|
||||
/// property of the trace's *creation*, which is what a rebuild is for.
|
||||
std::array<float, kSlotsPerRow> slot_alpha = {1.0f, 1.0f, 1.0f, 1.0f};
|
||||
plot::u32 axis_x = 0;
|
||||
plot::u32 axis_y = 0;
|
||||
plot::u32 cursor_a = 0;
|
||||
@@ -200,6 +210,42 @@ class RowSet {
|
||||
/// The channel index a row/slot holds, or -1.
|
||||
int channel_of(int row, int slot) const;
|
||||
|
||||
// --- persistence layers ---------------------------------------------------
|
||||
/// The brightness a persistence layer is drawn at, `a` acquisitions back: the acquisition on
|
||||
/// screen is the full-bright one and each older one is **half** the previous, so the schedule is
|
||||
/// exponential in age -- 1, 0.5, 0.25, 0.125, ... and by the seventh (0.008, `kMaxPersistDepth`)
|
||||
/// the layer is gone. Halving is per acquisition and not per *depth*, so changing N re-cuts how
|
||||
/// many are kept without rescaling the ones already on screen.
|
||||
static float persist_alpha(int age) noexcept;
|
||||
|
||||
/// Binds a row's slot to a persistence *layer*: the acquisition `age` latches back, at `alpha`
|
||||
/// (see `Row::layer`). The name carries the layer's suffix (`sine (A)`, `sine #4`), which is what
|
||||
/// keeps a layer out of the signal list and out of the measurements -- it is not a signal -- while
|
||||
/// `signal_color()` still resolves it to the base signal's own colour.
|
||||
void assign_layer(int row, int slot, std::string signal_name, std::string display_name,
|
||||
int color_index, int age, float alpha);
|
||||
/// Frees every slot bound to a layer. What turning persistence off -- or the trigger -- is.
|
||||
void clear_layers();
|
||||
/// Lays the persistence layers out over the rows' free slots, one per (signal, age): the signal's
|
||||
/// own trace stays where it is and its layers take the free slots of the *same* row, layer by
|
||||
/// layer (age 1 for every signal first, then age 2, as the accumulate path does), so a row that
|
||||
/// runs out of room draws as many layers as it has room for and no more. Every slot the plan does
|
||||
/// not fill is freed, which is what makes the display follow a layout change, a depth change and a
|
||||
/// signal's removal without any caller having to reset anything.
|
||||
///
|
||||
/// Returns the number of layers placed; `want` is how many the depth asked for, so a caller can
|
||||
/// say when a layout could not show them all.
|
||||
int sync_layers(int depth, int* want);
|
||||
/// The layer a slot is drawing, 0 for a signal's own trace.
|
||||
int layer_of(int row, int slot) const;
|
||||
/// The slot's own brightness (1 unless it is a layer).
|
||||
float slot_alpha_of(int row, int slot) const;
|
||||
/// How many slots are layers, over every row: the reading that says the layers exist.
|
||||
int layer_slots() const;
|
||||
/// The layers of one row, oldest first, as `(age, alpha)` pairs -- the ordering a reader has to
|
||||
/// see to know the decay is the right way round.
|
||||
std::vector<std::pair<int, float>> row_layers(int row) const;
|
||||
|
||||
/// Rebinds every slot to what `mode` says it should show this frame. Called every
|
||||
/// frame, because the channel list is renumbered by any structural change.
|
||||
///
|
||||
|
||||
Reference in new issue
Block a user