feat(udpscope): per-element timestamp reconstruction from UDPS frames

Implements FrameDecoder with five timing rules that mirror
UDPSourceSession.cpp: FullArray (per-element time signal), FirstSample
and LastSample (rate-spread from anchor), accumulated scalar with
declared rate (forward-chain anchoring, immune to arrival jitter), and
PACKET burst (backward-span from previous arrival). 9 new tests, 38
pass total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-08-27 20:01:30 +02:00
co-authored by Claude Sonnet 4.6
parent c89decef8e
commit 5a8479cda9
5 changed files with 583 additions and 0 deletions
+65
View File
@@ -35,4 +35,69 @@ struct Rect {
}
};
/* Protocol constants, spelled out rather than included, so the framework-free
* modules stay independent of udps_client.h. They mirror Common/UDP/UDPSProtocol.h. */
constexpr uint8_t kTimePacket = 0;
constexpr uint8_t kTimeFullArray = 1;
constexpr uint8_t kTimeFirstSample = 2;
constexpr uint8_t kTimeLastSample = 3;
constexpr uint32_t kNoTimeSignal = 0xFFFFFFFFu;
/** Framework-free mirror of udps_signal_t, plus UI state. */
struct SignalMeta {
std::string name;
uint8_t typeCode = 255;
uint8_t quantType = 0;
uint32_t numRows = 1;
uint32_t numCols = 1;
double rangeMin = 0.0;
double rangeMax = 0.0;
uint8_t timeMode = kTimePacket;
double samplingRate = 0.0;
uint32_t timeSignalIdx = kNoTimeSignal;
std::string unit;
/** User override: treat an ambiguous PACKET array as a profile, not a burst. */
bool profileOverride = false;
uint32_t numElements() const {
const uint64_t n = static_cast<uint64_t>(numRows ? numRows : 1u) *
static_cast<uint64_t>(numCols ? numCols : 1u);
return n == 0u ? 1u : static_cast<uint32_t>(n);
}
bool hasTimeSignal(uint32_t numSignals) const {
return timeSignalIdx != kNoTimeSignal && timeSignalIdx < numSignals;
}
/**
* @brief True when this array should be plotted against element index
* rather than unrolled onto the time axis.
*
* Only PACKET arrays are ambiguous: the producer stamped the whole datagram
* with one time, which is what a genuine vector looks like and also what a
* burst carrying no time metadata looks like. Default is burst, matching
* UDPSourceSession, with this flag as the user's override.
*/
bool isVectorProfile() const {
return profileOverride && numElements() > 1u && timeMode == kTimePacket;
}
};
/**
* @brief Non-owning mirror of udps_frame_t.
*
* Kept separate from the C struct so FrameDecoder can be tested with plain
* arrays and no socket. Points at memory owned by the caller.
*/
struct FrameView {
uint32_t counter = 0;
uint64_t hrt = 0;
double recvTime = 0.0;
uint32_t numSamples = 1;
uint32_t numSignals = 0;
const double* const* values = nullptr; /**< values[i][0..counts[i]) */
const uint32_t* counts = nullptr;
};
} /* namespace udpscope */