diff --git a/demo/main.cpp b/demo/main.cpp index 55a0767..8e5aaa0 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -70,9 +70,20 @@ static void packetProcessor(const Packet &packet) { * (tools/precoder/stream_rx.py) to decode. Tag is distinct so the * regular dump_body capture stays uncluttered. */ static const bool stream_out = std::getenv("DEVOURER_STREAM_OUT") != nullptr; - if (stream_out) { - printf("rate=%u len=%zu body=", - packet.RxAtrib.data_rate, packet.Data.size()); + /* DEVOURER_RX_KEEP_CORRUPTED=1: surface the body even when the chip + * flagged CRC/ICV error. Default is to filter them out for the byte- + * stream consumer (stream_rx.py), since a body with a wrong tail is + * the byte-mode parser's worst-case input. The flag is the entry + * point for the corruption_analysis.py tool — by-design opt-in so + * accidental enablement doesn't cause IP-stack misery. */ + static const bool keep_corrupted = + 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=", + packet.RxAtrib.data_rate, packet.Data.size(), + packet.RxAtrib.crc_err ? 1u : 0u, + packet.RxAtrib.icv_err ? 1u : 0u); for (size_t i = 24; i < packet.Data.size(); ++i) printf("%02x", packet.Data[i]); printf("\n"); diff --git a/src/FrameParser.cpp b/src/FrameParser.cpp index 566ec02..5a162ed 100644 --- a/src/FrameParser.cpp +++ b/src/FrameParser.cpp @@ -149,16 +149,15 @@ std::vector FrameParser::recvbuf2recvframe(std::span ptr) { do { auto pattrib = rtl8812_query_rx_desc_status(pbuf.data()); - if ((pattrib.crc_err) || (pattrib.icv_err)) { - _logger->info("RX Warning! crc_err={} " - "icv_err={}, skip!", - pattrib.crc_err, pattrib.icv_err); - break; - } - auto pkt_offset = RXDESC_SIZE + pattrib.drvinfo_sz + pattrib.shift_sz + pattrib.pkt_len; // this is offset for next package + /* The packet-length sanity check has to run BEFORE deciding what to do + * about CRC/ICV errors. If pkt_len is unreadable we can't find the next + * aggregate boundary either way, so we still have to give up. But a + * surviving descriptor with a bad-CRC body is recoverable: we can + * surface the (corrupted) packet to the consumer and advance to the + * next one in the same USB aggregate. */ if ((pattrib.pkt_len <= 0) || (pkt_offset > pbuf.size())) { _logger->warn( "RX Warning!,pkt_len <= 0 or pkt_offset > transfer_len; pkt_len: " @@ -167,6 +166,19 @@ std::vector FrameParser::recvbuf2recvframe(std::span ptr) { break; } + /* Corrupted-frame surfacing: previously this was `break`, which threw + * away the bad frame AND every subsequent frame in the same USB + * aggregate (typically 4-8 frames). Now we log + continue: the packet + * still ends up in `ret` with crc_err / icv_err set on its RxAtrib so a + * consumer can either filter (existing behaviour) or analyse the + * corruption (corruption_analysis.py, FEC layers). The pkt_len check + * above already guards the slice math against a corrupted descriptor. */ + if ((pattrib.crc_err) || (pattrib.icv_err)) { + _logger->debug("RX corrupted frame surfaced: crc_err={} icv_err={} " + "pkt_len={}", + pattrib.crc_err, pattrib.icv_err, pattrib.pkt_len); + } + if (pattrib.mfrag) { // !!! We skips this packages because ohd not use fragmentation _logger->warn("mfrag scipping"); diff --git a/src/RadioManagementModule.cpp b/src/RadioManagementModule.cpp index 72c2352..8f8f58a 100644 --- a/src/RadioManagementModule.cpp +++ b/src/RadioManagementModule.cpp @@ -7,6 +7,7 @@ extern "C" { } #include +#include #include #include #include @@ -110,6 +111,20 @@ void RadioManagementModule::hw_var_set_monitor() { /* Append FCS */ rcr_bits |= RCR_APPFCS; + /* DEVOURER_RX_KEEP_CORRUPTED: also pass frames whose 802.11 FCS (CRC32) or + * decryption-ICV check failed. By default the chip drops them at the WMAC + * filter — fine for clean-or-missing IP traffic, but it also hides any + * partial-bit-error information that a FEC layer could otherwise use. With + * the bits below set the frames reach the host with `crc_err` / `icv_err` + * set on the RX descriptor; FrameParser surfaces them so a consumer like + * tools/precoder/corruption_analysis.py can characterise the corruption. + * Guarded by the same env var as the demo's filter — keep them in lockstep + * so a noisy RX never surprises an IP-stack consumer that didn't ask for + * it. */ + if (std::getenv("DEVOURER_RX_KEEP_CORRUPTED") != nullptr) { + rcr_bits |= RCR_ACRC32 | RCR_AICV; + } + // rtw_hal_get_hwreg(adapterState, HW_VAR_RCR, pHalData.rcr_backup); hw_var_rcr_config(rcr_bits); diff --git a/tests/precoder_stream_roundtrip.py b/tests/precoder_stream_roundtrip.py index d061cf9..278bc4f 100644 --- a/tests/precoder_stream_roundtrip.py +++ b/tests/precoder_stream_roundtrip.py @@ -51,7 +51,10 @@ DESC_RATE6M = 0x04 _STREAM_RE = re.compile( - r"rate=(?P\d+) len=(?P\d+) body=(?P[0-9a-fA-F]*)" + r"rate=(?P\d+)\s+len=(?P\d+)" + r"(?:\s+crc_err=(?P\d+))?" + r"(?:\s+icv_err=(?P\d+))?" + r"\s+body=(?P[0-9a-fA-F]*)" ) diff --git a/tools/precoder/corruption_analysis.py b/tools/precoder/corruption_analysis.py new file mode 100644 index 0000000..2862de2 --- /dev/null +++ b/tools/precoder/corruption_analysis.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Corruption analysis for the precoder stream link. + +Reads `` lines on stdin (typically piped from +`WiFiDriverDemo` with both `DEVOURER_STREAM_OUT=1` and +`DEVOURER_RX_KEEP_CORRUPTED=1`), reconstructs what each received body +*should* have been from a known source file, and reports byte/bit-level +error statistics. + +Workflow (TX side): + python3 tools/precoder/stream_tx.py --input source.bin --repeat 1 | \ + ./build/StreamTxDemo + +Workflow (RX side, this tool): + DEVOURER_STREAM_OUT=1 DEVOURER_RX_KEEP_CORRUPTED=1 ./build/WiFiDriverDemo | + python3 tools/precoder/corruption_analysis.py --source source.bin + +The TX side encodes `source.bin` deterministically into N body frames. RX +captures every body matching the canonical SA, including those the chip +flagged with crc_err / icv_err (without `KEEP_CORRUPTED` the parser drops +them; with it on they reach us with the descriptor flags set). For each +captured body we: + + 1. Read the (possibly corrupt) seq from envelope bytes 2-3, + 2. Look up what the encoder would have produced for that seq, + 3. XOR the received body prefix against the expected envelope, + 4. Accumulate per-byte and per-bit error counts plus a histogram of + errors against in-frame byte offset (helpful for spotting whether + the corruption is uniform, clustered near the start/end, or + coincides with the 802.11 SERVICE field offset). + +What this tells you that the chip's CRC bit doesn't: + * Whether corrupted frames are mostly clean with a single byte off + (good FEC opportunity) or wholly scrambled. + * Whether errors are uniformly distributed across the body or + concentrated in a band (e.g. last few bytes, where the chip's + 802.11 FCS sits and the trailing OFDM symbols are most fragile). + * Whether a particular seq pattern (e.g. wrap-around) corrupts more + often than others. + +The output is plain text; pipe to `column -t` or similar if you want a +quick table. +""" + +from __future__ import annotations + +import argparse +import collections +import os +import re +import sys +from pathlib import Path +from typing import Optional + +_HERE = Path(__file__).resolve().parent +if str(_HERE) not in sys.path: + sys.path.insert(0, str(_HERE)) + +import stream # noqa: E402 + +_STREAM_RE = re.compile( + r"rate=(?P\d+)\s+len=(?P\d+)" + r"(?:\s+crc_err=(?P\d+))?" + r"(?:\s+icv_err=(?P\d+))?" + r"\s+body=(?P[0-9a-fA-F]*)" +) + + +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 + only — shape mode's bodies are seed/offset/state-dependent and would + need their full encoder state to reconstruct.""" + frames = stream.pack_stream(source, mtu=mtu, seq_start=seq_start) + return {f.seq: f.envelope_bytes(body_bytes) for f in frames} + + +def main(argv: Optional[list[str]] = None) -> int: + ap = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + ap.add_argument("--source", required=True, + help="path to the byte stream the TX side sent") + ap.add_argument("--mtu", type=int, + default=stream.DEFAULT_BODY_BYTES - stream.ENVELOPE_LEN, + help="payload size per frame (must match the TX side)") + ap.add_argument("--body-bytes", type=int, + default=stream.DEFAULT_BODY_BYTES, + help="encoded body size per frame (must match TX)") + ap.add_argument("--seq-start", type=int, default=0, + help="starting seq the TX side used (must match)") + ap.add_argument("--top-positions", type=int, default=20, + help="how many top-error byte positions to print") + args = ap.parse_args(argv) + + src = Path(args.source).read_bytes() + expected = _expected_bodies(src, args.mtu, args.body_bytes, + seq_start=args.seq_start) + if not expected: + sys.stderr.write("corruption: source is empty — no expected frames\n") + return 2 + sys.stderr.write( + f"corruption: {len(expected)} expected frame(s), body={args.body_bytes}B, " + f"mtu={args.mtu}B, seq range [{min(expected)}..{max(expected)}]\n" + ) + + total_captured = 0 + total_corrupted = 0 + total_clean = 0 + matched_seq = 0 + unmatched_seq = 0 + bits_compared = 0 + bit_errors = 0 + byte_pos_errors = collections.Counter() + byte_pos_examined = collections.Counter() + per_frame_byte_errs: list[int] = [] + per_frame_bit_errs: list[int] = [] + + for line in sys.stdin: + m = _STREAM_RE.search(line) + if not m: + continue + total_captured += 1 + crc_err = int(m.group("crc_err") or 0) + icv_err = int(m.group("icv_err") or 0) + if crc_err or icv_err: + total_corrupted += 1 + else: + total_clean += 1 + body = bytes.fromhex(m.group("hex")) + if len(body) < stream.HEADER_LEN: + unmatched_seq += 1 + continue + # Seq lives at bytes 2-3 of the envelope; magic at 0-1 may be wrong + # if the descriptor is intact but the body got mangled, so we read + # seq regardless and match against expected. + seq = int.from_bytes(body[2:4], "little") + if seq not in expected: + unmatched_seq += 1 + continue + matched_seq += 1 + exp = expected[seq] + compare_len = min(len(body), len(exp)) + frame_byte_errs = 0 + frame_bit_errs = 0 + for i in range(compare_len): + byte_pos_examined[i] += 1 + xor = body[i] ^ exp[i] + if xor: + frame_byte_errs += 1 + bits = bin(xor).count("1") + frame_bit_errs += bits + byte_pos_errors[i] += 1 + bits_compared += compare_len * 8 + bit_errors += frame_bit_errs + per_frame_byte_errs.append(frame_byte_errs) + per_frame_bit_errs.append(frame_bit_errs) + + if not matched_seq: + sys.stderr.write( + "corruption: no captured frames matched a known seq — check " + "--source / --seq-start / --mtu / --body-bytes versus the TX side\n" + ) + return 1 + + ber = bit_errors / max(1, bits_compared) + print(f"=== corruption analysis ({matched_seq} matched / " + f"{total_captured} captured) ===") + print(f"captured : {total_captured}") + print(f" chip-clean : {total_clean}") + print(f" chip-corrupt : {total_corrupted} (crc_err or icv_err set)") + print(f"matched seq : {matched_seq}") + print(f"unmatched seq : {unmatched_seq} (likely lost, foreign, or " + f"seq-bytes corrupted)") + print(f"bits compared : {bits_compared}") + print(f"bit errors : {bit_errors}") + print(f"BER (compared) : {ber:.3e}") + + if per_frame_byte_errs: + clean_frames = sum(1 for e in per_frame_byte_errs if e == 0) + print(f"per-frame errors:") + print(f" fully clean : {clean_frames}/{matched_seq}") + print(f" byte errors : " + f"avg={sum(per_frame_byte_errs) / matched_seq:.2f}, " + f"max={max(per_frame_byte_errs)}") + print(f" bit errors : " + f"avg={sum(per_frame_bit_errs) / matched_seq:.2f}, " + f"max={max(per_frame_bit_errs)}") + + if byte_pos_errors: + print(f"\nbyte-position error histogram " + f"(top {args.top_positions} positions):") + print(f" pos err/exam pct") + for pos, count in byte_pos_errors.most_common(args.top_positions): + exam = byte_pos_examined[pos] + pct = 100.0 * count / max(1, exam) + print(f" {pos:3d} {count:5d}/{exam:5d} {pct:5.1f}%") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/precoder/stream_rx.py b/tools/precoder/stream_rx.py index 0b977ff..f30ba79 100644 --- a/tools/precoder/stream_rx.py +++ b/tools/precoder/stream_rx.py @@ -43,7 +43,10 @@ # Mirrors stream_tx.py's parser without re-importing it (kept side-effect free). _STREAM_RE = re.compile( - r"rate=(?P\d+) len=(?P\d+) body=(?P[0-9a-fA-F]*)" + r"rate=(?P\d+)\s+len=(?P\d+)" + r"(?:\s+crc_err=(?P\d+))?" + r"(?:\s+icv_err=(?P\d+))?" + r"\s+body=(?P[0-9a-fA-F]*)" ) diff --git a/tools/precoder/tun_p2p.py b/tools/precoder/tun_p2p.py index 71bdf18..2d895da 100644 --- a/tools/precoder/tun_p2p.py +++ b/tools/precoder/tun_p2p.py @@ -85,7 +85,10 @@ IFF_NO_PI = 0x1000 _STREAM_RE = re.compile( - r"rate=(?P\d+) len=(?P\d+) body=(?P[0-9a-fA-F]*)" + r"rate=(?P\d+)\s+len=(?P\d+)" + r"(?:\s+crc_err=(?P\d+))?" + r"(?:\s+icv_err=(?P\d+))?" + r"\s+body=(?P[0-9a-fA-F]*)" ) diff --git a/txdemo/stream_tx_demo/main.cpp b/txdemo/stream_tx_demo/main.cpp index 026c4ba..c1582e8 100644 --- a/txdemo/stream_tx_demo/main.cpp +++ b/txdemo/stream_tx_demo/main.cpp @@ -200,7 +200,14 @@ int main(int argc, char **argv) { logger->info("DEVOURER_CHANNEL set — tuning TX to channel {}", channel); } - rtlDevice->SetTxPower(40); + /* DEVOURER_TX_POWER overrides the per-rate "txpower" register value + * (default 40, low single-digits for an attenuated/noisy bench). Useful + * for stress-testing the RX path's corruption handling — lowering this + * forces marginal SNR, which raises the chip's CRC-failure rate so the + * corrupted-frame surfacing path actually gets exercised. */ + int tx_power = 40; + if (const char *p = std::getenv("DEVOURER_TX_POWER")) tx_power = std::atoi(p); + rtlDevice->SetTxPower(static_cast(tx_power)); rtlDevice->InitWrite(SelectedChannel{.Channel = static_cast(channel), .ChannelOffset = 0, .ChannelWidth = CHANNEL_WIDTH_20});