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
7 changes: 5 additions & 2 deletions docs/aggregation.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ re-airing one frame N times.

## TX-status reports (`tx.report`)

`DeviceConfig tx.report` (env `DEVOURER_TX_REPORT`) sets `SPE_RPT` in every
data descriptor; the firmware answers each transmission with a CCX report,
`DeviceConfig tx.report` (env `DEVOURER_TX_REPORT`, value = sampling divisor
N) sets `SPE_RPT` in every Nth data descriptor — 1 = every frame; above
~1.25 k fps sample with N ≥ fps/1300 or the fw's ~1.3 k reports/s CCX
emission ceiling collapses coverage (docs/scheduled-mac.md). The firmware
answers each requested transmission with a CCX report,
decoded at the C2H RX sites into `tx.report` events (`src/TxReport.h`): `state`
(0 = delivered/completed, 1 = retry-drop), `retries` (hardware
retransmissions), `queue_time_raw`, `final_rate`, `bmc`. HalMAC reports echo
Expand Down
10 changes: 8 additions & 2 deletions docs/scheduled-mac.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,14 @@ separately proven (`tests/ack_responder_check.sh`).
MISSED_RPT_NUM field is stuffed with a constant on this fw (verified
against the 8822B/C/E vendor headers — parse is exact, the fw just
doesn't populate it), so tag gaps are the only drop signal. Above the
ceiling, either sample SPE_RPT 1-in-N to keep the demanded rate under
~1.3 k/s or account report-less frames as "unknown".
ceiling, sample: `DEVOURER_TX_REPORT=N` requests the report on every Nth
frame while the tag still stamps every frame, so received-tag deltas are
exact multiples of N and coverage of the sampled frames is deterministic
(measured at 2.4 k fps: N=1 collapses to 56%, N=2 delivers 100.0% of the
sampled reports, zero off-modulo anomalies; the sampled ok-rate read
99.71% against a 99.99% ledger truth — pessimistic by the ACK-loss
asymmetry, the safe direction). Pick N ≥ fps/1300, or account
report-less frames as "unknown".
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
Expand Down
3 changes: 2 additions & 1 deletion examples/common/env_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ devourer::DeviceConfig devourer_config_from_env() {
cfg.tx.cw_tone_gain = static_cast<uint8_t>(v) & 0x1F;
if (env_long("DEVOURER_TX_USB_AGG", &v) && v > 0)
cfg.tx.usb_agg_max = static_cast<unsigned>(v);
cfg.tx.report = env_flag("DEVOURER_TX_REPORT");
if (env_long("DEVOURER_TX_REPORT", &v)) /* sampling divisor N, 0..255 */
cfg.tx.report = static_cast<int>(v < 0 ? 0 : v > 255 ? 255 : v);
if (const char *e = env_str("DEVOURER_TX_AMPDU_MODE")) {
devourer::AmpduMode m;
if (devourer::parse_ampdu_mode(e, m))
Expand Down
13 changes: 11 additions & 2 deletions src/DeviceConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,17 @@ struct DeviceConfig {
* `tx.report` events. The TX-side link sensor. On the HalMAC chips the
* descriptor SW_DEFINE also carries a rotating 8-bit tag the report
* echoes (per-frame correlation). Default off (descriptors
* byte-identical). Needs an RX loop to deliver the C2H reports. */
bool report = false;
* byte-identical). Needs an RX loop to deliver the C2H reports.
*
* Value = sampling divisor N: 1 requests a report on EVERY frame, N > 1
* on every Nth (0..255). The CCX emission path saturates at ~1.3–1.4 k
* reports/s (docs/scheduled-mac.md), so above ~1.25 k fps pick
* N >= fps/1300 and coverage of the SAMPLED frames stays deterministic
Comment thread
josephnef marked this conversation as resolved.
* instead of load-collapsing. HalMAC dies stamp the SW_DEFINE tag on
* every frame regardless of sampling, so consecutive received-report
* tags differ by exactly N — any other delta is a dropped report
* (consumers must know N; tests/txrpt_coverage_attrib.py --sample-n). */
int report = 0;
/* env: DEVOURER_TX_AMPDU_MODE="tid/maxnum[/density[/noack[/maxtime_hex]]]"
* — arm the first-class A-MPDU TX mode (src/AmpduMode.h) at the end of
* bring-up: mark data frames aggregatable and program the MAC pacing
Expand Down
14 changes: 10 additions & 4 deletions src/jaguar1/RtlJaguarDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1163,11 +1163,17 @@ size_t RtlJaguarDevice::build_tx_block(const uint8_t *packet, size_t length,
SET_TX_DESC_GID_8812(usb_frame, static_cast<uint8_t>(0x3F));
}
SET_TX_DESC_SW_DEFINE_8812(usb_frame, static_cast<uint16_t>(0x001));
/* DEVOURER_TX_REPORT: SPE_RPT asks the fw for a per-frame CCX TX report
* (delivered / retry count / queue time — src/TxReport.h). Dword2, inside
* the checksummed 32 bytes. */
/* DEVOURER_TX_REPORT: SPE_RPT asks the fw for a CCX TX report (delivered /
* retry count / queue time — src/TxReport.h), sampled every Nth frame
* (cfg value = N). The 8812 report format has no tag echo, so sampling
* here only relieves the report rate — per-frame attribution stays
* order-based. Dword2, inside the checksummed 32 bytes. */
if (_cfg.tx.report)
SET_TX_DESC_SPE_RPT_8812(usb_frame, 1);
SET_TX_DESC_SPE_RPT_8812(
usb_frame,
_tx_ccx_ctr.fetch_add(1) % static_cast<uint64_t>(_cfg.tx.report) == 0
? 1
: 0);
SET_TX_DESC_RETRY_LIMIT_ENABLE_8812(usb_frame, 1);
if (!is_8814a) {
/* 88XXau leaves DATA_RETRY_LIMIT=0 for monitor injection on 8814A
Expand Down
7 changes: 7 additions & 0 deletions src/jaguar1/RtlJaguarDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ class RtlJaguarDevice : public IRtlDevice {
* see SetTxPacketPowerStep. */
std::atomic<uint8_t> _tx_pkt_pwr_step{0};

/* CCX report sampling counter (cfg.tx.report = N — request a report on
* every Nth frame). The 8812 report format has no SW_DEFINE tag echo, so
* unlike Jaguar2/3 this counter drives only the request cadence. 64-bit:
* a narrow counter's wrap jumps the sampling phase for any N that doesn't
* divide it (2^32 is ~20 days at field frame rates). */
std::atomic<uint64_t> _tx_ccx_ctr{0};

/* CW single-tone (StartCwTone/StopCwTone) saved state for a clean restore:
* the pre-tone RF 0x00 and four BB dwords — RFE-pinmux words on 8812/8821
* (0xCB0/0xEB0/0xCB4/0xEB4), per-path TX-scale words on 8814 (0xC1C/0xE1C/
Expand Down
17 changes: 11 additions & 6 deletions src/jaguar2/RtlJaguar2Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1521,12 +1521,17 @@ size_t RtlJaguar2Device::build_tx_block(const uint8_t *packet, size_t length,
static_cast<uint8_t>(hdrlen >> 1), ndpa, data_sc, pkt_pwr_step,
pkt_offset);
if (_cfg.tx.report) {
/* DEVOURER_TX_REPORT: SPE_RPT asks the fw for a per-frame CCX TX report;
* the report echoes SW_DEFINE's low byte, so stamp a rotating tag for
* per-frame correlation (src/TxReport.h). Both fields sit inside the
* checksummed span — re-checksum (idempotent). */
SET_TX_DESC_SPE_RPT_8822B(out, 1);
SET_TX_DESC_SW_DEFINE_8822B(out, _tx_rpt_tag.fetch_add(1) & 0xff);
/* DEVOURER_TX_REPORT: SPE_RPT asks the fw for a CCX TX report; the
* report echoes SW_DEFINE's low byte, so stamp a rotating tag for
* per-frame correlation (src/TxReport.h). The tag goes on EVERY frame
* while the request samples every Nth (cfg value = N): the fw's CCX
* emission saturates at ~1.3k reports/s, and with the tag continuous a
* received-report tag delta other than N is a dropped report. Both
* fields sit inside the checksummed span — re-checksum (idempotent). */
const uint64_t k = _tx_rpt_tag.fetch_add(1);
SET_TX_DESC_SPE_RPT_8822B(
out, k % static_cast<uint64_t>(_cfg.tx.report) == 0 ? 1 : 0);
SET_TX_DESC_SW_DEFINE_8822B(out, static_cast<uint16_t>(k & 0xff));
jaguar2::cal_txdesc_chksum_8822b(out);
}
/* Per-frame retry limit from cfg (DEVOURER_TX_RETRY_LIMIT, default 0) —
Expand Down
8 changes: 6 additions & 2 deletions src/jaguar2/RtlJaguar2Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,12 @@ class RtlJaguar2Device : public IRtlDevice {
/* Default per-packet TXPWR_OFSET LUT step (0 = none) — see SetTxPacketPowerStep. */
std::atomic<uint8_t> _tx_pkt_pwr_step{0};
/* Rotating SW_DEFINE tag stamped when tx.report is on — the CCX report
* echoes its low byte, correlating reports to frames (src/TxReport.h). */
std::atomic<uint16_t> _tx_rpt_tag{0};
* echoes its low byte, correlating reports to frames (src/TxReport.h).
* 64-bit: the counter also gates the sampled SPE_RPT request (every Nth
* frame), and a narrow counter's wrap would jump the sampling phase for
* any N that doesn't divide it — off-modulo tag deltas masquerading as
* dropped reports at every seam. */
std::atomic<uint64_t> _tx_rpt_tag{0};
/* A-MPDU TX mode (SetAmpduMode). Read lock-free in the TX descriptor path
* (same pattern as _tx_mode_default); a control-plane write during TX is
* the caller's to sequence and at worst tears one frame's mode benignly. */
Expand Down
17 changes: 11 additions & 6 deletions src/jaguar3/RtlJaguar3Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1999,12 +1999,17 @@ size_t RtlJaguar3Device::build_tx_block(const uint8_t *packet, size_t length,
bw_desc, sgi != 0, ldpc != 0, stbc, bmc, ndpa, data_sc, pwr_type,
pkt_offset);
if (_cfg.tx.report) {
/* DEVOURER_TX_REPORT: SPE_RPT asks the fw for a per-frame CCX TX report;
* the report echoes SW_DEFINE's low byte, so stamp a rotating tag for
* per-frame correlation (src/TxReport.h). Both fields sit inside the
* checksummed span — re-checksum (idempotent). */
SET_TX_DESC_SPE_RPT_8822C(out, 1);
SET_TX_DESC_SW_DEFINE_8822C(out, _tx_rpt_tag.fetch_add(1) & 0xff);
/* DEVOURER_TX_REPORT: SPE_RPT asks the fw for a CCX TX report; the
* report echoes SW_DEFINE's low byte, so stamp a rotating tag for
* per-frame correlation (src/TxReport.h). The tag goes on EVERY frame
* while the request samples every Nth (cfg value = N): the fw's CCX
* emission saturates at ~1.3k reports/s, and with the tag continuous a
* received-report tag delta other than N is a dropped report. Both
* fields sit inside the checksummed span — re-checksum (idempotent). */
const uint64_t k = _tx_rpt_tag.fetch_add(1);
SET_TX_DESC_SPE_RPT_8822C(
out, k % static_cast<uint64_t>(_cfg.tx.report) == 0 ? 1 : 0);
SET_TX_DESC_SW_DEFINE_8822C(out, static_cast<uint16_t>(k & 0xff));
jaguar3::cal_txdesc_chksum_8822c(out);
}
/* Per-frame retry limit from cfg (DEVOURER_TX_RETRY_LIMIT, default 0) —
Expand Down
8 changes: 6 additions & 2 deletions src/jaguar3/RtlJaguar3Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,12 @@ class RtlJaguar3Device : public IRtlDevice {
std::atomic<int> _tx_pwr_override{-1};
std::atomic<int> _tx_pwr_offset_steps{0};
/* Rotating SW_DEFINE tag stamped when tx.report is on — the CCX report
* echoes its low byte, correlating reports to frames (src/TxReport.h). */
std::atomic<uint16_t> _tx_rpt_tag{0};
* echoes its low byte, correlating reports to frames (src/TxReport.h).
* 64-bit: the counter also gates the sampled SPE_RPT request (every Nth
* frame), and a narrow counter's wrap would jump the sampling phase for
* any N that doesn't divide it — off-modulo tag deltas masquerading as
* dropped reports at every seam. */
std::atomic<uint64_t> _tx_rpt_tag{0};
/* Per-packet TX-power banks (SetTxPacketPowerOffsetQdb / radiotap
* DBM_TX_POWER). _txpkt_banks is the allocation policy,
* mutated under _reg_mu; _txpkt_img mirrors its committed 0x1e70[31:16]
Expand Down
3 changes: 2 additions & 1 deletion tests/arq_e2e_delivery.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ CH=${CH:-36}
MAC1=${MAC1:-02:12:34:56:78:9a} # DUT responder identity = drone RA
TX_SA=${TX_SA:-02:aa:bb:cc:dd:01} # drone TA (unicast — the I/G footgun)
RETRY_LIMIT=${RETRY_LIMIT:-3} # field report used 3
DRONE_REPORT_N=${DRONE_REPORT_N:-1} # CCX sampling divisor (1 = every frame)
DRONE_RATE=${DRONE_RATE:-MCS3}
DRONE_PAYLOAD=${DRONE_PAYLOAD:-512} # >= 30 so the pctr stamp fits
DRONE_GAP_US=${DRONE_GAP_US:-1000} # ~1k fps video-sim
Expand Down Expand Up @@ -163,7 +164,7 @@ env DEVOURER_VID="$DRONE_VID" DEVOURER_PID="$DRONE_PID" DEVOURER_CHANNEL="$CH" \
DEVOURER_TX_QOS_DATA=1 DEVOURER_TX_RA="$MAC1" DEVOURER_TX_SA="$TX_SA" \
DEVOURER_TX_RATE="$DRONE_RATE" DEVOURER_TX_PAYLOAD_BYTES="$DRONE_PAYLOAD" \
DEVOURER_TX_GAP_US="$DRONE_GAP_US" \
DEVOURER_TX_REPORT=1 DEVOURER_TX_RETRY_LIMIT="$RETRY_LIMIT" \
DEVOURER_TX_REPORT="$DRONE_REPORT_N" DEVOURER_TX_RETRY_LIMIT="$RETRY_LIMIT" \
DEVOURER_DIS_CCA="$DRONE_DIS_CCA" \
DEVOURER_TX_PWR_OFFSET_QDB="$PWR_QDB" \
DEVOURER_LOG_LEVEL=warn DEVOURER_EVENTS=stdout \
Expand Down
59 changes: 45 additions & 14 deletions tests/txrpt_coverage_attrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from collections import Counter


def analyze(rundir):
def analyze(rundir, sample_n=1):
n = 0
submitted = 0
prev_tag = None
Expand Down Expand Up @@ -66,20 +66,41 @@ def analyze(rundir):
raise SystemExit(f"{rundir}: no tagged tx.report events "
f"(J1-format reports carry no tag)")
name = rundir.rstrip("/").split("/")[-1]
cov = 100.0 * n / max(1, submitted)
dur_s = (t_last - t_first) / 1000.0 if (t_first is not None and
t_last and t_last > t_first) else 0
rate = n / dur_s if dur_s else 0
unrep = sum(k * v for k, v in gaps.items())
small = sum(k * v for k, v in gaps.items() if k <= 2)
top = ", ".join(f"{k}x{v}" for k, v in sorted(gaps.items())[:8])
print(f"\n== {name}: submitted={submitted} reports={n} "
f"coverage={cov:.1f}%"
+ (f" achieved={rate:.0f} rpt/s" if rate else ""))
print(f" unreported={unrep} gap-hist [{top}"
f"{', ...' if len(gaps) > 8 else ''}] "
f"max={max(gaps) if gaps else 0} "
f"in-gaps<=2: {100.0 * small / max(1, unrep):.1f}%")
if sample_n > 1:
# Sampled run: only every Nth frame requested a report, so the raw
# tag deltas should be exact multiples of N. k*N = k-1 sampled
# reports dropped; a non-multiple delta is an anomaly.
# The request fires on k % N == 0, i.e. frame 0 first — ceil, not
# floor, or odd totals undercount the expectation by one.
expected = (submitted + sample_n - 1) // sample_n
lost = anomalies = 0
for d, c in gaps.items(): # keys are delta-1
delta = d + 1
if delta % sample_n == 0:
lost += (delta // sample_n - 1) * c
else:
anomalies += c
cov = 100.0 * n / max(1, expected)
print(f"\n== {name}: submitted={submitted} sample_n={sample_n} "
f"expected={expected} reports={n} sampled-coverage={cov:.1f}%"
+ (f" achieved={rate:.0f} rpt/s" if rate else ""))
print(f" sampled reports lost={lost} off-modulo anomalies="
f"{anomalies}")
else:
cov = 100.0 * n / max(1, submitted)
unrep = sum(k * v for k, v in gaps.items())
small = sum(k * v for k, v in gaps.items() if k <= 2)
top = ", ".join(f"{k}x{v}" for k, v in sorted(gaps.items())[:8])
print(f"\n== {name}: submitted={submitted} reports={n} "
f"coverage={cov:.1f}%"
+ (f" achieved={rate:.0f} rpt/s" if rate else ""))
print(f" unreported={unrep} gap-hist [{top}"
f"{', ...' if len(gaps) > 8 else ''}] "
f"max={max(gaps) if gaps else 0} "
f"in-gaps<=2: {100.0 * small / max(1, unrep):.1f}%")
if len(missed_vals) == 1:
(mv, _), = missed_vals.items()
print(f" missed field: constant {mv} on every report "
Expand All @@ -96,8 +117,18 @@ def analyze(rundir):


def main():
for rd in sys.argv[1:]:
analyze(rd)
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("runs", nargs="+")
ap.add_argument("--sample-n", type=int, default=1,
help="DEVOURER_TX_REPORT sampling divisor the run used: "
"reports are requested on every Nth frame, so "
"received-tag deltas should be exact multiples of N "
"(k*N = k-1 sampled reports dropped; a non-multiple "
"is an anomaly)")
a = ap.parse_args()
for rd in a.runs:
analyze(rd, a.sample_n)


if __name__ == "__main__":
Expand Down
Loading