Skip to content

rtl8733b: harden ARQ and qualify BlockAck response - #407

Open
snokvist wants to merge 2 commits into
OpenIPC:masterfrom
snokvist:feat/rtl8733b-arq-hardening
Open

rtl8733b: harden ARQ and qualify BlockAck response#407
snokvist wants to merge 2 commits into
OpenIPC:masterfrom
snokvist:feat/rtl8733b-arq-hardening

Conversation

@snokvist

@snokvist snokvist commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #406 after its merge.

Summary

  • Make ACK-responder arm/disarm fail closed: check register writes, close the gate before retargeting, verify RTL8733B rollback/disarm, and keep teardown progressing after a failed safety clear.
  • Propagate construction-time responder refusal on Jaguar1/2/3 instead of green-initializing a passive monitor.
  • Apply tx.ack_timeout_us to both RTL8733B response-window registers (REG_ACKTO and REG_ACKTO_CCK) and verify both readbacks.
  • Replace sampled/ambient-prone retry evidence with run-unique payload-counter witness ledgers; add an independent RTL8733B soliciting-TX ACK-closure test.
  • Add a CCX-independent three-radio BlockAck test. The witness proves A-MPDU structure and retry-copy closure, and directly decodes addressed 0x94 compressed BlockAck frames, including the bitmap.
  • Keep CCX wording scoped: no H2C/MACID registration transport is added here, and the absence of reports is not assigned to firmware.

Hardware evidence

Current rig:

  • TX: 0bda:b812, RTL8822B/88x2BU, Jaguar2
  • RTL8733B DUT: 0bda:f72b
  • independent witness: 0bda:8812, RTL8812AU/Jaguar1

BlockAck, ch36/MCS3/retry 12 (tests/rtl8733b_blockack_onair.sh):

  • active but unarmed RTL8733B: 1,605 unique aggregated payloads, 12.720 witnessed copies/payload, paggr=0.665, max burst 9, 0 matching BlockAcks;
  • armed RTL8733B: 128,702 unique aggregated payloads, 1.001 copies/payload, paggr=1.0, max burst 9, 14,402 addressed compressed BlockAcks; all 14,402 carried nonzero bitmaps;
  • the harness checks responder and witness liveness before and after TX and aborts unless the preparatory disarm and shutdown are verified.

Other reruns:

  • RTL8733B soliciting normal-ACK TX, MCS3: responder on/off = 1.016/12.879 copies per observed payload, with 99.8%/100% witness coverage.
  • Retry dose response 0/3/12/0/12: 1.00/4.00/12.34/1.00/12.34 copies per frame; verdict true.
  • J2 construction-time group responder: nonzero initialization failure with the unicast diagnostic; valid unicast still arms. After valid-process exit, a J1 probe measured 0/1,311 ACK successes and retries pinned at 12, so no teardown leak was inferred.
  • 11M CCK current J2-witness rerun separated on/off at 1.004/12.113 copies, but its one-shot on arm captured 55.7%, below the harness's fixed 60% coverage floor; this rerun is supportive, not counted as a new formal pass. The documented qualifying CCK cell used the prior J3 witness.

Checks

  • Default build succeeds.
  • ctest --test-dir build --output-on-failure: 54/54 pass.
  • bash -n and shellcheck pass for all three RTL8733B on-air ARQ harnesses.
  • git diff --check passes.

Scope

This qualifies RTL8733B as a BlockAck responder only on the measured Jaguar2-to-RTL8733B combination. RTL8733B A-MPDU TX remains unported and unmeasured. CCX/tx.report remains unimplemented pending a separately tested H2C queue plus MEDIA_STATUS_RPT registration path.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Harden RTL8733B ARQ and qualify BlockAck responses

🐞 Bug fix 🧪 Tests ✨ Enhancement 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Fail closed on responder and timeout register failures across RTL8733B and Jaguar backends.
• Add air-side tests for retry closure, normal ACKs, and compressed BlockAck responses.
• Document measured RTL8733B ARQ capabilities, evidence, and remaining CCX/A-MPDU TX limits.
Diagram

