fixed event/live transition
This commit is contained in:
1 parent
bb99be5889
commit
881b52748e
9 files changed
+314
-2
No files matched your search
@@ -583,6 +583,22 @@ A replay leaves trigger mode, because a file has no edge to wait for -- an armed
|
||||
held display draws that snapshot rather than the file that was just loaded. The events pane stays
|
||||
open through it: it is available while a stored event is on screen.
|
||||
|
||||
**Going back to *Live* puts the live view back as it was.** Opening a file changes two things that
|
||||
are not the user's choice -- the time window becomes the event's own span, because that is what
|
||||
framing an event means, and trigger mode goes off for the reason above -- and neither reason survives
|
||||
the way back. So the settings the live view had are taken on the way in and restored on the way out:
|
||||
the time window, trigger mode, the trigger's channel, level, condition, pre-trigger share and
|
||||
hold-off. The trigger's channel is remembered by **name**, since a comparison renames its channels
|
||||
(`square (A)`) and the way back has to find the stream's own. The restore waits for the first CONFIG
|
||||
of the restarted client, which is a frame or two, and is the last word on the window -- it was
|
||||
previously the *saved* session's window that came back, which is to say, somebody else's.
|
||||
|
||||
A related repair: **the session file is no longer written while a stored event is on screen.** A
|
||||
comparison rebinds every plot slot to its own renamed channels, and the automatic save wrote those
|
||||
names into the session -- so the next *Live* re-applied a layout that named no live signal, and the
|
||||
scope came back with an empty plot. Nothing is lost by skipping: a file view configured nothing about
|
||||
the stream. The explicit *Save as* is unaffected; that one is an answer to a question the user asked.
|
||||
|
||||
Three things worth knowing:
|
||||
|
||||
- **The samples are the export format**, so an event is readable by everything that reads an
|
||||
@@ -1181,7 +1197,9 @@ row's `×` forgets a source by deleting its session file, with no confirmation:
|
||||
confirmation, and what it destroys is one layout the app rewrites on the next clean exit. A session
|
||||
is **not written** when no CONFIG arrived during the run — otherwise a source whose streamer was
|
||||
down during one launch would silently vanish from the chooser list because its file was flattened
|
||||
to empty signal names, which the filter refuses.
|
||||
to empty signal names, which the filter refuses — nor while a **stored event is on screen**, for
|
||||
the same reason stated the other way round: a file view configured nothing about the stream, and a
|
||||
comparison's renamed slots written into the session came back as an empty plot.
|
||||
|
||||
**Math channels.** A math channel is derived from the others by a graph of operations and is then
|
||||
an *ordinary channel*: the model synthesises a signal entry for it, so an enabled one is acquired,
|
||||
|
||||
@@ -59,6 +59,17 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
out — a source whose streamer was merely down during one run would silently vanish from the
|
||||
only list it can be picked from, for ever. Keeping the old file loses nothing: a run that
|
||||
configured nothing had nothing worth persisting.
|
||||
- **It refuses again while `replaying` is set: a file view is not the source's session.** Same
|
||||
argument, different cause — this view configured nothing about the stream. A plain replay leaves
|
||||
the rows where the user put them, so its save would merely be pointless; a **group** rebinds every
|
||||
slot to its own renamed channels and the save wrote `signals = ["square (A)", …]` into the file.
|
||||
Nothing noticed until the way back: *Go live* re-applies the session, those names match no live
|
||||
signal, so nothing was enabled and **the live view came back with no channels at all** — on the
|
||||
window the poisoned file carried, which is also how it swallowed the window restore below it. The
|
||||
reading is `[events] live restored (after a group)`: the symptom was a stuck `pending=1` beside
|
||||
`channels=0`, because the restore waits for a channel list that was never going to arrive. The
|
||||
explicit *Save as* is deliberately **not** guarded: that one is a user's own answer to "save this
|
||||
view", and its dialog reports the outcome.
|
||||
|
||||
## A configuration is a named session, and a source may hold several
|
||||
|
||||
|
||||
@@ -182,6 +182,53 @@ deliberately **not** one of the conditions: a stored event being examined is a f
|
||||
trigger, and tying the pane to the trigger would close the list under the click that opened an event.
|
||||
The recording switch lives in the trigger panel, not in the pane it empties.
|
||||
|
||||
## Opening a file is a detour, so *Go live* is a restore
|
||||
|
||||
A file view overwrites the live view's settings and every overwrite has a reason: the window becomes
|
||||
the event's own span because that is what *framing* an event means, and the trigger goes off because
|
||||
a file has no edge to wait for (leaving it on kept a frozen capture on screen *instead* of the file,
|
||||
so the click looked like it did nothing at all). None of those reasons survive the way back. What
|
||||
the user had set is theirs, and "going back to live loses the trigger I spent a minute setting up"
|
||||
is the complaint this answers.
|
||||
|
||||
- **`snapshot_live_state()` on the way in, `restore_live_state()` on the way out.** The snapshot is
|
||||
taken by both ways into a file view -- `replay_event()` and `build_event_group()` -- *before* the
|
||||
acquisition is stopped, and is a **no-op while `replaying`**: a second event opened from the first,
|
||||
or a group opened from a replay, would otherwise snapshot a file's settings as "live", which are
|
||||
the very values the restore exists to undo. The group takes it after its argument checks, because a
|
||||
group that is refused leaves the live view alone.
|
||||
- **The trigger channel is stored by *name*.** Same reason `TriggerEngine::configure()` takes one: the
|
||||
index is a position in a channel list that a file view rebuilds, and a group **renames** its
|
||||
channels outright (`square (A)`). Putting an index back would select a different signal.
|
||||
- **`enabled` and `armed` are deliberately not restored from the snapshot.** They belong to the
|
||||
toolbar toggle, which owns an atomic mirror of `enabled` (`set_trigger_enabled()`) that a plain
|
||||
field write under the lock would leave stale. The restore writes the rest of the `TriggerConfig`
|
||||
and then moves the *toggle*, whose handler states both.
|
||||
- **The restore runs from the tick, not from `go_live_again()`, and *after* the deferred layout
|
||||
block.** Two writers would otherwise beat it:
|
||||
- `apply_layout()` ends in `apply_time_window(st.window_s)` -- the window the **session file**
|
||||
holds, not the one the live view had. `go_live_again()` clears `layout_loaded`, so that block
|
||||
fires on the next tick; a restore done inside `go_live_again()` is overwritten by the saved value
|
||||
a frame later. Being the next statement after it in `tick()` is the fix.
|
||||
- `rebuild_trigger_signals()`, finding a `channel` of `-1` after the rebuild renumbered the
|
||||
channels, silently settles on the first signal *and* re-defaults the level -- which is why
|
||||
`trigger_level_touched` is restored with the configuration.
|
||||
- **It waits for the stream's own channel list, gated on `generation()` having moved.** The restored
|
||||
name has to be resolved against the live channels, and those do not exist until the first CONFIG of
|
||||
the restarted client lands on the producer thread. `App::live_restore_generation` is the generation
|
||||
read when *Go live* was pressed; `live_restore_pending` stays set until it differs **and**
|
||||
`channels()` is non-empty. A `pending=1` that never clears is therefore a reading about the
|
||||
*stream*, not about the restore -- see `invariants-session-and-export.md` for the save that made it
|
||||
stick at `channels=0`.
|
||||
- **Only the two controls a file view actually moved are restated** -- the signal dropdown and the
|
||||
level spin. The rest of the trigger panel was never touched, and writing to a control fires its
|
||||
handler, so the ones with nothing to put back are left alone. Both writes are inside
|
||||
`updating_panels`.
|
||||
- **`pinned` is not a pass/fail reading on the way back.** `apply_time_window()` drops the file's pin
|
||||
and re-arms the live follow, but if the restored trigger was *on* it can latch again within the
|
||||
frame or two before the probe reads -- and a held capture is pinned, correctly. That is what
|
||||
`held=` and `mode=` are beside it for.
|
||||
|
||||
## A comparison is two plots of one model
|
||||
|
||||
*Compare* and *accumulate* are still one model -- the buffers cannot hold two independent loads -- but a
|
||||
|
||||
@@ -992,6 +992,7 @@ configuration that carries settings, is the defect this exists for — a saved h
|
||||
[events] activated row0: replaying=1 pane=1 pane_w=240 pane_box=228 tick=0 window=2.000 axis=[-1.600,0.400] marker=0.000 thumbnail=1 thumb_size=138x46 thumb_visible=1 rows_kept=1 selected=2/2
|
||||
[events] replayed: channels=3 samples=20001 replaying=1 waveform=1
|
||||
[events] live again: replaying=0 waveform=0
|
||||
[events] live restored (after replay): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=10/5
|
||||
[events] recording off, trigger off: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
|
||||
[events] recording off, trigger on: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
|
||||
[events] compare buttons: cached=2 selected=2 compare=1 accumulate=1
|
||||
@@ -1000,6 +1001,7 @@ configuration that carries settings, is the defect this exists for — a saved h
|
||||
[events] compare sync: y_a=-3.500 y_b=-3.500 mirrored=1
|
||||
[events] accumulate: kind=2 rows=1 channels=8 samples0=32964 selected=2 traces0=8 window=2.000
|
||||
[events] group left: replaying=0 kind=0 waveform=0
|
||||
[events] live restored (after a group): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=22/16
|
||||
```
|
||||
|
||||
`thumb_size` is the built size of the row's picture: `kEventThumbW` wide, and as tall as the *stored*
|
||||
@@ -1016,6 +1018,21 @@ header — `480` wide and `44 * signals` tall — and `row_drawn=1` says the row
|
||||
The probe therefore needs a cache a **previous** run populated: it fires ~4.5 s in, before this run's
|
||||
first latch.
|
||||
|
||||
`[events] live restored (…)` is the way *back*, read once per way into a file view — after a plain
|
||||
replay and after a group — and four of its fields are a **pair**: the value now, and the value the
|
||||
live view held before the file view opened over it. The probe captures that expectation itself
|
||||
(`capture_live_expectation()`, `App::dbg_live_*`) rather than asking the restore path for it: a
|
||||
reading taken from the mechanism under test asserts nothing. The window is set to 0.5 s just before
|
||||
the event is opened precisely so the pair can disagree — with the event's own span there would be
|
||||
nothing to tell "restored" from "untouched".
|
||||
|
||||
Both lines are read a *later* step, not in the handler that pressed *Live*: the restore waits for the
|
||||
restarted client's first CONFIG, so it is performed by a tick ~900 ms on. `pending=0` is what says it
|
||||
happened; `gen=a/b` is the generation now against the one *Go live* read. A **stuck `pending=1`** is
|
||||
not a reading about the restore but about the stream behind it — the generation never moved, or the
|
||||
channel list came back empty, which is how a session file poisoned by a save taken during a group
|
||||
produced `pending=1 channels=0` and a live view with nothing on it.
|
||||
|
||||
| field | value it must read | the defect a wrong reading names |
|
||||
| --- | --- | --- |
|
||||
| `n`, `stem`, `seq` | one entry per latch, named by its time and the run's counter | two events in one second overwriting each other, or a cache that is never written |
|
||||
@@ -1032,6 +1049,9 @@ first latch.
|
||||
| `axis`, `marker`, `pane_box` (in `activated row0`) | `[-pre, +post]` of the event just opened, `marker=0.000`, and a `pane_box` close to `pane_w` | an opened event framed on something other than its own pin; a trigger line left **parked** (a large negative) rather than on the edge, which is a file opened without its marker; or a pane whose *rendered* width is 0 while the divider says 240 -- the position is what was asked for, `pane_box` is what the pane actually got, and a divider at 240 with a zero-width pane is still no pane |
|
||||
| `rows_kept`, `selected` (in `activated row0`) | `rows_kept=1` and `selected=N/N` -- the counts either side of the activation equal | the list **rebuilt** by the activation. It is the row *object* that is compared, not its index: a rebuilt list has a row 0 too, and only identity tells a list that was left alone from one that was torn down under the user's scroll position and their comparison selection. `mark_events_on_screen()` must rewrite the titles in place |
|
||||
| `replaying` (last line) | 0 | a replay with no way back: *Live* must start a fresh client and thread |
|
||||
| `window`, `trigger`, `channel`, `level` (in `live restored`) | each equal to its `(want …)` | the way back out of a file view discarding what the live view was set to. One wrong half names which writer won: a `window` that reads the **session file's** value is the deferred `apply_layout()` beating the restore; `trigger=0` is the toggle the file view switched off and nobody switched back; an empty or shifted `channel` is an index restored instead of a name, after a rebuild renumbered (or a group renamed) the channels; and a `level` that moved on its own is `rebuild_trigger_signals()` re-defaulting it because `trigger_level_touched` was not restored with the configuration |
|
||||
| `pending`, `gen` (in `live restored`) | `pending=0`, and the two generations different | the restore still waiting. It is gated on the stream's first CONFIG, so `pending=1` says the channel list the stored trigger name needs never arrived -- with `channels=0` behind it, a session file that enabled nothing |
|
||||
| `pinned`, `held`, `mode` (in `live restored`) | `pinned=0`, **or** `pinned=1` with `held=1` | a file's pin surviving the way back, which is a live view that cannot be navigated. `apply_time_window()` drops it; the second reading is the legitimate case -- a restored trigger that was *on* can latch again before the probe reads, and a held capture is pinned, correctly |
|
||||
| `pane_w` | the divider's position: 0 while the pane is closed, its remembered width (240 by default) while it is open | the pane reserving a column while closed (a paned hands its start child its minimum unless the position says otherwise), or a width that is not remembered |
|
||||
| `tick` | 1 in live mode, **0** once a file is on screen | the signal list still offering "acquire this signal" for samples that are already stored |
|
||||
| `window` (in `activated row0`) | the **event's** span -- the probe sets 0.5 s just before the click, so reading the event's span says opening it framed it | a replay that leaves the previous window alone, or one that restates it every frame instead of once |
|
||||
|
||||
+10
@@ -3294,6 +3294,16 @@ gboolean tick(gpointer user) {
|
||||
}
|
||||
}
|
||||
|
||||
// ... and *after* it, which is the point: the block above ends in `apply_time_window()` with the
|
||||
// window the session file holds, so a restore done before it would be overwritten by the saved
|
||||
// value. The generation is the other half of the wait -- the stream's first CONFIG, which is what
|
||||
// gives the stored trigger channel a list to be found in.
|
||||
if (app->live_restore_pending && !app->replaying && !model.channels().empty() &&
|
||||
model.generation() != app->live_restore_generation) {
|
||||
app->live_restore_pending = false;
|
||||
restore_live_state(*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.
|
||||
|
||||
@@ -452,6 +452,30 @@ struct App {
|
||||
std::vector<EventRecord> group;
|
||||
/// True while a stored event is on screen instead of the live stream.
|
||||
bool replaying = false;
|
||||
/// What the live view was set to when a file view was opened over it, so that *Go live* can put it
|
||||
/// back. A file view overwrites all of it and for good reasons -- the window is pinned to the
|
||||
/// event's own span because that is what framing an event means, and the trigger goes off because a
|
||||
/// file has no edge to wait for -- but none of those reasons survive the return to the stream, and
|
||||
/// what the user set is theirs. Taken on the way *in*, and only when the view being left is the live
|
||||
/// one, so a replay opened from inside a group does not snapshot the group's settings as "live".
|
||||
///
|
||||
/// The trigger's channel is kept by **name**: the index is a position in a channel list that a file
|
||||
/// view rebuilds (a group renames its channels outright), so putting the index back would select a
|
||||
/// different signal, or none.
|
||||
struct LiveState {
|
||||
bool valid = false;
|
||||
double window_s = 0.0;
|
||||
bool trigger_on = false;
|
||||
TriggerConfig trigger;
|
||||
std::string trigger_channel;
|
||||
bool level_touched = false;
|
||||
};
|
||||
LiveState live_state;
|
||||
/// Set by *Go live*, cleared by the tick that performs the restore, with the model generation it
|
||||
/// has to see move first -- the stream's own channel list is what the stored trigger channel is
|
||||
/// resolved against.
|
||||
bool live_restore_pending = false;
|
||||
unsigned live_restore_generation = 0;
|
||||
/// Set when a latch frames a new snapshot, consumed at the *end* of that tick: the record needs the
|
||||
/// rows bound to the event (for the thumbnail) and the bind happens after the display decision.
|
||||
bool record_pending = false;
|
||||
@@ -749,6 +773,16 @@ struct App {
|
||||
/// position and their comparison selection.
|
||||
GtkListBoxRow* dbg_events_row0 = nullptr;
|
||||
int dbg_events_selected = 0;
|
||||
/// What the *live* view was set to just before the probe opened an event: the time window, the
|
||||
/// trigger's mode, its channel (by name, the only stable identity) and its level. Read back after
|
||||
/// each way out of a file view, because a file view overwrites all four -- the window is pinned to
|
||||
/// the event's own span and the trigger is switched off, since a file has no edge to wait for --
|
||||
/// and going live has to put them back. The probe keeps its own expectation rather than asking
|
||||
/// `App`: a reading taken from the mechanism under test asserts nothing.
|
||||
double dbg_live_window = 0.0;
|
||||
bool dbg_live_trigger_on = false;
|
||||
std::string dbg_live_trigger_channel;
|
||||
double dbg_live_level = 0.0;
|
||||
/// Which step of the plot-context-menu probe runs next (`UDPSCOPE_PROBE_ROWMENU_MS`), and the
|
||||
/// popover it is measuring.
|
||||
int dbg_rowmenu_step = 0;
|
||||
@@ -1335,6 +1369,8 @@ void update_capture_row(App& app);
|
||||
bool record_event(App& app);
|
||||
bool replay_event(App& app, const EventRecord& record);
|
||||
bool go_live_again(App& app);
|
||||
void snapshot_live_state(App& app);
|
||||
void restore_live_state(App& app);
|
||||
bool read_event_samples(const std::string& path, EventSamples* out, std::string* error);
|
||||
std::vector<EventRecord> selected_events(App& app);
|
||||
void update_group_buttons(App& app);
|
||||
|
||||
+93
-1
@@ -141,11 +141,94 @@ bool record_event(App& app) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Remembers the live view's settings before a file view is opened over them.
|
||||
///
|
||||
/// A no-op unless the view being left *is* the live one: replaying a second event from inside the
|
||||
/// first, or a group opened from a replay, must not overwrite the snapshot with a file's settings --
|
||||
/// those are the very values *Go live* exists to undo.
|
||||
void snapshot_live_state(App& app) {
|
||||
if (app.replaying || app.model == nullptr || app.rows == nullptr) return;
|
||||
App::LiveState& s = app.live_state;
|
||||
s.valid = true;
|
||||
s.window_s = app.rows->time_window();
|
||||
s.trigger_on = app.trigger_toggle != nullptr &&
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle)) != FALSE;
|
||||
s.level_touched = app.trigger_level_touched;
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
s.trigger = app.model->trigger();
|
||||
s.trigger_channel = s.trigger.channel >= 0 &&
|
||||
s.trigger.channel < static_cast<int>(app.model->channels().size())
|
||||
? app.model->channels()[static_cast<std::size_t>(s.trigger.channel)].name
|
||||
: std::string();
|
||||
}
|
||||
|
||||
/// Puts those settings back on the way out of a file view.
|
||||
///
|
||||
/// Deferred by `go_live_again()` to the first tick that sees the stream's *own* channel list, which
|
||||
/// is what makes this the last writer over the two things that would otherwise overwrite it: the
|
||||
/// deferred layout re-apply (which ends in `apply_time_window()` with the *saved* window, not the
|
||||
/// live one) and `rebuild_trigger_signals()`, which, finding a `channel` of `-1` after a rebuild
|
||||
/// renumbered or renamed the channels, silently settles on the first signal and re-defaults the level
|
||||
/// with it.
|
||||
///
|
||||
/// Inside, the order is the whole of it:
|
||||
///
|
||||
/// - The configuration goes in under the lock, with the channel resolved from the stored *name*.
|
||||
/// `enabled` and `armed` are deliberately *not* written: they belong to the toolbar toggle, which
|
||||
/// owns an atomic mirror of `enabled` that a plain field write would leave stale.
|
||||
/// - The two controls a file view actually moved are restated from it -- the signal dropdown and the
|
||||
/// level spin. The rest of the panel was never touched, and writing to a control fires its handler,
|
||||
/// so the ones that have nothing to put back are left alone.
|
||||
/// - `apply_time_window()`, the only path allowed to restate the time scale; it also drops the event
|
||||
/// pin the file view left on the axis and re-arms the live follow.
|
||||
/// - The toggle last, because its handler reads the configuration it is turning on.
|
||||
void restore_live_state(App& app) {
|
||||
App::LiveState& s = app.live_state;
|
||||
if (!s.valid) return;
|
||||
s.valid = false; // consumed: the next way in takes its own snapshot
|
||||
int channel = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
const std::vector<ChannelInfo>& channels = app.model->channels();
|
||||
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||
if (channels[i].name == s.trigger_channel) {
|
||||
channel = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
TriggerConfig& tc = app.model->trigger();
|
||||
const bool enabled = tc.enabled;
|
||||
const bool armed = tc.armed;
|
||||
tc = s.trigger;
|
||||
tc.enabled = enabled;
|
||||
tc.armed = armed;
|
||||
tc.channel = channel;
|
||||
}
|
||||
app.trigger_level_touched = s.level_touched;
|
||||
app.updating_panels = true;
|
||||
if (channel >= 0 && app.trigger_signal_drop != nullptr) {
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_signal_drop),
|
||||
static_cast<guint>(channel));
|
||||
}
|
||||
if (app.trigger_level_spin != nullptr) {
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level_spin), s.trigger.condition.level);
|
||||
}
|
||||
app.updating_panels = false;
|
||||
apply_time_window(app, s.window_s);
|
||||
if (app.trigger_toggle != nullptr &&
|
||||
(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle)) != FALSE) !=
|
||||
s.trigger_on) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), s.trigger_on ? TRUE : FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
/// Replays a cached event: the same loader `--open` uses, on the event's file. The acquisition is
|
||||
/// stopped first, because a live producer overwrites the loaded samples within a frame -- so the
|
||||
/// scope becomes a file viewer until *Go live* starts it again.
|
||||
bool replay_event(App& app, const EventRecord& record) {
|
||||
if (app.acq == nullptr || app.model == nullptr) return false;
|
||||
// Before the stop, while the live view is still the view: everything below overwrites it.
|
||||
snapshot_live_state(app);
|
||||
app.acq->stop();
|
||||
WaveformLoadReport report;
|
||||
std::string error;
|
||||
@@ -199,6 +282,13 @@ bool go_live_again(App& app) {
|
||||
app.layout_loaded = false; // the session's layout applies to the source again
|
||||
app.event_held = false;
|
||||
app.model->set_display_epoch(0.0);
|
||||
// What the live view had, put back from the tick rather than here: the stream's own channel list is
|
||||
// what the stored trigger channel has to be resolved against, and it does not exist until the first
|
||||
// CONFIG of the restarted client lands on the producer thread. The generation read here is what the
|
||||
// tick compares against to know that it has (see `restore_live_state()` for what else that ordering
|
||||
// buys).
|
||||
app.live_restore_generation = app.model->generation();
|
||||
app.live_restore_pending = app.live_state.valid;
|
||||
try {
|
||||
app.acq->start();
|
||||
} catch (const std::exception& e) {
|
||||
@@ -307,7 +397,9 @@ bool build_event_group(App& app, const std::vector<EventRecord>& records, EventG
|
||||
|
||||
// The acquisition first: a live producer keeps pushing into the model it was started with -- and it
|
||||
// pushes *four* stream signals into the group's eight channels, so the group's own samples are the
|
||||
// ones that go missing.
|
||||
// ones that go missing. The live view's settings are taken ahead of it, for `go_live_again()`:
|
||||
// after the checks above, because a group that is refused leaves the live view alone.
|
||||
snapshot_live_state(app);
|
||||
app.acq->stop();
|
||||
|
||||
std::vector<EventSamples> events(records.size());
|
||||
|
||||
@@ -232,6 +232,14 @@ void save_layout_now(App& app) {
|
||||
// run would silently vanish from the only place it can be picked. Keeping the old file loses
|
||||
// nothing: this run configured nothing.
|
||||
if (app.model->signals().empty()) return;
|
||||
// A file view is not the source's session, and the same argument applies: this view configured
|
||||
// nothing about the stream. A replay leaves the rows where the user put them, so its save would
|
||||
// merely be pointless -- but a *group* rebinds every slot to its own renamed channels, and a save
|
||||
// taken while one was up wrote `signals = ["square (A)", ...]` into the file. The next load then
|
||||
// matched no live signal, enabled nothing, and the live view came back with no channels at all,
|
||||
// on the window the poisoned file carried. Skipping costs nothing: the live session's own file is
|
||||
// already on disk, and `go_live_again()` puts the rest back.
|
||||
if (app.replaying) return;
|
||||
if (app.config_name.empty()) app.config_name = active_configuration(app.source_host,
|
||||
app.source_port);
|
||||
const std::string path =
|
||||
|
||||
@@ -3107,6 +3107,62 @@ gboolean rowmenu_probe_step(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// The live view's own settings, taken just before a file view is opened over them, so that every
|
||||
/// way back out can be read against what was there. The trigger is read under the lock and its
|
||||
/// channel by *name*: a file view rebuilds the channel list, so an index means nothing on the way
|
||||
/// back. The probe keeps its own copy rather than asking the restore path for one -- a reading taken
|
||||
/// from the mechanism under test asserts nothing.
|
||||
void capture_live_expectation(App& a) {
|
||||
a.dbg_live_window = a.rows->time_window();
|
||||
a.dbg_live_trigger_on =
|
||||
a.trigger_toggle != nullptr &&
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a.trigger_toggle)) != FALSE;
|
||||
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||
const TriggerConfig& tc = a.model->trigger();
|
||||
a.dbg_live_level = tc.condition.level;
|
||||
a.dbg_live_trigger_channel = tc.channel >= 0 && tc.channel < int(a.model->channels().size())
|
||||
? a.model->channels()[std::size_t(tc.channel)].name
|
||||
: std::string();
|
||||
}
|
||||
|
||||
/// What came back when a file view was left: every setting the live view had before it opened, read
|
||||
/// against the expectation the probe captured itself. Four of the five readings are a *pair* on
|
||||
/// purpose -- a file view overwrites the window, the trigger mode, the trigger's channel and its
|
||||
/// level, so a line where the two halves of a pair disagree names the one that was not put back.
|
||||
/// `pinned` is the fifth and has no pair: a file pins the axis to the event's span, and going live
|
||||
/// must leave the axis following again, so the only passing reading is 0.
|
||||
void report_live_restored(App& a, const char* what) {
|
||||
std::string channel;
|
||||
double level = 0.0;
|
||||
bool enabled = false;
|
||||
bool armed = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||
const TriggerConfig& tc = a.model->trigger();
|
||||
enabled = tc.enabled;
|
||||
armed = tc.armed;
|
||||
level = tc.condition.level;
|
||||
if (tc.channel >= 0 && tc.channel < int(a.model->channels().size())) {
|
||||
channel = a.model->channels()[std::size_t(tc.channel)].name;
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[events] live restored (%s): window=%.6f (want %.6f) trigger=%d (want %d) "
|
||||
"channel='%s' (want '%s') level=%.6g (want %.6g) enabled=%d armed=%d pinned=%d "
|
||||
"held=%d mode=%d pending=%d gen=%u/%u\n",
|
||||
what, a.rows->time_window(), a.dbg_live_window,
|
||||
a.trigger_toggle != nullptr &&
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a.trigger_toggle))
|
||||
? 1
|
||||
: 0,
|
||||
a.dbg_live_trigger_on ? 1 : 0, channel.c_str(), a.dbg_live_trigger_channel.c_str(),
|
||||
level, a.dbg_live_level, enabled ? 1 : 0, armed ? 1 : 0,
|
||||
a.rows->window_pinned() ? 1 : 0, a.event_held ? 1 : 0,
|
||||
static_cast<int>(a.model->display_mode()), a.live_restore_pending ? 1 : 0,
|
||||
a.model->generation(), a.live_restore_generation);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
void events_probe_next(App& a, int step, guint delay_ms) {
|
||||
a.dbg_events_step = step;
|
||||
g_timeout_add(delay_ms, events_probe_step, &a);
|
||||
@@ -3227,6 +3283,7 @@ gboolean events_probe_step(gpointer user) {
|
||||
// says whether opening the event framed it: with the same window there would be nothing to tell
|
||||
// "framed" from "untouched".
|
||||
apply_time_window(*a, 0.5);
|
||||
capture_live_expectation(*a);
|
||||
GtkListBoxRow* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(a->events_list), 0);
|
||||
// Selected *and* remembered, because what the activation must not do is rebuild the list:
|
||||
// a selection the user made for a comparison, and the row object itself, are what a
|
||||
@@ -3312,6 +3369,7 @@ gboolean events_probe_step(gpointer user) {
|
||||
|
||||
std::fprintf(stderr, "[events] live again: replaying=%d waveform=%d\n", a->replaying ? 1 : 0,
|
||||
a->waveform_name.empty() ? 0 : 1);
|
||||
report_live_restored(*a, "after replay");
|
||||
// The pane's *other* precondition, through the real switch, in both trigger states: disabling
|
||||
// recording has to take the button and the pane with it, and arming the trigger afterwards must not
|
||||
// bring back a pane over an empty cache.
|
||||
@@ -3360,6 +3418,10 @@ gboolean events_group_probe_step(gpointer user) {
|
||||
a->events_compare != nullptr && gtk_widget_get_sensitive(a->events_compare) ? 1 : 0,
|
||||
a->events_accumulate != nullptr && gtk_widget_get_sensitive(a->events_accumulate) ? 1
|
||||
: 0);
|
||||
// The live settings this leg will be read against, taken *here* rather than inherited from the
|
||||
// replay leg: a group is a second, independent way into a file view, and its way back has to put
|
||||
// back what was on screen when the button was pressed.
|
||||
capture_live_expectation(*a);
|
||||
if (a->events_compare != nullptr) g_signal_emit_by_name(a->events_compare, "clicked");
|
||||
// Read one step later: a rebuilt row's traces are bound by the next *frame*, so a reading taken in
|
||||
// the same handler as the click reads the frame before the bind.
|
||||
@@ -3502,6 +3564,14 @@ gboolean events_group_probe_step(gpointer user) {
|
||||
std::fprintf(stderr, "[events] group left: replaying=%d kind=%d waveform=%d\n", a->replaying ? 1 : 0,
|
||||
static_cast<int>(a->group_kind), a->waveform_name.empty() ? 0 : 1);
|
||||
std::fflush(stderr);
|
||||
// The restore is not this handler's: it waits for the restarted client's first CONFIG, so it is a
|
||||
// *later tick* that performs it. Reading here would read the frame before the stream came back.
|
||||
events_group_probe_next(*a, 61, 900);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
if (a->dbg_events_step == 61) {
|
||||
report_live_restored(*a, "after a group");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user