fix(udpscope): stop a wrong hrtDt from displacing the trace permanently

On the hrt branch the derived period is not just a spacing: it is the
burst width ClockOffset latches against, so a wrong one shifts the whole
trace by an amount that is usually too small for kRecalibThresholdS to
ever heal. Three routes to a wrong period were open.

Packet loss. elapsed spans every packet since the last one seen, but it
was divided by prevAccCount alone, so a lost datagram scaled the period
by the whole counter gap. Since a burst is anchored on its LAST element,
too wide means it ends in the FUTURE: +22.5 ms for one loss, +225 ms for
ten, at 10 samples per 25 ms packet, mis-spacing 2.7% of all samples at
1% loss. The declared branch already reads the counter for exactly this;
the hrt branch now does too.

Producer restart and reorder. Both leave elapsed at zero, so no period
can be measured -- and the restart packet is also the one that re-latches
after offset.reset(). Falling back to kDefaultDt is only right at 1 kHz;
measured standing displacement was +13.5 ms at 10 samples per 25 ms and
-89 ms at 100 per 10 ms. Remember the last measured period instead.

A stray hrt == 0 packet re-enters the warm-up branch, which spans from
packetBurst's lastPacketWall -- a field the hrt branch never wrote, so it
still held the start of the session. After 153 packets that emitted a
burst 3.8 s in the past, worse the longer the scope had run.

Also: rule 2 with no declared rate stacked every element of the array on
one instant (as UDPSourceSession.cpp:522 does, harmlessly, for a
host-local consumer). Spread it from consecutive time-signal anchors,
which measure the burst on the producer's own clock.

Reverts the previous commit's wallElapsed <= 0 change: it was measurably
inert -- the step floor two lines below already yields the same number --
and its comment claimed a divergence it did not stop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-08-28 06:11:45 +02:00
co-authored by Claude Opus 4.6
parent 3add2c42b9
commit f97fd825c4
4 changed files with 665 additions and 87 deletions
+33 -1
View File
@@ -10,7 +10,17 @@
* Source/Applications/StreamHub/UDPSourceSession.cpp documents this failure and
* solves it; these are the same rules, computed from udps_frame_t's own fields.
*
* Three rules deliberately differ, all in the accumulated-scalar case (rule 3).
* They are NOT the same code, and the differences are not a short list. Every
* one of them comes from the same root: StreamHub runs on the producer's host,
* so its arrival time IS the producer's clock and its local
* HighResolutionTimer::Frequency() IS the frequency behind the packet's hrt.
* Neither holds over a network, so anything StreamHub can read directly this
* decoder has to estimate (HrtRateFit, ClockOffset), and anything it estimates
* it must also defend — hence the monotonic clamps, the kWallBleedFraction
* bleed, the reorder and restart guards and the duplicate-datagram drop, none of
* which exist in UDPSourceSession.cpp. Do not read the three sections below as
* exhaustive; they are the three that change where a sample LANDS, and so the
* three worth checking first when a trace looks wrong.
*
* First, the anchor. StreamHub anchors every accumulated-scalar burst on the
* packet's own hrt, converted with the LOCAL MARTe HighResolutionTimer
@@ -42,6 +52,15 @@
* datagram and reinstates a hole that never existed. So a signal that has
* already burst keeps every later update on rule 3 regardless of its length; a
* signal that has never burst is a genuine scalar and is left to rule 5.
*
* Two divergences OUTSIDE rule 3 are known and deliberately left as they are.
* Rule 1 keys ClockOffset on the consuming signal, where UDPSourceSession.cpp:516
* keys it on the time-signal index, so signals sharing a time signal share an
* offset there and not here — immaterial, since the mapping they compute is the
* same. And a FIRST_SAMPLE/LAST_SAMPLE signal whose time signal is absent falls
* through to rule 4 rather than using its declared rate; that is a malformed
* CONFIG, and spanning arrivals is the more honest answer than trusting a rate
* whose anchor never arrived.
*/
#pragma once
@@ -92,6 +111,19 @@ private:
double accProdSec = 0.0;
bool lastAccValid = false;
uint32_t prevAccCount = 0;
/** Last inter-element period the hrt branch actually MEASURED, used
* whenever this packet cannot measure one of its own (no previous tick,
* or hrt went backwards). The constant kDefaultDt is a poor substitute:
* it is only right at 1 kHz, and a wrong period here is not merely a
* wrong spacing for one burst — it is the burst width ClockOffset
* latches against, and the resulting displacement is usually too small
* for kRecalibThresholdS to ever heal. Zero until first measured. */
double lastHrtDt = 0.0;
/** Rule 2 only: the previous packet's time-signal anchor, in PRODUCER
* seconds. Consecutive anchors are what lets an array with no declared
* sampling rate be spread at all. */
double prevAnchorProdSec = 0.0;
bool prevAnchorValid = false;
/** For accumulated scalars (rule 3, either branch): end timestamp of the
* most recently emitted burst, and the packet counter it came from. The
* next burst is chained onto that end, with the counter gap reinstating