From 0876ecfe22c584fac83d75c0364144f9b594709e Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 7 Oct 2026 17:48:25 +0200 Subject: [PATCH] fix: condition-probe assertion uses snapshot width, not mock-clock phase The old `phase=` field compared `trigger_time()` against the mock's absolute `glitch_at + glitch_width` target. The app's stream 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, making the comparison meaningless. Replace it with a snapshot-relative measurement: scan backwards from x = 0 in the held event snapshot for the most recent upward crossing of `condition.level` (the same `prev < level && val >= level` test the detector uses) and negate the crossing's stamp to get the measured pulse width. A `measured` near 0.002 s is the glitch caught exactly; near 0.200 s means an ordinary edge was latched. Remove the now-dead `glitch_at`, `glitch_width`, `square_period` fields from `ConditionProbe`. No other files changed. Co-Authored-By: Claude Sonnet 4.6 --- src/ui.cpp | 64 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/src/ui.cpp b/src/ui.cpp index 29708b4..e3a88b1 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -8403,17 +8403,26 @@ gboolean fields_probe(gpointer user) { /// 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 latched t0 against where the pulse's closing edge must be. +/// the measured pulse width from the held event snapshot. /// -/// The assertion is the *phase* rather than an absolute t0: the probe arms at whatever stream -/// time the app happened to reach, so which glitch it catches is not fixed, but every glitch -/// closes at `--glitch-at + --glitch-width + k * (2 / --freq)`. A `phase` of a few sample -/// periods is the pulse being found exactly; a phase near half the square period means the -/// condition fired on an ordinary edge instead. +/// 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. +/// +/// 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. struct ConditionProbe { - double glitch_at = 0.25; - double glitch_width = 0.002; - double square_period = 0.4; int waited = 0; }; ConditionProbe g_condition; @@ -8428,13 +8437,38 @@ gboolean condition_probe_report(gpointer user) { std::fflush(stderr); return G_SOURCE_REMOVE; } - const double want = g_condition.glitch_at + g_condition.glitch_width; - double phase = std::fmod(t0 - want, g_condition.square_period); - if (phase < 0.0) phase += g_condition.square_period; - if (phase > 0.5 * g_condition.square_period) phase -= g_condition.square_period; - std::fprintf(stderr, "[condition-probe] type=%d cmp=%d width=%.6f t0=%.6f phase=%.6f mode=%d\n", + // 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::quiet_NaN(); + const double level = a->model->trigger().condition.level; + const std::size_t trig_ch = static_cast(a->model->trigger().channel); + if (std::shared_ptr snap = a->model->display_snapshot()) { + if (snap != nullptr && trig_ch < snap->values.size()) { + const std::vector& st = snap->stamps; + const std::vector& vals = snap->values[trig_ch]; + if (vals.size() == st.size() && !st.empty()) { + // Find the last index whose stamp is <= 0. + const auto it = std::upper_bound(st.begin(), st.end(), 0.0); + std::size_t end = static_cast(it - st.begin()); + // Scan backwards for the most recent upward crossing (prev < level, value >= level). + 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) { + measured = -st[i - 1]; + break; + } + } + } + } + } + std::fprintf(stderr, "[condition-probe] type=%d cmp=%d width=%.6f t0=%.6f measured=%.6f mode=%d\n", int(a->model->trigger().condition.type), int(a->model->trigger().condition.cmp), - a->model->trigger().condition.width_s, t0, phase, int(a->model->display_mode())); + a->model->trigger().condition.width_s, t0, measured, + int(a->model->display_mode())); std::fflush(stderr); return G_SOURCE_REMOVE; }