Files
MARTe-Integrated-Components/Client/udpscope/FrameDecoder.h
T

175 lines
9.7 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.
*
* 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 of DECLARED ARRAY shape 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. Note the shape qualifier: an
* accumulated SCALAR is declared with one element, so a frame that carries no
* time signal for it routes to rule 3, which does use the declared rate — the
* normal case, since Accumulate flushes per signal on a timer. 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 <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 {
/** Rules 1 and 2: maps the TIME SIGNAL's epoch onto the wall clock. */
ClockOffset offset;
/** Rule 3's hrt branch, which maps a different epoch — accProdSec counts
* from this signal's first usable packet, not from whatever zero the
* time signal uses. One signal can reach both: a declared scalar with
* FIRST_SAMPLE takes rule 2 in frames where its time signal carries an
* element and rule 3 in frames where it does not, which Accumulate's
* per-signal timer flushing makes ordinary. Sharing one ClockOffset
* across the two forces a re-latch on every alternation. */
ClockOffset accOffset;
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<SignalMeta> signals_;
std::vector<SigState> state_;
HrtRateFit hrtFit_;
};
} /* namespace udpscope */