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
15 changes: 12 additions & 3 deletions docs/scheduled-mac.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ generation three phases: **on** (responder armed with MAC1, unicast QoS-Data
to MAC1 → expect ~100% `tx.report ok`, retries ~0), **retarget** (responder
re-armed to a different MAC2, TX to MAC2 → proves RA and responder MAC are
arbitrary), **off** (no responder → expect 0% ok, retries pinned at the
descriptor limit: the no-ACK outcome must be *visible*, per frame).
descriptor limit set by `DEVOURER_TX_RETRY_LIMIT` — this matrix runs it at
12 — so the no-ACK outcome must be *visible*, per frame).
`report_coverage` = reports / frames sent (`tx.stats.submitted`); HalMAC adds
SW_DEFINE tag-echo gap counting.

Expand All @@ -172,6 +173,11 @@ scheduled MAC runs TX+RX anyway, so this is the relevant session shape.
| Jaguar2 8812BU | 0.91 / 2.1 (run-to-run 0.12–0.91) | 0.64 / 5.3 | yes (12) | 0.86 | 0 |
| Jaguar3 8822CU | 1.00 / 0.24 | 1.00 / 0.13 | yes (12) | 0.96 | 0 |

The OFF-phase pin is set by `DEVOURER_TX_RETRY_LIMIT` (the matrix runs 12,
the value the descriptors used to hardcode) — the knob, not a descriptor
constant, is now the single source of truth for the retry limit on
jaguar1/2/3 (inert on Kestrel and the 8814A die).

Responder-side capability (same setup, J3 TX as the reference soliciting
station): **8814AU** closes the loop at retries ~0.1 (the bench responder of
choice); **8812AU** works but degraded (97% delivery at ~7 mean retries —
Expand All @@ -182,7 +188,8 @@ separately proven (`tests/ack_responder_check.sh`).
### The contract

1. **Per-frame delivery detection is GO on all three generations**: the OFF
phase pins retries at the descriptor limit with `state=1` on every report —
phase pins retries at the configured limit (`DEVOURER_TX_RETRY_LIMIT`,
set to 12 by the matrix) with `state=1` on every report —
a no-ACK outcome is unambiguously visible per frame, which is all a
software retransmission layer needs. Report coverage 86–100% with zero
HalMAC tag gaps (interior losses); the reliability layer must tolerate a
Expand All @@ -191,7 +198,9 @@ separately proven (`tests/ack_responder_check.sh`).
2. **Closed-loop hardware ACK + autonomous retry is GO on Jaguar1 and
Jaguar3** (100% delivery, retries ≈ 0.2–0.3) including retargeting an
arbitrary UE MAC mid-session (re-arm `SetAckResponder`, change the
descriptor RA — both fully dynamic).
descriptor RA — both fully dynamic). Requires a nonzero
`DEVOURER_TX_RETRY_LIMIT` — the hardware ARQ loop retransmits until ACK
only up to that per-frame limit.
3. **Jaguar2 as the soliciting TX is MARGINAL as measured**: ACK closure
varied 12–91% across identical runs (mean retries 2–11) against both
8814AU and 8812AU responders, and its TX pace in the TX+RX-thread shape is
Expand Down
2 changes: 2 additions & 0 deletions examples/common/env_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ devourer::DeviceConfig devourer_config_from_env() {
if (devourer::parse_ampdu_mode(e, m))
cfg.tx.ampdu = m;
}
if (env_long("DEVOURER_TX_RETRY_LIMIT", &v))
cfg.tx.retry_limit = static_cast<int>(v < 0 ? 0 : (v > 63 ? 63 : v));