graph TD
  CFG["Device Config"] --> BACK["Chip Backends"] --> ACK["ACK Helper"] --> REGS["ARQ Registers"]
  HARNESS["Air Test Harness"] --> TX["Jaguar2 TX"] --> DUT["RTL8733B DUT"] --> WIT["Passive Witness"]
  TX --> WIT
  DUT --> BACK
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use transmitter CCX reports
  • ➕ Would provide transmitter-local retry and delivery accounting.
  • ➕ Could simplify the external witness setup.
  • ➖ RTL8733B lacks the required H2C and MACID registration path.
  • ➖ Per-frame CCX accounting is not a valid A-MPDU retry oracle.
2. Capture all received frame bodies
  • ➕ Provides complete packet contents for offline analysis.
  • ➕ Avoids adding a dedicated BlockAck event.
  • ➖ Produces excessive logs under saturated A-MPDU traffic.
  • ➖ Adds processing pressure that could perturb witness measurements.

Recommendation: Keep the independent payload-counter witness and narrow BlockAck telemetry. This approach avoids unsupported or invalid CCX assumptions, limits observer overhead, and directly proves aggregate structure, retry closure, addressing, subtype, and bitmap content.

Files changed (18) +876 / -232

Enhancement (2) +36 / -5
main.cppEmit narrow BlockAck control-frame telemetry +25/-0

Emit narrow BlockAck control-frame telemetry

• Adds DEVOURER_RX_CONTROL-gated decoding for 0x94 BlockAck frames. Emits CRC, rate, control, sequence, addresses, and the first 64 bitmap bits without streaming all packet bodies.

examples/rx/main.cpp

Halmac8733bMac.hReturn structured dual-register ACK timeout state +11/-5

Return structured dual-register ACK timeout state

• Introduces AckTimeoutState to carry CCK and non-CCK readbacks plus combined write status. Updates the HALMAC interface contract accordingly.

src/rtl8733b/Halmac8733bMac.h

Bug fix (6) +169 / -76
AckResponder.hMake shared ACK responder transitions fail closed +46/-19

Make shared ACK responder transitions fail closed

• Returns register-write status, closes the responder gate before retargeting, and best-effort rolls back failed arm operations. Adds disarm-state readback helpers for safety verification.

src/AckResponder.h

RtlJaguarDevice.cppPropagate Jaguar1 responder arm and disarm failures +16/-6

Propagate Jaguar1 responder arm and disarm failures

• Fails initialization when a configured responder cannot arm and reports runtime arm failures. Throws on unsuccessful disarm instead of silently continuing.

src/jaguar1/RtlJaguarDevice.cpp

RtlJaguar2Device.cppPropagate Jaguar2 responder arm and disarm failures +16/-6

Propagate Jaguar2 responder arm and disarm failures

• Checks shared responder writes during runtime and construction-time setup. Initialization now refuses a passive monitor when responder configuration fails.

src/jaguar2/RtlJaguar2Device.cpp

RtlJaguar3Device.cppPropagate Jaguar3 responder arm and disarm failures +16/-6

Propagate Jaguar3 responder arm and disarm failures

• Checks responder operations under the existing register mutex and aborts initialization on arm refusal. Disarm failures now surface as exceptions.

src/jaguar3/RtlJaguar3Device.cpp

Halmac8733bMac.cppVerify both ACK timeout families and teardown disarm +17/-7

Verify both ACK timeout families and teardown disarm

• Programs and reads back the OFDM/HT and CCK ACK timeout registers with transport status. Teardown verifies responder disarm while allowing remaining shutdown steps to proceed after safety-clear errors.

src/rtl8733b/Halmac8733bMac.cpp

Rtl8733bDevice.cppFail RTL8733B bring-up and responder operations safely +58/-32

Fail RTL8733B bring-up and responder operations safely

• Aborts bring-up unless both ACK timeout registers match the configured value. Adds verified responder rollback, verified disarm, unknown-state reporting, and accurately scoped CCX warnings.

src/rtl8733b/Rtl8733bDevice.cpp

Tests (3) +527 / -36
rtl8733b_arq_tx_onair.shAdd independent RTL8733B normal-ACK closure test +149/-0

Add independent RTL8733B normal-ACK closure test

• Uses separate DUT, responder, and witness radios to compare payload-copy counts with the responder on and off. Enforces submission, coverage, retry-collapse, and retry-limit thresholds.

tests/rtl8733b_arq_tx_onair.sh

