/** * @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. * * 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 * 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. * * Second, which end of the burst is anchored. StreamHub converts the packet's * hrt into the position of sample 0 and steps forward, so the burst STARTS at * the anchor. Here the anchor is arrival time, and the samples were acquired * before the packet carrying them landed — so the burst must END there instead. * Both branches of rule 3 do this, or two accumulated scalars in one scope, one * with a declared rate and one without, would sit a whole burst apart on the * shared X axis. * * Third, the entry condition. UDPSourceSession.cpp:554 routes any update * carrying nElems <= 1 to plain arrival time. That is safe for a host-local * consumer whose arrival time is the producer's own clock, but wrong here: * Accumulate mode flushes on a TIMER, so a short RT cycle legitimately delivers * a single sample between two full bursts. Dating that one sample from arrival * while its neighbours are chained puts it off the chain, and — worse — leaves * lastCounter behind, so the next full burst reads the skipped counter as a lost * 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. * * Divergences OUTSIDE rule 3 exist as well; two are named here only because they * are the ones easily mistaken for bugs. Rules 1 and 2 key ClockOffset on the * CONSUMING signal, where UDPSourceSession.cpp:538 and :516 key 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. Neither this list nor the three above is closed: any other * difference you find is unexamined, not sanctioned. */ #pragma once #include "TimeBase.h" #include "Types.h" #include namespace udpscope { class FrameDecoder { public: /** Installs the signal table. Clears all per-signal timing history. */ void setSignals(const std::vector& signals); const std::vector& 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& tsOut); /** Forgets all timing history; call on reconnect. */ void reset(); private: bool packetBurst(uint32_t idx, uint32_t nElems, double wallNow, std::vector& 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; /** 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, the element count that anchor spanned, and the last period * actually derived from a pair of them. Consecutive anchors are what * lets an array with no declared sampling rate be spread at all. */ double prevAnchorProdSec = 0.0; uint32_t prevAnchorCount = 0u; double prevAnchorDt = 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 * the exact duration of any lost datagrams. */ double lastEmittedEnd = 0.0; uint32_t lastCounter = 0u; /** Whether lastCounter holds a counter this signal has actually seen. * Distinct from lastEmittedValid, which is about the emitted TIMELINE: * rules 1 and 2 keep a counter (so the duplicate-datagram guard can * fire for them too) without ever joining rule 3's chain. */ bool counterValid = false; /** ARRIVAL time of the packet that produced lastEmittedEnd. Valid * exactly when lastEmittedValid is, so it needs no flag of its own. * Deliberately not lastPacketWall, which belongs to packetBurst() and * is updated on frames rule 3 never emits. This is the only reference * against which a leading timeline can be pulled back: the correction * has to be expressed as a fraction of the wall time that has really * elapsed since this signal's previous burst, because within a single * timestamps() call the wall clock is frozen and every forward step, * however small, increases the lead measured at that instant. */ double lastEmittedWall = 0.0; bool lastEmittedValid = false; }; std::vector signals_; std::vector state_; HrtRateFit hrtFit_; }; } /* namespace udpscope */