mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-18 21:00:22 -05:00
fix(ruview): CSI loss survives stray and reordered sequence numbers
Live RTL8721Dx capture (node 3, COM10) reported 97% loss: one stray sequence value (+52,835, then straight back) was counted as 52,834 lost frames, and every late UDP packet re-counted its gap. The tracker now treats a single out-of-window value as a stray, needs two agreeing frames to accept a counter reset, and lets a late packet fill its own gap (seqReordered / seqStrays / seqResyncs in the summary). Against ground truth on the live board (unique sequence numbers over the span): 4,783 of 4,783 present, loss 0, 9 strays; the tracker matched both. ADR-373 Amendment 2 records the live Realtek run: devices -> realtek, 7,404/7,404 packets decoded, monitor at 1500000 baud without a reset, and 12,549 live frames analyzed by the WASM kernel. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
1 parent
d31d9daba2
commit
3cd360f9e2
5 files changed
+77
-6
No files matched your search
@@ -175,6 +175,20 @@ scrubbed child env) and aborted on the firmware's U+2192 log character, so
|
||||
`bootLog.captured` was false and an earlier abort would have hidden the CSI
|
||||
evidence. It now writes raw bytes; a regression test forces cp1252.
|
||||
|
||||
Live RTL8721Dx re-run (MEASURED, board 4 on COM10, node 3, alongside the C6):
|
||||
`devices` classified COM10 as `realtek`; a 20 s capture decoded 7,404/7,404
|
||||
packets (6,959 RAC1 frames at 348 Hz, RSSI −38.9 dBm, 200 RHB1 heartbeats);
|
||||
`monitor --baud 1500000` saw 62 CSI log lines and did not reset the board
|
||||
(sequence kept rising, 80,856 → 88,257). `--analyze --node-id 3` ran 12,549
|
||||
live frames at a measured 313.7 Hz through the WASM kernel (integrity
|
||||
verified). The board also exposed a loss-accounting bug carried over from the
|
||||
original code: one stray sequence value (+52,835, then straight back) was
|
||||
counted as 52,834 lost frames, giving 97% "loss". The tracker now treats a
|
||||
single out-of-window value as a stray, needs two agreeing frames to accept a
|
||||
counter reset, and lets a late packet fill its own gap. Against ground truth
|
||||
(unique sequence numbers over the span, 15 s): 4,783/4,783 present, true loss
|
||||
0, 9 strays — the tracker reported loss 0 and 9 strays.
|
||||
|
||||
## Consequences
|
||||
|
||||
- One command per modality works across CLI, MCP and SDK, with actionable
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"skills/train-pose.md": "8cb132e468392497377828f7974c744e4c877134ed9a87f960f77fa1221540ba",
|
||||
"skills/verify.md": "2d38d240e9810a7827e2ebd3717dc0f85c646cc92e46c3812fe77c5b9eb40b76",
|
||||
"src/brain.js": "0f16a75aea943acdacc430ff11d5df7ecdec9cca2ab497795ff6f33eaebdfab6",
|
||||
"src/devices/esp32.js": "54e234f41d88a5032f473791e78cfc2fd2a34776af19c9ac4943c3a1e9be381e",
|
||||
"src/devices/esp32.js": "59c445b9d36188405d075953941a0abf261ea3ee8c0523ffe71ac1d00fcccbff",
|
||||
"src/devices/lidar.js": "d753d88375358ced5174376753c276b5ca621f38ae48eac58dbb307b9004b629",
|
||||
"src/devices/mmwave.js": "09d828e3b3e140926194ab83bf27e7c63cbda1cf438385c82c0e89b9d8e4b526",
|
||||
"src/devices/registry.js": "26725e43adaa908b41462f492c5f6bf63e9193f3ee076e3d64ebaa012b66f6be",
|
||||
@@ -70,7 +70,7 @@
|
||||
"src/tools.js": "cd72ac87e85e0bfbef7af11c98d03c4cb63a5bdd5da760460e9df8de9468bc34",
|
||||
"src/training.js": "b8451c48db69c7d077d75f07500996213beaa52705116f64afafe084e68e3956"
|
||||
},
|
||||
"filesDigest": "df2350f6a9989615e1978dfc57377edca97e794001ed23a3348ded984441d033",
|
||||
"filesDigest": "b8c8a261e647535ccfafc54783fc25d7de961eab584b3365c9457f9cf9cfe46f",
|
||||
"brainDigest": "c0fb7b079ded157059b91601361429944697dae3cc42abc00dfe1a680986b0f4",
|
||||
"gateFingerprint": "6e53c784eee38310188948fc75fb49e6b4ebc04e247d01b903fa8c8a92d67bdd",
|
||||
"developmentPins": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
3dc0ee81f9b5ded15ed1016e7cc4e5ac31d4cb45064af154242d441e237ebde9 manifest.json
|
||||
36990fd5c552e61b410ac8771d6a55769e8fcc9f695a1710b107a57e409a27de manifest.json
|
||||
@@ -160,6 +160,37 @@ export function parsePacket(buf, opts = {}) {
|
||||
|
||||
const shapeKey = (pkt) => `${pkt.antennas}x${pkt.subcarriers}`;
|
||||
|
||||
/** Jumps larger than this are a counter reset or a stray frame, not loss. */
|
||||
export const SEQ_WINDOW = 1024;
|
||||
|
||||
/**
|
||||
* Loss accounting that survives UDP reordering and stray sequence values.
|
||||
* A late packet fills the hole it left instead of being re-counted. A single
|
||||
* out-of-window value is a stray (observed live on an RTL8721Dx: sporadic
|
||||
* seq 0 and +52,835 frames between in-order ones) and leaves the baseline
|
||||
* alone; two consecutive frames agreeing on a new range are a counter reset.
|
||||
*/
|
||||
function trackSequence(n, seq) {
|
||||
if (n.lastSeq === null) { n.lastSeq = seq; return; }
|
||||
const d = seq - n.lastSeq;
|
||||
if (d >= 1 && d <= SEQ_WINDOW) {
|
||||
n.seqGaps += d - 1;
|
||||
n.lastSeq = seq;
|
||||
n.pendingSeq = null;
|
||||
} else if (d <= 0 && d > -SEQ_WINDOW) {
|
||||
n.reordered += 1; // late or duplicate; a late packet fills one counted gap
|
||||
if (d < 0 && n.seqGaps > 0) n.seqGaps -= 1;
|
||||
} else if (n.pendingSeq != null && seq - n.pendingSeq >= 1 && seq - n.pendingSeq <= SEQ_WINDOW) {
|
||||
n.strays -= 1; // the previous out-of-window frame started a new range
|
||||
n.resyncs += 1;
|
||||
n.lastSeq = seq;
|
||||
n.pendingSeq = null;
|
||||
} else {
|
||||
n.strays += 1;
|
||||
n.pendingSeq = seq;
|
||||
}
|
||||
}
|
||||
|
||||
/** Aggregate parsed packets into a per-node summary. */
|
||||
export class NodeStats {
|
||||
constructor() {
|
||||
@@ -191,14 +222,13 @@ export class NodeStats {
|
||||
const key = `${pkt.source}:${pkt.nodeId ?? -1}`;
|
||||
let n = this.nodes.get(key);
|
||||
if (!n) {
|
||||
n = { source: pkt.source, nodeId: pkt.nodeId ?? -1, packets: {}, rssiSum: 0, rssiCount: 0, lastSeq: null, seqGaps: 0, shapes: new Map(), vitals: null };
|
||||
n = { source: pkt.source, nodeId: pkt.nodeId ?? -1, packets: {}, rssiSum: 0, rssiCount: 0, lastSeq: null, pendingSeq: null, seqGaps: 0, reordered: 0, strays: 0, resyncs: 0, shapes: new Map(), vitals: null };
|
||||
this.nodes.set(key, n);
|
||||
}
|
||||
n.packets[pkt.kind] = (n.packets[pkt.kind] || 0) + 1;
|
||||
if (pkt.kind === 'csi') {
|
||||
n.rssiSum += pkt.rssi; n.rssiCount += 1;
|
||||
if (n.lastSeq !== null && pkt.seq > n.lastSeq + 1) n.seqGaps += pkt.seq - n.lastSeq - 1;
|
||||
n.lastSeq = pkt.seq;
|
||||
trackSequence(n, pkt.seq);
|
||||
const shape = shapeKey(pkt);
|
||||
const sh = n.shapes.get(shape) || { count: 0, ampSum: 0, last: null };
|
||||
sh.count += 1; sh.ampSum += pkt.meanAmplitude; sh.last = pkt;
|
||||
@@ -231,6 +261,9 @@ export class NodeStats {
|
||||
packets: n.packets,
|
||||
csiRateHz: Number((csi / seconds).toFixed(2)),
|
||||
csiLossFraction: received ? Number((n.seqGaps / received).toFixed(4)) : null,
|
||||
...(n.reordered ? { seqReordered: n.reordered } : {}),
|
||||
...(n.strays ? { seqStrays: n.strays } : {}),
|
||||
...(n.resyncs ? { seqResyncs: n.resyncs } : {}),
|
||||
rssiMean: n.rssiCount ? Number((n.rssiSum / n.rssiCount).toFixed(1)) : null,
|
||||
csi: shapes.length ? describe(shapes[0]) : null,
|
||||
...(shapes.length > 1 ? { csiShapes: shapes.map(describe) } : {}),
|
||||
|
||||
@@ -114,6 +114,30 @@ test('mixed ESP32 + Realtek fleet: per-source nodes, shapes, loss, heartbeats',
|
||||
assert.equal(r.heartbeatOnlySenders, undefined, 'the sender also delivered CSI');
|
||||
});
|
||||
|
||||
test('loss survives reordering and stray sequence values (live RTL8721Dx pattern)', async () => {
|
||||
// B+0..B+19 (B = 50,000, live counters are ~80k) with: B+5 missing, B+8 arriving after B+9, B+12 duplicated, a
|
||||
// stray +52,835 frame after B+15, and a stray 0 after B+17 that hides a
|
||||
// real loss (B+18) — both stray patterns were seen live on COM10. Then a genuine
|
||||
// counter reset: 1, 2, 3.
|
||||
const B = 50000;
|
||||
const seqs = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
if (i === 5 || i === 8 || i === 18) continue;
|
||||
seqs.push(B + i);
|
||||
if (i === 9) seqs.push(B + 8);
|
||||
if (i === 12) seqs.push(B + 12);
|
||||
if (i === 15) seqs.push(B + 15 + 52835);
|
||||
if (i === 17) seqs.push(0);
|
||||
}
|
||||
seqs.push(1, 2, 3);
|
||||
const r = await capture({ max_packets: seqs.length }, seqs.map((seq) => rac1({ seq })));
|
||||
const n = r.nodes[0];
|
||||
assert.equal(n.packets.csi, seqs.length);
|
||||
assert.deepEqual([n.seqReordered, n.seqStrays, n.seqResyncs], [2, 2, 1]);
|
||||
// B+5 and B+18 are truly lost: 2 gaps over csi + gaps slots.
|
||||
assert.equal(n.csiLossFraction, Number((2 / (seqs.length + 2)).toFixed(4)));
|
||||
});
|
||||
|
||||
test('heartbeat-only and undecodable streams fail instead of claiming MEASURED', async () => {
|
||||
const hb = await capture({ max_packets: 3 }, [rhb1(), rhb1(), rhb1()]);
|
||||
assert.deepEqual([hb.ok, hb.reason, hb.evidence, hb.heartbeats], [false, 'heartbeat_only', null, 3]);
|
||||
|
||||
Reference in new issue
Block a user