rtl8733b_blockack_onair.shAdd three-radio RTL8733B BlockAck qualification +306/-0

Add three-radio RTL8733B BlockAck qualification

• Proves A-MPDU structure, retry-copy closure, and addressed compressed BlockAck frames with nonzero bitmaps. Verifies responder and witness liveness, establishes a passive control state, and requires graceful verified shutdown.

tests/rtl8733b_blockack_onair.sh

rtl8733b_retry_limit_onair.shReplace sampled retry evidence with payload ledgers +72/-36

Replace sampled retry evidence with payload ledgers

• Uses run-unique source addresses and per-copy rx.seq events instead of quantized rx.txhit sampling. Requires a valid multi-level dose response, complete runs, and scoped process cleanup.

tests/rtl8733b_retry_limit_onair.sh

Documentation (7) +144 / -115
aggregation.mdScope and document RTL8733B BlockAck responder evidence +11/-2

Scope and document RTL8733B BlockAck responder evidence

• Qualifies BlockAck behavior only for measured adapter combinations and records the three-radio RTL8733B evidence. Explicitly keeps RTL8733B A-MPDU transmission out of scope.

docs/aggregation.md

logging.mdDocument the rx.blockack telemetry event +1/-0

Document the rx.blockack telemetry event

• Adds the control-frame event schema, including addressing, sequence, control, and bitmap fields. Explains its narrow use in BlockAck qualification.

docs/logging.md

rtl8733b.mdRefresh RTL8733B ARQ capabilities and limitations +30/-34

Refresh RTL8733B ARQ capabilities and limitations

• Documents dual-family ACK timeout validation, normal-ACK TX closure, BlockAck response evidence, and corrected retry measurements. Reframes missing CCX reports as unresolved pending H2C/MEDIA_STATUS_RPT support.

docs/rtl8733b.md

scheduled-mac.mdAdd witness-based RTL8733B ARQ results +17/-10

Add witness-based RTL8733B ARQ results

• Adds soliciting-TX, retry-dose, and BlockAck responder measurements outside the CCX matrix. Updates responder counts and scopes conclusions to the tested hardware sample.

docs/scheduled-mac.md

AdapterCaps.hUpdate RTL8733B ARQ capability evidence +2/-2

Update RTL8733B ARQ capability evidence

• Refreshes responder and retry-limit measurements in capability documentation comments to match the new witness-ledger results.

src/AdapterCaps.h

DeviceConfig.hClarify RTL8733B timeout and CCX contracts +12/-8

Clarify RTL8733B timeout and CCX contracts

• Documents that RTL8733B applies ACK timeout to CCK and non-CCK registers. Also scopes tx.report as unsupported while H2C/MACID registration is absent.

src/DeviceConfig.h

CLAUDE.mdRecord hardened RTL8733B ARQ qualification +71/-59

Record hardened RTL8733B ARQ qualification

• Captures the revised timeout, normal-ACK, retry-limit, and BlockAck bench evidence and its single-sample limits. Corrects prior CCX root-cause claims and documents the missing registration path.

src/rtl8733b/CLAUDE.md

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. cleanup skips responder teardown 📘 Rule violation ☼ Reliability
Description
The soliciting-TX ARQ harness runs the responder-ON phase first and ends it with SIGKILL,
preventing Halmac8733bMac::stop() from explicitly disarming the persistent hardware ACK gate
before the OFF phase. The supposedly responder-off measurement can therefore still receive ACKs from
an armed radio, collapsing retries, violating orderly device teardown, and invalidating the A/B
result.
Code

tests/rtl8733b_arq_tx_onair.sh[R34-36]

