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>
97 lines
3.4 KiB
C++
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 */
|