Skip to content
Merged
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
35 changes: 33 additions & 2 deletions demo/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@ static void packetProcessor(const Packet &packet) {
printf("<devourer>RX pkt #%d (len=%zu)\n", g_rx_count, packet.Data.size());
fflush(stdout);
}
/* DEVOURER_RX_DUMP_ALL=1: emit a `<devourer-corrupt-any>` line for EVERY
* frame regardless of SA, with chip-flag bits and phy-soft metrics.
* Consumed by tools/precoder/corruption_survey.py for the FEC-design
* corruption-pattern survey. Pairs with DEVOURER_RX_KEEP_CORRUPTED to
* also pass through chip-FCS-error frames. The body is omitted from this
* line by design (a hot survey would inflate the log past usable size);
* pkt_len + the chip flags + phy metrics is what aggregates carry. */
static const bool dump_all = std::getenv("DEVOURER_RX_DUMP_ALL") != nullptr;
if (dump_all) {
printf("<devourer-corrupt-any>len=%zu crc_err=%u icv_err=%u "
"rate=%u rssi=%d,%d evm=%d,%d snr=%d,%d\n",
packet.Data.size(),
packet.RxAtrib.crc_err ? 1u : 0u,
packet.RxAtrib.icv_err ? 1u : 0u,
packet.RxAtrib.data_rate,
packet.RxAtrib.rssi[0], packet.RxAtrib.rssi[1],
packet.RxAtrib.evm[0], packet.RxAtrib.evm[1],
packet.RxAtrib.snr[0], packet.RxAtrib.snr[1]);
fflush(stdout);
}
/* TX-validation hook: detect frames whose SA matches the txdemo's hardcoded
* injected beacon (57:42:75:05:d6:00). When running this RX demo against
* one adapter while WiFiDriverTxDemo runs against another on the same
Expand Down Expand Up @@ -80,10 +100,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("<devourer-stream>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 <devourer-body>'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("<devourer-stream>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");
Expand Down
3 changes: 3 additions & 0 deletions tests/precoder_stream_roundtrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
r"<devourer-stream>rate=(?P<rate>\d+)\s+len=(?P<len>\d+)"
r"(?:\s+crc_err=(?P<crc_err>\d+))?"
r"(?:\s+icv_err=(?P<icv_err>\d+))?"
r"(?:\s+rssi=(?P<rssi>-?\d+,-?\d+))?"
r"(?:\s+evm=(?P<evm>-?\d+,-?\d+))?"
r"(?:\s+snr=(?P<snr>-?\d+,-?\d+))?"
r"\s+body=(?P<hex>[0-9a-fA-F]*)"
)

Expand Down
75 changes: 75 additions & 0 deletions tools/precoder/corruption_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,45 @@
r"<devourer-stream>rate=(?P<rate>\d+)\s+len=(?P<len>\d+)"
r"(?:\s+crc_err=(?P<crc_err>\d+))?"
r"(?:\s+icv_err=(?P<icv_err>\d+))?"
r"(?:\s+rssi=(?P<rssi>-?\d+,-?\d+))?"
r"(?:\s+evm=(?P<evm>-?\d+,-?\d+))?"
r"(?:\s+snr=(?P<snr>-?\d+,-?\d+))?"
r"\s+body=(?P<hex>[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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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


Expand Down
Loading
Loading