+cleanup() {
+  sudo pkill -9 -f "^$ESC_BUILD/rxdemo" 2>/dev/null
+  sudo pkill -9 -f "^$ESC_BUILD/txdemo" 2>/dev/null
Evidence
Rule 11 requires the device to stop before its USB resources are released, but the harness kills the
armed rxdemo process with SIGKILL and then begins the OFF phase without resetting or disarming
the adapter. The RTL8733B implementation documents that the net_type ACK gate survives ordinary
MAC shutdown and must be explicitly cleared, while Jaguar1's hardware deinitialization writes live
in Stop(); the sibling BlockAck harness accordingly documents that SIGKILL cannot establish a
passive responder and uses graceful SIGINT plus a verified passive-reset session.

CLAUDE.md: Preserve Correct libusb Ownership and Teardown Order
tests/rtl8733b_arq_tx_onair.sh[34-39]
tests/rtl8733b_arq_tx_onair.sh[93-95]
tests/rtl8733b_arq_tx_onair.sh[130-131]
src/rtl8733b/Halmac8733bMac.cpp[811-826]
tests/rtl8733b_blockack_onair.sh[79-114]
tests/rtl8733b_arq_tx_onair.sh[55-75]
tests/rtl8733b_blockack_onair.sh[91-114]
src/jaguar1/RtlJaguarDevice.cpp[1998-2009]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The ARQ harness kills the ON-phase `rxdemo` process with `SIGKILL`, bypassing the RTL8733B stop path that clears the persistent hardware ACK-responder gate. The subsequent OFF measurement may therefore still receive ACKs from the previously armed adapter.

## Issue Context
The harness runs the armed responder phase before the OFF control, and its common cleanup directly kills the responder without resetting or disarming the adapter. Use graceful receiver shutdown and verify responder disarm before accepting the OFF control, following the established BlockAck harness pattern; reserve `SIGKILL` for fallback cleanup after the measurement has already been rejected. The BlockAck harness documents that killing the process cannot guarantee passive state because `SIGKILL` cannot execute device stop logic, and Jaguar1 performs the relevant hardware deinitialization from `Stop()`.

## Fix Focus Areas
- tests/rtl8733b_arq_tx_onair.sh[34-39]
- tests/rtl8733b_arq_tx_onair.sh[55-95]
- tests/rtl8733b_arq_tx_onair.sh[130-131]
- tests/rtl8733b_blockack_onair.sh[91-114]
- src/jaguar1/RtlJaguarDevice.cpp[1998-2009]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Failed arm may stay active 🐞 Bug ☼ Reliability
Description
ack::enable() returns failure after only a best-effort, unverified gate clear, while Jaguar1/2/3
callers trust that result and perform no verified rollback. If the failed arm write landed and the
cleanup write did not, the API or constructor reports refusal while the radio remains an active
ACK/BlockAck responder.
Code

src/AckResponder.h[R64-67]

+  /* A failed status does not prove the gate write had no side effect. Make a
+   * best-effort close before reporting failure; RTL8733B additionally reads
+   * this back in its caller. */
+  (void)dev.rtw_write8(0x0102, static_cast<uint8_t>(nt & ~0x03u));
Evidence
The helper explicitly acknowledges that a failed write status may still have changed hardware, but
its fallback clear is not read back. RTL8733B compensates with disable_verified(), whereas all
three Jaguar callers immediately return false.

src/AckResponder.h[43-68]
src/AckResponder.h[81-86]
src/jaguar1/RtlJaguarDevice.cpp[773-775]
src/jaguar2/RtlJaguar2Device.cpp[383-385]
src/jaguar3/RtlJaguar3Device.cpp[2173-2177]
src/rtl8733b/Rtl8733bDevice.cpp[844-874]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The shared ACK-responder arm helper can return `false` without proving that the net-type gate is closed. Jaguar callers do not add the verified rollback used by RTL8733B, so a failed arm can leave the radio responding after the caller was told it is passive.

## Issue Context
The transport write result is not sufficient to establish register state, as the new code itself notes. This applies both when the initial gate-close write fails and when the final arm write reports failure after potentially taking effect.

## Fix Focus Areas
- src/AckResponder.h[43-68]
- src/jaguar1/RtlJaguarDevice.cpp[773-775]
- src/jaguar2/RtlJaguar2Device.cpp[383-385]
- src/jaguar3/RtlJaguar3Device.cpp[2174-2176]
- src/rtl8733b/Rtl8733bDevice.cpp[844-874]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Final disarm goes unchecked 🐞 Bug ☼ Reliability
Description
After each BlockAck phase, the harness only checks that receiver processes exit; it never checks the
responder's stop-time disarm warning as it does during the preparatory passive reset. Since
Halmac8733bMac::stop() catches a failed verified clear and continues shutdown, the test can pass
while leaving the responder gate active and while its claimed shutdown verification actually failed.
Code

tests/rtl8733b_blockack_onair.sh[R190-195]

+  if ! stop_receivers; then
+    echo "ABORT: phase=$phase receivers did not stop gracefully" >&2
+    cleanup
+    exit 1
+  fi
+  cleanup
Evidence
The preparatory reset explicitly greps for both stop-time failure messages, but run_phase() omits
that check. The MAC stop implementation catches these failures and only logs warnings, so
stop_receivers() can return success despite an unverified active gate.

tests/rtl8733b_blockack_onair.sh[79-114]
tests/rtl8733b_blockack_onair.sh[179-195]
src/rtl8733b/Halmac8733bMac.cpp[811-826]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The BlockAck harness validates the preparatory passive reset but does not inspect stop-time disarm results after the OFF and ON phases. A process can exit gracefully even when the verified responder clear failed, allowing a passing test to leave active radio state behind.

## Issue Context
`Halmac8733bMac::stop()` intentionally logs and suppresses disarm failures so the rest of teardown continues. Process exit therefore cannot serve as proof that the gate was cleared.

## Fix Focus Areas
- tests/rtl8733b_blockack_onair.sh[79-114]
- tests/rtl8733b_blockack_onair.sh[179-195]
- src/rtl8733b/Halmac8733bMac.cpp[811-826]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. BlockAck properties are disconnected 🐞 Bug ≡ Correctness
Description
The harness counts compressed BlockAcks and nonzero bitmaps independently, then requires only that
each total exceed 100. A mix of compressed frames with zero bitmaps and other addressed frames with
nonzero bitmap bytes can therefore pass without proving that any compressed BlockAck carries a
nonzero bitmap.
Code

tests/rtl8733b_blockack_onair.sh[R251-253]

+nonzero_bitmaps = sum(ba.get("bitmap", "0" * 16) != "0" * 16
+                      for ba in blockacks)
+compressed = sum(bool(int(ba.get("ctrl", 0)) & 0x4) for ba in blockacks)
Evidence
The analysis computes nonzero_bitmaps over all addressed BlockAcks and compressed over all
addressed BlockAcks as separate sums, and the verdict independently compares both totals to 100.
There is no per-frame conjunction between the compressed bit and a nonzero bitmap.

tests/rtl8733b_blockack_onair.sh[215-224]
tests/rtl8733b_blockack_onair.sh[251-263]
tests/rtl8733b_blockack_onair.sh[286-302]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The verdict combines separate compressed-frame and nonzero-bitmap totals, so the two required properties may come from different BlockAck events. This weakens the claimed proof that addressed compressed BlockAcks contain meaningful bitmaps.

## Issue Context
Each decoded event already contains both `ctrl` and `bitmap`, allowing the witness analysis to count their conjunction directly and optionally reject unexpected BlockAck variants.

## Fix Focus Areas
- tests/rtl8733b_blockack_onair.sh[215-224]
- tests/rtl8733b_blockack_onair.sh[251-263]
- tests/rtl8733b_blockack_onair.sh[286-302]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +34 to +36
cleanup() {
sudo pkill -9 -f "^$ESC_BUILD/rxdemo" 2>/dev/null
sudo pkill -9 -f "^$ESC_BUILD/txdemo" 2>/dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. cleanup skips responder teardown 📘 Rule violation ☼ Reliability

The soliciting-TX ARQ harness runs the responder-ON phase first and ends it with SIGKILL,
preventing Halmac8733bMac::stop() from explicitly disarming the persistent hardware ACK gate
before the OFF phase. The supposedly responder-off measurement can therefore still receive ACKs from
an armed radio, collapsing retries, violating orderly device teardown, and invalidating the A/B
result.
Agent Prompt
## Issue description
The ARQ harness kills the ON-phase `rxdemo` process with `SIGKILL`, bypassing the RTL8733B stop path that clears the persistent hardware ACK-responder gate. The subsequent OFF measurement may therefore still receive ACKs from the previously armed adapter.

## Issue Context
The harness runs the armed responder phase before the OFF control, and its common cleanup directly kills the responder without resetting or disarming the adapter. Use graceful receiver shutdown and verify responder disarm before accepting the OFF control, following the established BlockAck harness pattern; reserve `SIGKILL` for fallback cleanup after the measurement has already been rejected. The BlockAck harness documents that killing the process cannot guarantee passive state because `SIGKILL` cannot execute device stop logic, and Jaguar1 performs the relevant hardware deinitialization from `Stop()`.

## Fix Focus Areas
- tests/rtl8733b_arq_tx_onair.sh[34-39]
- tests/rtl8733b_arq_tx_onair.sh[55-95]
- tests/rtl8733b_arq_tx_onair.sh[130-131]
- tests/rtl8733b_blockack_onair.sh[91-114]
- src/jaguar1/RtlJaguarDevice.cpp[1998-2009]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/AckResponder.h
Comment on lines +64 to +67
/* A failed status does not prove the gate write had no side effect. Make a
* best-effort close before reporting failure; RTL8733B additionally reads
* this back in its caller. */
(void)dev.rtw_write8(0x0102, static_cast<uint8_t>(nt & ~0x03u));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Failed arm may stay active 🐞 Bug ☼ Reliability

ack::enable() returns failure after only a best-effort, unverified gate clear, while Jaguar1/2/3
callers trust that result and perform no verified rollback. If the failed arm write landed and the
cleanup write did not, the API or constructor reports refusal while the radio remains an active
ACK/BlockAck responder.
Agent Prompt
## Issue description
The shared ACK-responder arm helper can return `false` without proving that the net-type gate is closed. Jaguar callers do not add the verified rollback used by RTL8733B, so a failed arm can leave the radio responding after the caller was told it is passive.

## Issue Context
The transport write result is not sufficient to establish register state, as the new code itself notes. This applies both when the initial gate-close write fails and when the final arm write reports failure after potentially taking effect.

## Fix Focus Areas
- src/AckResponder.h[43-68]
- src/jaguar1/RtlJaguarDevice.cpp[773-775]
- src/jaguar2/RtlJaguar2Device.cpp[383-385]
- src/jaguar3/RtlJaguar3Device.cpp[2174-2176]
- src/rtl8733b/Rtl8733bDevice.cpp[844-874]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +190 to +195
if ! stop_receivers; then
echo "ABORT: phase=$phase receivers did not stop gracefully" >&2
cleanup
exit 1
fi
cleanup

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Final disarm goes unchecked 🐞 Bug ☼ Reliability

After each BlockAck phase, the harness only checks that receiver processes exit; it never checks the
responder's stop-time disarm warning as it does during the preparatory passive reset. Since
Halmac8733bMac::stop() catches a failed verified clear and continues shutdown, the test can pass
while leaving the responder gate active and while its claimed shutdown verification actually failed.
Agent Prompt
## Issue description
The BlockAck harness validates the preparatory passive reset but does not inspect stop-time disarm results after the OFF and ON phases. A process can exit gracefully even when the verified responder clear failed, allowing a passing test to leave active radio state behind.

## Issue Context
`Halmac8733bMac::stop()` intentionally logs and suppresses disarm failures so the rest of teardown continues. Process exit therefore cannot serve as proof that the gate was cleared.

## Fix Focus Areas
- tests/rtl8733b_blockack_onair.sh[79-114]
- tests/rtl8733b_blockack_onair.sh[179-195]
- src/rtl8733b/Halmac8733bMac.cpp[811-826]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +251 to +253
nonzero_bitmaps = sum(ba.get("bitmap", "0" * 16) != "0" * 16
for ba in blockacks)
compressed = sum(bool(int(ba.get("ctrl", 0)) & 0x4) for ba in blockacks)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Blockack properties are disconnected 🐞 Bug ≡ Correctness

The harness counts compressed BlockAcks and nonzero bitmaps independently, then requires only that
each total exceed 100. A mix of compressed frames with zero bitmaps and other addressed frames with
nonzero bitmap bytes can therefore pass without proving that any compressed BlockAck carries a
nonzero bitmap.
Agent Prompt
## Issue description
The verdict combines separate compressed-frame and nonzero-bitmap totals, so the two required properties may come from different BlockAck events. This weakens the claimed proof that addressed compressed BlockAcks contain meaningful bitmaps.

## Issue Context
Each decoded event already contains both `ctrl` and `bitmap`, allowing the witness analysis to count their conjunction directly and optionally reject unexpected BlockAck variants.

## Fix Focus Areas
- tests/rtl8733b_blockack_onair.sh[215-224]
- tests/rtl8733b_blockack_onair.sh[251-263]
- tests/rtl8733b_blockack_onair.sh[286-302]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the ARQ hardening + BlockAck qualification. The goal is right — no silently dead responder, a verified ACK window — but the hardening contradicts its own stated contracts in three places (throw-on-write-status vs readback-is-authority, fail-passive with a stale-responder hole, a bool error contract the read path bypasses), and the three on-air harnesses have already drifted apart from each other inside this one PR. Details inline; one finding is outside the diff:

examples/chanmig/main.cpp:576 — the chanmig drone discards SetAckResponder's bool. With this PR's new deliberate fail-passive paths (enable() now closes the gate and returns false on a failed write), a single transient control-transfer failure at drone bring-up leaves the radio passive by design while chanmig proceeds anyway. The ground side's hardware-ARQ frames all retry to the limit against a silent responder and the operator debugs the RF link — the exact silent-dead-responder scenario the 8733B comments say this hardening exists to prevent, unhandled at the one in-tree runtime call site.

}
_logger->info(
"RTL8733B: ACK window {} us (REG_ACKTO 0x640 + CCK 0x639, verified)",
ackto_want);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bring_up_to_phy throws on !ackto.writes_ok even when both ACK-window readbacks equal the wanted value — contradicting this PR's own readback-is-the-authority rule (AckResponder.h's disable_verified comment calls a failed-status-but-landed write realistic). A transient control-transfer status error becomes a hard bring-up abort: set_ack_timeout_us returns writes_ok=false with non_cck==cck==wanted, and a session with verified-correct register state refuses to start.

