feat: a level band and a signal that stops can be the trigger's event too

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-07 18:01:07 +02:00
1 parent 0876ecfe22
commit a26770ec32
3 files changed
+198 -41

No files matched your search

+50 -1
View File
@@ -17,12 +17,23 @@ void EventDetector::reset() noexcept {
in_pulse_ = false; in_pulse_ = false;
pulse_up_ = false; pulse_up_ = false;
pulse_start_s_ = 0.0; pulse_start_s_ = 0.0;
inside_ = false;
last_cross_s_ = 0.0;
dropped_ = false;
} }
double EventDetector::feed(double value, double t) noexcept { double EventDetector::feed(double value, double t) noexcept {
if (!have_prev_) { if (!have_prev_) {
prev_ = value; prev_ = value;
have_prev_ = true; have_prev_ = true;
// The window condition needs a side to change *from*, and the dropout a last-seen time
// to count from; neither can be derived from a crossing that has not happened. Seeding
// them here is what lets both handlers run without a "have we started" guard.
const double lo = cond_.level < cond_.level2 ? cond_.level : cond_.level2;
const double hi = cond_.level < cond_.level2 ? cond_.level2 : cond_.level;
inside_ = value >= lo && value <= hi;
last_cross_s_ = t;
dropped_ = false;
return kNoEvent; return kNoEvent;
} }
double event = kNoEvent; double event = kNoEvent;
@@ -34,8 +45,11 @@ double EventDetector::feed(double value, double t) noexcept {
event = feed_pulse(value, t); event = feed_pulse(value, t);
break; break;
case TriggerType::Window: case TriggerType::Window:
event = feed_window(value, t);
break;
case TriggerType::Dropout: case TriggerType::Dropout:
break; // implemented in the tasks that add their panel rows event = feed_dropout(value, t);
break;
} }
// After the handler: every crossing test above reads `prev_` as the previous sample. // After the handler: every crossing test above reads `prev_` as the previous sample.
prev_ = value; prev_ = value;
@@ -98,4 +112,39 @@ double EventDetector::feed_pulse(double value, double t) noexcept {
return qualifies(width) ? t : kNoEvent; return qualifies(width) ? t : kNoEvent;
} }
double EventDetector::feed_window(double value, double t) noexcept {
const double lo = cond_.level < cond_.level2 ? cond_.level : cond_.level2;
const double hi = cond_.level < cond_.level2 ? cond_.level2 : cond_.level;
const bool inside = value >= lo && value <= hi;
if (inside == inside_) return kNoEvent;
inside_ = inside;
// The event is the *change of side*, so `Rising` reads as "into the band" and `Falling`
// as "out of it". A band of zero width therefore never fires at all, because no sample
// can be on a different side of it than its neighbour: the panel refuses one.
const bool fired = (cond_.edge == TriggerEdge::Rising && inside) ||
(cond_.edge == TriggerEdge::Falling && !inside) ||
cond_.edge == TriggerEdge::Both;
return fired ? t : kNoEvent;
}
double EventDetector::feed_dropout(double value, double t) noexcept {
const bool up = crossed_up(value);
const bool down = crossed_down(value);
const bool crossed = (cond_.edge == TriggerEdge::Rising && up) ||
(cond_.edge == TriggerEdge::Falling && down) ||
(cond_.edge == TriggerEdge::Both && (up || down));
if (crossed) {
last_cross_s_ = t;
dropped_ = false;
return kNoEvent;
}
// One event per silence, not one per sample of it: without the latch a dead signal would
// report an event on every sample after the timeout and fill the search's hit list with
// one answer repeated.
if (dropped_) return kNoEvent;
if (t - last_cross_s_ <= cond_.timeout_s) return kNoEvent;
dropped_ = true;
return t;
}
} // namespace udpscope } // namespace udpscope
+5
View File
@@ -83,6 +83,8 @@ class EventDetector {
} }
double feed_edge(double value, double t) noexcept; double feed_edge(double value, double t) noexcept;
double feed_pulse(double value, double t) noexcept; double feed_pulse(double value, double t) noexcept;
double feed_window(double value, double t) noexcept;
double feed_dropout(double value, double t) noexcept;
/// Whether a measured width satisfies `cond_.cmp`. `Within`/`Outside` take their two /// Whether a measured width satisfies `cond_.cmp`. `Within`/`Outside` take their two
/// bounds in either order, so a panel cannot make an empty band by typing the larger /// bounds in either order, so a panel cannot make an empty band by typing the larger
/// figure into the first spin. /// figure into the first spin.
@@ -94,6 +96,9 @@ class EventDetector {
bool in_pulse_ = false; ///< PulseWidth: an opening edge has been seen. bool in_pulse_ = false; ///< PulseWidth: an opening edge has been seen.
bool pulse_up_ = false; ///< PulseWidth: its polarity, so we know which edge closes it. bool pulse_up_ = false; ///< PulseWidth: its polarity, so we know which edge closes it.
double pulse_start_s_ = 0.0; double pulse_start_s_ = 0.0;
bool inside_ = false; ///< Window: which side of the band the previous sample was.
double last_cross_s_ = 0.0; ///< Dropout: when the signal was last seen crossing.
bool dropped_ = false; ///< Dropout: the timeout has already been reported.
}; };
} // namespace udpscope } // namespace udpscope
+143 -40
View File
@@ -135,12 +135,16 @@ struct App {
GtkWidget* trigger_cmp_drop = nullptr; GtkWidget* trigger_cmp_drop = nullptr;
GtkWidget* trigger_width_spin = nullptr; GtkWidget* trigger_width_spin = nullptr;
GtkWidget* trigger_width2_spin = nullptr; GtkWidget* trigger_width2_spin = nullptr;
GtkWidget* trigger_level2_spin = nullptr;
GtkWidget* trigger_timeout_spin = nullptr;
/// The `labeled_box` rows a condition may not use: held so `sync_trigger_rows()` can /// The `labeled_box` rows a condition may not use: held so `sync_trigger_rows()` can
/// hide them. A hidden row keeps its widget's value, so switching condition and back /// hide them. A hidden row keeps its widget's value, so switching condition and back
/// does not lose what was typed. /// does not lose what was typed.
GtkWidget* trigger_cmp_row = nullptr; GtkWidget* trigger_cmp_row = nullptr;
GtkWidget* trigger_width_row = nullptr; GtkWidget* trigger_width_row = nullptr;
GtkWidget* trigger_width2_row = nullptr; GtkWidget* trigger_width2_row = nullptr;
GtkWidget* trigger_level2_row = nullptr;
GtkWidget* trigger_timeout_row = nullptr;
GtkWidget* trigger_mode_drop = nullptr; GtkWidget* trigger_mode_drop = nullptr;
GtkWidget* trigger_level_spin = nullptr; GtkWidget* trigger_level_spin = nullptr;
GtkWidget* trigger_pre_spin = nullptr; GtkWidget* trigger_pre_spin = nullptr;
@@ -1612,6 +1616,10 @@ TriggerType trigger_type_at(guint i) {
switch (i) { switch (i) {
case 1: case 1:
return TriggerType::PulseWidth; return TriggerType::PulseWidth;
case 2:
return TriggerType::Window;
case 3:
return TriggerType::Dropout;
default: default:
return TriggerType::Edge; return TriggerType::Edge;
} }
@@ -1646,6 +1654,8 @@ void publish_trigger_fields(App* app) {
const guint cmp = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_cmp_drop)); const guint cmp = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_cmp_drop));
const double width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width_spin)); const double width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width_spin));
const double width2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width2_spin)); const double width2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width2_spin));
const double level2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_level2_spin));
const double timeout = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_timeout_spin));
{ {
std::lock_guard<std::mutex> lock(app->model->mutex()); std::lock_guard<std::mutex> lock(app->model->mutex());
TriggerConfig& tc = app->model->trigger(); TriggerConfig& tc = app->model->trigger();
@@ -1660,6 +1670,8 @@ void publish_trigger_fields(App* app) {
tc.condition.cmp = width_cmp_at(cmp); tc.condition.cmp = width_cmp_at(cmp);
tc.condition.width_s = width; tc.condition.width_s = width;
tc.condition.width2_s = width2; tc.condition.width2_s = width2;
tc.condition.level2 = level2;
tc.condition.timeout_s = timeout;
} }
} }
@@ -1673,10 +1685,27 @@ void sync_trigger_rows(App& app) {
const WidthCmp cmp = const WidthCmp cmp =
width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop))); width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop)));
const bool pulse = type == TriggerType::PulseWidth; const bool pulse = type == TriggerType::PulseWidth;
const bool window = type == TriggerType::Window;
gtk_widget_set_visible(app.trigger_cmp_row, pulse); gtk_widget_set_visible(app.trigger_cmp_row, pulse);
gtk_widget_set_visible(app.trigger_width_row, pulse); gtk_widget_set_visible(app.trigger_width_row, pulse);
gtk_widget_set_visible(app.trigger_width2_row, gtk_widget_set_visible(app.trigger_width2_row,
pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside)); pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside));
gtk_widget_set_visible(app.trigger_level2_row, window);
gtk_widget_set_visible(app.trigger_timeout_row, type == TriggerType::Dropout);
// A band of zero width is not a degenerate edge trigger -- a window event is a change of
// *side*, so equal levels silently never fire, which looks exactly like a broken trigger.
// The panel is where that is refused: the second level is pushed at least one increment
// clear of the first, in whichever direction it already lay.
if (window) {
GtkSpinButton* l2 = GTK_SPIN_BUTTON(app.trigger_level2_spin);
const double level = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app.trigger_level_spin));
double step = 0.0;
gtk_spin_button_get_increments(l2, &step, nullptr);
const double other = gtk_spin_button_get_value(l2);
if (std::fabs(other - level) < step) {
gtk_spin_button_set_value(l2, other < level ? level - step : level + step);
}
}
} }
void on_trigger_field_changed(GtkWidget*, gpointer user) { void on_trigger_field_changed(GtkWidget*, gpointer user) {
@@ -6614,7 +6643,7 @@ GtkWidget* build_trigger_panel(App& app) {
G_CALLBACK(on_trigger_signal_changed), &app); G_CALLBACK(on_trigger_signal_changed), &app);
gtk_box_append(GTK_BOX(box), labeled_box("signal", app.trigger_signal_drop)); gtk_box_append(GTK_BOX(box), labeled_box("signal", app.trigger_signal_drop));
const char* types[] = {"edge", "pulse width", nullptr}; const char* types[] = {"edge", "pulse width", "window", "dropout", nullptr};
app.trigger_type_drop = gtk_drop_down_new_from_strings(types); app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
gtk_widget_set_tooltip_text(app.trigger_type_drop, gtk_widget_set_tooltip_text(app.trigger_type_drop,
"what counts as an event: a level crossing, or a pulse whose " "what counts as an event: a level crossing, or a pulse whose "
@@ -6643,6 +6672,19 @@ GtkWidget* build_trigger_panel(App& app) {
app.trigger_width2_row = labeled_box("and s", app.trigger_width2_spin); app.trigger_width2_row = labeled_box("and s", app.trigger_width2_spin);
gtk_box_append(GTK_BOX(box), app.trigger_width2_row); gtk_box_append(GTK_BOX(box), app.trigger_width2_row);
app.trigger_level2_spin = spin(-1.0e12, 1.0e12, 0.01, 4);
gtk_widget_set_tooltip_text(app.trigger_level2_spin,
"the band's other edge; it is kept at least one increment away "
"from the level, because a band of zero width can never be "
"crossed");
app.trigger_level2_row = labeled_box("to level", app.trigger_level2_spin);
gtk_box_append(GTK_BOX(box), app.trigger_level2_row);
app.trigger_timeout_spin = spin(0.0, 3600.0, 0.001, 6);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_timeout_spin), 0.001);
app.trigger_timeout_row = labeled_box("timeout s", app.trigger_timeout_spin);
gtk_box_append(GTK_BOX(box), app.trigger_timeout_row);
app.trigger_level_spin = spin(-1.0e12, 1.0e12, 0.01, 4); app.trigger_level_spin = spin(-1.0e12, 1.0e12, 0.01, 4);
g_signal_connect(app.trigger_level_spin, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) { g_signal_connect(app.trigger_level_spin, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
static_cast<App*>(u)->trigger_level_touched = true; static_cast<App*>(u)->trigger_level_touched = true;
@@ -6674,7 +6716,8 @@ GtkWidget* build_trigger_panel(App& app) {
gtk_box_append(GTK_BOX(box), live); gtk_box_append(GTK_BOX(box), live);
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_spin, for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_spin,
app.trigger_width_spin, app.trigger_width2_spin}) { app.trigger_width_spin, app.trigger_width2_spin,
app.trigger_level2_spin, app.trigger_timeout_spin}) {
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app); g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
} }
g_signal_connect(app.trigger_edge_drop, "notify::selected", g_signal_connect(app.trigger_edge_drop, "notify::selected",
@@ -6685,6 +6728,14 @@ GtkWidget* build_trigger_panel(App& app) {
G_CALLBACK(on_trigger_shape_changed), &app); G_CALLBACK(on_trigger_shape_changed), &app);
g_signal_connect(app.trigger_cmp_drop, "notify::selected", g_signal_connect(app.trigger_cmp_drop, "notify::selected",
G_CALLBACK(on_trigger_shape_changed), &app); G_CALLBACK(on_trigger_shape_changed), &app);
// The clamp lives in `sync_trigger_rows()`, so the two level spins have to run it rather
// than the plain field publish.
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_level2_spin}) {
g_signal_connect(w, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
sync_trigger_rows(*static_cast<App*>(u));
}),
&app);
}
sync_trigger_rows(app); sync_trigger_rows(app);
return box; return box;
} }
@@ -8401,28 +8452,39 @@ gboolean fields_probe(gpointer user) {
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
/// Headless self-check for the qualified conditions: arms a pulse-width condition on the /// Headless self-check for the qualified conditions. Selected by
/// mock's `square`, whose narrow pulse the mock injects once per square period, and reports /// `UDPSCOPE_PROBE_CONDITION_SHAPE` (0 = pulse `<`, 1 = pulse `>`, 2 = window, 3 = dropout).
/// the measured pulse width from the held event snapshot.
/// ///
/// The assertion is snapshot-relative rather than an absolute t0. The app's stream clock /// The assertion is snapshot-relative rather than an absolute t0. The app's stream clock
/// epoch is the first packet it received, not the mock's t = 0; the two clocks differ by /// epoch is the first packet it received, not the mock's t = 0; the two clocks differ by
/// however many packets the CONNECT handshake cost (typically a whole number of 100-sample / /// however many packets the CONNECT handshake cost (typically a whole number of 100-sample /
/// 10 ms packets), so comparing trigger_time() against a mock-clock target is meaningless. /// 10 ms packets), so comparing trigger_time() against a mock-clock target is meaningless.
/// ///
/// A held event snapshot's stamps are edge-relative: stamps == 0 is the latching event, /// Shape 0 (pulse `<`): x = 0 is the glitch's closing edge. Scans backwards for the most
/// which for a pulse-width condition is the pulse's *closing* edge. The pulse's opening /// recent upward crossing; `measured = -stamp` is the glitch width (≈ 0.002 s).
/// edge therefore sits at stamps ≈ -glitch_width. The probe scans backwards from x = 0 to
/// find the most recent upward crossing of level 0 (the same prev < level && value >= level
/// test the detector uses) and negates the crossing's stamp to get the measured pulse width.
/// ///
/// A `measured` near 0.002 s means the condition caught the 2 ms glitch pulse exactly. /// Shape 1 (pulse `>`): same scan; `measured` is the ordinary square half-period (≈ 0.200 s).
/// A `measured` near 0.200 s (the ordinary square half-period) means the condition fired on ///
/// an ordinary edge rather than the narrow pulse -- that is the wrong answer to distinguish /// Shape 2 (window): x = 0 is the moment the signal left the band. Reports `at0` (the
/// against. If no snapshot is available or no upward crossing is found before x = 0, the /// sample value at x = 0, which must be outside the band) and `earlier` (a sample a few
/// line still prints with `measured=nan` rather than being skipped, so a failure is still /// stored periods back, which must be inside).
/// one readable line. ///
/// Shape 3 (dropout): x = 0 is `quiet_at + timeout`, the moment the timeout expired. Scans
/// backwards for the most recent level crossing; `measured = -stamp` should equal `timeout`.
///
/// All legs print one `[condition-probe]` line with `nan` for any unmeasurable field.
struct ConditionProbe { struct ConditionProbe {
double glitch_at = 0.25;
double glitch_width = 0.002;
double square_period = 0.4;
/// With `--quiet-at`, the sine stops at a level on the other side of zero, so `quiet_at`
/// is its last crossing and the dropout's event time is exactly `quiet_at + timeout`.
/// Unlike the glitch this happens *once*, so the assertion is the absolute t0.
double quiet_at = -1.0;
double timeout = 0.050;
/// 0 = pulse width `<` (the glitch), 1 = pulse width `>`, 2 = window, 3 = dropout. Selected
/// by `UDPSCOPE_PROBE_CONDITION_SHAPE`; the default keeps Task 3's glitch leg.
int shape = 0;
int waited = 0; int waited = 0;
}; };
ConditionProbe g_condition; ConditionProbe g_condition;
@@ -8437,37 +8499,53 @@ gboolean condition_probe_report(gpointer user) {
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
// Measure the pulse width from the snapshot's own edge-relative stamps rather than const double nan = std::numeric_limits<double>::quiet_NaN();
// comparing t0 against a mock-clock target. stamps == 0 is the closing edge (the
// latching event for a pulse-width condition); the opening edge sits at stamps ≈ -width.
// Scan backwards from the last sample at or before x = 0 for the most recent upward
// crossing of the condition level, then negate the crossing stamp to get the width.
double measured = std::numeric_limits<double>::quiet_NaN();
const double level = a->model->trigger().condition.level; const double level = a->model->trigger().condition.level;
const double level2 = a->model->trigger().condition.level2;
const std::size_t trig_ch = static_cast<std::size_t>(a->model->trigger().channel); const std::size_t trig_ch = static_cast<std::size_t>(a->model->trigger().channel);
// Shape 2 reports two sample values rather than a measured width.
double at0 = nan;
double earlier = nan;
double measured = nan;
if (std::shared_ptr<const EventSnapshot> snap = a->model->display_snapshot()) { if (std::shared_ptr<const EventSnapshot> snap = a->model->display_snapshot()) {
if (snap != nullptr && trig_ch < snap->values.size()) { if (snap != nullptr && trig_ch < snap->values.size()) {
const std::vector<f64>& st = snap->stamps; const std::vector<f64>& st = snap->stamps;
const std::vector<f32>& vals = snap->values[trig_ch]; const std::vector<f32>& vals = snap->values[trig_ch];
if (vals.size() == st.size() && !st.empty()) { if (vals.size() == st.size() && !st.empty()) {
// Find the last index whose stamp is <= 0. // Index of the last stamp <= 0 (the latching event).
const auto it = std::upper_bound(st.begin(), st.end(), 0.0); const auto it = std::upper_bound(st.begin(), st.end(), 0.0);
std::size_t end = static_cast<std::size_t>(it - st.begin()); const std::size_t end = static_cast<std::size_t>(it - st.begin());
// Scan backwards for the most recent upward crossing (prev < level, value >= level).
for (std::size_t i = end; i >= 2; --i) { if (g_condition.shape == 2) {
const double prev = static_cast<double>(vals[i - 2]); // Window leg: x=0 is the moment the signal left the band.
const double val = static_cast<double>(vals[i - 1]); // `at0` is the sample at x=0 (must be outside [lo,hi]).
if (prev < level && val >= level) { // `earlier` is a sample a few stored periods back (must be inside).
measured = -st[i - 1]; if (end >= 1) at0 = static_cast<double>(vals[end - 1]);
break; // "A few stored periods" back: use half the snapshot length as a rough offset.
const std::size_t back = end > 4 ? end / 2 : 0;
if (back < vals.size()) earlier = static_cast<double>(vals[back]);
} else {
// Pulse and dropout legs: scan backwards for the most recent upward crossing
// (prev < level, value >= level) and negate the stamp to get a duration.
for (std::size_t i = end; i >= 2; --i) {
const double prev = static_cast<double>(vals[i - 2]);
const double val = static_cast<double>(vals[i - 1]);
if (prev < level && val >= level) {
measured = -st[i - 1];
break;
}
} }
} }
} }
} }
} }
std::fprintf(stderr, "[condition-probe] type=%d cmp=%d width=%.6f t0=%.6f measured=%.6f mode=%d\n", std::fprintf(stderr,
"[condition-probe] type=%d cmp=%d width=%.6f t0=%.6f measured=%.6f "
"at0=%.6f earlier=%.6f level2=%.6f mode=%d\n",
int(a->model->trigger().condition.type), int(a->model->trigger().condition.cmp), int(a->model->trigger().condition.type), int(a->model->trigger().condition.cmp),
a->model->trigger().condition.width_s, t0, measured, a->model->trigger().condition.width_s, t0, measured, at0, earlier, level2,
int(a->model->display_mode())); int(a->model->display_mode()));
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
@@ -8480,19 +8558,35 @@ gboolean condition_probe_arm(gpointer user) {
apply_time_window(*a, 0.2); apply_time_window(*a, 0.2);
const std::vector<ChannelInfo>& channels = a->model->channels(); const std::vector<ChannelInfo>& channels = a->model->channels();
int sel = -1; int sel = -1;
const bool dropout = g_condition.shape == 3;
const char* want_name = dropout ? "sine" : "square";
for (std::size_t i = 0; i < channels.size(); ++i) { for (std::size_t i = 0; i < channels.size(); ++i) {
if (channels[i].name == "square") sel = static_cast<int>(i); if (channels[i].name == want_name) sel = static_cast<int>(i);
} }
std::fprintf(stderr, "[condition-probe] square=%d of %zu channels\n", sel, channels.size()); std::fprintf(stderr, "[condition-probe] %s=%d of %zu channels\n", want_name, sel,
channels.size());
if (sel >= 0) { if (sel >= 0) {
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_signal_drop), gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_signal_drop),
static_cast<guint>(sel)); static_cast<guint>(sel));
} }
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_type_drop), 1); // pulse width // Shape 2 is `window`, which the dropdown numbers 2; the two pulse-width legs share type 1
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_cmp_drop), 0); // < // and differ only in the comparison.
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_edge_drop), 0); // rising const guint type = g_condition.shape == 2 ? 2u : (dropout ? 3u : 1u);
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_type_drop), type);
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_cmp_drop),
g_condition.shape == 1 ? 1u : 0u); // `>` for leg 1, `<` otherwise
// `both` for the window and the dropout: a band is entered *and* left, and a dropout has no
// direction at all. Rising for the two pulse-width legs, which open on a rising excursion.
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_edge_drop),
(dropout || g_condition.shape == 2) ? 2u : 0u);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_width_spin), 0.010); gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_width_spin), 0.010);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_level_spin), 0.0); gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_timeout_spin), g_condition.timeout);
// The band for the window leg: straddling zero, well inside the square's +/-1, so each
// transition both enters and leaves it. Harmless for the other legs, which ignore `level2`.
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_level2_spin), 0.5);
// Zero for every leg but the window, whose band is `[-0.5, +0.5]`.
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_level_spin),
g_condition.shape == 2 ? -0.5 : 0.0);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_pre_spin), 10.0); gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_pre_spin), 10.0);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_holdoff_spin), 0.0); gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_holdoff_spin), 0.0);
publish_trigger_fields(a); publish_trigger_fields(a);
@@ -8506,11 +8600,20 @@ gboolean condition_probe_arm(gpointer user) {
gboolean condition_probe(gpointer user) { gboolean condition_probe(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE; if (a->window == nullptr) return G_SOURCE_REMOVE;
// `UDPSCOPE_PROBE_QUIET_AT` mirrors the mock's `--quiet-at`: it selects the dropout leg of
// the probe and tells it where the sine's last crossing is. Absent, the glitch leg runs.
if (const char* q = g_getenv("UDPSCOPE_PROBE_QUIET_AT")) g_condition.quiet_at = std::atof(q);
if (const char* s = g_getenv("UDPSCOPE_PROBE_CONDITION_SHAPE")) {
g_condition.shape = std::atoi(s);
} else if (g_condition.quiet_at >= 0.0) {
g_condition.shape = 3;
}
// `square` is not acquired until it is enabled, and the trigger can only name a channel // `square` is not acquired until it is enabled, and the trigger can only name a channel
// that is: enabling is what starts acquisition. // that is: enabling is what starts acquisition.
const char* want = g_condition.shape == 3 ? "sine" : "square";
const std::vector<SignalInfo>& signals = a->model->signals(); const std::vector<SignalInfo>& signals = a->model->signals();
for (std::size_t i = 0; i < signals.size(); ++i) { for (std::size_t i = 0; i < signals.size(); ++i) {
if (signals[i].name == "square") a->model->set_signal_enabled(i, true); if (signals[i].name == want) a->model->set_signal_enabled(i, true);
} }
// One beat for the channel list to be rebuilt and the ring to refill behind the glitch. // One beat for the channel list to be rebuilt and the ring to refill behind the glitch.
g_timeout_add(1500, condition_probe_arm, a); g_timeout_add(1500, condition_probe_arm, a);