diff --git a/docs/aggregation.md b/docs/aggregation.md index ab0e074..3498c37 100644 --- a/docs/aggregation.md +++ b/docs/aggregation.md @@ -192,10 +192,13 @@ numbers above came from. arms the MAC's autonomous ACK engine while monitor RX/injection continue unchanged: port identity (MACID/BSSID 0x610/0x618 = `mac`) + net_type (0x102 [1:0] = AP). No beacon machinery, no ADDBA session state, no CAM entry. -Which half of that pair is actually the gate is per-die: on the generations -the AP-mode work covered, net_type is; on the RTL8733B it is inert and the -engine matches MACID alone, so a disarm there must move the identity -(`src/AckResponder.h` retarget(), `src/AdapterCaps.h`). +Which half of that pair ends live response behavior is per-die. net_type +participates on the Jaguar generations covered by the AP-mode work, but a +reference RTL8812AU still answered on the old MACID after NoLink read back and +needed its pre-arm MACID restored (BSSID is restored too as defensive port +state, not as a claimed response gate). On RTL8733B net_type is wholly inert +and the engine matches MACID alone. See the per-die evidence in +`src/AdapterCaps.h` and the shared mechanics in `src/AckResponder.h`. With a responder armed, a peer TXing unicast QoS-Data (normal ack-policy) to `mac` runs a full hardware ARQ loop — SIFS-timed ACKs from the responder, diff --git a/docs/scheduled-mac.md b/docs/scheduled-mac.md index 1459186..ce09cf9 100644 --- a/docs/scheduled-mac.md +++ b/docs/scheduled-mac.md @@ -164,12 +164,21 @@ SW_DEFINE tag-echo gap counting. An off verdict also requires at least 90% of reported frames at the configured retry limit (`MIN_RETRY_PIN_RATE`); one pinned outlier cannot stand in for the claimed retry-pinned distribution. -When the configured responder is an RTL8733B (`0bda:f72b` or `0bda:b733`), -the harness adds a fourth **disarmed** phase: arm MAC1, start a timer only -after the backend has completed bring-up and arming, clear it in the same live -process, then solicit MAC1 and expect the off verdict. That phase is -deliberately skipped for the default Jaguar1 responder; its disarm semantics -require separate backend-specific evidence. +When the configured responder has a measured backend-owned hook — RTL8733B +(`0bda:f72b` / `0bda:b733`) or the reference RTL8812AU (`0bda:8812`) — the +harness adds a fourth **disarmed** phase: arm MAC1, start a timer only after +that backend has completed bring-up and arming, clear it in the same live +process, then solicit MAC1 and expect the off verdict. Other responders are +skipped rather than using a generic timer whose ordering against `Init()` is +undefined. + +The Jaguar1 disarm cell was measured on a reference `0bda:8812` responder +with a `0bda:c812` solicitor at channel 36/MCS3/retry limit 12. It reproduced +the gate-only failure and passed after restoring the captured pre-arm MACID; +`src/AdapterCaps.h` owns the exact counts and the same-address adversary. The +implementation also restores and readback-verifies BSSID as defensive +port-state cleanup; the ACK-rate result does not establish that BSSID affected +response behavior. TX sessions run `DEVOURER_TX_WITH_RX=thread`: CCX reports arrive on the C2H RX path, so J1/J2 TX-only sessions never see them (measured: J2 TX-only = 0 diff --git a/examples/rx/main.cpp b/examples/rx/main.cpp index 9216a2f..4879a9e 100644 --- a/examples/rx/main.cpp +++ b/examples/rx/main.cpp @@ -1447,9 +1447,9 @@ int main(int argc, char **argv) { .f("stage", "demo.create_device") .f("ms", ms_since_start()); devourer::emit_adapter_caps(*g_ev, rtlDevice); - /* RTL8733B-only measurement hook. Scheduling belongs to the concrete - * backend so the delay starts after its verified arm/bring-up rather than - * racing Init from a generic side thread. Refuse other generations: a green + /* Backend-scoped measurement hook. Scheduling belongs to each measured + * concrete backend so the delay starts after its arm/bring-up rather than + * racing Init from a generic side thread. Refuse unmeasured paths: a green * run that cleared a cold port before Init armed it is false evidence. */ if (const char *d = std::getenv("DEVOURER_ACK_DISARM_AFTER_MS")) { const char *responder = std::getenv("DEVOURER_ACK_RESPONDER"); @@ -1471,22 +1471,28 @@ int main(int argc, char **argv) { d); return 1; } + bool scheduled = false; +#if defined(DEVOURER_HAVE_JAGUAR1) + if (auto *jaguar1 = dynamic_cast(rtlDevice)) + scheduled = jaguar1->ScheduleAckResponderDisarmForTest( + static_cast(ms)); +#endif #if defined(DEVOURER_HAVE_8733B) - auto *rtl8733b = dynamic_cast(rtlDevice); - if (rtl8733b == nullptr) { - logger->error("DEVOURER_ACK_DISARM_AFTER_MS is RTL8733B-only: " - "refusing a race-prone measurement on {}", - devourer::generation_name( - rtlDevice->GetAdapterCaps().generation)); - return 1; + if (auto *rtl8733b = dynamic_cast(rtlDevice)) { + rtl8733b->ScheduleAckResponderDisarmForTest( + static_cast(ms)); + scheduled = true; } - rtl8733b->ScheduleAckResponderDisarmForTest( - static_cast(ms)); -#else - logger->error("DEVOURER_ACK_DISARM_AFTER_MS requires an RTL8733B-enabled " - "build"); - return 1; #endif + if (!scheduled) { + logger->error( + "DEVOURER_ACK_DISARM_AFTER_MS is supported only by RTL8733B " + "and the Jaguar1/CHIP_8812 path (measured on RTL8812AU); " + "refusing {}", + devourer::generation_name( + rtlDevice->GetAdapterCaps().generation)); + return 1; + } } } /* The BB-debug-port / queue-depth research helpers are Jaguar1-only, so diff --git a/src/AckResponder.h b/src/AckResponder.h index 35642e5..b5dcbee 100644 --- a/src/AckResponder.h +++ b/src/AckResponder.h @@ -9,12 +9,13 @@ * matches MACID while net_type reads NoLink, so MAC bring-up already permits * responses to the adapter's own address and SetAckResponder retargets that * match to the requested address. See the measured truth table in - * AdapterCaps.h and Rtl8733bDevice::ClearAckResponder. + * AdapterCaps.h and the RTL8733B/Jaguar1 clear implementations. * * This header carries that register recipe minus the beacon machinery. Which * half controls a live disarm is a measured per-die property: use the shared * gate-only clear only where net_type is sufficient; otherwise the backend - * must also move the identity off the responder address with retarget(). + * must also move the identity off the responder address with retarget() or an + * exact captured-identity restore. * * On the adapter combinations exercised by tests/ampdu_ba_check.sh, the SAME * gate also enables the hardware BlockAck responder. RTL8733B has its own @@ -99,13 +100,13 @@ inline bool enable(RtlAdapter &dev, const uint8_t mac[6]) noexcept { return false; } catch (...) { /* SetAckResponder is a bool contract. Its callers perform a verified - * rollback and report UNKNOWN state if transport reads remain unavailable. */ + * rollback and diagnose whichever register readbacks remain unavailable. */ return false; } } /* Shared gate-only clear for dies where net_type controls the responder. - * RTL8733B callers must additionally retarget MACID. */ + * RTL8733B and Jaguar1/CHIP_8812 callers must additionally retarget MACID. */ inline bool disable(RtlAdapter &dev) { const uint8_t nt = dev.rtw_read8(0x0102); return dev.rtw_write8(0x0102, static_cast(nt & ~0x03u)); @@ -113,7 +114,7 @@ inline bool disable(RtlAdapter &dev) { /* Point the ACK-match identity at another address WITHOUT touching the gate. * - * On the RTL8733B this is the ONLY thing that changes whether the port answers. + * On RTL8733B this is the ONLY thing that changes whether the port answers. * net_type is inert there — measured, three cells, single-shot ACK rate at * MCS3 with a Jaguar1 soliciting: * @@ -141,6 +142,11 @@ inline bool disable(RtlAdapter &dev) { * (src/jaguar1/HalModule.cpp, EepromManager.h) — and a radio being disarmed may * still be injecting. Zero would not remove the match either, only move it: * 00:00:00:00:00:00 has the I/G bit clear, so is_unicast() accepts it. + * Jaguar1/CHIP_8812 has a distinct measured failure with the same remedy: + * NoLink verifies after a gate-only clear, but the old MACID continues + * answering until the backend restores its captured pre-arm port identity. + * AdapterCaps.h owns that before/after evidence; the result is not generalized + * to other Jaguar1 dies. * * Gate untouched on purpose: this is the identity half, so a caller composes it * with disable() in whichever order its die needs, and no generation gets a @@ -176,11 +182,92 @@ inline bool disable_verified(RtlAdapter &dev) noexcept { } } +/* Exact port-0 identity snapshot/restore for a backend that must return more + * than MACID to its pre-arm state. Keep the packed register representation so + * no byte-order conversion can diverge between capture and restore. */ +struct PortIdentity { + uint32_t macid_lo = 0; + uint16_t macid_hi = 0; + uint32_t bssid_lo = 0; + uint16_t bssid_hi = 0; +}; + +inline bool snapshot_port_identity(RtlAdapter &dev, PortIdentity &out) noexcept { + try { + out = {.macid_lo = dev.rtw_read(0x0610), + .macid_hi = dev.rtw_read16(0x0614), + .bssid_lo = dev.rtw_read(0x0618), + .bssid_hi = dev.rtw_read16(0x061c)}; + return true; + } catch (...) { + return false; + } +} + +inline bool port_mac_is(const PortIdentity &identity, + const uint8_t mac[6]) noexcept { + return identity.macid_lo == macid_lo(mac) && + identity.macid_hi == macid_hi(mac); +} + +inline bool restore_port_identity(RtlAdapter &dev, + const PortIdentity &identity) noexcept { + /* Keep each operation exception-contained so all four halves are attempted + * even if a transport throws instead of returning false. */ + auto write32 = [&dev](uint16_t reg, uint32_t value) noexcept { + try { + return dev.rtw_write(reg, value); + } catch (...) { + return false; + } + }; + auto write16 = [&dev](uint16_t reg, uint16_t value) noexcept { + try { + return dev.rtw_write16(reg, value); + } catch (...) { + return false; + } + }; + const bool ml = write32(0x0610, identity.macid_lo); + const bool mh = write16(0x0614, identity.macid_hi); + const bool bl = write32(0x0618, identity.bssid_lo); + const bool bh = write16(0x061c, identity.bssid_hi); + return ml && mh && bl && bh; +} + +inline bool port_identity_is(RtlAdapter &dev, + const PortIdentity &identity) noexcept { + try { + return dev.rtw_read(0x0610) == identity.macid_lo && + dev.rtw_read16(0x0614) == identity.macid_hi && + dev.rtw_read(0x0618) == identity.bssid_lo && + dev.rtw_read16(0x061c) == identity.bssid_hi; + } catch (...) { + return false; + } +} + /* The MAC must be UNICAST: a station cannot ACK-target a group address, so an * arm on one can never fire. Lives here rather than in each backend because * the precondition is a property of the recipe, not of any one die. */ inline bool is_unicast(const uint8_t mac[6]) { return (mac[0] & 0x01u) == 0; } +/* A rollback target must be a usable station identity, not merely readable. + * An all-zero MAC has the I/G bit clear (so is_unicast accepts it), remains a + * matchable address, and also stops TX scheduling on the Jaguar1 8812 path. */ +inline bool has_safe_restore_mac(const PortIdentity &identity) noexcept { + return (identity.macid_lo != 0 || identity.macid_hi != 0) && + (identity.macid_lo & 0x01u) == 0; +} + +/* Retargeting can only move a responder off its armed address when the restore + * identity differs. The caller owns both values and must reject equality on a + * die where the gate clear alone was measured insufficient. */ +inline bool disarmable_by_retarget(const uint8_t responder[6], + const PortIdentity &restore) noexcept { + return !port_mac_is(restore, responder); +} + /* Did the requested arm recipe land? Reads back net_type and the RA the ACK * engine matches (MACID), composed exactly as enable() writes them — keeping * the register map in ONE file, so a change to enable() cannot silently diff --git a/src/AdapterCaps.h b/src/AdapterCaps.h index 50d8144..45bcc5d 100644 --- a/src/AdapterCaps.h +++ b/src/AdapterCaps.h @@ -180,9 +180,18 @@ struct AdapterCaps { * tests/ack_txreport_matrix.sh run with the 8733B as the responder). * * The `on`, `retarget`, and legacy `off` rows establish arming, retargeting, - * and a never-armed control. Only the RTL8733B-scoped `disarmed` row arms and - * clears within one process; it supports the RTL8733B live-disarm claim only - * and establishes nothing about disarm on other dies. + * and a never-armed control. A backend-owned `disarmed` row supports only + * the live-disarm claim for the responder used in that run. On the reference + * RTL8812AU, that cell found that the old gate-only clear left 1946/1946 + * soliciting reports ACKed. Restoring the captured pre-arm MACID produced + * 0/1052 ACKed with retries pinned at 12; the never-armed counterpart was + * 0/1033. A separate own-MAC adversary was 0/1074 never armed but 1120/1120 + * after arm then same-address clear, so an arm equal to the captured MACID + * is refused. The implementation also restores and readback-verifies BSSID + * as port-state hygiene; the ACK-rate result does not attribute the + * behavioral change to BSSID. The implementation covers the shared + * CHIP_8812 path, but its 1T1R RTL8811AU cut was not separately measured; + * 8814A/8821A and the HalMAC generations do not inherit the result. * * On the 8733B the net_type gate is INERT and the engine matches MACID * alone: at single-shot ACK rate a never-armed port answers on its own EFUSE diff --git a/src/IRtlDevice.h b/src/IRtlDevice.h index 179affd..b56a6c0 100644 --- a/src/IRtlDevice.h +++ b/src/IRtlDevice.h @@ -225,11 +225,11 @@ class IRtlDevice { * when unsupported or when arm/verification fails; false is not proof of * passive state, so implementations log if rollback cannot be verified. * Clear is a non-throwing best effort to return net_type to No Link — and, - * on a die whose engine does not consult net_type, to move the port identity - * off `mac` as well, which is the only thing that changes its behaviour - * there (see Rtl8733bDevice::disarm_ack_responder). Clear does not promise - * silence: a die that matches MACID alone answers for whatever address is - * left programmed, including the one MAC bring-up wrote. */ + * where clearing net_type does not end the measured response behavior, to + * move the port identity off `mac` as well (see the RTL8733B and + * Jaguar1/CHIP_8812 backends). Clear does not promise silence: a die that + * matches MACID alone answers for whatever address is left programmed, + * including the one MAC bring-up wrote. */ virtual bool SetAckResponder(const devourer::MacAddr &mac) { (void)mac; return false; @@ -426,7 +426,9 @@ class IRtlDevice { * process does NOT silence it (bench-bitten: a killed probe's beacon kept * airing and contaminated the next test's witness) — so any beaconing * session that ends without a device power-cycle must call this. Idempotent; - * returns false when no beacon was active. */ + * returns false when no beacon was active or shutdown could not be verified. + * A failed verified stop must be retried (or followed by hardware shutdown) + * before a shared port is reused. */ virtual bool StopBeacon() { return false; } /* Disable / restore the MAC carrier-sense gate that defers TX — both primary diff --git a/src/jaguar1/BeaconPort.h b/src/jaguar1/BeaconPort.h new file mode 100644 index 0000000..7600181 --- /dev/null +++ b/src/jaguar1/BeaconPort.h @@ -0,0 +1,66 @@ +/* Jaguar1 port-0 beacon control helpers. + * + * The beacon engine is autonomous once armed. Its logical port ownership may + * therefore be released only after all three hardware stop controls read back + * inactive, irrespective of the transport write return values. */ +#ifndef DEVOURER_JAGUAR1_BEACON_PORT_H +#define DEVOURER_JAGUAR1_BEACON_PORT_H + +#include + +#include "RtlAdapter.h" + +namespace devourer::jaguar1 { + +struct BeaconStopResult { + bool transfers_ok = false; + bool function_off = false; + bool tx_stopped = false; + bool no_link = false; + + bool verified() const noexcept { + return function_off && tx_stopped && no_link; + } +}; + +inline BeaconStopResult stop_port0_beacon_verified(RtlAdapter &dev) noexcept { + auto write8 = [&dev](uint16_t reg, uint8_t value) noexcept { + try { + return dev.rtw_write8(reg, value); + } catch (...) { + return false; + } + }; + auto clear8 = [&dev](uint16_t reg, uint8_t mask) noexcept { + try { + const uint8_t value = dev.rtw_read8(reg); + return dev.rtw_write8(reg, static_cast(value & ~mask)); + } catch (...) { + return false; + } + }; + auto bits_clear = [&dev](uint16_t reg, uint8_t mask) noexcept { + try { + return (dev.rtw_read8(reg) & mask) == 0; + } catch (...) { + return false; + } + }; + + /* Keep the operations independent: a failure stopping one control must not + * suppress the attempts to stop the other two. */ + const bool function_transfer = write8(0x0550, 0x10); + const bool tx_transfer = clear8(0x0422, 0x40); + const bool link_transfer = clear8(0x0102, 0x03); + + BeaconStopResult result; + result.transfers_ok = function_transfer && tx_transfer && link_transfer; + result.function_off = bits_clear(0x0550, 0x08); + result.tx_stopped = bits_clear(0x0422, 0x40); + result.no_link = bits_clear(0x0102, 0x03); + return result; +} + +} // namespace devourer::jaguar1 + +#endif // DEVOURER_JAGUAR1_BEACON_PORT_H diff --git a/src/jaguar1/RtlJaguarDevice.cpp b/src/jaguar1/RtlJaguarDevice.cpp index eca90ea..91d14c7 100644 --- a/src/jaguar1/RtlJaguarDevice.cpp +++ b/src/jaguar1/RtlJaguarDevice.cpp @@ -1,4 +1,5 @@ #include "RtlJaguarDevice.h" +#include "jaguar1/BeaconPort.h" #include "BeamformingSounder.h" #include "ChannelFreq.h" #include "EepromManager.h" @@ -24,6 +25,7 @@ #include #include #include +#include #include /* comma-joined 0xNN hex dump for the DVR_TRACE TX-buffer dumps (argument is @@ -58,6 +60,31 @@ static constexpr uint16_t RF_LNA_LOW_GAIN_3 = 0x58; static constexpr uint16_t rC_TxScale_8814 = 0x181C; static constexpr uint16_t rD_TxScale_8814 = 0x1A1C; +namespace { + +template class JaguarScopeExit { +public: + explicit JaguarScopeExit(Action action) : _action(std::move(action)) {} + JaguarScopeExit(const JaguarScopeExit &) = delete; + JaguarScopeExit &operator=(const JaguarScopeExit &) = delete; + ~JaguarScopeExit() noexcept { + if (_active) { + try { + _action(); + } catch (...) { + /* Preserve the original exception during initialization unwind. */ + } + } + } + void release() noexcept { _active = false; } + +private: + Action _action; + bool _active = true; +}; + +} // namespace + RtlJaguarDevice::RtlJaguarDevice(RtlAdapter device, Logger_t logger, devourer::DeviceConfig cfg) : _cfg{std::move(cfg)}, @@ -69,14 +96,31 @@ RtlJaguarDevice::RtlJaguarDevice(RtlAdapter device, Logger_t logger, _logger{logger} {} void RtlJaguarDevice::InitWrite(SelectedChannel channel) { + std::optional configured_arm_generation; + JaguarScopeExit rollback([&] { + if (!configured_arm_generation) + return; + std::lock_guard lock(_port0_mu); + if (!ack_arm_token_is_current(*configured_arm_generation)) { + _logger->info("Jaguar1: post-arm InitWrite rollback found its ACK arm " + "cleared or replaced; leaving the current port unchanged"); + return; + } + if (!disarm_ack_responder()) + _logger->error("Jaguar1: post-arm InitWrite rollback was not fully " + "verified; see the register-specific error above"); + }); StartWithMonitorMode(channel); SetMonitorChannel(channel); _logger->info("In Monitor Mode"); - if (_cfg.rx.ack_responder && - !SetAckResponder(*_cfg.rx.ack_responder)) /* DEVOURER_ACK_RESPONDER */ - throw std::runtime_error( - "Jaguar1: configured ACK responder could not be armed"); + if (_cfg.rx.ack_responder) { /* DEVOURER_ACK_RESPONDER */ + std::lock_guard lock(_port0_mu); + if (!SetAckResponder(*_cfg.rx.ack_responder)) + throw std::runtime_error( + "Jaguar1: configured ACK responder could not be armed"); + configured_arm_generation = _active_ack_arm_generation; + } /* Carrier-sense default: EDCCA + primary CCA enabled unless * DEVOURER_DIS_CCA. Always applied — the enable path is what programs @@ -110,6 +154,7 @@ void RtlJaguarDevice::InitWrite(SelectedChannel channel) { if (_cfg.tx.ampdu) SetAmpduMode(*_cfg.tx.ampdu); /* DEVOURER_TX_AMPDU_MODE */ + rollback.release(); } /* MP single-tone (CW carrier), Jaguar-1 path A. The RF writes are common to the @@ -493,6 +538,12 @@ bool RtlJaguarDevice::download_rsvd_beacon(const uint8_t *mpdu, bool RtlJaguarDevice::StartBeacon(const uint8_t *beacon, size_t len, int interval_tu) { + std::lock_guard lock(_port0_mu); + if (_port0_ack_claimed) { + _logger->error("beacon(J1): cannot claim port 0 while an ACK " + "responder is armed; clear the responder first"); + return false; + } /* Mirrors RtlJaguar2Device::StartBeacon on the pre-HalMAC registers, in the * VENDOR ORDER: port/beacon configuration first, reserved-page download * LAST. A download issued before the port is configured latches BCN_VALID @@ -504,6 +555,7 @@ bool RtlJaguarDevice::StartBeacon(const uint8_t *beacon, size_t len, if (rt > len) rt = 0; const uint8_t *mpdu = beacon + rt; size_t mpdu_len = len - rt; + _port0_beacon_claimed = true; /* Port identity: MAC (REG_MACID 0x0610) + BSSID (REG_BSSID 0x0618) from the * MPDU's addr2/addr3. */ if (mpdu_len >= 24) { @@ -581,7 +633,8 @@ bool RtlJaguarDevice::StartBeacon(const uint8_t *beacon, size_t len, } bool RtlJaguarDevice::UpdateBeaconPayload(const uint8_t *beacon, size_t len) { - if (_bcn_mpdu.empty()) { + std::lock_guard lock(_port0_mu); + if (!_port0_beacon_claimed || _bcn_mpdu.empty()) { _logger->error("beacon(J1): UpdateBeaconPayload without an active beacon"); return false; } @@ -600,22 +653,34 @@ bool RtlJaguarDevice::UpdateBeaconPayload(const uint8_t *beacon, size_t len) { } bool RtlJaguarDevice::StopBeacon() { - if (_bcn_mpdu.empty()) + std::lock_guard lock(_port0_mu); + if (!_port0_beacon_claimed) return false; /* EN_BCN_FUNCTION off (keep DIS_TSF_UDT), StopTxBeacon (0x422[6] clear — - * the ResumeTxBeacon inverse), net_type back to No Link. */ - _device.rtw_write8(0x0550 /* REG_BCN_CTRL */, 0x10); - _device.rtw_write8(0x0422, static_cast( - _device.rtw_read8(0x0422) & ~0x40u)); - uint8_t nt = _device.rtw_read8(0x0102); - _device.rtw_write8(0x0102, static_cast(nt & ~0x03u)); + * the ResumeTxBeacon inverse), net_type back to No Link. The engine runs + * autonomously, so retain ownership unless all three controls read back + * inactive; otherwise ACK setup could overwrite a beacon that still airs. */ + const auto stopped = + devourer::jaguar1::stop_port0_beacon_verified(_device); + if (!stopped.verified()) { + _logger->error( + "beacon(J1): stop not verified (EN_BCN_FUNCTION_off={}, " + "StopTxBeacon={}, net_type_NoLink={}); port 0 remains beacon-owned", + stopped.function_off, stopped.tx_stopped, stopped.no_link); + return false; + } + if (!stopped.transfers_ok) + _logger->warn("beacon(J1): a stop write reported a transport failure, " + "but all stop controls read back inactive"); _bcn_mpdu.clear(); _bcn_interval_tu = 0; + _port0_beacon_claimed = false; _logger->info("beacon(J1): stopped (EN_BCN off, StopTxBeacon, net_type->NoLink)"); return true; } int32_t RtlJaguarDevice::AdjustBeaconTiming(int32_t microseconds) { + std::lock_guard lock(_port0_mu); int nominal = _bcn_interval_tu; if (nominal <= 0) return 0; // no active beacon int delta_tu = (microseconds >= 0 ? microseconds + 512 : microseconds - 512) / 1024; @@ -631,6 +696,7 @@ int32_t RtlJaguarDevice::AdjustBeaconTiming(int32_t microseconds) { } int32_t RtlJaguarDevice::AdjustBeaconTimingFine(int32_t microseconds) { + std::lock_guard lock(_port0_mu); if (_bcn_interval_tu <= 0) return 0; // no active beacon /* The J2 fine steer on the same registers: beacon function off, shift the * port-0 TSF, back on (TBTT re-derives from the shifted TSF), then @@ -663,6 +729,7 @@ int32_t RtlJaguarDevice::AdjustBeaconTimingFine(int32_t microseconds) { } int32_t RtlJaguarDevice::PinBeaconTbtt(int32_t offset_us) { + std::lock_guard lock(_port0_mu); if (_bcn_interval_tu <= 0) return 0; // no active beacon const int64_t period_us = static_cast(_bcn_interval_tu) * 1024; const int64_t off = @@ -756,6 +823,7 @@ bool RtlJaguarDevice::send_packet(const uint8_t *packet, size_t length) { } bool RtlJaguarDevice::SetAckResponder(const devourer::MacAddr &mac) { + std::lock_guard lock(_port0_mu); if (!devourer::ack::is_unicast(mac.data())) { /* A station cannot ACK-target a group address, so this arm could never * fire. Refusing beats returning true for a responder that will read as @@ -768,17 +836,69 @@ bool RtlJaguarDevice::SetAckResponder(const devourer::MacAddr &mac) { "Jaguar1", mac.bytes[0]); return false; } + if (_port0_beacon_claimed) { + _logger->error("Jaguar1: ACK responder cannot be armed while the port-0 " + "beacon owns MACID/BSSID/net_type"); + return false; + } + if (_eepromManager->version_id.ICType == CHIP_8812) { + const bool had_restore_identity = _ack_restore_identity.has_value(); + if (!_ack_restore_identity) { + devourer::ack::PortIdentity identity; + if (!devourer::ack::snapshot_port_identity(_device, identity)) { + _logger->error("Jaguar1/CHIP_8812: ACK responder cannot be armed: " + "the current port identity could not be read"); + return false; + } + if (!devourer::ack::has_safe_restore_mac(identity)) { + _logger->error("Jaguar1/CHIP_8812: ACK responder cannot be armed: " + "the current MACID is not a safe rollback identity"); + return false; + } + _ack_restore_identity = identity; + } + if (!devourer::ack::disarmable_by_retarget( + mac.data(), *_ack_restore_identity)) { + _logger->error("Jaguar1/CHIP_8812: ACK responder cannot be armed to " + "the pre-arm MACID: this die keeps answering after " + "NoLink, so Clear could not move it off that address"); + if (!had_restore_identity) + _ack_restore_identity.reset(); + return false; + } + } + /* Claim before the first register mutation. A failed arm clears this only + * after its rollback verifies; otherwise beacon setup remains blocked and a + * later ClearAckResponder can retry the incomplete cleanup. Invalidate any + * prior successful-arm token before mutation so its delayed clear cannot act + * on a failed or partially applied replacement. */ + _port0_ack_claimed = true; + _active_ack_arm_generation.reset(); /* Hardware ACK responder (src/AckResponder.h) — same register recipe as * the HalMAC generations (0x610/0x618/0x102 are map-identical here). */ if (!devourer::ack::enable(_device, mac.data())) { - if (!devourer::ack::disable_verified(_device)) { + if (!disarm_ack_responder()) { _logger->error("Jaguar1: ACK responder arm failed and rollback did " - "not latch; hardware state is unknown"); + "not fully verify; see the register-specific error above"); + } else { + _logger->error("Jaguar1: ACK responder arm register write failed; " + "responder rollback was verified"); + } + return false; + } + if (_eepromManager->version_id.ICType == CHIP_8812 && + !devourer::ack::verify(_device, mac.data())) { + if (!disarm_ack_responder()) { + _logger->error("Jaguar1/CHIP_8812: ACK responder arm readback failed " + "and rollback was not fully verified"); } else { - _logger->error("Jaguar1: ACK responder arm register write failed"); + _logger->error("Jaguar1/CHIP_8812: ACK responder arm readback failed; " + "responder rollback was verified"); } return false; } + ++_ack_arm_generation; + _active_ack_arm_generation = _ack_arm_generation; _logger->info("Jaguar1: hardware ACK responder armed for " "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", mac.bytes[0], mac.bytes[1], mac.bytes[2], mac.bytes[3], @@ -786,12 +906,78 @@ bool RtlJaguarDevice::SetAckResponder(const devourer::MacAddr &mac) { return true; } -void RtlJaguarDevice::ClearAckResponder() { - if (!devourer::ack::disable_verified(_device)) { - _logger->error("Jaguar1: ACK responder disarm did not latch"); - return; +bool RtlJaguarDevice::disarm_ack_responder() { + std::lock_guard lock(_port0_mu); + + /* A clear without ACK ownership must not close net_type: port 0 may belong + * to a beacon, including a setup that failed after its first mutation. */ + if (!_port0_ack_claimed) { + _logger->info("Jaguar1: no configured ACK responder to disarm"); + return true; + } + + if (_eepromManager->version_id.ICType == CHIP_8812 && + !_ack_restore_identity) { + _logger->error("Jaguar1/CHIP_8812: ACK responder ownership has no " + "pre-arm identity; refusing an unverifiable clear"); + return false; + } + + const bool gate = devourer::ack::disable_verified(_device); + + /* The RTL8812AU bench result is the same safety failure as RTL8733B but not + * the same gate model: NoLink reads back successfully, yet the old responder + * MAC continues answering soliciting frames with SIFS ACKs. Restore the + * identity on CHIP_8812 (8812AU and its 1T1R 8811AU cut) while leaving the + * unmeasured 8814A/8821A clear path unchanged. */ + if (_eepromManager->version_id.ICType != CHIP_8812) { + if (!gate) { + _logger->error("Jaguar1: ACK responder disarm did not latch"); + return false; + } + _port0_ack_claimed = false; + _active_ack_arm_generation.reset(); + _logger->info("Jaguar1: hardware ACK responder disarmed " + "(net_type=NoLink)"); + return true; + } + + const bool transfer = devourer::ack::restore_port_identity( + _device, *_ack_restore_identity); + const bool identity = devourer::ack::port_identity_is( + _device, *_ack_restore_identity); + if (!gate) { + if (identity) { + _logger->error("Jaguar1/CHIP_8812: configured responder identity was " + "removed, but net_type did not read NoLink; pre-arm " + "port state was not fully restored"); + } else { + _logger->error("Jaguar1/CHIP_8812: ACK responder gate did not latch " + "closed and pre-arm MACID/BSSID was not restored"); + } + return false; + } + if (!identity) { + _logger->error("Jaguar1/CHIP_8812: pre-arm MACID/BSSID could not be " + "restored; the port may still answer for the responder " + "address"); + return false; } - _logger->info("Jaguar1: hardware ACK responder disarmed (net_type=NoLink)"); + if (!transfer) + _logger->warn("Jaguar1/CHIP_8812: port-identity restore reported a " + "transport failure, but MACID/BSSID readback confirms the " + "pre-arm values"); + _ack_restore_identity.reset(); + _port0_ack_claimed = false; + _active_ack_arm_generation.reset(); + _logger->info("Jaguar1/CHIP_8812: hardware ACK responder disarmed " + "(MACID/BSSID back to the pre-arm identity; " + "net_type=NoLink)"); + return true; +} + +void RtlJaguarDevice::ClearAckResponder() { + (void)disarm_ack_responder(); } void RtlJaguarDevice::SetCcaMode(bool disabled) { @@ -1366,13 +1552,31 @@ size_t RtlJaguarDevice::build_tx_block(const uint8_t *packet, size_t length, void RtlJaguarDevice::Init(Action_ParsedRadioPacket packetProcessor, SelectedChannel channel) { + std::optional configured_arm_generation; + JaguarScopeExit rollback([&] { + if (!configured_arm_generation) + return; + std::lock_guard lock(_port0_mu); + if (!ack_arm_token_is_current(*configured_arm_generation)) { + _logger->info("Jaguar1: post-arm Init rollback found its ACK arm cleared " + "or replaced; leaving the current port unchanged"); + return; + } + if (!disarm_ack_responder()) + _logger->error("Jaguar1: post-arm Init rollback was not fully verified; " + "see the register-specific error above"); + }); StartWithMonitorMode(channel); SetMonitorChannel(channel); - if (_cfg.rx.ack_responder && - !SetAckResponder(*_cfg.rx.ack_responder)) /* DEVOURER_ACK_RESPONDER */ - throw std::runtime_error( - "Jaguar1: configured ACK responder could not be armed"); + if (_cfg.rx.ack_responder) { /* DEVOURER_ACK_RESPONDER */ + std::lock_guard lock(_port0_mu); + if (!SetAckResponder(*_cfg.rx.ack_responder)) { + throw std::runtime_error( + "Jaguar1: configured ACK responder could not be armed"); + } + configured_arm_generation = _active_ack_arm_generation; + } /* Carrier-sense default: EDCCA + primary CCA enabled unless * DEVOURER_DIS_CCA. Always applied — the enable path is what programs @@ -1409,7 +1613,59 @@ void RtlJaguarDevice::Init(Action_ParsedRadioPacket packetProcessor, _eepromManager->version_id.ICType != CHIP_8814A) measure_idle_noise_floor(); + /* Measurement-only live disarm. The worker is created after every Init + * operation that can arm or reconfigure the responder, so zero ms means + * immediately after completed bring-up rather than before SetAckResponder. + * Its local lifetime also makes exceptional/normal RX-loop exit cancel and + * join an outstanding long-delay request. */ + std::jthread ack_disarm_thread; + std::optional ack_disarm_after_ms; + { + std::lock_guard lock(_port0_mu); + ack_disarm_after_ms = std::exchange(_ack_disarm_after_ms, std::nullopt); + } + if (ack_disarm_after_ms) { + const uint32_t delay_ms = *ack_disarm_after_ms; + if (!configured_arm_generation) { + _logger->error("DEVOURER_ACK_DISARM_AFTER_MS: no configured Jaguar1 " + "ACK arm to associate with the delayed clear"); + } else { + const uint64_t arm_generation = *configured_arm_generation; + ack_disarm_thread = std::jthread( + [this, delay_ms, arm_generation](std::stop_token stop) { + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(delay_ms); + while (!stop.stop_requested() && + std::chrono::steady_clock::now() < deadline) { + const auto left = deadline - std::chrono::steady_clock::now(); + const auto quantum = + std::chrono::duration_cast( + std::chrono::milliseconds(25)); + std::this_thread::sleep_for(left < quantum ? left : quantum); + } + if (stop.stop_requested()) + return; + std::lock_guard lock(_port0_mu); + if (stop.stop_requested()) + return; + if (!ack_arm_token_is_current(arm_generation)) { + _logger->info( + "DEVOURER_ACK_DISARM_AFTER_MS: scheduled Jaguar1/CHIP_8812 " + "ACK arm was cleared or replaced; leaving the current " + "responder unchanged"); + return; + } + _logger->info( + "DEVOURER_ACK_DISARM_AFTER_MS: disarming Jaguar1/CHIP_8812 " + "ACK responder {} ms after completed bring-up", + delay_ms); + (void)disarm_ack_responder(); + }); + } + } + StartRxLoop(std::move(packetProcessor)); + rollback.release(); } void RtlJaguarDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) { diff --git a/src/jaguar1/RtlJaguarDevice.h b/src/jaguar1/RtlJaguarDevice.h index 062d19d..3254375 100644 --- a/src/jaguar1/RtlJaguarDevice.h +++ b/src/jaguar1/RtlJaguarDevice.h @@ -7,10 +7,12 @@ #include #include #include +#include #include #include #include +#include "AckResponder.h" #include "logger.h" #include "BbDbgportReader.h" #include "LaCapture.h" @@ -69,6 +71,36 @@ class RtlJaguarDevice : public IRtlDevice { * divide it (2^32 is ~20 days at field frame rates). */ std::atomic _tx_ccx_ctr{0}; + /* rxdemo's live-disarm measurement request. Consumed by Init only after + * bring-up and a configured responder arm have completed. */ + std::optional _ack_disarm_after_ms; + std::optional _ack_restore_identity; + /* Port 0 backs both the hardware ACK responder and beacon engine on every + * Jaguar1 die. Track ACK ownership independently of CHIP_8812's restore + * snapshot: the other dies still use the same MACID/BSSID/net-type registers + * and must not overwrite a beacon (or have a stray clear disable one). */ + bool _port0_ack_claimed = false; + /* Successful-arm identity for delayed test clears and Init rollback. A + * replacement arm gets a new generation even when it uses the same MAC. The + * active token is invalidated before the replacement's first mutation and + * installed only after full success, so a failed/ambiguous replacement + * cannot inherit an older timer. Protected by _port0_mu. */ + uint64_t _ack_arm_generation = 0; + std::optional _active_ack_arm_generation; + /* The test disarm worker is concurrent with callers, and StartBeacon calls + * back into PinBeaconTbtt, so serialize the complete ownership checks and + * multi-register transactions with a recursive mutex. */ + std::recursive_mutex _port0_mu; + + /* Shared by ClearAckResponder and SetAckResponder rollback. On 8812 + * silicon a gate-only clear was measured to leave the old MACID answering, + * so this restores and verifies the captured pre-arm identity as well. */ + bool disarm_ack_responder(); + bool ack_arm_token_is_current(uint64_t expected) const noexcept { + return _port0_ack_claimed && _active_ack_arm_generation && + *_active_ack_arm_generation == expected; + } + /* 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/ @@ -256,6 +288,16 @@ class RtlJaguarDevice : public IRtlDevice { /* Hardware ACK responder (IRtlDevice contract; src/AckResponder.h). */ bool SetAckResponder(const devourer::MacAddr &mac) override; void ClearAckResponder() override; + /* Schedule rxdemo's hardware-only live-disarm cell. False leaves the + * request unset. Enabled for the shared CHIP_8812 implementation; measured + * on a reference RTL8812AU, not separately on its RTL8811AU cut. */ + bool ScheduleAckResponderDisarmForTest(uint32_t delay_ms) { + std::lock_guard lock(_port0_mu); + if (_eepromManager->version_id.ICType != CHIP_8812) + return false; + _ack_disarm_after_ms = delay_ms; + return true; + } /* Carrier-sense gate (IRtlDevice contract): MAC 0x520[14]/[15] like the * HalMAC generations, plus this family's BB EDCCA thresholds (0x8a4) — * parked at never-trigger by the BB table, programmed to the vendor @@ -402,6 +444,12 @@ class RtlJaguarDevice : public IRtlDevice { * steer rides the fine mechanism, which keeps the grid TSF-derived.) */ std::vector _bcn_mpdu; int _bcn_interval_tu = 0; + /* Set before StartBeacon's first port mutation and retained if setup fails, + * so ACK arming cannot mistake a partially configured beacon port for free. + * StopBeacon releases the claim only after every hardware stop control reads + * back inactive, even when no payload was retained. Mutual exclusion with + * _port0_ack_claimed applies to every Jaguar1 die. */ + bool _port0_beacon_claimed = false; /* Download `mpdu` to the reserved page at the BCNQ boundary via the vendor * rtl8812_download_rsvd_page bracket; polls BCN_VALID (0x20A[0]). Leaves * BCN_CTRL as it found it. */ diff --git a/tests/ack_responder_selftest.cpp b/tests/ack_responder_selftest.cpp index 3b5caaa..9c7f6a2 100644 --- a/tests/ack_responder_selftest.cpp +++ b/tests/ack_responder_selftest.cpp @@ -12,18 +12,21 @@ * and zero would not remove the match anyway — 00:00:00:00:00:00 has the I/G * bit clear, so is_unicast() accepts it. Both properties are pinned below. * - * This test does NOT cover silicon behaviour or the device-layer composition - * in Rtl8733bDevice (nothing here instantiates it). Those are covered on - * hardware by ack_txreport_matrix.sh's RTL8733B-only `disarmed` phase; its - * legacy `off` phase is still only a never-armed control. */ + * This test does NOT cover silicon behaviour or the full device-layer + * composition (nothing here instantiates a device). Those are covered on + * hardware by ack_txreport_matrix.sh's backend-owned `disarmed` phases; each + * legacy `off` phase is still only a never-armed control. It does cover the + * Jaguar1 port-0 stop recipe whose verified result releases beacon ownership. */ #include #include #include #include #include +#include #include "AckResponder.h" #include "RtlAdapter.h" +#include "jaguar1/BeaconPort.h" static int failures = 0; @@ -45,6 +48,8 @@ class FakeRegs final : public devourer::IRtlTransport { public: std::map mem; bool fail_writes = false; + bool fail_writes_after_store = false; + bool throw_writes = false; int write_calls = 0; bool is_usb() const override { return true; } @@ -58,8 +63,10 @@ class FakeRegs final : public devourer::IRtlTransport { } bool write8(uint16_t a, uint8_t v) override { ++write_calls; + if (throw_writes) throw std::runtime_error("injected write failure"); if (fail_writes) return false; mem[a] = v; + if (fail_writes_after_store) return false; return true; } bool write16(uint16_t a, uint16_t v) override { @@ -80,6 +87,8 @@ class FakeRegs final : public devourer::IRtlTransport { }; constexpr uint16_t kNetType = 0x0102; +constexpr uint16_t kBeaconCtrl = 0x0550; +constexpr uint16_t kTxBeaconCtrl = 0x0422; constexpr uint16_t kMacId = 0x0610; } // namespace @@ -91,6 +100,72 @@ int main() { * and what the disarm restores. Distinct from the responder MAC. */ const uint8_t own[6] = {0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01}; + { + /* Jaguar1 beacon ownership is released from the device layer only when + * all three autonomous-engine controls read back stopped. */ + auto regs = std::make_shared(); + regs->mem[kBeaconCtrl] = 0x1a; + regs->mem[kTxBeaconCtrl] = 0xd5; + regs->mem[kNetType] = 0xa7; + RtlAdapter dev(regs, logger); + const auto stopped = + devourer::jaguar1::stop_port0_beacon_verified(dev); + CHECK(stopped.transfers_ok); + CHECK(stopped.verified()); + CHECK((regs->read8(kBeaconCtrl) & 0x08u) == 0); + CHECK(regs->read8(kTxBeaconCtrl) == 0x95u); + CHECK(regs->read8(kNetType) == 0xa4u); + CHECK(regs->write_calls == 3); + } + { + /* A false-returning transport that stored nothing leaves every readback + * active. All three stop writes must still be attempted. */ + auto regs = std::make_shared(); + regs->mem[kBeaconCtrl] = 0x1a; + regs->mem[kTxBeaconCtrl] = 0x40; + regs->mem[kNetType] = 0x03; + regs->fail_writes = true; + RtlAdapter dev(regs, logger); + const auto stopped = + devourer::jaguar1::stop_port0_beacon_verified(dev); + CHECK(!stopped.transfers_ok); + CHECK(!stopped.verified()); + CHECK(!stopped.function_off); + CHECK(!stopped.tx_stopped); + CHECK(!stopped.no_link); + CHECK(regs->write_calls == 3); + } + { + /* Exceptions are isolated per control so one failed operation cannot skip + * either of the remaining stop attempts. */ + auto regs = std::make_shared(); + regs->mem[kBeaconCtrl] = 0x1a; + regs->mem[kTxBeaconCtrl] = 0x40; + regs->mem[kNetType] = 0x03; + regs->throw_writes = true; + RtlAdapter dev(regs, logger); + const auto stopped = + devourer::jaguar1::stop_port0_beacon_verified(dev); + CHECK(!stopped.transfers_ok); + CHECK(!stopped.verified()); + CHECK(regs->write_calls == 3); + } + { + /* Register readback is authoritative when a control transfer reports + * failure after the hardware accepted the write. */ + auto regs = std::make_shared(); + regs->mem[kBeaconCtrl] = 0x1a; + regs->mem[kTxBeaconCtrl] = 0x40; + regs->mem[kNetType] = 0x03; + regs->fail_writes_after_store = true; + RtlAdapter dev(regs, logger); + const auto stopped = + devourer::jaguar1::stop_port0_beacon_verified(dev); + CHECK(!stopped.transfers_ok); + CHECK(stopped.verified()); + CHECK(regs->write_calls == 3); + } + { /* Arm: MACID programmed, gate open, verify() agrees. The raw register * values are asserted independently of the readback helpers, so a @@ -132,7 +207,7 @@ int main() { CHECK(!devourer::ack::verify(dev, mac)); /* gate shut, so not armed */ } { - /* The composed disarm the RTL8733B uses: gate closed, then the identity + /* The composed MACID disarm RTL8733B uses: gate closed, then the identity * moved off the responder address and onto the adapter's own. */ auto regs = std::make_shared(); RtlAdapter dev(regs, logger); @@ -147,6 +222,31 @@ int main() { /* NOT zero — the T1 state that stops MAC TX scheduling. */ CHECK(regs->read32(kMacId) != 0 || regs->read16(kMacId + 4) != 0); } + { + /* Jaguar1/CHIP_8812 restores the exact pre-arm port identity, including + * BSSID, rather than assuming EFUSE is still what a prior beacon path + * left in the registers. This is the helper the device-layer clear calls. */ + auto regs = std::make_shared(); + RtlAdapter dev(regs, logger); + CHECK(dev.rtw_write(kMacId, devourer::ack::macid_lo(own))); + CHECK(dev.rtw_write16(kMacId + 4, devourer::ack::macid_hi(own))); + CHECK(dev.rtw_write(0x0618, 0x44332202u)); + CHECK(dev.rtw_write16(0x061c, 0x6655u)); + devourer::ack::PortIdentity baseline; + CHECK(devourer::ack::snapshot_port_identity(dev, baseline)); + CHECK(devourer::ack::has_safe_restore_mac(baseline)); + CHECK(devourer::ack::disarmable_by_retarget(mac, baseline)); + CHECK(!devourer::ack::disarmable_by_retarget(own, baseline)); + CHECK(devourer::ack::enable(dev, mac)); + CHECK(devourer::ack::disable_verified(dev)); + CHECK(devourer::ack::restore_port_identity(dev, baseline)); + CHECK(devourer::ack::port_identity_is(dev, baseline)); + CHECK(!devourer::ack::macid_is(dev, mac)); + CHECK(regs->read32(0x0618) == 0x44332202u); + CHECK(regs->read16(0x061c) == 0x6655u); + CHECK(dev.rtw_write16(0x061c, 0x6654u)); + CHECK(!devourer::ack::port_identity_is(dev, baseline)); + } { /* retarget() does not touch the gate: composition is the caller's, so a * die that wants the identity moved without reopening anything gets that. */ @@ -185,6 +285,32 @@ int main() { CHECK(regs->write_calls - before == 2); /* both halves attempted */ regs->fail_writes = false; } + { + /* A transport exception on one half is contained per operation, too; it + * must not suppress attempts to restore the remaining register halves. */ + auto regs = std::make_shared(); + RtlAdapter dev(regs, logger); + devourer::ack::PortIdentity baseline; + CHECK(devourer::ack::snapshot_port_identity(dev, baseline)); + regs->throw_writes = true; + const int before = regs->write_calls; + CHECK(!devourer::ack::restore_port_identity(dev, baseline)); + CHECK(regs->write_calls - before == 4); + regs->throw_writes = false; + } + { + /* The full port restore follows the same no-short-circuit rule: a failed + * first half must not prevent attempts of MACID-high or either BSSID half. */ + auto regs = std::make_shared(); + RtlAdapter dev(regs, logger); + devourer::ack::PortIdentity baseline; + CHECK(devourer::ack::snapshot_port_identity(dev, baseline)); + regs->fail_writes = true; + const int before = regs->write_calls; + CHECK(!devourer::ack::restore_port_identity(dev, baseline)); + CHECK(regs->write_calls - before == 4); + regs->fail_writes = false; + } { /* Zero is a UNICAST address by the I/G bit, which is exactly why it is not * a safe "no match" value: is_unicast() accepts it, so a peer could @@ -194,6 +320,14 @@ int main() { CHECK(devourer::ack::is_unicast(mac)); CHECK(devourer::ack::is_unicast(zero)); CHECK(!devourer::ack::is_unicast(group)); + const devourer::ack::PortIdentity zero_identity{}; + const devourer::ack::PortIdentity group_identity{ + .macid_lo = 0x00000057u, .macid_hi = 0}; + const devourer::ack::PortIdentity broadcast_identity{ + .macid_lo = 0xffffffffu, .macid_hi = 0xffffu}; + CHECK(!devourer::ack::has_safe_restore_mac(zero_identity)); + CHECK(!devourer::ack::has_safe_restore_mac(group_identity)); + CHECK(!devourer::ack::has_safe_restore_mac(broadcast_identity)); } if (failures) { diff --git a/tests/ack_txreport_matrix.sh b/tests/ack_txreport_matrix.sh index 5ced623..4a6df63 100644 --- a/tests/ack_txreport_matrix.sh +++ b/tests/ack_txreport_matrix.sh @@ -11,7 +11,8 @@ # 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. -# An RTL8733B responder adds a fourth, same-process `disarmed` phase. +# A responder with a measured backend-owned hook adds a fourth, same-process +# `disarmed` phase (currently RTL8733B and reference RTL8812AU USB IDs). # Every phase also measures report_coverage (reports / frames sent) and, on # HalMAC (J2/J3), SW_DEFINE tag-echo gaps + the firmware missed counter. # @@ -33,7 +34,7 @@ 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 -DISARM_MS=${DISARM_MS:-2000} # RTL8733B post-bring-up disarm delay +DISARM_MS=${DISARM_MS:-2000} # backend-anchored post-bring-up delay MIN_SENT=${MIN_SENT:-100} # reject dead/too-short transmitter cells MIN_REPORT_COVERAGE=${MIN_REPORT_COVERAGE:-0.80} # reject sparse CCX samples MIN_RETRY_PIN_RATE=${MIN_RETRY_PIN_RATE:-0.90} # OFF must be broadly pinned @@ -130,10 +131,11 @@ run_phase() { # $1 cell $2 phase $3 tx vid $4 tx pid $5 RA mac $6 responder mac 'async ring of .* URBs submitted' "$tag responder RX readiness" if [ -n "$disarm_ms" ]; then wait_for_log "$resp_pid" "$OUT/resp_$tag.err" \ - 'hardware ACK responder disarmed \(MACID back to' \ + 'hardware ACK responder disarmed \(MACID(/BSSID)? back to' \ "$tag responder disarm" fi - if grep -qE 'ACK responder (gate did not latch|MACID could not|.*UNKNOWN)' \ + if grep -qE \ + 'ACK responder gate did not latch|net_type did not read NoLink|MACID(/BSSID)? (could not be|was not) restored|rollback was not fully verified|hardware state UNKNOWN' \ "$OUT/resp_$tag.err"; then echo "ABORT: $tag responder state was not verified" >&2 tail -8 "$OUT/resp_$tag.err" >&2 @@ -195,16 +197,16 @@ for cell in $CELLS; do run_phase "$name" on "$vid" "$pid" "$MAC1" "$MAC1" on run_phase "$name" retarget "$vid" "$pid" "$MAC2" "$MAC2" on run_phase "$name" off "$vid" "$pid" "$MAC1" "" off - # The RTL8733B-only cell that measures a DISARM rather than a never-armed + # The backend-scoped cell that measures a DISARM rather than a never-armed # chip: arm on MAC1, start the timer after verified bring-up, disarm, then - # solicit MAC1. The hook is intentionally not generic: Jaguar1/2/3 have no - # init-wide synchronization, and their disarm semantics need separate cells. + # solicit MAC1. Each accepted backend owns that sequencing; unmeasured + # generations are skipped rather than racing a generic timer against Init. case "${RESP_VID,,}:${RESP_PID,,}" in - 0x0bda:0xf72b|0x0bda:0xb733) + 0x0bda:0xf72b|0x0bda:0xb733|0x0bda:0x8812) run_phase "$name" disarmed "$vid" "$pid" "$MAC1" "$MAC1" off "$DISARM_MS" ;; *) - echo "-- ${name}_disarmed: SKIP (RTL8733B responder required)" + echo "-- ${name}_disarmed: SKIP (no measured backend-owned hook)" ;; esac done