Round 4 of Task 4 review. Four defects in FrameDecoder's rule 3: - The undeclared-rate (hrt) path positioned each burst at an ABSOLUTE hrt/ticksPerSecond(). hrt counts from the producer's boot, so it is ~1e11 ticks by the time a scope attaches, and the rate is refitted every packet with a few parts in 1e4 of wobble. The product is tens of milliseconds of jitter in BOTH directions -- not merely imprecise, non-monotonic. Integrate short tick deltas into accProdSec instead and let ClockOffset latch the epoch that leaves behind. - The lead bleed used a fixed 0.9 factor, which converges only while the declared rate is within ~10%. Squeeze proportionally to the excess instead (floored at kMinBleedFactor), settling it in a single burst. - A single-sample flush fell through to the plain-scalar rule, dating it from arrival and leaving lastCounter stale so the next real burst reinstated a hole that never existed. Accumulate mode flushes on a timer, so a short cycle legitimately yields one sample; keep it on the chain. - kMaxCounterGap was inert: an absurd gap yields an absurd prediction that the arrival backstop already rejects, and no input can distinguish the two rules. Removed rather than left implying a behaviour it did not have. FrameDecoder.h now states the deliberate divergence from StreamHub -- which converts hrt with the LOCAL MARTe timer frequency, valid only because it runs on the producer's host -- and why a remote scope's drift is irreducible. Three new tests, each sabotage-proven non-vacuous: producer restart, short flushes staying on the chain, and a 20000-packet undeclared run after a day of producer uptime that asserts SPACING as well as ordering (the monotonic guard alone restores order while leaving positions wrong). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
89 lines
3.9 KiB
C++
89 lines
3.9 KiB
C++
/**
|
|
* @file FrameDecoder.h
|
|
* @brief Per-element timestamp reconstruction for UDPS frames.
|
|
*
|
|
* The C client's udps_frame_element_time() is explicitly an arrival-anchored
|
|
* estimate. It is not sufficient: the kernel frequently delivers several queued
|
|
* datagrams in one burst, so two packets are processed microseconds apart even
|
|
* though each represents ~10 ms of signal, and arrival-time interpolation then
|
|
* crams a packet's samples into that tiny gap — the trace renders as a sawtooth.
|
|
* Source/Applications/StreamHub/UDPSourceSession.cpp documents this failure and
|
|
* solves it; these are the same rules, computed from udps_frame_t's own fields.
|
|
*
|
|
* One rule deliberately differs. StreamHub anchors every accumulated-scalar
|
|
* burst on the packet's own hrt, converted with the LOCAL MARTe
|
|
* HighResolutionTimer frequency — correct only because StreamHub runs on the
|
|
* producer's host. A bench scope attaches over the network and has no access to
|
|
* that frequency; it can only regress hrt against arrival time, which is
|
|
* exactly what the bursty delivery above corrupts. So when a SamplingRate is
|
|
* declared this decoder chains bursts instead, using the packet counter to
|
|
* account for loss and arrival time only as a backstop. The consequence is that
|
|
* a declared rate measured against the producer's crystal rather than ours makes
|
|
* the reconstructed timeline drift, and drift that only arrival time can
|
|
* observe must be corrected against arrival time — see rule 3.
|
|
*/
|
|
#pragma once
|
|
|
|
#include "TimeBase.h"
|
|
#include "Types.h"
|
|
|
|
#include <vector>
|
|
|
|
namespace udpscope {
|
|
|
|
class FrameDecoder {
|
|
public:
|
|
/** Installs the signal table. Clears all per-signal timing history. */
|
|
void setSignals(const std::vector<SignalMeta>& signals);
|
|
|
|
const std::vector<SignalMeta>& signals() const { return signals_; }
|
|
|
|
/** Call once per frame, before any timestamps() call for that frame. */
|
|
void beginFrame(const FrameView& f);
|
|
|
|
/**
|
|
* @brief Timestamps for every value of signal @p idx in this frame.
|
|
* @return false when the signal produced nothing usable — an empty slot, or
|
|
* the first PACKET burst after connect, which has no previous
|
|
* arrival to span from and would otherwise poison the ring with
|
|
* wrongly spaced timestamps.
|
|
*/
|
|
bool timestamps(const FrameView& f, uint32_t idx, std::vector<double>& tsOut);
|
|
|
|
/** Forgets all timing history; call on reconnect. */
|
|
void reset();
|
|
|
|
private:
|
|
bool packetBurst(uint32_t idx, uint32_t nElems, double wallNow,
|
|
std::vector<double>& tsOut);
|
|
|
|
struct SigState {
|
|
ClockOffset offset;
|
|
double lastPacketWall = 0.0;
|
|
bool lastPacketValid = false;
|
|
/** Raw ticks, not seconds — see the comment in the samplingRate == 0
|
|
* branch for why a tick difference is the only safe way to measure a
|
|
* producer-side interval while the rate is still being re-estimated. */
|
|
uint64_t lastAccHrt = 0u;
|
|
/** Producer seconds since this signal's first usable packet, built by
|
|
* SUMMING short tick deltas. Never recomputed from an absolute tick
|
|
* count; see the samplingRate == 0 branch. */
|
|
double accProdSec = 0.0;
|
|
bool lastAccValid = false;
|
|
uint32_t prevAccCount = 0;
|
|
/** For accumulated scalars with a declared sampling rate: 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 the exact duration of any lost datagrams. */
|
|
double lastEmittedEnd = 0.0;
|
|
uint32_t lastCounter = 0u;
|
|
bool lastEmittedValid = false;
|
|
};
|
|
|
|
std::vector<SignalMeta> signals_;
|
|
std::vector<SigState> state_;
|
|
HrtRateFit hrtFit_;
|
|
};
|
|
|
|
} /* namespace udpscope */
|