fixed issue on udpstreamer trigger logic

This commit is contained in:
Martino Ferrari
2026-08-28 16:53:17 +02:00
parent 1c61e814c0
commit 044ce57ba3
170 changed files with 16958 additions and 24036 deletions
+249 -13
View File
@@ -184,6 +184,84 @@ TEST(FrameDecoder, FirstSampleWithNoRateSpreadsFromConsecutiveAnchors) {
EXPECT_NEAR(ts[1] - ts[0], 0.001, 1e-9) << "loss stretched the array";
}
// Rule 2 faces the same reorder-or-restart question as rule 3, asked of the time
// signal rather than of hrt, and the two halves need opposite answers. A late
// datagram is DROPPED: its anchor is genuine producer time, so emitting it would
// place a whole array before stamps already handed out, and this rule keeps no
// emitted chain to clamp it against. Dropping also protects the next packet,
// which would otherwise divide one packet's worth of anchor difference by a
// counter gap of two and halve its spacing.
TEST(FrameDecoder, FirstSampleDropsAReorderedAnchor) {
FrameDecoder dec;
dec.setSignals({burst("Sine", kTimeFirstSample, 0.0, 4, 1),
timeSignal("Time", 1)});
/* Anchors 4 ms apart, packets 2 and 3 delivered in the opposite order. */
const int order[5] = {0, 1, 3, 2, 4};
double lastEnd = 0.0;
bool seen = false;
std::vector<double> ts;
for (int slot = 0; slot < 5; slot++) {
const int p = order[slot];
FrameBuilder fb;
fb.addSignal({1.0, 2.0, 3.0, 4.0});
fb.addSignal({7.0e9 + p * 4.0e6});
const FrameView& f = fb.build(0, 2000.0 + slot * 0.004, 4,
static_cast<uint32_t>(p + 1));
dec.beginFrame(f);
if (!dec.timestamps(f, 0, ts)) { continue; }
ASSERT_EQ(ts.size(), 4u);
if (seen) {
EXPECT_GT(ts[0], lastEnd)
<< "slot " << slot << " stepped back " << (lastEnd - ts[0]) << " s";
}
if (slot > 0) {
EXPECT_NEAR(ts[1] - ts[0], 0.001, 1e-9) << "slot " << slot;
}
lastEnd = ts[3];
seen = true;
}
}
// The counterweight, exactly as in rule 3: a restart drops the time signal to a
// fresh epoch, and refusing every backward anchor would then freeze the anchor
// pair for the rest of the session — no period could ever be measured again and
// the rule would run on whatever it derived before the restart.
TEST(FrameDecoder, FirstSampleRebasesAfterAProducerRestart) {
FrameDecoder dec;
dec.setSignals({burst("Sine", kTimeFirstSample, 0.0, 4, 1),
timeSignal("Time", 1)});
std::vector<double> ts;
for (int p = 0; p < 5; p++) {
FrameBuilder fb;
fb.addSignal({1.0, 2.0, 3.0, 4.0});
fb.addSignal({7.0e9 + p * 4.0e6});
const FrameView& f = fb.build(0, 2000.0 + p * 0.004, 4,
static_cast<uint32_t>(p + 1));
dec.beginFrame(f);
ASSERT_TRUE(dec.timestamps(f, 0, ts));
}
/* Producer restarts: the time signal comes back near zero, and the counter
* with it. Anchors now step 2 ms, a different period from before. */
for (int p = 0; p < 5; p++) {
FrameBuilder fb;
fb.addSignal({1.0, 2.0, 3.0, 4.0});
fb.addSignal({0.5e9 + p * 2.0e6});
const FrameView& f = fb.build(0, 2000.020 + p * 0.002, 4,
static_cast<uint32_t>(p + 1 + 100));
dec.beginFrame(f);
ASSERT_TRUE(dec.timestamps(f, 0, ts)) << "restart packet " << p;
/* From the second post-restart packet the NEW period must be measured;
* a frozen anchor pair would keep returning the old 1 ms. */
if (p > 0) {
EXPECT_NEAR(ts[1] - ts[0], 0.0005, 1e-9) << "restart packet " << p;
}
}
}
TEST(FrameDecoder, LastSampleAnchorsTheFinalElementAndCountsBackward) {
FrameDecoder dec;
dec.setSignals({burst("Sine", kTimeLastSample, 1000.0, 4, 1),
@@ -311,6 +389,48 @@ TEST(FrameDecoder, AccumulatedScalarReinstatesLostPacketsFromTheCounterGap) {
EXPECT_NEAR(ts[9], 501.100, 1e-9);
}
// The lockstep rule applies to the declared branch too, and there the damage is
// permanent rather than transient. A late datagram that rolls lastCounter back
// gives the NEXT packet a gap of dist+1, so `lost` reinstates dist bursts of
// duration that were never lost. Unlike the absurd gap a reorder itself
// produces, that prediction is close enough to arrival to pass the resync
// backstop and be accepted — after which the chain is self-consistent, base
// stays above lastEmittedEnd, the squeeze never fires and nothing ever pulls it
// back. Measured +10 ms at distance 1 and +200 ms at distance 20, held to the
// end of the session.
TEST(FrameDecoder, AccumulatedScalarKeepsItsChainThroughAReorder) {
for (uint32_t dist : {uint32_t(1), uint32_t(20)}) {
FrameDecoder dec;
dec.setSignals({accSignal()});
/* 300 contiguous 10-sample bursts at 1 kHz, one payload delayed by
* `dist` delivery slots at packet 150. Arrival times belong to the slot;
* only the counter travels, since the declared branch reads no hrt. */
std::vector<uint32_t> counters(300);
for (uint32_t p = 0; p < 300u; p++) { counters[p] = p + 1u; }
for (uint32_t i = 0; i < dist; i++) {
std::swap(counters[150u + i], counters[151u + i]);
}
std::vector<double> ts;
double lastArrival = 0.0;
for (uint32_t p = 0; p < 300u; p++) {
FrameBuilder fb;
fb.addSignal(std::vector<double>(10, 1.0));
lastArrival = 500.0 + p * 0.010;
const FrameView& f = fb.build(0, lastArrival, 10, counters[p]);
dec.beginFrame(f);
if (!dec.timestamps(f, 0, ts)) { continue; }
}
/* A healthy chain ends its burst on the moment the packet landed, less
* the burst it spans. */
EXPECT_NEAR(ts[9], lastArrival, 1.0e-6)
<< "distance " << dist << " left the chain "
<< (ts[9] - lastArrival) << " s ahead for good";
}
}
// A producer that never advances the counter, or restarts it, leaves nothing to
// reconstruct from. Arrival time is then the better of two bad answers, and the
// chain has to be abandoned rather than left to drift forever.
@@ -810,20 +930,29 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarIgnoresReorderedDatagrams) {
// squeezes bursts, because the late datagram's samples belong in the past and
// downstream demands increasing stamps, so the monotonic clamp walks them
// forward instead — and the timeline it leaves ahead of the producer takes a few
// packets to bleed off, squeezing those too. But that clamp has an exact floor:
// its cap is kWallBleedFraction * wallElapsed / nElems, and wallElapsed / nElems
// IS the producer's true period at steady cadence, so no burst it touches can
// ever be narrower than kWallBleedFraction of true. Anything below that floor
// did not come from the clamp; it came from a mis-derived period. That is what
// separates the defect from the design, and it is why the check is a floor
// rather than a target.
// packets to bleed off, squeezing those too. The clamp's cap is
// kWallBleedFraction * wallElapsed / nElems, so on THIS schedule — uniform 25 ms
// arrivals, where wallElapsed / nElems is exactly the producer's period — no
// burst it touches can come out below 0.5x true. A burst narrower than that did
// not come from the clamp; it came from a mis-derived period.
//
// That floor is a property of the SCHEDULE, not of the decoder, and must not be
// read as a general invariant: the cap is proportional to the ARRIVAL gap, and
// the premise of this whole file is that arrival gaps are not uniform. Under a
// drained delivery — a reorder plus a queue flushed at 100 us per datagram — a
// clamped burst measures 0.002x true, and that is the design working as
// intended, because the lead can only bleed off against wall time that has
// really passed. Keep the arrivals here uniform, or the bound stops meaning
// anything.
TEST(FrameDecoder, UndeclaredAccumulatedScalarKeepsItsSpacingAfterAReorder) {
const double trueDt = 0.0025; /* 10 samples per 25 ms packet */
const double floorDt = 0.5 * trueDt; /* kWallBleedFraction * trueDt */
/* Distance 1 sits exactly ON the floor either way and is here to pin it;
* 5 and 20 are where the defect drops through it, to 0.167x and 0.048x. */
for (size_t dist : {size_t(1), size_t(5), size_t(20)}) {
/* Distance 1 is deliberately absent: the defect and the clamp both yield
* exactly 0.5x there, so it discriminates nothing while sitting on the
* assertion boundary with zero margin. 5 and 20 drop through the floor, to
* 0.167x and 0.048x. */
for (size_t dist : {size_t(5), size_t(20)}) {
const std::vector<std::vector<double> > out =
runUndeclaredPerPacket(delayOne(cleanUndeclaredStream(), 150, dist));
@@ -840,9 +969,10 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarKeepsItsSpacingAfterAReorder) {
}
// The same defect under a network that reorders continuously rather than once.
// Same floor, applied to every burst in the run including the late datagrams'
// own — under sustained reordering there is no quiet packet to exempt, and the
// floor holds for all of them anyway.
// Same floor and the same caveat about it being a property of these uniform
// arrivals, applied to every burst in the run including the late datagrams' own
// — under sustained reordering there is no quiet packet to exempt, and the floor
// holds for all of them anyway.
TEST(FrameDecoder, UndeclaredAccumulatedScalarKeepsItsSpacingUnderSustainedReordering) {
std::vector<HrtPacket> pkts = cleanUndeclaredStream();
@@ -1233,6 +1363,112 @@ TEST(FrameDecoder, UndeclaredAccumulatedScalarSurvivesAStrayZeroHrtPacket) {
}
}
// prevAccCount is the third member of the lockstep group and the one easiest to
// miss, because every other test in this file sends bursts of a fixed length,
// which makes it invisible. It is the OTHER factor of the denominator —
// cycles = prevAccCount * gap — and it means "cycles spanned by the reference
// packet", so a packet that cannot become the reference must not set it either.
// Accumulate flushes on a TIMER, so a short packet is ordinary rather than
// exotic: a 2-sample stray with hrt == 0 that moved prevAccCount alone left the
// next real burst dividing 25 ms of ticks by 2 cycles instead of 10, drawing it
// five times too wide and — burst anchored on its last element — ending it
// +90 ms in the future.
TEST(FrameDecoder, UndeclaredAccumulatedScalarSurvivesAShortStrayZeroHrtPacket) {
FrameDecoder dec;
dec.setSignals({undeclaredAcc()});
const double ticks = 1.0e9;
const uint64_t bootHrt = static_cast<uint64_t>(86400.0 * ticks);
const double packetSec = 0.025;
for (int p = 0; p < 200; p++) {
/* The stray carries two samples, not ten: a short RT cycle flushed by
* the timer, with no hrt on it. */
const bool stray = (p == 153);
const uint32_t nElems = stray ? 2u : 10u;
FrameBuilder fb;
fb.addSignal(std::vector<double>(nElems, 1.0));
const uint64_t hrt = stray
? 0u
: bootHrt + static_cast<uint64_t>(p * packetSec * ticks);
const double arrival = 700.0 + p * packetSec;
const FrameView& f =
fb.build(hrt, arrival, nElems, static_cast<uint32_t>(p + 1));
dec.beginFrame(f);
std::vector<double> ts;
if (!dec.timestamps(f, 0, ts)) { continue; }
/* The packet after the stray is the one that divides by the reference,
* so check every burst from there on: a burst drawn too wide ends in the
* future and then takes several squeezed bursts to bleed back. */
if (p > 100) {
EXPECT_NEAR(ts.back(), arrival, packetSec / 5.0)
<< "at packet " << p << " (" << ts.size() << " samples)";
}
}
}
// One signal can reach both rule 2 and rule 3's hrt branch, and they map
// DIFFERENT epochs: rule 2 maps the time signal's own zero, the hrt branch maps
// accProdSec, which counts from this signal's first usable packet. A declared
// scalar with FIRST_SAMPLE alternates between them whenever its time signal is
// empty in some frames — ordinary, since Accumulate flushes per signal on a
// timer. Sharing one ClockOffset across the two puts the epochs' whole
// difference through the recalibration threshold on every single alternation.
TEST(FrameDecoder, AlternatingBetweenTheAnchorAndHrtRulesDoesNotRelatch) {
SignalMeta acc;
acc.name = "Acc";
acc.typeCode = 9;
acc.numRows = 1; /* scalar: rule 3 is reachable */
acc.timeMode = kTimeFirstSample; /* rule 2 is reachable too */
acc.samplingRate = 0.0; /* rule 3 takes the hrt branch */
acc.timeSignalIdx = 1;
FrameDecoder dec;
dec.setSignals({acc, timeSignal("Time", 1)});
const double ticks = 1.0e9;
const uint64_t bootHrt = static_cast<uint64_t>(86400.0 * ticks);
double last = 0.0;
bool seen = false;
for (int p = 0; p < 120; p++) {
FrameBuilder fb;
fb.addSignal(std::vector<double>(10, 1.0));
/* Even packets carry the anchor (epoch 5 s); odd packets do not, and
* fall through to the hrt branch (epoch 0 s at first packet). */
if (p % 2 == 0) {
fb.addSignal({5.0e9 + p * 0.025e9});
} else {
fb.addSignal({});
}
const double arrival = 700.0 + p * 0.025;
const FrameView& f =
fb.build(bootHrt + static_cast<uint64_t>(p * 0.025 * ticks),
arrival, 10, static_cast<uint32_t>(p + 1));
dec.beginFrame(f);
std::vector<double> ts;
if (!dec.timestamps(f, 0, ts)) { continue; }
/* Packet 0 is rule 2's legal first-packet stack — no predecessor to
* measure a period from — so ordering is only checked from packet 1. */
for (double t : ts) {
if (seen && p > 0) {
ASSERT_GT(t, last) << "packet " << p << " stepped back "
<< (last - t) << " s";
}
last = t;
seen = true;
}
/* Neither rule may be dragged onto the other's epoch: both must stay
* within a packet of the moment the datagram landed. */
if (p > 40) {
EXPECT_NEAR(ts.back(), arrival, 0.025) << "at packet " << p;
}
}
}
// The same double delivery that the declared branch guards against — a host
// joined on two interfaces receives every unfragmented update twice — reaches an
// undeclared-rate signal identically. The guard can only fire if this branch