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:
Martino FerrariandClaude Opus 4.6 committed 2026-10-07 10:29:05 +02:00
1 parent 4003084648
commit f179fbbb44
13 files changed
+406 -96

No files matched your search

+14 -3
View File
@@ -4,6 +4,7 @@
#pragma once
#include <cstdint>
#include <string>
namespace udpscope {
@@ -39,7 +40,16 @@ struct TriggerConfig {
class TriggerEngine {
public:
/// Configures the channel and the post-trigger window. Resets the state when the
/// channel changes.
/// trigger moves to a different channel.
///
/// `channel_name` is that channel's name, and it -- not `cfg.channel` -- is what
/// identifies it here. The index is not stable: every rebuild of the channel list
/// renumbers it (enabling a signal that sorts before the trigger's own is enough),
/// and the model re-resolves the trigger by name afterwards. Keying the reset on the
/// index therefore made any such rebuild abandon an open capture *and set no
/// hold-off*, so the next edge inside the post window became t0 -- which is the
/// "re-triggers mid-capture and ignores the hold-off" defect. Choosing a genuinely
/// different source still resets, because the name changes with it.
///
/// `post_seconds` is the post-trigger window in **stream seconds**, the same clock
/// `process()` is fed and the same clock the x axis reads, so the capture covers
@@ -57,8 +67,8 @@ class TriggerEngine {
/// event cannot latch onto a half-filled pre window. `oldest_s` has to be the real
/// buffer's, not a timer since arming -- the ring keeps filling with the trigger
/// off, so the history is usually already there and waiting would be wrong.
void configure(const TriggerConfig& cfg, double post_seconds, double pre_seconds,
double oldest_s);
void configure(const TriggerConfig& cfg, const std::string& channel_name,
double post_seconds, double pre_seconds, double oldest_s);
/// Disarms without forgetting the configuration.
void disarm() noexcept { armed_ = false; pending_ = false; }
void arm(double now_s) noexcept;
@@ -95,6 +105,7 @@ class TriggerEngine {
private:
TriggerConfig cfg_{};
std::string channel_name_; ///< the trigger channel's stable identity (see `configure`).
bool armed_ = false;
bool have_prev_ = false;
double prev_ = 0.0;