Files
MARTe-Integrated-Components/Client/udpscope/TimeBase.h
T
Martino FerrariandClaude Opus 4.6 3270284cfe fix(udpscope): bound the reconstructed timeline against the wall clock
Round 3 of the Task 4 review. Three defects, all in FrameDecoder rule 3.

The resync backstop was one-directional. `predicted` is never below
lastEmittedEnd + dt, so rejecting a correction that would step backwards
meant only a LAGGING chain could ever be pulled back; a chain running fast
drifted ahead without bound. Two hosts' crystals differ by tens of ppm, so a
declared SamplingRate is always slightly wrong in one direction or the other
and this is certain on a long session. A leading timeline cannot be corrected
in one burst without going backwards -- lastEmittedEnd is by definition past
arrival -- so the excess is bled off by drawing each burst 10 % narrower until
the timeline is back inside the threshold.

A repeated packet counter was treated as a normal packet. The C client
de-duplicates fragments only, so an unfragmented update reaching a host that
joined the group on two interfaces was emitted twice, doubling the values and
advancing the timeline by a burst that never existed.

The samplingRate == 0 path differenced two HrtRateFit::toSeconds() results.
toSeconds() divides an absolute tick count -- ~1e11 on a producer that has
been up a day -- by a rate refitted on every packet, so its few-parts-in-1e4
wobble arrives multiplied by the whole elapsed epoch: tens of milliseconds of
jitter on a value whose consecutive difference is a few milliseconds. Raw
ticks are differenced instead, anchored on the first usable packet so the
wobble applies only to the interval since attach.

The existing hrt-gap test could not have caught the last one: its 10 ms
producer period made the expected answer exactly kDefaultDt, so a decoder
that derived nothing passed. It now uses 25 ms.

Four tests added, all sabotage-proven. The plan is updated to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-27 21:55:33 +02:00

97 lines
3.4 KiB
C++

/**
* @file TimeBase.h
* @brief Producer-clock to wall-clock reconstruction.
*
* Framework-free. A UDPS stream's accurate timestamps come from a producer
* clock — either a declared time signal or the packet's embedded hrt — and both
* need mapping onto the client's wall clock before they can be plotted.
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <deque>
namespace udpscope {
/**
* @brief Seconds per count of a time signal, from its type code.
*
* The protocol carries uint64 time signals in nanoseconds and everything else
* in microseconds; this mirrors UDPSourceSession so the two agree on a stream.
*/
double TimeSignalScale(uint8_t typeCode);
/**
* @brief A latched producer-to-wall offset.
*
* Established from the first sample and then held, so network jitter does not
* wobble the trace. Only a drift beyond kRecalibThresholdS — a producer restart
* or re-phase, not delivery noise — forces a new calibration.
*/
class ClockOffset {
public:
static constexpr double kRecalibThresholdS = 0.5;
/** @return producerSec mapped onto wall clock. */
double map(double producerSec, double wallSec);
bool valid() const { return valid_; }
void reset() { valid_ = false; offset_ = 0.0; }
double offset() const { return offset_; }
private:
double offset_ = 0.0;
bool valid_ = false;
};
/**
* @brief Recovers the producer's hrt tick rate by least squares against arrival
* time.
*
* The protocol does not carry the tick rate, and StreamHub's approach of using
* the local MARTe HighResolutionTimer frequency is only valid when the client
* runs on the producer's host. A remote bench scope cannot assume that, so the
* rate is measured: hrt against recv_time is a straight line whose slope is
* ticks per second.
*/
class HrtRateFit {
public:
static constexpr size_t kMinSamples = 32;
static constexpr size_t kWindow = 256;
void add(uint64_t hrt, double wallSec);
bool ready() const { return n_ >= kMinSamples && rate_ > 0.0; }
double ticksPerSecond() const { return rate_; }
/**
* @brief Converts a tick count to seconds on the PRODUCER's own epoch.
*
* The fit recovers the slope only and discards the intercept, so this is
* `hrt / ticksPerSecond()` — not a wall-clock time. A producer's hrt counts
* from its own boot, not from the Unix epoch. Pass the result to
* ClockOffset::map() to land it on the wall clock; latching that arbitrary
* epoch difference is precisely what ClockOffset is for.
*
* @warning Never subtract two of these results to measure a short interval.
* The rate is refitted on every add() and wobbles by a few parts in 1e4,
* while hrt is already ~1e11 ticks by the time a scope attaches to a
* long-running producer — so the division carries that relative wobble
* multiplied by the entire elapsed epoch, tens of milliseconds of jitter on
* an absolute value. The jitter is common to both operands only if the rate
* did not change between them, which is exactly what it does. To measure an
* interval, difference the raw ticks and divide once by ticksPerSecond().
*/
double toSeconds(uint64_t hrt) const;
void reset();
private:
void refit();
struct Sample { double hrt; double wall; };
std::deque<Sample> samples_;
size_t n_ = 0;
double rate_ = 0.0;
};
} /* namespace udpscope */