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:
1 parent
0876ecfe22
commit
a26770ec32
3 files changed
+192
-35
No files matched your search
+50
-1
@@ -17,12 +17,23 @@ void EventDetector::reset() noexcept {
|
||||
in_pulse_ = false;
|
||||
pulse_up_ = false;
|
||||
pulse_start_s_ = 0.0;
|
||||
inside_ = false;
|
||||
last_cross_s_ = 0.0;
|
||||
dropped_ = false;
|
||||
}
|
||||
|
||||
double EventDetector::feed(double value, double t) noexcept {
|
||||
if (!have_prev_) {
|
||||
prev_ = value;
|
||||
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;
|
||||
}
|
||||
double event = kNoEvent;
|
||||
@@ -34,8 +45,11 @@ double EventDetector::feed(double value, double t) noexcept {
|
||||
event = feed_pulse(value, t);
|
||||
break;
|
||||
case TriggerType::Window:
|
||||
event = feed_window(value, t);
|
||||
break;
|
||||
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.
|
||||
prev_ = value;
|
||||
@@ -98,4 +112,39 @@ double EventDetector::feed_pulse(double value, double t) noexcept {
|
||||
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
|
||||
@@ -83,6 +83,8 @@ class EventDetector {
|
||||
}
|
||||
double feed_edge(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
|
||||
/// bounds in either order, so a panel cannot make an empty band by typing the larger
|
||||
/// figure into the first spin.
|
||||
@@ -94,6 +96,9 @@ class EventDetector {
|
||||
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.
|
||||
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
|
||||
+137
-34
@@ -135,12 +135,16 @@ struct App {
|
||||
GtkWidget* trigger_cmp_drop = nullptr;
|
||||
GtkWidget* trigger_width_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
|
||||
/// hide them. A hidden row keeps its widget's value, so switching condition and back
|
||||
/// does not lose what was typed.
|
||||
GtkWidget* trigger_cmp_row = nullptr;
|
||||
GtkWidget* trigger_width_row = nullptr;
|
||||
GtkWidget* trigger_width2_row = nullptr;
|
||||
GtkWidget* trigger_level2_row = nullptr;
|
||||
GtkWidget* trigger_timeout_row = nullptr;
|
||||
GtkWidget* trigger_mode_drop = nullptr;
|
||||
GtkWidget* trigger_level_spin = nullptr;
|
||||
GtkWidget* trigger_pre_spin = nullptr;
|
||||
@@ -1612,6 +1616,10 @@ TriggerType trigger_type_at(guint i) {
|
||||
switch (i) {
|
||||
case 1:
|
||||
return TriggerType::PulseWidth;
|
||||
case 2:
|
||||
return TriggerType::Window;
|
||||
case 3:
|
||||
return TriggerType::Dropout;
|
||||
default:
|
||||
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 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 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());
|
||||
TriggerConfig& tc = app->model->trigger();
|
||||
@@ -1660,6 +1670,8 @@ void publish_trigger_fields(App* app) {
|
||||
tc.condition.cmp = width_cmp_at(cmp);
|
||||
tc.condition.width_s = width;
|
||||
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 =
|
||||
width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop)));
|
||||
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_width_row, pulse);
|
||||
gtk_widget_set_visible(app.trigger_width2_row,
|
||||
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) {
|
||||
@@ -6614,7 +6643,7 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
G_CALLBACK(on_trigger_signal_changed), &app);
|
||||
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);
|
||||
gtk_widget_set_tooltip_text(app.trigger_type_drop,
|
||||
"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);
|
||||
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);
|
||||
g_signal_connect(app.trigger_level_spin, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
|
||||
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);
|
||||
|
||||
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(app.trigger_edge_drop, "notify::selected",
|
||||
@@ -6685,6 +6728,14 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
G_CALLBACK(on_trigger_shape_changed), &app);
|
||||
g_signal_connect(app.trigger_cmp_drop, "notify::selected",
|
||||
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);
|
||||
return box;
|
||||
}
|
||||
@@ -8401,28 +8452,39 @@ gboolean fields_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Headless self-check for the qualified conditions: arms a pulse-width condition on the
|
||||
/// mock's `square`, whose narrow pulse the mock injects once per square period, and reports
|
||||
/// the measured pulse width from the held event snapshot.
|
||||
/// Headless self-check for the qualified conditions. Selected by
|
||||
/// `UDPSCOPE_PROBE_CONDITION_SHAPE` (0 = pulse `<`, 1 = pulse `>`, 2 = window, 3 = dropout).
|
||||
///
|
||||
/// 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
|
||||
/// 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.
|
||||
///
|
||||
/// A held event snapshot's stamps are edge-relative: stamps == 0 is the latching event,
|
||||
/// which for a pulse-width condition is the pulse's *closing* edge. The pulse's opening
|
||||
/// 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.
|
||||
/// Shape 0 (pulse `<`): x = 0 is the glitch's closing edge. Scans backwards for the most
|
||||
/// recent upward crossing; `measured = -stamp` is the glitch width (≈ 0.002 s).
|
||||
///
|
||||
/// A `measured` near 0.002 s means the condition caught the 2 ms glitch pulse exactly.
|
||||
/// 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
|
||||
/// against. If no snapshot is available or no upward crossing is found before x = 0, the
|
||||
/// line still prints with `measured=nan` rather than being skipped, so a failure is still
|
||||
/// one readable line.
|
||||
/// Shape 1 (pulse `>`): same scan; `measured` is the ordinary square half-period (≈ 0.200 s).
|
||||
///
|
||||
/// Shape 2 (window): x = 0 is the moment the signal left the band. Reports `at0` (the
|
||||
/// sample value at x = 0, which must be outside the band) and `earlier` (a sample a few
|
||||
/// stored periods back, which must be inside).
|
||||
///
|
||||
/// 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 {
|
||||
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;
|
||||
};
|
||||
ConditionProbe g_condition;
|
||||
@@ -8437,23 +8499,36 @@ gboolean condition_probe_report(gpointer user) {
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
// Measure the pulse width from the snapshot's own edge-relative stamps rather than
|
||||
// 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 nan = std::numeric_limits<double>::quiet_NaN();
|
||||
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);
|
||||
|
||||
// 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 (snap != nullptr && trig_ch < snap->values.size()) {
|
||||
const std::vector<f64>& st = snap->stamps;
|
||||
const std::vector<f32>& vals = snap->values[trig_ch];
|
||||
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);
|
||||
std::size_t end = static_cast<std::size_t>(it - st.begin());
|
||||
// Scan backwards for the most recent upward crossing (prev < level, value >= level).
|
||||
const std::size_t end = static_cast<std::size_t>(it - st.begin());
|
||||
|
||||
if (g_condition.shape == 2) {
|
||||
// Window leg: x=0 is the moment the signal left the band.
|
||||
// `at0` is the sample at x=0 (must be outside [lo,hi]).
|
||||
// `earlier` is a sample a few stored periods back (must be inside).
|
||||
if (end >= 1) at0 = static_cast<double>(vals[end - 1]);
|
||||
// "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]);
|
||||
@@ -8465,9 +8540,12 @@ gboolean condition_probe_report(gpointer user) {
|
||||
}
|
||||
}
|
||||
}
|
||||
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),
|
||||
a->model->trigger().condition.width_s, t0, measured,
|
||||
a->model->trigger().condition.width_s, t0, measured, at0, earlier, level2,
|
||||
int(a->model->display_mode()));
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
@@ -8480,19 +8558,35 @@ gboolean condition_probe_arm(gpointer user) {
|
||||
apply_time_window(*a, 0.2);
|
||||
const std::vector<ChannelInfo>& channels = a->model->channels();
|
||||
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) {
|
||||
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) {
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_signal_drop),
|
||||
static_cast<guint>(sel));
|
||||
}
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_type_drop), 1); // pulse width
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_cmp_drop), 0); // <
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_edge_drop), 0); // rising
|
||||
// Shape 2 is `window`, which the dropdown numbers 2; the two pulse-width legs share type 1
|
||||
// and differ only in the comparison.
|
||||
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_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_holdoff_spin), 0.0);
|
||||
publish_trigger_fields(a);
|
||||
@@ -8506,11 +8600,20 @@ gboolean condition_probe_arm(gpointer user) {
|
||||
gboolean condition_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
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
|
||||
// that is: enabling is what starts acquisition.
|
||||
const char* want = g_condition.shape == 3 ? "sine" : "square";
|
||||
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||
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.
|
||||
g_timeout_add(1500, condition_probe_arm, a);
|
||||
|
||||
Reference in new issue
Block a user