diff --git a/demo/main.cpp b/demo/main.cpp index 8e5aaa0..be9a6e8 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -80,10 +80,21 @@ static void packetProcessor(const Packet &packet) { std::getenv("DEVOURER_RX_KEEP_CORRUPTED") != nullptr; const bool corrupted = packet.RxAtrib.crc_err || packet.RxAtrib.icv_err; if (stream_out && (!corrupted || keep_corrupted)) { - printf("rate=%u len=%zu crc_err=%u icv_err=%u body=", + /* Per-stream phy soft metrics (RSSI / EVM / SNR for paths A,B; on + * 8814AU paths C,D would also be non-zero but we surface only A,B + * here to stay aligned with 's format). These are + * link-quality measurements at the PHY before decoding — same + * source as the Tier-2 diagnostics — so a consumer like + * corruption_analysis.py can correlate BER with link quality on a + * per-frame basis instead of relying on aggregated statistics. */ + printf("rate=%u len=%zu crc_err=%u icv_err=%u " + "rssi=%d,%d evm=%d,%d snr=%d,%d body=", packet.RxAtrib.data_rate, packet.Data.size(), packet.RxAtrib.crc_err ? 1u : 0u, - packet.RxAtrib.icv_err ? 1u : 0u); + packet.RxAtrib.icv_err ? 1u : 0u, + packet.RxAtrib.rssi[0], packet.RxAtrib.rssi[1], + packet.RxAtrib.evm[0], packet.RxAtrib.evm[1], + packet.RxAtrib.snr[0], packet.RxAtrib.snr[1]); for (size_t i = 24; i < packet.Data.size(); ++i) printf("%02x", packet.Data[i]); printf("\n"); diff --git a/tests/precoder_stream_roundtrip.py b/tests/precoder_stream_roundtrip.py index 278bc4f..4f45281 100644 --- a/tests/precoder_stream_roundtrip.py +++ b/tests/precoder_stream_roundtrip.py @@ -54,6 +54,9 @@ r"rate=(?P\d+)\s+len=(?P\d+)" r"(?:\s+crc_err=(?P\d+))?" r"(?:\s+icv_err=(?P\d+))?" + r"(?:\s+rssi=(?P-?\d+,-?\d+))?" + r"(?:\s+evm=(?P-?\d+,-?\d+))?" + r"(?:\s+snr=(?P-?\d+,-?\d+))?" r"\s+body=(?P[0-9a-fA-F]*)" ) diff --git a/tools/precoder/corruption_analysis.py b/tools/precoder/corruption_analysis.py index 2862de2..a8acbfc 100644 --- a/tools/precoder/corruption_analysis.py +++ b/tools/precoder/corruption_analysis.py @@ -62,10 +62,45 @@ r"rate=(?P\d+)\s+len=(?P\d+)" r"(?:\s+crc_err=(?P\d+))?" r"(?:\s+icv_err=(?P\d+))?" + r"(?:\s+rssi=(?P-?\d+,-?\d+))?" + r"(?:\s+evm=(?P-?\d+,-?\d+))?" + r"(?:\s+snr=(?P-?\d+,-?\d+))?" r"\s+body=(?P[0-9a-fA-F]*)" ) +def _parse_pair(s: Optional[str]) -> Optional[tuple[int, int]]: + if not s: + return None + a, b = s.split(",") + return int(a), int(b) + + +def _effective_snr(snr: Optional[tuple[int, int]]) -> Optional[int]: + """Pick the SNR value that actually drove decode quality. + + The two-path field carries SNR for paths A and B; on single-antenna + USB sticks path B reads 0 (no signal, not "0 dB SNR"), so a naive + min(A,B) would always report 0 and the BER-vs-SNR view collapses. + `max(A,B)` works for both 1T1R (B is 0, A drives) and 2T2R single- + stream operation (the chip picks the stronger path for the only + stream). For an honest 2T2R two-stream analysis a finer model + would be needed; this is a single-stream PoC. + """ + if snr is None: + return None + return max(snr) + + +def _snr_bucket(snr: Optional[tuple[int, int]]) -> str: + """Group SNR into 5-dB buckets. Returns 'no-snr' when absent.""" + eff = _effective_snr(snr) + if eff is None: + return "no-snr" + base = (eff // 5) * 5 + return f"{base:>3d}-{base + 5} dB" + + def _expected_bodies(source: bytes, mtu: int, body_bytes: int, seq_start: int = 0) -> dict[int, bytes]: """Reproduce the TX side's encoded envelopes for `source`. Byte mode @@ -116,6 +151,13 @@ def main(argv: Optional[list[str]] = None) -> int: byte_pos_examined = collections.Counter() per_frame_byte_errs: list[int] = [] per_frame_bit_errs: list[int] = [] + # Per-frame phy metrics (parsed but only used when present). + snr_clean: list[int] = [] + snr_corrupt: list[int] = [] + # (snr_bucket, corrupted_or_not) -> count; for the BER-vs-SNR table. + bucket_frames: collections.Counter = collections.Counter() + bucket_bit_errors: collections.Counter = collections.Counter() + bucket_bits_compared: collections.Counter = collections.Counter() for line in sys.stdin: m = _STREAM_RE.search(line) @@ -124,6 +166,10 @@ def main(argv: Optional[list[str]] = None) -> int: total_captured += 1 crc_err = int(m.group("crc_err") or 0) icv_err = int(m.group("icv_err") or 0) + snr = _parse_pair(m.group("snr")) + eff = _effective_snr(snr) + if eff is not None: + (snr_corrupt if crc_err or icv_err else snr_clean).append(eff) if crc_err or icv_err: total_corrupted += 1 else: @@ -156,6 +202,10 @@ def main(argv: Optional[list[str]] = None) -> int: bit_errors += frame_bit_errs per_frame_byte_errs.append(frame_byte_errs) per_frame_bit_errs.append(frame_bit_errs) + bucket = _snr_bucket(snr) + bucket_frames[bucket] += 1 + bucket_bit_errors[bucket] += frame_bit_errs + bucket_bits_compared[bucket] += compare_len * 8 if not matched_seq: sys.stderr.write( @@ -197,6 +247,31 @@ def main(argv: Optional[list[str]] = None) -> int: pct = 100.0 * count / max(1, exam) print(f" {pos:3d} {count:5d}/{exam:5d} {pct:5.1f}%") + # Phy-metrics correlation. Two views: distribution of weakest-path SNR + # for chip-clean vs chip-corrupt frames, and per-SNR-bucket BER. + if snr_clean or snr_corrupt: + def _stat(xs: list[int]) -> str: + if not xs: + return "n=0" + xs = sorted(xs) + n = len(xs) + return (f"n={n} min={xs[0]} p25={xs[n // 4]} " + f"med={xs[n // 2]} p75={xs[(3 * n) // 4]} max={xs[-1]}") + print(f"\nphy SNR (stronger path, dB):") + print(f" chip-clean : {_stat(snr_clean)}") + print(f" chip-corrupt : {_stat(snr_corrupt)}") + + if bucket_frames and any(b != "no-snr" for b in bucket_frames): + print(f"\nBER by SNR bucket (stronger path, 5-dB buckets):") + print(f" bucket frames bits-cmp bit-err BER") + for bucket in sorted(bucket_frames): + n = bucket_frames[bucket] + bits = bucket_bits_compared[bucket] + errs = bucket_bit_errors[bucket] + ber = errs / max(1, bits) + print(f" {bucket:>11s} {n:6d} {bits:8d} {errs:6d} " + f"{ber:.3e}") + return 0 diff --git a/tools/precoder/stream_rx.py b/tools/precoder/stream_rx.py index f30ba79..f5352c3 100644 --- a/tools/precoder/stream_rx.py +++ b/tools/precoder/stream_rx.py @@ -46,6 +46,9 @@ r"rate=(?P\d+)\s+len=(?P\d+)" r"(?:\s+crc_err=(?P\d+))?" r"(?:\s+icv_err=(?P\d+))?" + r"(?:\s+rssi=(?P-?\d+,-?\d+))?" + r"(?:\s+evm=(?P-?\d+,-?\d+))?" + r"(?:\s+snr=(?P-?\d+,-?\d+))?" r"\s+body=(?P[0-9a-fA-F]*)" ) diff --git a/tools/precoder/tun_p2p.py b/tools/precoder/tun_p2p.py index 2d895da..ad3e76f 100644 --- a/tools/precoder/tun_p2p.py +++ b/tools/precoder/tun_p2p.py @@ -88,6 +88,9 @@ r"rate=(?P\d+)\s+len=(?P\d+)" r"(?:\s+crc_err=(?P\d+))?" r"(?:\s+icv_err=(?P\d+))?" + r"(?:\s+rssi=(?P-?\d+,-?\d+))?" + r"(?:\s+evm=(?P-?\d+,-?\d+))?" + r"(?:\s+snr=(?P-?\d+,-?\d+))?" r"\s+body=(?P[0-9a-fA-F]*)" )