Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions lib/ble/ble_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,33 @@ bool burstPacketCountMatches({
}) =>
expectedPacketCount == actualBurstPacketCount + droppedThisBurst;

/// Honest burst-completeness signal for TELEMETRY ONLY — this NEVER gates the
/// commit/ACK decision (see the log-only call site).
///
/// [receivedTrafficCount] is every frame we actually received this burst, ALL
/// types (historical R24 data + interleaved console/event/unknown) — i.e.
/// [BurstStats.totalTrafficPacketCount], NOT the banked historical subset. The
/// band's [expectedPacketCount] (num_packets) likewise counts every frame it
/// transmitted, so comparing the two all-types totals is type-agnostic and
/// interleaving-immune: benign console/event frames riding along cannot fake a
/// shortfall the way comparing against the R24-only subset did.
///
/// [droppedThisBurst] (RecordGate plausibility rejections this burst) is added
/// back because the band counted those frames but they never entered
/// [receivedTrafficCount]. A POSITIVE result is frames the band counted that we
/// did NOT count as valid received traffic — i.e. missing OR corrupted traffic
/// (would-flag / potential loss): CRC-failed frames also never enter
/// [receivedTrafficCount], so a positive shortfall cannot by itself prove a
/// frame never arrived. Zero is complete; negative just means we tallied more
/// than expected (retried/duplicate frames), which is not loss.
@visibleForTesting
int burstPacketShortfall({
required int expectedPacketCount,
required int receivedTrafficCount,
int droppedThisBurst = 0,
}) =>
expectedPacketCount - (receivedTrafficCount + droppedThisBurst);

/// Fired for every LIVE high-rate frame (0x28/0x2B/0x33). These are EPHEMERAL —
/// they are NOT persisted to raw_records (that bloated storage ~50x and stalled
/// derivation). The caller routes them to an in-memory sink for the live UI /
Expand Down Expand Up @@ -2596,6 +2623,20 @@ class BleEngine {
expectedPacketCount: expected,
droppedThisBurst: droppedThisBurst,
);
// Honest, LOG-ONLY completeness signal (never gates the ACK). Compares
// num_packets against the ALL-TYPES received total (currentBurstTrafficCount),
// not the banked R24 subset — see burstPacketShortfall. Only a POSITIVE
// shortfall means frames the band counted that we did not count as valid
// received traffic (missing OR CRC-corrupted — potential loss); this is
// the signal we want visible in telemetry BEFORE ever wiring a FAIL gate
// (which needs its own design + field validation to avoid re-flood).
final shortfall = expected == null
? 0
: burstPacketShortfall(
expectedPacketCount: expected,
receivedTrafficCount: d.currentBurstTrafficCount,
droppedThisBurst: droppedThisBurst,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// ADVISORY ONLY, never a gate: `expectedPacketCount`'s exact semantics
// (which transport packet types the band itself counts — command
// responses interleaved with the burst? retried/duplicate frames?) are
Expand Down Expand Up @@ -2635,11 +2676,27 @@ class BleEngine {
'traffic_burst_packets': d.currentBurstTrafficCount,
'burst_validation_failures': d.consecutiveValidationFailures,
'burst_breakdown': d.currentBurstBreakdown,
'burst_shortfall': shortfall,
},
));
} else {
_burstMismatchStreak = 0;
}
// Would-flag: the correct-signal completeness diagnostic. LOG-ONLY — the
// commit + verbatim-token ACK below are unchanged. A positive shortfall
// is the honest missing/corrupted-traffic telemetry we want to watch
// before a later, field-validated FAIL gate ever acts on it.
if (shortfall > 0) {
_log(
'[SYNC] burst completeness would-flag (LOG-ONLY, commit+ACK '
'unchanged): expected=$expected '
'received=${d.currentBurstTrafficCount} '
'dropped_this_burst=$droppedThisBurst shortfall=$shortfall '
'(all-types received total — frames the band counted that we did '
'not; missing or CRC-corrupted, potential loss; groundwork for a '
'future FAIL gate, NOT gating today)',
);
}
final r = d.bufferedRecTsRange;
final droppedThisBurstForLog = droppedThisBurst;
final hadDurableRows =
Expand Down
97 changes: 97 additions & 0 deletions test/ble_engine_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,103 @@ void main() {
});
});

group('burst completeness shortfall (log-only would-flag signal)', () {
test('no shortfall when all-types received total equals num_packets', () {
// Band sent 49 frames (30 R24 + 17 console + 2 event); we received all.
final received = countBurstTrafficPackets(
dataPacketCountsByRevision: const {24: 30},
consoleCount: 17,
eventCount: 2,
);
expect(
burstPacketShortfall(
expectedPacketCount: 49,
receivedTrafficCount: received,
),
0,
);
});

test(
'interleaved console/event frames do NOT false-positive: comparing '
'against the all-types received total (not the banked R24 subset) '
'keeps shortfall at zero',
() {
final received = countBurstTrafficPackets(
dataPacketCountsByRevision: const {24: 15},
consoleCount: 37,
eventCount: 2,
);
// Banked R24 subset alone is 15 — comparing THAT to num_packets=54
// would fabricate a 39-frame "loss". The correct all-types total is 54.
expect(received, 54);
expect(
burstPacketShortfall(
expectedPacketCount: 54,
receivedTrafficCount: received,
),
0,
);
},
);

test(
'positive shortfall flags missing-or-corrupted traffic (band counted '
'more than we did)', () {
final received = countBurstTrafficPackets(
dataPacketCountsByRevision: const {24: 20},
consoleCount: 3,
);
// Band reported 30, we counted 23 all-types, nothing gate-dropped → 7
// frames the band sent that we did not count (never arrived or CRC-failed).
expect(
burstPacketShortfall(
expectedPacketCount: 30,
receivedTrafficCount: received,
),
7,
);
});

test('gate-dropped records are added back so they never read as loss', () {
// 26 all-types received, 24 legitimately gate-dropped, band expected 50 →
// fully explained, no true loss.
expect(
burstPacketShortfall(
expectedPacketCount: 50,
receivedTrafficCount: 26,
droppedThisBurst: 24,
),
0,
);
});

test('negative shortfall (retries/dupes counted extra) is not loss', () {
expect(
burstPacketShortfall(
expectedPacketCount: 26,
receivedTrafficCount: 28,
),
lessThan(0),
);
});

test('shortfall==0 is exactly burstPacketCountMatches', () {
const expected = 50, received = 26, dropped = 24;
final matches = burstPacketCountMatches(
expectedPacketCount: expected,
actualBurstPacketCount: received,
droppedThisBurst: dropped,
);
final shortfall = burstPacketShortfall(
expectedPacketCount: expected,
receivedTrafficCount: received,
droppedThisBurst: dropped,
);
expect(matches, (shortfall == 0));
});
});

group('maintenance traffic gating', () {
test('maintenance traffic is paused while offload is active', () {
expect(shouldPauseMaintenanceTraffic(offloadActive: true), isTrue);
Expand Down
Loading