From d79810c7ba8d42e9a3468ea4d1a24972091dc679 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 7 Oct 2026 18:25:33 +0200 Subject: [PATCH] fix: dead probe fields are removed and the channel name is chosen once, so arming cannot disagree with enabling Co-Authored-By: Claude Sonnet 4.6 --- src/ui.cpp | 56 ++++++++++++++++++++++-------------------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/src/ui.cpp b/src/ui.cpp index 52cc718..d534e6c 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -8474,21 +8474,15 @@ gboolean fields_probe(gpointer user) { /// must be outside the band) and `earlier` (the sample immediately before it, which must be /// inside). Window 0.2 s, 10% pre — the crossing is one sample before x = 0. /// -/// Shape 3 (dropout): x = 0 is `quiet_at + timeout`, the moment the timeout expired. Scans -/// backwards for the most recent crossing in either direction, because the last sine crossing -/// before the quiet period is downward for the `--quiet-at 4.26` case and an upward-only scan -/// would miss it. `timeout` must exceed the sine's 0.1 s half-period; 0.3 s is used so that -/// an ordinary live sine cannot satisfy the condition. Needs a 1.0 s window with 50% pre -/// because the crossing is `timeout` ≈ 0.3 s before x = 0. +/// Shape 3 (dropout): x = 0 is the moment the timeout expired. Scans backwards for the most +/// recent crossing in either direction, because the last sine crossing before the quiet period +/// may be downward, and an upward-only scan would miss it. `timeout` must exceed the sine's +/// 0.1 s half-period; 0.3 s is used so that an ordinary live sine cannot satisfy the +/// condition. Needs a 1.0 s window with 50% pre because the crossing is `timeout` ≈ 0.3 s +/// before x = 0. /// /// 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`. - double quiet_at = -1.0; double timeout = 0.300; /// 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. @@ -8497,6 +8491,13 @@ struct ConditionProbe { }; ConditionProbe g_condition; +// Returns the channel name required by the given shape: shapes 2 and 3 use the sine (the +// square has no sample inside [-0.5, +0.5] and cannot satisfy a window condition, and the +// dropout relies on the sine going quiet); shapes 0 and 1 use the square. +static const char* condition_probe_channel(int shape) { + return (shape == 2 || shape == 3) ? "sine" : "square"; +} + gboolean condition_probe_report(gpointer user) { App* a = static_cast(user); if (a->window == nullptr) return G_SOURCE_REMOVE; @@ -8507,15 +8508,15 @@ gboolean condition_probe_report(gpointer user) { std::fflush(stderr); return G_SOURCE_REMOVE; } - const double nan = std::numeric_limits::quiet_NaN(); + const double kNan = std::numeric_limits::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(a->model->trigger().channel); // Shape 2 reports two sample values rather than a measured width. - double at0 = nan; - double earlier = nan; - double measured = nan; + double at0 = kNan; + double earlier = kNan; + double measured = kNan; if (std::shared_ptr snap = a->model->display_snapshot()) { if (snap != nullptr && trig_ch < snap->values.size()) { @@ -8540,7 +8541,7 @@ gboolean condition_probe_report(gpointer user) { // before the quiet period may be downward, and an upward-only scan would miss it. for (std::size_t i = end; i >= 2; --i) { const double prev = static_cast(vals[i - 2]); - const double val = static_cast(vals[i - 1]); + const double val = static_cast(vals[i - 1]); const bool crossed = (g_condition.shape == 3) ? ((prev < level) != (val < level)) // either direction : (prev < level && val >= level); // upward only @@ -8570,16 +8571,13 @@ gboolean condition_probe_arm(gpointer user) { // Legs 1 and 3 need a 1.0 s window with 50% pre so the opening edge of a 0.2 s pulse and // the last sine crossing before a 0.3 s timeout are both captured. const bool wide = (g_condition.shape == 1 || g_condition.shape == 3); - const double win_s = wide ? 1.0 : 0.2; + const double win_s = wide ? 1.0 : 0.2; const double pre_pct = wide ? 50.0 : 10.0; apply_time_window(*a, win_s); const std::vector& channels = a->model->channels(); int sel = -1; const bool dropout = g_condition.shape == 3; - // Shape 2 (window) and shape 3 (dropout) both use the sine: the square has no sample inside - // [-0.5, +0.5] and cannot produce a band change-of-side, and the dropout relies on the sine - // going quiet. Shapes 0 and 1 (pulse-width) use the square. - const char* want_name = (g_condition.shape == 2 || dropout) ? "sine" : "square"; + const char* want_name = condition_probe_channel(g_condition.shape); for (std::size_t i = 0; i < channels.size(); ++i) { if (channels[i].name == want_name) sel = static_cast(i); } @@ -8598,9 +8596,7 @@ gboolean condition_probe_arm(gpointer user) { // Window leg uses `falling` (index 1), meaning out-of-band, which makes the latch deterministic: // x=0 is always a band exit, so at0 is outside and the previous sample is inside. // Dropout has no direction (index 2, `both`). Pulse-width legs use `rising` (index 0). - guint edge_idx = 0u; - if (g_condition.shape == 2) edge_idx = 1u; - else if (dropout) edge_idx = 2u; + const guint edge_idx = (g_condition.shape == 2) ? 1u : (dropout ? 2u : 0u); gtk_drop_down_set_selected(GTK_DROP_DOWN(a->trigger_edge_drop), edge_idx); gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_width_spin), 0.010); gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_timeout_spin), g_condition.timeout); @@ -8624,18 +8620,12 @@ gboolean condition_probe_arm(gpointer user) { gboolean condition_probe(gpointer user) { App* a = static_cast(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. Shape 2 (window) needs the sine because the - // square has no sample inside [-0.5, +0.5] and can never change sides of the band. - const char* want = (g_condition.shape == 2 || g_condition.shape == 3) ? "sine" : "square"; + // that is: enabling is what starts acquisition. + const char* want = condition_probe_channel(g_condition.shape); const std::vector& signals = a->model->signals(); for (std::size_t i = 0; i < signals.size(); ++i) { if (signals[i].name == want) a->model->set_signal_enabled(i, true);