The same warn→throw escalation also applies to pure monitor-RX bring-up, where the ACK window doesn't matter — and the claim that REG_ACKTO_CCK 0x639 always latches rests on one f72b unit. Pre-PR behavior was warn-and-continue. Suggest: throw only when the readback disagrees; treat write status as diagnostic.


void RtlJaguarDevice::ClearAckResponder() {
devourer::ack::disable(_device);
if (!devourer::ack::disable(_device)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClearAckResponder now throws std::runtime_error from a void, teardown-shaped API on a raw write status — on all four backends (also RtlJaguar2Device.cpp:394, RtlJaguar3Device.cpp:2185, Rtl8733bDevice.cpp:912) — while this same PR treats the identical failure as swallow-and-warn in Halmac8733bMac::stop() because "teardown is best-effort after disconnect".

A consumer disarming the responder during shutdown after a USB error/unplug (the common reason the 0x0102 write fails) now gets a throw; in a destructor or unwind path that's std::terminate. And per AckResponder.h's own comment, a failed write status doesn't prove the write missed — so the throw can fire on a successfully closed gate.

Comment thread src/AckResponder.h
/* Close the gate before changing identity. Besides avoiding a transient
* responder for a half-written MAC during retargeting, this makes every
* failed identity write leave the radio passive. */
if (!dev.rtw_write8(0x0102, static_cast<uint8_t>(nt & ~0x03u)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail-passive has a hole on re-arm: if enable()'s first write (the gate close, rtw_write8(0x0102, nt & ~0x03)) fails during a retarget, it returns false having changed nothing — gate still open, MACID still the old MAC. The radio keeps SIFS-ACKing MAC A while SetAckResponder(B) reports failure, and the comment claims every failure leaves the radio passive. On Jaguar2 (no teardown power-down) the stale responder answers until re-enumeration.

Only Rtl8733bDevice attempts disable_verified rollback on enable()==false; the three Jaguar callers just log "not armed". Either roll back in enable() itself or in all four callers.

Comment thread src/AckResponder.h
((uint32_t)mac[3] << 24));
dev.rtw_write16(0x061c, (uint16_t)(mac[4] | (mac[5] << 8)));
inline bool enable(RtlAdapter &dev, const uint8_t mac[6]) {
const uint8_t nt = dev.rtw_read8(0x0102);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bool error contract only covers writes. enable()/disable()/is_disabled() all begin with rtw_read8(0x0102), which throws std::ios_base::failure on a failed transfer (UsbTransport.h:167) — read failures bypass the return-false/rollback/log paths entirely. A timed-out 0x0102 read inside ack::enable propagates uncaught out of SetAckResponder (and out of Init/InitWrite as a different exception type than the intended runtime_error).

Likewise (void)disable(dev) inside disable_verified is not actually best-effort — its internal read can throw before is_disabled runs — and is_disabled's "a control transfer may report failure" comment is unreachable for reads: they never report, they throw.

on, off = rows["on"], rows["off"]
on_c = on["copies_per_observed_frame"]
off_c = off["copies_per_observed_frame"]
ok = (on["coverage"] >= 0.60 and off["coverage"] >= 0.60 and

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verdict band is arithmetically unsatisfiable at the guard's own floor: the guard admits RETRY_LIMIT 3..63, but the verdict requires both off_c <= 1.15*(limit+1) and off_c >= 5*on_c. At RETRY_LIMIT=3, perfect hardware gives on_c >= 1.0 (copies per observed frame is ≥1 by construction), so the verdict demands off_c >= 5.0 while the band caps it at 4.6 — contradiction, exit 1 on every healthy run (and it's near-unsatisfiable at 4). The sibling rtl8733b_blockack_onair.sh floors RETRY_LIMIT at 5 for exactly this reason — floor this guard the same way (or derive the floor from the band arithmetic).

tail -1 "$RESULTS"
}

run_phase on 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The responder-ON phase runs before the OFF control arm and teardown is pkill -9, which skips Stop() — Jaguar1's responder disarm lives only in Stop()/the destructor, and its own comment says the chip "stays in ACT with its RF front end live" after the owner dies. So the 8812AU's hardware responder (net_type=AP + MACID in silicon) can stay armed into the OFF arm: if the orphaned MAC still ACKs RESP_MAC, off_c collapses toward 1 and the run FAILs on healthy hardware. The sibling blockack script added force_responder_passive() and runs off-before-on for exactly this SIGKILL hazard; this script needs both.

sudo env DEVOURER_VID="$DUT_VID" DEVOURER_PID="$DUT_PID" \
DEVOURER_CHANNEL="$CH" DEVOURER_TX_QOS_DATA=1 \
DEVOURER_TX_RA="$RA" DEVOURER_TX_SA="$TX_SA" \
DEVOURER_TX_RATE="$RATE" DEVOURER_TX_PAYLOAD_BYTES=200 \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Airings are counted with grep -c on rx.seq with no CRC filter, while both sibling harnesses in this PR discard crc-flagged copies — and the documented witness for these cells (RTL8812CU, Jaguar3) keeps FCS-error frames unconditionally (ACRC32|AICV set in monitor bring-up). A marginal-RF arm delivering crc-error duplicates inflates airings_per_frame past the 1.15*expected ceiling and FAILs the dose-response on healthy hardware (or masks a real shortfall at the floor). The three harnesses silently measure different quantities while src/rtl8733b/CLAUDE.md describes them as one method — filter crc here like the siblings do.

exit 2
fi

ESC_BUILD=$(printf '%s' "$BUILD" | sed 's/[][\.*^$/]/\\&/g')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESC_BUILD's sed class [][\.*^$/] escapes sed/BRE metachars but not the ERE metachars + ? ( ) { } | that pkill -f matches with. A BUILD path containing any of them (e.g. /home/user/devourer-g++/build: the + survives unescaped, anchored pattern never matches — verified with grep -E) makes cleanup()/stop_receivers() silently kill nothing, stderr discarded — the previous arm's receiver keeps the adapter claimed into the next arm, which is the cross-arm contamination this harness rewrite exists to prevent. Same construct in tests/rtl8733b_arq_tx_onair.sh:33 and tests/rtl8733b_retry_limit_onair.sh; either escape the ERE set too or match on pkill -f -x-safe fixed strings.

Comment thread src/rtl8733b/CLAUDE.md
re-armed on a **different** MAC, 1728/1728 again (the address is arbitrary, not
baked in); disarmed, 0/1723 successful reports with retries pinned at 12.

The other direction is now measured independently rather than inferred.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historical/changelog framing in current-state docs: "is now measured independently rather than inferred" (here), "BlockAck response is now separately measured" / "The replacement tests/rtl8733b_blockack_onair.sh" (:337, :341), "The corrected harness uses..." (:359), and docs/rtl8733b.md:399 "avoiding the old sampled/quantized rx.txhit readout" — each references a deleted past state a future reader must reconstruct from git. Repo convention is current-state only (git is the changelog): "is measured independently by tests/..." carries the same facts without the narration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants