fix: a clock glitch is bridged and a channel is known by name, so a capture keeps its t0
Two defects behind one report ("a trigger during a capture overrides the first,
and the hold-off does not apply"); the second is also what made a spike in one
channel look like the acquisition resetting.
The trigger engine keyed its invalidation on the trigger channel's *index*, which
every rebuild of the channel list renumbers -- enabling a signal that sorts before
it is enough. Any rebuild therefore abandoned an open capture and set no hold-off,
so the next edge inside the post window became t0. It keys on the channel's name
now, which changes only when the source genuinely does.
A packet whose stamps jump more than 256 packet spans ahead is now rejected as a
fault and *bridged* at the declared cadence, instead of poisoning the monotonic
mark (which pinned every later packet flat at the glitched value) and condemning
the next good packet. Only four consecutive failures abandon the declared time
signal, and that fallback no longer wipes the rings: the arrival clock is anchored
to continue from the monotonic mark, so the history is still on one timeline --
the wipe is what made one glitching packet look like the stream restarting from 0.
The declared unit is resolved before those checks, since they weigh the stamps
against a cadence in seconds.
Guards: UDPSCOPE_PROBE_TRIGRENUM_MS prints t0_held=1 through the real widgets,
and the mock's --time-glitch-at/--time-glitch-by injects the clock fault.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
4003084648
commit
f179fbbb44
13 files changed
+406
-96
No files matched your search
+96
@@ -8186,6 +8186,99 @@ gboolean arm_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// The capture the renumber probe opened: the edge it opened on, and the latch count
|
||||
/// before it, so step 3 can tell a new latch from the one already on screen.
|
||||
double g_trigrenum_edge = 0.0;
|
||||
std::uint64_t g_trigrenum_latches = 0;
|
||||
|
||||
/// Headless self-check, step 3: waits for the capture to latch and reports whether the
|
||||
/// edge it latched on is still the one it opened on.
|
||||
///
|
||||
/// `t0_held=1` is the reading. With the engine's invalidation keyed on the trigger
|
||||
/// channel's *index* rather than its name, step 2's renumber abandoned the capture and
|
||||
/// set no hold-off, so the next edge inside the post window became t0 and this read 0.
|
||||
gboolean trigrenum_probe_step3(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
if (a->model->trigger_latches().load(std::memory_order_relaxed) == g_trigrenum_latches)
|
||||
return G_SOURCE_CONTINUE;
|
||||
const double latched = a->model->trigger_time().load(std::memory_order_relaxed);
|
||||
std::fprintf(stderr, "[trigrenum-probe] latched t0=%.4f opened on %.4f t0_held=%d\n", latched,
|
||||
g_trigrenum_edge, int(std::fabs(latched - g_trigrenum_edge) < 1.0e-6));
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Headless self-check, step 2: the moment a capture is open, enables a signal that sorts
|
||||
/// *before* the trigger's own -- which is what renumbers the trigger channel under the
|
||||
/// engine. Polls rather than times, so it lands inside the post window whatever the rate.
|
||||
gboolean trigrenum_probe_step2(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
if (!a->model->trigger_pending()) return G_SOURCE_CONTINUE;
|
||||
g_trigrenum_edge = a->model->capture_edge();
|
||||
g_trigrenum_latches = a->model->trigger_latches().load(std::memory_order_relaxed);
|
||||
int ch = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
ch = a->model->trigger().channel;
|
||||
}
|
||||
std::fprintf(stderr, "[trigrenum-probe] capture open: t0=%.4f ch=%d -- enabling signal 1 now\n",
|
||||
g_trigrenum_edge, ch);
|
||||
std::fflush(stderr);
|
||||
a->model->set_signal_enabled(1, true);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
ch = a->model->trigger().channel;
|
||||
}
|
||||
std::fprintf(stderr, "[trigrenum-probe] after enable: trigger ch=%d channels=%zu\n", ch,
|
||||
a->model->channel_count());
|
||||
std::fflush(stderr);
|
||||
g_timeout_add(25, trigrenum_probe_step3, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Headless self-check, step 1: the reported geometry, driven through the real widgets --
|
||||
/// 1 s window, 10 % pre share, hold-off from `--holdoff` -- on the *square* alone, so that
|
||||
/// enabling the sine later moves the trigger channel's index.
|
||||
gboolean trigrenum_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr || a->window_entry == nullptr) return G_SOURCE_REMOVE;
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(a->window_unit_drop), 2);
|
||||
gtk_editable_set_text(GTK_EDITABLE(a->window_entry), "1");
|
||||
on_window_entry_activate(GTK_ENTRY(a->window_entry), a);
|
||||
// Signal 2 is the mock's square; signal 1 is its sine. Only the square to begin with.
|
||||
a->model->set_signal_enabled(1, false);
|
||||
a->model->set_signal_enabled(2, true);
|
||||
a->model->set_signal_enabled(3, false);
|
||||
if (a->model->channel_count() > 0) {
|
||||
const ChannelInfo& c0 = a->model->channels()[0];
|
||||
a->rows->assign(0, 0, c0.name, c0.name,
|
||||
signal_color_index(a->model->signals(), c0.signal));
|
||||
a->relayout_pending = true;
|
||||
}
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_pre_spin), 10.0);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(a->trigger_level_spin), 0.0);
|
||||
publish_trigger_fields(a);
|
||||
if (a->trigger_toggle != nullptr)
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
const TriggerConfig& tc = a->model->trigger();
|
||||
std::fprintf(stderr,
|
||||
"[trigrenum-probe] window=%.4f event_span=%.4f pre_pct=%.1f post=%.4f "
|
||||
"holdoff=%.4f level=%.4g edge=%d mode=%d enabled=%d armed=%d ch=%d "
|
||||
"channels=%zu\n",
|
||||
a->rows->time_window(), a->model->event_span(), tc.pretrigger_pct,
|
||||
tc.post_span_s(a->model->event_span()), tc.holdoff_s, tc.level, int(tc.edge),
|
||||
int(tc.mode), int(tc.enabled), int(tc.armed), tc.channel,
|
||||
a->model->channel_count());
|
||||
}
|
||||
std::fflush(stderr);
|
||||
g_timeout_add(25, trigrenum_probe_step2, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Headless self-check: changes the trigger panel's two dropdowns, which is what a user
|
||||
/// does to switch Normal/Single or the edge -- mid-capture, if the probe is timed into
|
||||
/// one. Reaching the dropdowns directly is the point: their `notify::selected` handler
|
||||
@@ -9581,6 +9674,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_ARM_TRIGGER_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), arm_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_TRIGRENUM_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), trigrenum_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_FIELDS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), fields_probe, &app);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user