Fixed triggering mode change when acquiring

This commit is contained in:
Martino Ferrari committed 2026-10-01 17:54:17 +02:00
1 parent 3d4085898e
commit 0d741c401b
10 files changed
+504 -106

No files matched your search

+31
View File
@@ -349,6 +349,32 @@ void Acquisition::on_data(const udps_frame_t* frame) {
model_.trigger_armed().store(true, std::memory_order_relaxed);
}
} 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();
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;
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());
static std::int64_t next_engine_log = 0;
const std::int64_t now_e = static_cast<std::int64_t>(frame->recv_time * 1.0e6);
+6 -4
View File
@@ -98,11 +98,13 @@ ExportTable collect_export(ScopeModel& model, RowSet& rows) {
: std::string("ch") + std::to_string(c);
};
// What the rows bind to is what gets exported, which is the same branch `RowSet::bind()`
// takes: the frozen event in trigger mode, the live ring otherwise. Reading the ring
// while an event is on screen would export data the user cannot see.
// What the rows bind to is what gets exported, so the split is the same `DisplayMode`
// `RowSet::bind()` switches on: the frozen event only while one is held, the live ring
// 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();
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
// binary searches over its stamps and the same index range for every channel.
const std::vector<f32>& st = snap->stamps;
+3
View File
@@ -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->post_s = snap->stamps.empty() ? 0.0 : static_cast<double>(snap->stamps.back());
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 {
+48 -7
View File
@@ -127,6 +127,21 @@ struct StreamStats {
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 trigger configuration and the counters. Structural changes (a CONFIG
/// 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 {
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.
void acquire_all();
@@ -211,16 +231,35 @@ class ScopeModel {
}
void set_trigger_enabled(bool on);
/// Whether the engine is mid-capture (an edge was seen and the post-trigger
/// window is still filling). Purely informational: it drives the status line's
/// wording. The plot deliberately does *not* react to it -- in trigger mode the
/// picture changes only on a latch (SPECS: "data visualisation should not be
/// refreshed till next event").
/// Whether the engine is mid-capture (an edge was seen -- so the pre window was
/// full -- and the post-trigger window is still filling). This is what puts the
/// display in `DisplayMode::Capturing`: the plot follows the live ring, on an axis
/// pinned to the coming event's window, until the snapshot replaces it.
///
/// 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 {
return trigger_pending_.load(std::memory_order_relaxed);
return trigger_pending_.load(std::memory_order_acquire);
}
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
@@ -283,7 +322,9 @@ class ScopeModel {
f64 epoch_ = 0.0;
std::atomic<double> event_span_{1.0};
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::uint64_t> event_seq_{0};
std::atomic<double> display_epoch_{0.0};
std::atomic<bool> trigger_armed_{false};
std::atomic<bool> trigger_enabled_{false};
+20 -14
View File
@@ -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)];
}
void RowSet::bind(ScopeModel& model) {
void RowSet::bind(ScopeModel& model, DisplayMode mode) {
const std::vector<SignalInfo>& signals = model.signals();
const std::vector<ChannelInfo>& channels = model.channels();
for (int i = 0; i < kMaxRows; ++i) {
@@ -181,20 +181,26 @@ void RowSet::bind(ScopeModel& model) {
r.slots[si].bind(nullptr);
continue;
}
// In trigger mode the trace shows the captured event and nothing else: the
// last one latched, or nothing at all before the first. Never the live ring,
// not even while the post window is filling -- a frozen picture that only
// changes on an event is the whole point of trigger mode, and binding the
// live ring mid-capture made the plot scroll for most of every event cycle.
// The source is the frame's one `DisplayMode`, decided by the caller: `Held` is
// the frozen snapshot, `Waiting` is blank (armed, nothing captured yet), and both
// `Live` and `Capturing` are the live ring -- what differs between those two is
// the *clock*, which the display epoch carries, not the binding.
const std::size_t ci = static_cast<std::size_t>(channel);
if (model.trigger_enabled()) {
if (auto snap = model.event()) {
r.slots[si].bind_event(std::move(snap), ci);
} else {
r.slots[si].bind(nullptr); // armed, waiting for the first event: blank
}
} else {
r.slots[si].bind(model.live().shared_channel(ci));
switch (mode) {
case DisplayMode::Held:
if (auto snap = model.event()) {
r.slots[si].bind_event(std::move(snap), ci);
} else {
r.slots[si].bind(nullptr);
}
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
View File
@@ -117,10 +117,13 @@ class RowSet {
/// The channel index a row/slot holds, or -1.
int channel_of(int row, int slot) const;
/// Rebinds every slot to what it should show this frame: the model's live buffer,
/// or -- in trigger mode -- the captured event. Called every frame, because the
/// channel list is renumbered by any structural change.
void bind(ScopeModel& model);
/// 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.
///
/// `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.
void acquire(ScopeModel& model) { model.acquire_all(); }
+231 -53
View File
@@ -99,12 +99,26 @@ struct App {
/// 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).
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;
/// 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
/// 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;
double timed_edge = 0.0;
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
/// 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;
std::vector<std::uint64_t> last_stored;
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_axes_w_min = 1 << 20;
int dbg_axes_w_max = -(1 << 20);
@@ -448,6 +466,7 @@ void on_arm_clicked(GtkButton* button, gpointer user) {
}
if (armed) {
app->event_held = false;
app->capturing = false;
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
}
// 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");
}
void on_trigger_field_changed(GtkWidget*, gpointer user) {
App* app = static_cast<App*>(user);
void publish_trigger_fields(App* app) {
if (app->updating_panels) return;
// 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
@@ -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) {
App* app = static_cast<App*>(user);
if (app->updating_panels) return;
@@ -897,6 +927,7 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
signal = tc.channel;
}
app->event_held = false;
app->capturing = false;
if (!on) {
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
@@ -1317,12 +1348,30 @@ gboolean tick(gpointer user) {
}
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
// UI's own structural changes: the bind takes shared handles into the live
// store, and acquire() reads it.
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);
}
@@ -1331,11 +1380,12 @@ gboolean tick(gpointer user) {
// 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.
// While a captured event is on screen the window is pinned to [edge-pre,
// edge+post]; clamp_to_data would fight that pin whenever the bound series
// still reports the acquisition buffer's raw coordinates, dragging the view
// along the ring. Clamping is a live-view concern only.
if (!app->event_held) app->rows->clamp_to_data();
// While an event is on screen -- held, or still being captured -- the window is pinned
// to [-pre, +post]; clamp_to_data would fight that pin whenever the bound series still
// reports the acquisition buffer's raw coordinates, dragging the view along the ring.
// Clamping is a live-view concern only, and a capture is live data on a pinned axis,
// 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
// 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);
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);
if (latches != app->seen_latches) {
app->seen_latches = latches;
app->event_held = true;
app->last_latch_us = g_get_monotonic_time();
// 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), 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);
const double edge = model.trigger_time().load(std::memory_order_relaxed);
// A capture the UI watched fill already started this clock at its edge; only one that
// opened and closed inside a single frame -- never reported pending, so never
// displayed as a capture -- starts it here.
if (edge != app->timed_edge) {
app->timed_edge = edge;
app->last_latch_us = g_get_monotonic_time();
}
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.
if (trigger_on && model.trigger_time_valid().load(std::memory_order_relaxed)) {
// x is seconds from the event: the display epoch is minus the edge time, so the
// edge sits at exactly x = 0 wherever the user has zoomed.
} else {
// Framing the held event is keyed on *which snapshot was published*, not on the latch
// count: a Disarm during a capture publishes what the capture got to without an event
// having fired, and that partial has to be framed and held exactly like one that ran
// to term. The latch count stays the answer to "how many events fired", which is what
// 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);
}
@@ -1568,7 +1658,9 @@ gboolean tick(gpointer user) {
StreamStats& st = model.stats();
const char* mode = "live";
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)";
} else if (armed && have_trigger_value) {
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(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_CALLBACK(on_trigger_field_changed), &app);
G_CALLBACK(on_trigger_drop_changed), &app);
return box;
}
@@ -2068,8 +2160,11 @@ gboolean trig_trace_probe(gpointer user) {
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
const std::size_t si = static_cast<std::size_t>(s);
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)";
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];
found = true;
break;
@@ -2160,10 +2255,11 @@ gboolean trig_trace_probe(gpointer user) {
bound_hi, dx_nominal, dx_real,
dx_real > 0.0 ? dx_nominal / dx_real : 0.0, got_lo, got_hi, want_lo, want_hi);
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] "
"data=[%.4f..%.4f] drawn=[%.4f..%.4f]\n",
m.trigger_enabled() ? "on" : "off", int(m.trigger_pending()), display,
m.display_epoch(),
static_cast<unsigned long long>(m.trigger_latches().load()),
int(m.trigger_armed().load()),
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;
}
/// 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
/// 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
@@ -2214,6 +2332,8 @@ gboolean enable_all_probe(gpointer user) {
return G_SOURCE_REMOVE;
}
gboolean disarm_probe_step3(gpointer user);
gboolean disarm_probe_step2(gpointer user) {
App* a = static_cast<App*>(user);
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();
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));
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;
}
/// 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) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
if (a->trigger_toggle != nullptr)
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;
}
@@ -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")) {
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")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), trig_trace_probe, &app);
}