/* ---- bf ---- */
if (const char *snd = env_str("DEVOURER_BF_ARM_SOUNDER")) {
Expand Down
9 changes: 9 additions & 0 deletions src/DeviceConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ struct DeviceConfig {
* Runtime equivalent: StartCwTone/StopCwTone on the concrete device. */
bool cw_tone = false;
uint8_t cw_tone_gain = 0;
/* env: DEVOURER_TX_RETRY_LIMIT — per-frame hardware retry limit (0..63).
* Maps to the TX descriptor DATA_RETRY_LIMIT / RTS_DATA_RTY_LMT field
* (Dword4 bits 18-23). 0 = no retries (WFB default: FEC provides
* reliability, not MAC retries). On a busy half-duplex link retries flood
* the air and blind the receiver. Hardware-ARQ (SetAckResponder + unicast
* TA, docs/scheduled-mac.md) needs a nonzero value. Inert on Kestrel
* (firmware-level retry) and on the 8814A die (vendor DATA_RETRY_LIMIT=0
* carve-out kept). */
int retry_limit = 0;
Comment thread
josephnef marked this conversation as resolved.
/* env: DEVOURER_TX_USB_AGG — USB TX aggregation: max frames packed into
* one bulk-OUT URB by send_packets (0 = off, the default: send_packets
* degrades to a per-frame loop and every TX path is byte-identical to
Expand Down
8 changes: 5 additions & 3 deletions src/jaguar1/RtlJaguarDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1171,8 +1171,10 @@ size_t RtlJaguarDevice::build_tx_block(const uint8_t *packet, size_t length,
SET_TX_DESC_RETRY_LIMIT_ENABLE_8812(usb_frame, 1);
if (!is_8814a) {
/* 88XXau leaves DATA_RETRY_LIMIT=0 for monitor injection on 8814A
* (RETRY_LIMIT_ENABLE stays set to 1 in both). */
SET_TX_DESC_DATA_RETRY_LIMIT_8812(usb_frame, 12);
* (RETRY_LIMIT_ENABLE stays set to 1 in both).
* Use cfg.tx.retry_limit (DEVOURER_TX_RETRY_LIMIT, default 0) instead of
* the hardcoded 12 — retries flood the air on a busy half-duplex link. */
SET_TX_DESC_DATA_RETRY_LIMIT_8812(usb_frame, _cfg.tx.retry_limit);
}
if (sgi) {
_logger->info("short gi enabled,set sgi");
Expand Down Expand Up @@ -1228,7 +1230,7 @@ size_t RtlJaguarDevice::build_tx_block(const uint8_t *packet, size_t length,
SET_TX_DESC_AGG_ENABLE_8812(usb_frame, 1);
SET_TX_DESC_MAX_AGG_NUM_8812(usb_frame, am.max_num & 0x1f);
SET_TX_DESC_AMPDU_DENSITY_8812(usb_frame, am.density & 0x7);
SET_TX_DESC_DATA_RETRY_LIMIT_8812(usb_frame, am.no_ack ? 0 : 12);
SET_TX_DESC_DATA_RETRY_LIMIT_8812(usb_frame, am.no_ack ? 0 : _cfg.tx.retry_limit);
}
if (_cfg.debug.tx_qsel)
SET_TX_DESC_QUEUE_SEL_8812(usb_frame, *_cfg.debug.tx_qsel);
Expand Down
8 changes: 7 additions & 1 deletion src/jaguar2/RtlJaguar2Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,12 @@ size_t RtlJaguar2Device::build_tx_block(const uint8_t *packet, size_t length,
SET_TX_DESC_SW_DEFINE_8822B(out, _tx_rpt_tag.fetch_add(1) & 0xff);
jaguar2::cal_txdesc_chksum_8822b(out);
}
/* Per-frame retry limit from cfg (DEVOURER_TX_RETRY_LIMIT, default 0) —
* the fill_data_tx_desc builder hardcodes 12, which floods a busy
* half-duplex link. Both fields sit inside the checksummed span. */
SET_TX_DESC_RTY_LMT_EN_8822B(out, 1);
SET_TX_DESC_RTS_DATA_RTY_LMT_8822B(out, _cfg.tx.retry_limit);
jaguar2::cal_txdesc_chksum_8822b(out);
Comment thread
josephnef marked this conversation as resolved.
const devourer::AmpduMode am = _ampdu; /* one lock-free load */
if (am.enabled || _cfg.debug.tx_qsel || _cfg.debug.tx_ampdu_max) {
/* A-MPDU descriptor half. The product SetAmpduMode state applies first
Expand All @@ -1541,7 +1547,7 @@ size_t RtlJaguar2Device::build_tx_block(const uint8_t *packet, size_t length,
SET_TX_DESC_AGG_EN_8822B(out, 1);
SET_TX_DESC_MAX_AGG_NUM_8822B(out, am.max_num & 0x1f);
SET_TX_DESC_AMPDU_DENSITY_8822B(out, am.density & 0x7);
SET_TX_DESC_RTS_DATA_RTY_LMT_8822B(out, am.no_ack ? 0 : 12);
SET_TX_DESC_RTS_DATA_RTY_LMT_8822B(out, am.no_ack ? 0 : _cfg.tx.retry_limit);
}
if (_cfg.debug.tx_qsel)
SET_TX_DESC_QSEL_8822B(out, *_cfg.debug.tx_qsel);
Expand Down
8 changes: 7 additions & 1 deletion src/jaguar3/RtlJaguar3Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2006,6 +2006,12 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length,
SET_TX_DESC_SW_DEFINE_8822C(out, _tx_rpt_tag.fetch_add(1) & 0xff);
jaguar3::cal_txdesc_chksum_8822c(out);
}
/* Per-frame retry limit from cfg (DEVOURER_TX_RETRY_LIMIT, default 0) —
* the fill_data_tx_desc builder hardcodes 12, which floods a busy
* half-duplex link. Both fields sit inside the checksummed span. */
SET_TX_DESC_RTY_LMT_EN_8822C(out, 1);
SET_TX_DESC_RTS_DATA_RTY_LMT_8822C(out, _cfg.tx.retry_limit);
jaguar3::cal_txdesc_chksum_8822c(out);
const devourer::AmpduMode am = _ampdu; /* one lock-free load */
if (am.enabled || _cfg.debug.tx_qsel || _cfg.debug.tx_ampdu_max) {
/* A-MPDU descriptor half. The product SetAmpduMode state applies first,
Expand All @@ -2017,7 +2023,7 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length,
SET_TX_DESC_AGG_EN_8822C(out, 1);
SET_TX_DESC_MAX_AGG_NUM_8822C(out, am.max_num & 0x1f);
SET_TX_DESC_AMPDU_DENSITY_8822C(out, am.density & 0x7);
SET_TX_DESC_RTS_DATA_RTY_LMT_8822C(out, am.no_ack ? 0 : 12);
SET_TX_DESC_RTS_DATA_RTY_LMT_8822C(out, am.no_ack ? 0 : _cfg.tx.retry_limit);
}
if (_cfg.debug.tx_qsel)
SET_TX_DESC_QSEL_8822C(out, *_cfg.debug.tx_qsel);
Expand Down
25 changes: 19 additions & 6 deletions tests/ack_txreport_analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
# a different unicast RA (SetAckResponder re-armed) -> expect ON behavior.
#
# Usage: python3 ack_txreport_analyze.py <tx.jsonl> --sent N --cell NAME \
# --expect on|off
# --expect on|off [--expect-retries PIN]
# (off-phase verdict pins at PIN, default 12)
# python3 ack_txreport_analyze.py --selftest
import json, sys

Expand All @@ -31,7 +32,7 @@ def load(path):
out.append(r)
return out

def analyze(reports, sent, cell="", expect=None):
def analyze(reports, sent, cell="", expect=None, expect_retries=12):
n = len(reports)
v = {"ev": "ackrep.verdict", "cell": cell, "sent": sent, "reports": n}
if n == 0:
Expand All @@ -57,9 +58,11 @@ def analyze(reports, sent, cell="", expect=None):
if expect == "on":
v["capability_ok"] = bool(v["ack_rate"] >= 0.9 and v["retries_mean"] < 2)
elif expect == "off":
# Nobody ACKs: delivery must FAIL and retries pin at the limit — this
# proves the no-ACK outcome is visible, not that the link is bad.
v["capability_ok"] = bool(v["ack_rate"] <= 0.1 and v["retries_max"] >= 8)
# Nobody ACKs: delivery must FAIL and retries pin at the configured
# limit (--expect-retries, DEVOURER_TX_RETRY_LIMIT) — this proves the
# no-ACK outcome is visible, not that the link is bad.
v["capability_ok"] = bool(v["ack_rate"] <= 0.1 and
v["retries_max"] >= expect_retries)
else:
v["capability_ok"] = None
return v
Expand All @@ -79,6 +82,12 @@ def check(cond, msg):
"tag": i % 256, "missed": 0} for i in range(100)]
v = analyze(off, 100, "off", expect="off")
check(v["capability_ok"] and v["retries_max"] == 12, "pinned OFF passes")
off8 = [{"ev": "tx.report", "ok": False, "state": 1, "retries": 8}
for _ in range(100)]
v = analyze(off8, 100, "off8", expect="off", expect_retries=8)
check(v["capability_ok"], "custom pin (8) OFF passes")
v = analyze(off8, 100, "off8-mismatch", expect="off", expect_retries=12)
check(not v["capability_ok"], "below-pin OFF fails")
v = analyze(off, 100, "off-as-on", expect="on")
check(not v["capability_ok"], "OFF behavior fails an ON expectation")
v = analyze([], 100, "dead", expect="on")
Expand All @@ -102,13 +111,17 @@ def main():
sys.exit(selftest())
args = sys.argv[1:]
sent, cell, expect = 0, "", None
expect_retries = 12
if "--sent" in args:
i = args.index("--sent"); sent = int(args[i + 1]); del args[i:i + 2]
if "--cell" in args:
i = args.index("--cell"); cell = args[i + 1]; del args[i:i + 2]
if "--expect" in args:
i = args.index("--expect"); expect = args[i + 1]; del args[i:i + 2]
v = analyze(load(args[0]), sent, cell, expect)
if "--expect-retries" in args:
i = args.index("--expect-retries")
expect_retries = int(args[i + 1]); del args[i:i + 2]
v = analyze(load(args[0]), sent, cell, expect, expect_retries)
print(json.dumps(v))
sys.exit(0 if v.get("capability_ok") else 1)

Expand Down
9 changes: 7 additions & 2 deletions tests/ack_txreport_matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
# MAC2: proves the injected descriptor's RA and the responder MAC
# are both arbitrary, not baked-in.
# off — no responder, TX to MAC1: expect ok~0, retries pinned at the
# descriptor limit — the no-ACK outcome is VISIBLE per frame.
# descriptor limit set by DEVOURER_TX_RETRY_LIMIT (this matrix
# pins 12 so the hardware-ARQ capability stays visible) — the
# no-ACK outcome is VISIBLE per frame.
# Every phase also measures report_coverage (reports / frames sent) and, on
# HalMAC (J2/J3), SW_DEFINE tag-echo gaps + the firmware missed counter.
#
Expand All @@ -29,6 +31,7 @@ CH=${CH:-36}; SECS=${SECS:-8}; GAP_US=${GAP_US:-5000}
MAC1=${MAC1:-02:12:34:56:78:9a}
MAC2=${MAC2:-02:12:34:56:78:9b}
TX_SA=${TX_SA:-02:aa:bb:cc:dd:01} # unicast TA (the ACK RA I/G footgun)
RETRY_LIMIT=${RETRY_LIMIT:-12} # descriptor retry pin for the off phase
OUT=${OUT:-/tmp/ack_txreport}
CELLS=${CELLS:-"j1-8812au:0x0bda:0x8812 j2-8812bu:0x2357:0x012d j3-8822cu:0x0bda:0xc812"}

Expand All @@ -51,6 +54,7 @@ run_phase() { # $1 cell $2 phase $3 tx vid $4 tx pid $5 RA mac $6 responder mac
DEVOURER_TX_QOS_DATA=1 DEVOURER_TX_RA=$ra DEVOURER_TX_SA=$TX_SA \
DEVOURER_TX_RATE=MCS3 DEVOURER_TX_PAYLOAD_BYTES=200 \
DEVOURER_TX_GAP_US=$GAP_US DEVOURER_TX_REPORT=1 \
DEVOURER_TX_RETRY_LIMIT=$RETRY_LIMIT \
DEVOURER_TX_WITH_RX=thread DEVOURER_LOG_LEVEL=warn \
timeout -s INT $SECS ./build/txdemo \
>"$OUT/tx_$tag.jsonl" 2>"$OUT/tx_$tag.err" || true
Expand All @@ -64,7 +68,8 @@ run_phase() { # $1 cell $2 phase $3 tx vid $4 tx pid $5 RA mac $6 responder mac
sent=${sent:-0}
echo "-- $tag: sent=$sent reports=$(grep -c '"ev":"tx.report"' "$OUT/tx_$tag.jsonl" || true)"
python3 tests/ack_txreport_analyze.py "$OUT/tx_$tag.jsonl" \
--sent "$sent" --cell "$tag" --expect "$expect" | tee -a "$VERDICTS" || true
--sent "$sent" --cell "$tag" --expect "$expect" \
--expect-retries "$RETRY_LIMIT" | tee -a "$VERDICTS" || true
}

for cell in $CELLS; do
Expand Down
Loading