diff --git a/src/ui.cpp b/src/ui.cpp index 3e7aa1e..4d48820 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -8461,16 +8461,25 @@ gboolean fields_probe(gpointer user) { /// 10 ms packets), so comparing trigger_time() against a mock-clock target is meaningless. /// /// 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). +/// recent upward crossing; `measured = -stamp` is the glitch width (≈ 0.002 s). Window 0.2 s, +/// 10% pre (0.02 s back) — the glitch is 0.002 s wide so 0.02 s comfortably contains it. /// /// Shape 1 (pulse `>`): same scan; `measured` is the ordinary square half-period (≈ 0.200 s). +/// Needs a 1.0 s window with 50% pre (0.5 s) because the opening edge of the captured pulse +/// is 0.2 s before x = 0, beyond the 0.02 s pre a 0.2 s window would provide. /// -/// 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 2 (window): edge is set to `falling` (out-of-band), so x = 0 is the moment the +/// signal left the band. Uses the sine channel because the square has no sample inside +/// [-0.5, +0.5] and can never produce a band exit. Reports `at0` (the sample at x = 0, which +/// 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 level crossing; `measured = -stamp` should equal `timeout`. +/// 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. /// /// All legs print one `[condition-probe]` line with `nan` for any unmeasurable field. struct ConditionProbe { @@ -8479,9 +8488,8 @@ struct ConditionProbe { 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; + 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. int shape = 0; @@ -8519,20 +8527,24 @@ gboolean condition_probe_report(gpointer user) { const std::size_t end = static_cast(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). + // Window leg: x=0 is the moment the signal left the band (falling / out-of-band edge). + // `at0` is the sample at x=0: must be outside [lo, hi], i.e. |at0| >= 0.5. + // `earlier` is the sample immediately before x=0: must be inside, i.e. |earlier| < 0.5. + // Using the adjacent sample makes the assertion exact and independent of snapshot size. if (end >= 1) at0 = static_cast(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(vals[back]); + if (end >= 2) earlier = static_cast(vals[end - 2]); } else { - // Pulse and dropout legs: scan backwards for the most recent upward crossing + // Pulse legs: scan backwards for the most recent upward crossing // (prev < level, value >= level) and negate the stamp to get a duration. + // Dropout leg: accept a crossing in either direction because the last sine crossing + // 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]); - if (prev < level && val >= level) { + const bool crossed = (g_condition.shape == 3) + ? ((prev < level) != (val < level)) // either direction + : (prev < level && val >= level); // upward only + if (crossed) { measured = -st[i - 1]; break; } @@ -8554,12 +8566,20 @@ gboolean condition_probe_report(gpointer user) { gboolean condition_probe_arm(gpointer user) { App* a = static_cast(user); if (a->window == nullptr) return G_SOURCE_REMOVE; - // A window short enough that the pre-trigger share is already behind the glitch. - apply_time_window(*a, 0.2); + // Legs 0 and 2 need only a short snapshot (the event is one sample or one glitch-width back). + // 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 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; - const char* want_name = dropout ? "sine" : "square"; + // 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"; for (std::size_t i = 0; i < channels.size(); ++i) { if (channels[i].name == want_name) sel = static_cast(i); } @@ -8575,10 +8595,13 @@ gboolean condition_probe_arm(gpointer user) { 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); + // 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; + 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); // The band for the window leg: straddling zero, well inside the square's +/-1, so each @@ -8587,7 +8610,7 @@ gboolean condition_probe_arm(gpointer user) { // 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), pre_pct); gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_holdoff_spin), 0.0); publish_trigger_fields(a); if (a->trigger_toggle != nullptr) @@ -8609,8 +8632,9 @@ gboolean condition_probe(gpointer user) { 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"; + // 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"; 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);