diff --git a/src/kestrel/FrameParserKestrel.h b/src/kestrel/FrameParserKestrel.h index cb89964..b4b5c82 100644 --- a/src/kestrel/FrameParserKestrel.h +++ b/src/kestrel/FrameParserKestrel.h @@ -146,6 +146,11 @@ struct KestrelPhySts { inline bool parse_physts_8852(const uint8_t *p, size_t len, bool is_8852c, KestrelPhySts &out) { + /* Pure output param: the result depends only on this call's buffer, never + * on what a reused struct held before — including the failure return (an + * is_valid=0 stub, an absent IE, or a too-short buffer must all leave 0, + * not a previous blob's values), so the clear precedes the validation. */ + out = KestrelPhySts{}; if (p == nullptr || len < 8) return false; out.rssi_avg = static_cast(p[3] >> 1); diff --git a/src/kestrel/RtlKestrelDevice.cpp b/src/kestrel/RtlKestrelDevice.cpp index 64edbd1..36bd7cd 100644 --- a/src/kestrel/RtlKestrelDevice.cpp +++ b/src/kestrel/RtlKestrelDevice.cpp @@ -338,16 +338,21 @@ void RtlKestrelDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { p.RxAtrib.snr[i] = _last_physts.snr[i]; p.RxAtrib.evm[i] = _last_physts.evm[i]; } - /* Feed the windowed RX-quality aggregate (passive rssi-snr floor + - * LinkHealth) with path-A RSSI + the IE01 average SNR (the - * all-paths quantity the passive floor was validated against; - * per-path SNR goes to _rxpaths instead). A frame with no IE_01 - * SNR (snr_avg=0) still counts toward RSSI. */ - if (_last_physts.rssi[0] > 0) - _rxq.add(_last_physts.rssi[0], _last_physts.snr_avg, 0); - /* Per-antenna window means (GetActiveRxPaths). Both dies are 2 RX - * chains; C/D read 0 and are excluded by n_chains. */ - _rxpaths.add(p.RxAtrib.rssi, p.RxAtrib.snr, p.RxAtrib.evm, 2); + /* Window aggregates fold CRC-clean frames only (the Jaguar + * convention): a garbled frame's cached physts would bias the + * means and the active-chain classification. + * + * _rxq: passive rssi-snr floor + LinkHealth, fed path-A RSSI + + * the IE01 average SNR (the all-paths quantity the passive floor + * was validated against; per-path SNR goes to _rxpaths instead). + * A frame with no IE_01 SNR (snr_avg=0) still counts toward RSSI. + * _rxpaths: per-antenna window means (GetActiveRxPaths). Both + * dies are 2 RX chains; C/D read 0 and are excluded by n_chains. */ + if (!f.crc_err) { + if (_last_physts.rssi[0] > 0) + _rxq.add(_last_physts.rssi[0], _last_physts.snr_avg, 0); + _rxpaths.add(p.RxAtrib.rssi, p.RxAtrib.snr, p.RxAtrib.evm, 2); + } p.Data = std::span(const_cast(f.payload), f.payload_len); packetProcessor(p); diff --git a/tests/kestrel_rxparse_selftest.cpp b/tests/kestrel_rxparse_selftest.cpp index 2e1a588..e9f19de 100644 --- a/tests/kestrel_rxparse_selftest.cpp +++ b/tests/kestrel_rxparse_selftest.cpp @@ -216,7 +216,8 @@ int main() { CHECK(pb.evm[0] == -50 && pb.evm[1] == -30); /* EVM page read on both */ } - /* --- physts: is_valid=0 keeps header RSSI but skips the IE walk --- */ + /* --- physts: is_valid=0 keeps header RSSI but skips the IE walk; a reused + * output struct is cleared (result depends only on this buffer) --- */ { std::vector b(8 + 32, 0); b[0] = 0x01; /* no is_valid bit */ @@ -225,8 +226,12 @@ int main() { b[8] = 1; b[16] = 30; KestrelPhySts ps; + ps.snr_avg = 44; /* stale garbage from a "previous parse" */ + ps.snr[0] = 33; + ps.evm[1] = -40; CHECK(parse_physts_8852(b.data(), b.size(), true, ps)); - CHECK(ps.rssi[0] == 65 && ps.snr_avg == 0 && ps.snr[0] == 0); + CHECK(ps.rssi[0] == 65 && ps.snr_avg == 0 && ps.snr[0] == 0 && + ps.evm[1] == 0); } /* --- physts: truncated / lying total-length is bounded by the buffer --- */ @@ -238,8 +243,13 @@ int main() { KestrelPhySts ps; CHECK(parse_physts_8852(b.data(), b.size(), true, ps)); CHECK(ps.snr_avg == 0); /* torn IE not parsed */ + /* The failure return also clears a reused struct — the pure-output + * contract holds on every path, not just successful parses. */ KestrelPhySts p2; + p2.rssi[0] = 65; + p2.snr_avg = 44; CHECK(!parse_physts_8852(b.data(), 4, true, p2)); /* below header size */ + CHECK(p2.rssi[0] == 0 && p2.snr_avg == 0); } if (failures == 0)