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
@@ -488,6 +488,37 @@ fallback when the stream provides no usable time signal (it is then the sample's
|
||||
via the arrival estimate); it is never used when the stream's own time signal
|
||||
carries a span.
|
||||
|
||||
**One bad packet is bridged; only a run of them changes the clock.** A real device's
|
||||
clock glitches, and a packet whose stamps jump far ahead of the previous packet's last
|
||||
is a fault rather than a gap — so a jump of more than **256 packet spans** is rejected
|
||||
alongside the three checks that were always there (a stamp that is not finite, a packet
|
||||
with no span at all, a packet that does not advance past the previous one). A rejected
|
||||
packet is **bridged**: its stamps are replaced by a continuation of the accepted timeline
|
||||
at the declared cadence, so the next good packet is weighed against an un-poisoned
|
||||
anchor and is accepted normally. Only **four consecutive** rejected packets abandon the
|
||||
declared time signal for the arrival clock, because that is what a signal which is not a
|
||||
time base at all looks like. The declared unit (`ns`, `us`, `ms`) is resolved *before*
|
||||
these checks rather than after, since they weigh the stamps against a cadence in seconds.
|
||||
|
||||
Believing a glitch instead is what produced *"a big spike in one channel and the
|
||||
acquisition seems to reset"*. The stamps pass through a monotonic clamp, so a glitched
|
||||
value poisoned the mark and pinned every later packet flat at it — the stream froze at one
|
||||
x and never recovered — and the glitch also condemned the *next*, perfectly good packet
|
||||
(now below the poisoned anchor, so it failed the advance test), which threw the whole
|
||||
stream onto the arrival clock. `tools/mock_udpstreamer.py --time-glitch-at <s>
|
||||
--time-glitch-by <s>` injects the fault; with a 10 s hold-off the two latches either side
|
||||
of it are 11.2 s apart, where before the fix they were 1.2 s apart.
|
||||
|
||||
**Switching to the arrival clock does not drop history.** `arrival_epoch_` is derived from
|
||||
the monotonic mark, so the arrival clock *continues* from it and the two timelines share a
|
||||
zero by construction — the recorded stamps are therefore still on one timeline and are
|
||||
kept. They used to be wiped on the reasoning that the old stamps belonged to a dead clock;
|
||||
they do not, and that wipe is what made a single glitching packet look like *"the live
|
||||
stream restarts from 0 with no change from the user"*. The same continuity is why
|
||||
`TriggerEngine` needs no rebase across the switch: it holds four absolute stamps
|
||||
(hold-off, pending edge, last trigger, last sample) and they stay valid. Anything that
|
||||
makes the clock genuinely *discontinuous* would break that and would have to reset them.
|
||||
|
||||
**Trigger: what freezes is the view, not acquisition.** `TriggerEngine` runs the
|
||||
edge/hold-off/normal-single state machine on the selected channel, and once the
|
||||
requested post-trigger share of the *event span* (the event-window control, not the
|
||||
@@ -577,6 +608,23 @@ after a rebuild — a CONFIG, enabling the signal, or a calibration change all d
|
||||
history — which is exactly when the first event used to latch on an almost empty pre
|
||||
window and draw half a plot.
|
||||
|
||||
**The engine identifies its trigger channel by name, never by index**, and that is what
|
||||
makes the hold-off hold. `TriggerEngine::configure()` drops an open capture when the
|
||||
trigger moves to a different channel, and it is re-called on *every* packet. The config's
|
||||
channel field is an index into the channel list, and that index is not stable: every
|
||||
rebuild of the list renumbers it — enabling a signal that sorts before the trigger's own
|
||||
is enough — and the model then re-resolves the trigger by name and writes the new index
|
||||
back. Keying the reset on the index therefore made any such rebuild abandon the capture
|
||||
**and set no hold-off**, so the next edge inside the post window became t0: with a 1 s
|
||||
window, a 10 % pre share and a 10 s hold-off, an edge 0.6 s into a capture stole it. That
|
||||
needed no glitch and no user action beyond enabling a channel. A deliberate re-point still
|
||||
resets, because the name changes with the source. Since the rebuild also replaces the
|
||||
buffer, the surviving capture's pre side is short and the snapshot reports its own `pre_s`
|
||||
— which may be **negative** when the new ring starts after the edge. That is a faithful
|
||||
reading rather than a defect: `[-pre_s, post_s]` still describes the samples the snapshot
|
||||
holds. `UDPSCOPE_PROBE_TRIGRENUM_MS` drives the whole sequence through the real widgets
|
||||
and prints `t0_held=1` as the assertion.
|
||||
|
||||
The event is published on the sample that latched it, but only after every channel of
|
||||
that sample has reached its ring: the cut is clamped to the shortest value ring, so
|
||||
publishing from the middle of the channel loop dropped the newest sample — the very one
|
||||
@@ -1833,13 +1881,15 @@ now — they are listed so they are not mistaken for anything else:
|
||||
- **The time reference is the first *enabled* channel's signal.** A CONFIG whose
|
||||
time signal belongs to a signal that is disabled contributes no stream time, and
|
||||
the app falls back to arrival-based timing (with a warning on stderr).
|
||||
- **The choice of time source is sticky, and a change of source empties the rings.**
|
||||
It is decided once (and re-decided on a CONFIG) and then kept: choosing per frame
|
||||
let a stream whose declared time signal fails one check and passes the next
|
||||
alternate between two timelines, so one ring held stamps from both and the x axis,
|
||||
the crosshair and an export read nonsense — a plateau of one timestamp, then a jump
|
||||
to another epoch. On the switch the recorded history is dropped rather than left
|
||||
half on each clock. The stamps themselves are spaced by the stream's *measured*
|
||||
- **The choice of time source is sticky.** It is decided once (and re-decided on a
|
||||
CONFIG) and then kept: choosing per frame let a stream whose declared time signal
|
||||
fails one check and passes the next alternate between two timelines, so one ring held
|
||||
stamps from both and the x axis, the crosshair and an export read nonsense — a plateau
|
||||
of one timestamp, then a jump to another epoch. The switch keeps the recorded history,
|
||||
because the arrival clock is anchored to continue from the monotonic mark and the two
|
||||
timelines therefore share a zero (see "Switching to the arrival clock does not drop
|
||||
history" above); it used to empty the rings, which is a worse reading of the same
|
||||
concern. The stamps themselves are spaced by the stream's *measured*
|
||||
arrival cadence whenever the declared rate disagrees with it, because a CONFIG's
|
||||
rate is a claim the stream may not keep (one that declared 1 MS/s and delivered a
|
||||
hundred samples every fifth of a second made every frame span microseconds while
|
||||
|
||||
Reference in new issue
Block a user