fixed event/live transition
This commit is contained in:
1 parent
bb99be5889
commit
881b52748e
9 files changed
+314
-2
No files matched your search
+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