diff --git a/CLAUDE.md b/CLAUDE.md index 0aa8ea89..a14a95a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. It holds **cross-cutting** facts only. Deep subtree facts live in nested `CLAUDE.md` files, auto-loaded when working there: -`src/{jaguar1,jaguar2,jaguar3,kestrel}/` for per-generation registers, +`src/{jaguar1,jaguar2,jaguar3,kestrel,rtl8733b}/` for per-generation registers, descriptors and per-chip mechanisms; `src/hopset/` for keyed FHSS and the adaptive hopset; `src/chanmig/` for channel migration. Add new facts to the narrowest file that covers them. @@ -15,13 +15,13 @@ its adversarial counterpart in the same breath. ## What this is -Userspace re-implementation of Realtek's USB Wi-Fi drivers (11ac RTL88xx and -11ax RTL8852 families) — speaks to the chip directly via libusb instead of a -kernel module. Static library `devourer` (CMake target) + example executables -under `examples/` (`rxdemo` and `txdemo` are the canonical RX/TX demos). Used -by the OpenIPC project for long-range video links. +Userspace re-implementation of Realtek's USB Wi-Fi drivers (11n RTL873x, 11ac +RTL88xx and 11ax RTL8852 families) — speaks to the chip directly via libusb +instead of a kernel module. Static library `devourer` (CMake target) + example +executables under `examples/` (`rxdemo` and `txdemo` are the canonical RX/TX +demos). Used by the OpenIPC project for long-range video links. -Four chip generations, each behind its own self-contained HAL, dispatched at +Five hardware backends, each behind its own self-contained HAL, dispatched at construction from the `SYS_CFG2` chip-id (Kestrel: PID-first): - **Jaguar1** (`src/jaguar1/`): RTL8812AU (2T2R reference), RTL8811AU (1T1R @@ -53,12 +53,24 @@ construction from the `SYS_CFG2` chip-id (Kestrel: PID-first): out at 80 MHz (the 6G+160 TX-enable path is un-ported). TX power is a fixed BB dBm (`DEVOURER_TX_PWR`, whole dBm here). The 8852A-family (RTL8832AU) is deliberately excluded. Quirks: `docs/8852c-quirks.md`. +- **RTL8733B** (`src/rtl8733b/`): the 802.11n generation — RTL8731BU/RTL8733BU + (chip-id `0x16`), a HALMAC 87xx part, not a Jaguar variant: its own power, + firmware, MAC, descriptor and PHY paths. 1T1R, and the advertised surface is + only what an independent witness decoded — legacy OFDM + HT MCS0-7, BCC, + 20/40 MHz on 2.4/5 GHz, plus long-preamble CCK on 2.4 GHz at 20 MHz. + Everything the backend has not ported (TSF/beacons, hardware ACK, A-MPDU, + FastRetune, the runtime TX-power levers) falls through to `IRtlDevice`'s + not-ported defaults rather than being faked, so read the base class before + assuming a cross-generation feature below applies here. SGI, LDPC, STBC, VHT + and HE are refused outright. Scope, one-unit validation record and the + deferred matrix: `docs/rtl8733b.md`. Naming traps: **RTL8821AU is Jaguar1** (not Jaguar2, despite the Jaguar2 RTL8821C's similar name); RTL8822**B**U (Jaguar2) ≠ RTL8822**C**U (Jaguar3); the TP-Link TX50UH is RTL8832**C**U (8852C-family) despite lab lore calling it -8832AU, while the TX20U **Nano** is RTL8852BU. Full chip / bench-throughput -table: README **Supported hardware**. +8832AU, while the TX20U **Nano** is RTL8852BU; RTL8733B**U** is USB and +supported, RTL8733B**S** is the SDIO sibling and has no transport here. Full +chip / bench-throughput table: README **Supported hardware**. **PCIe** (`DEVOURER_PCIE=ON`, Linux-only, default OFF): the RTL8821CE — the PCIe sibling of the 8821CU — rides the same Jaguar2 HAL through a vfio-pci @@ -92,8 +104,9 @@ before running `tools/extract_*.py` or the hardware-testing kernel cells. Per-chip options, all default ON: `DEVOURER_JAGUAR1`, `DEVOURER_8814` (requires JAGUAR1), `DEVOURER_JAGUAR2_8822B`, `DEVOURER_JAGUAR2_8821C`, -`DEVOURER_JAGUAR3_8822C`, `DEVOURER_JAGUAR3_8822E`, `DEVOURER_KESTREL_8852B`, -`DEVOURER_KESTREL_8852C`. `DEVOURER_PCIE` (default OFF, Linux-only, requires +`DEVOURER_JAGUAR3_8822C`, `DEVOURER_JAGUAR3_8822E`, `DEVOURER_8733B`, +`DEVOURER_KESTREL_8852B`, `DEVOURER_KESTREL_8852C`. +`DEVOURER_PCIE` (default OFF, Linux-only, requires JAGUAR2_8821C) adds the vfio-pci transport + `pcieprobe`; OFF builds are byte-identical to before it existed. Turning groups off drops their firmware blobs + PHY tables (an 8812AU-only `rxdemo` is ~1.6 MB vs ~6.3 MB all-on — the @@ -286,11 +299,11 @@ Behavioural traps the per-field docs can't carry: `0xAA:0xBB[:0xCC]@` cycles masks on a timer for mobility/MRC measurements (`docs/measuring-spatial-diversity.md`, `tests/mrc_mobility.py`). `DEVOURER_RX_ALLPATHS=1` emits per-chain - RSSI/SNR/EVM as a separate `rx.path` event on every generation (Kestrel - parses its halbb physts path pages; C/D nonzero only on the 8814AU — the - other dies are ≤2 RX chains). Windowed per-antenna means ride - `GetActiveRxPaths()` / the `adapter.rxpaths` event (rssi/snr/evm per - chain). + RSSI/SNR/EVM as a separate `rx.path` event on the four Jaguar/Kestrel + generations (Kestrel parses its halbb physts path pages; C/D nonzero only on + the 8814AU — the other dies are ≤2 RX chains; the 1T1R RTL8733B does not + emit it). Windowed per-antenna means ride `GetActiveRxPaths()` / the + `adapter.rxpaths` event (rssi/snr/evm per chain). - `DEVOURER_RX_KEEP_CORRUPTED=1` is the entry point for the fused-FEC salvage layer (`docs/fused-fec.md`), and stays opt-in for a reason: a body with a corrupt tail is the worst-case input for an IP-stack consumer that didn't @@ -300,8 +313,8 @@ Behavioural traps the per-field docs can't carry: that loss is pre-FCS sync/AGC, upstream of the equalizer. They target in-band spurs on otherwise decodable frames (`docs/pseudo-preamble-puncturing.md`). -- `DEVOURER_DIS_CCA=1` (every generation, runtime `SetCcaMode` — the - interface method is pure virtual so no generation can silently no-op it) +- `DEVOURER_DIS_CCA=1` (runtime `SetCcaMode` — the interface method is pure + virtual so no backend can silently no-op it) disables the MAC carrier-sense gate — **both** primary CCA (`0x520[14]`) and EDCCA (`[15]`). The default is carrier-sense + EDCCA **enabled** on Jaguar1/2/3; on Jaguar1 the enable is real work — its BB table parks the @@ -316,13 +329,20 @@ Behavioural traps the per-field docs can't carry: `DEVOURER_DIS_CCA=0` forces standard carrier-sense back. On Kestrel the 8852C runs the same enabled default (measured: full-rate TX, 2.4x flood deferral); the 8852B TX bring-up still clears the gates and WARNS pending - its measurement arm (`tests/kestrel_cca_default_check.sh`). Does NOT apply + its measurement arm (`tests/kestrel_cca_default_check.sh`). On the RTL8733B + the disable is **not ported** — the HALMAC 87xx carrier-sense gate has not + been located and measured there, so `SetCcaMode(true)` throws (loudly, but + without tearing the session down) while `SetCcaMode(false)`, the state its + MAC bring-up already leaves programmed, succeeds as a no-op; setting the + config knob warns at `InitWrite` and airs with carrier-sense. Does NOT apply the vendor BB CCA-off writes (they deafen the RX). RX-decode side is a separate null (`tests/dis_cca_onair.sh`). -**Runtime TX power** — the adaptive-link power lever, three knobs on every -generation: `SetTxPowerOffsetQdb` (relative, shape-preserving), -`SetTxPowerIndexOverride` (flat absolute), `SetTxPowerRateDiffs` (replace the +**Runtime TX power** — the adaptive-link power lever, three knobs on the four +Jaguar/Kestrel generations (the RTL8733B ports none of them: it runs a fixed +safe closed-loop TSSI target): `SetTxPowerOffsetQdb` (relative, +shape-preserving), `SetTxPowerIndexOverride` (flat absolute), +`SetTxPowerRateDiffs` (replace the calibrated per-rate shape). The contract — how they compose, the MCS7-anchor semantics, family step sizes, the write-only-family `hw_readback=false` shadow, and Kestrel's software send-time fold — is documented at the @@ -359,9 +379,10 @@ temporal layer and injects each at its ladder's rate ## Frequency hopping `IRtlDevice::FastRetune(channel)` — lean intra-band, same-bandwidth retune on -every generation (RF channel switch only, write-only from a compose cache); -falls back to full `SetMonitorChannel` on a band change. FHSS-grade: ~0.5–2.5 ms -per hop depending on chip. On the Jaguar2 dies (8822B, 8821C) and Jaguar3 +the four Jaguar/Kestrel generations (RF channel switch only, write-only from a +compose cache); falls back to full `SetMonitorChannel` on a band change, and to +the full path entirely on the RTL8733B, which has no fast path yet. +FHSS-grade: ~0.5–2.5 ms per hop depending on chip. On the Jaguar2 dies (8822B, 8821C) and Jaguar3 (8822C, 8822E), `DEVOURER_FASTRETUNE_FW=1` hands the hop to the chip firmware instead (H2C 0x1D, fire-and-confirm-later): ~1.4 ms dead air on the 8822B (a tie on-air on the 8822C/8822E, but ~3× cheaper host-side), and `=2` extends it @@ -410,8 +431,9 @@ validation matrix and the near-field bench note: `src/chanmig/CLAUDE.md`. ## Hardware time, beacons, AP mode `ReadTsf()` reads the 64-bit MAC TSF and every received frame carries the -MAC-latched `tsfl` RX timestamp — on all generations, µs-grade -(`docs/time-distribution.md`, measured vs NTP/PTP: `docs/timing-accuracy.md`). +MAC-latched `tsfl` RX timestamp — on the four Jaguar/Kestrel generations, +µs-grade (`docs/time-distribution.md`, measured vs NTP/PTP: +`docs/timing-accuracy.md`). `StartBeacon` loads a beacon into the MAC's reserved page and the chip auto-transmits at each TBTT with the live TSF stamped at the TX instant — the sub-µs downlink. The chip beacons **autonomously**, so `StopBeacon()` is what @@ -430,7 +452,8 @@ also the seed of AP mode — a real Linux station associates, open or WPA2-PSK (`docs/ap-mode.md`); the multi-cell architecture it enables is `docs/multi-ap-cellular.md`, and the measured scheduler contracts (submit→air guard time, dynamic beacon grants, ACK/TxReport, per-UE RX attribution) are -`docs/scheduled-mac.md`. +`docs/scheduled-mac.md`. None of this section is ported on the RTL8733B — +`ReadTsf` returns 0 and `StartBeacon` returns false there. ## Aggregation, hardware ACK, TX reports @@ -483,8 +506,12 @@ with TX still in flight. USB PID. `CreateRtlDevice` returns an `IRtlDevice` (`Init` = bring-up + RX loop; `InitWrite` = TX bring-up; `StartRxLoop` = blocking RX worker on an already-up chip, enabling TX+RX on one handle; `send_packet`) and constructs -`RtlJaguarDevice` / `RtlJaguar2Device` / `RtlJaguar3Device` per generation. -`Rtl8812aDevice` is a deprecated alias of `RtlJaguarDevice`. +`RtlJaguarDevice` / `RtlJaguar2Device` / `RtlJaguar3Device` / `RtlKestrelDevice` +/ `Rtl8733bDevice` per backend. `Rtl8812aDevice` is a deprecated alias of +`RtlJaguarDevice`. Optional device methods are **virtual with not-ported +defaults**, not pure virtual — a backend that hasn't ported a feature inherits +`false`/`0`/a full-path fallback rather than a fake. Check the override list in +the backend's header before believing a cross-generation claim. Generation-agnostic core in `src/` (always compiled; depends on no HAL): @@ -507,13 +534,16 @@ Generation-agnostic core in `src/` (always compiled; depends on no HAL): (`UeRxAttribution`: per-transmitter windowed RX statistics keyed by 802.11 TA); the device RX loops are untouched. -Per-generation HALs are self-contained under `src/jaguar1/`, `src/jaguar2/`, -`src/jaguar3/`, `src/kestrel/`; each subtree's `CLAUDE.md` maps its files, -strategy seams and chip-specific mechanisms. +Per-backend HALs are self-contained under `src/jaguar1/`, `src/jaguar2/`, +`src/jaguar3/`, `src/kestrel/`, `src/rtl8733b/`; each subtree's `CLAUDE.md` +maps its files, strategy seams and chip-specific mechanisms. -`hal/` holds vendor headers and tables. The per-chip PHY/limit tables and the -8821C firmware blob are **generated** by `tools/extract_*.py` — edit the -generators, never the output files. +`hal/` holds vendor headers and tables. The per-chip PHY/limit tables and most +firmware blobs are **generated** by `tools/extract_*.py` (check `tools/` for +which — the set has grown) — edit the generators, never the output files. The +newer extractors (`extract_8733b_*.py`) pin per-source and per-array SHA-256 +hashes and carry a `--check` mode that reproduces the checked-in output +byte-for-byte; prefer that shape when adding one. ## Hardware gotchas @@ -562,7 +592,11 @@ generators, never the output files. `DEVOURER_RX_URB_BYTES`); raise both together or MTK hosts go silent, and never let an aggregate exceed the URB size or the parse walk breaks. Kestrel is exempt (8852C RXAGG LEN_TH ~20 KB needs its 32 KB ring) and - unvalidated on MTK hosts. + unvalidated on MTK hosts. The RTL8733B is the worked example of the failure: + its vendor-default 20 KiB aggregate was observed being split by xHCI across + 16 KiB completions, tail and body landing separately. It now caps the device + at 12 KiB and floors its URB at the same constant, tied together by a + `static_assert` — raising one alone reintroduces the straddle. ## TX path @@ -587,5 +621,16 @@ moves. its USB2 round-trip is too long for a blocking send to saturate the link); Jaguar2/Jaguar3 send synchronously (`bulk_send_sync_ep` → `tx_sync` — their USB3 round-trip saturates on one blocking thread, and sync gives the HalMAC -bring-up a clean per-send NAK backoff). Don't unify the two onto one mode — -either direction regresses throughput or bring-up safety. +bring-up a clean per-send NAK backoff); the RTL8733B is synchronous too, so +every submission has a bounded result and no buffer outlives the `send_packet` +call. Don't unify the modes — either direction regresses throughput or +bring-up safety. + +**Nothing reads a register per frame on the send path.** Measured on one +RTL8733B unit during bring-up: a single thermal read (3 RF writes + a 15 µs +settle + an RF read, each several USB control transfers) cost 2.51 ms of a +2.71 ms per-frame budget on USB high speed — 93%, for a meter that tracks PA +bias and is not a validated degradation predictor. One bench, one part, and a +USB3 host would divide it; the shape of the result is the transferable part. +Sensor reads belong on the caller's own cadence via the runtime getters, never +inside `send_packet`. diff --git a/src/rtl8733b/CLAUDE.md b/src/rtl8733b/CLAUDE.md new file mode 100644 index 00000000..11a9078b --- /dev/null +++ b/src/rtl8733b/CLAUDE.md @@ -0,0 +1,148 @@ +# src/rtl8733b/ — RTL8733B (HALMAC 87xx, 11n) working context + +Deep per-backend facts for this subtree, loaded alongside the root CLAUDE.md. +Chips: RTL8731BU / RTL8733BU, chip-id `0x16`, USB PIDs `0bda:f72b` and +`0bda:b733`. 1T1R 802.11b/g/n. + +This is **not** a Jaguar variant. HALMAC 87xx has its own power sequence, +firmware layout, MAC register map, descriptor geometry and PHY path, which is +why it lives outside `src/jaguar*/` instead of behind a strategy seam in one of +them. Do not reach for a Jaguar file expecting a shared mechanism. + +## HAL layout + +`Rtl8733bDevice` (the `IRtlDevice` boundary), `Rtl8733bBringup` (card +enable/disable power sequence, system-cfg), `Halmac8733bMac` (MAC init, +firmware download, EFUSE read + packed-map decode, monitor RX config), +`Phy8733b` (BB/RF table apply, channel plan, TXAGC, TSSI), plus the header-only +`FrameParser8733b` (RX descriptor + PHY status), `TxDescriptor8733b` (40-byte +TX descriptor encoder and the modulation-admission predicates) and +`Rtl8733bUsbIds`. + +BB/AGC/RF tables ride the shared `PhyTableLoader` (`check_positive` walker) +with a Jaguar-style context; firmware and tables are generated into +`hal/hal8733b_{fw,tables}.c` — edit `tools/extract_8733b_*.py`, never the +output. + +## Dispatch + +Chip-id first: `SYS_CFG2` must read `0x16`. The PID table exists for discovery +and for one safety property — a device whose PID is a known 8733B identity but +whose chip-id read fails is **refused**, not allowed to fall through to the +historical Jaguar1 default. A wrong-HAL bring-up on this part writes an +unrelated register map. + +## Chip facts + +- **40-byte TX descriptor**, not the 48-byte form its 8822B/8822C neighbours + use. Encoded byte-wise because USB buffers are not guaranteed + `uint32_t`-aligned. The checksum is an XOR fold over the first 32 bytes with + the `0x1c` field skipped, stored inverted — so a valid descriptor folds to + `0xffff`, the opposite polarity to some sibling parts. Get this wrong and the + chip silently drops the frame. +- **RX aggregation is capped at 12 KiB** (`kRxAggregateBytes8733b`, HALMAC + `RXDMA_AGG` size field in 4 KiB pages). The vendor default of 20 KiB exceeds + one bulk-IN URB and was observed being split by xHCI, leaving a descriptor + tail and its body in separate completions. The RX loop floors its URB size at + the same constant and a `static_assert` ties the two together — raising + either alone reintroduces the straddle, from opposite sides. +- **TSSI closed loop.** Power runs from a fixed safe target + (`kSafeTssiTargetQdbm8733b`); none of the runtime TX-power levers are ported. + The CCK and OFDM/HT variants of the thermal-compensation table are different + tables, and the table cannot be changed while tracking is enabled — so + `select_tssi_rate_table` disables tracking, rewrites, and re-enables, with + each stage verified by register readback. Note what the failure path is and + is not: `Phy8733b::enable_tssi_tracking` restores its own analog/BB snapshot + when its verdict fails, but the *transition* has no rollback to the previous + table — `select_tssi_rate_table` returns false with tracking left off, and + the caller responds by tearing the session down rather than transmitting at + an unverified power setting. That transition costs **84 ms / 136 USB + register round trips** (bench, one unit), so a stream alternating CCK and + OFDM rates is capped around 11 fps; a single-rate stream early-returns and + pays nothing. +- **The loop needs settling time, so a fast rate-switching run misreports + power.** Alternating CCK and OFDM at a few ms per frame leaves CCK + transmitting above its settled level until the loop converges; pacing the + same stream to tens of ms per frame lands on the settled value. Pace + single-rate and let it settle before reading any power figure — otherwise + the tracking loop is what is being measured. No SDR has been on this part, + so the magnitude of the overshoot is not characterized; that is + SDR-gated work, not a number to quote. +- **Thermal is telemetry, like everywhere else.** `InitWrite` logs one + bring-up snapshot and `GetThermalStatus` serves the caller's own cadence. + Nothing in the send path reads it: measured at 2.51 ms of a 2.71 ms + per-frame budget on USB high speed, for a meter that tracks PA bias rather + than junction temperature. + +The timing figures above (84 ms, ~11 fps, 2.51/2.71 ms) come from the single +validation unit described below and have no in-repo oracle. They are +register-sequence and USB-transfer costs, which is why they are quotable at +all — nothing here asserts an RF-domain quantity, because no SDR has been on +this part. Treat them as the order of magnitude to design against, not as +constants. +- **EFUSE**: physical packed map walked into a logical map. Running off the end + of the readable span is address-space exhaustion, not corruption — a fully + written map has no `0xff` terminator left to find, so that case returns + success-with-whatever-parsed. Truncated headers and out-of-range block + targets are still hard rejections. +- **Power sequencing**: the teardown flag is armed *before* `power_on()` is + attempted, because the sequence can fail at any poll with several card-enable + writes already landed. `power_off()` is the full card-disable flow and is + safe against a partially enabled card; latching the flag on success would + skip rollback in exactly the case that needs it. + +## Admission contract + +What is admitted and why each refusal exists is doc-commented at the predicates +themselves in `TxDescriptor8733b.h` — read it there, not a copy here. Two +things that header cannot tell you: + +- **The predicates are not uniformly shared.** `ht_request_supported_8733b` + gates both the `SetTxMode` and radiotap MCS branches, so the HT contract + genuinely cannot drift. `legacy_request_supported_8733b` is reached only + through `SetTxMode`; the radiotap `RATE` branch takes the byte as given and + is constrained downstream by `valid_tx_desc_config`'s `rate_id <= 0x1f` + range check instead. Adequate today only because the radiotap RATE field + cannot express SGI/LDPC/STBC — do not assume the two legacy paths are kept + in step by construction. +- **The CCK band gate closes end-to-end**, not in two halves: + `tx_rate_id_8733b` returns a `0xff` sentinel off 2.4 GHz/20 MHz, and that + same range check refuses the sentinel rather than encoding it. + +The SGI refusal is the one worth re-opening: the descriptor bit was set and the +frame submitted, but the witness decoded long GI. Descriptor and air disagree, +and the reason is not yet understood. + +## Not ported + +`ReadTsf`/beacons, hardware ACK/BlockAck, A-MPDU, `FastRetune`, +`FastSetBandwidth`, the runtime TX-power knobs, `rx.path` per-chain telemetry, +and CCA disable. These inherit `IRtlDevice`'s not-ported defaults (`false`, +`0`, or a full-path fallback) rather than being faked. `SetCcaMode` is the one +exception to the silent-default rule: it is pure virtual, so `true` throws +loudly — without tearing the session down, since an unported optional knob is +not a hardware-safety event — while `false` succeeds as a no-op because that is +the state MAC bring-up already leaves programmed. + +`DeviceConfig::tuning::disable_cca` cannot be honoured either, and `InitWrite` +warns rather than dropping it — a config knob must not be the one door where a +request the setter refuses loudly instead vanishes without a word. + +## Validation status + +Every on-air claim rests on **one** physical unit: a bare unbranded 1T1R +RTL8731BU module, `0bda:f72b`, cut D, USB high speed, witnessed by an +RTL8812AU. No SDR was available, so occupied bandwidth, spectral mask, EVM and +absolute power are unmeasured — which is also why `narrowband_ok` stays false +despite the 5/10 MHz register sequence reading back. The code is more +permissive than the cap: `build_tx_block` accepts a 5/10 MHz session width, so +narrowband is reachable while unadvertised. No `0bda:b733` combo +module has been seen, no vendor-driver A/B control was obtained, and the bench +hub could not switch VBUS, so only warm re-init and physical replug were +tested. The full tested/deferred matrix, the provenance pins and the second +unit that overheated and was excluded: `docs/rtl8733b.md`. + +Headless coverage: `tests/rtl8733b_{efuse,phy_table,rx_parse,tx_desc}_selftest.cpp` +in `ctest`. Hardware: `tests/rtl8733b_lifecycle_soak.sh` (bounded warm +lifecycle, explicitly not a true VBUS cycle) and `examples/rtl8733bprobe` +(staged identity → power/EFUSE → firmware → MAC/PHY → TSSI audit). diff --git a/src/rtl8733b/Rtl8733bDevice.cpp b/src/rtl8733b/Rtl8733bDevice.cpp index c06cc785..2caecdc4 100644 --- a/src/rtl8733b/Rtl8733bDevice.cpp +++ b/src/rtl8733b/Rtl8733bDevice.cpp @@ -111,6 +111,17 @@ void Rtl8733bDevice::InitWrite(SelectedChannel channel) { _tx_ready = true; _tx_submits = 0; _tx_fatal = nullptr; + /* Say so rather than dropping it. SetCcaMode(true) refuses loudly, so the + * config path must not be the one door where the same request vanishes + * without a word — the operator would otherwise believe carrier-sense was + * off and read the resulting deferral as a transmitter problem. Warn + * rather than throw: the knob is on by default for the streamtx FPV + * downlink, and refusing to bring TX up over an unported optimisation is a + * worse trade than airing with standard carrier-sense. */ + if (_cfg.tuning.disable_cca) + _logger->warn("RTL8733B: CCA disable (DEVOURER_DIS_CCA) is not " + "implemented by this backend; transmitting with " + "carrier-sense enabled"); /* One-shot thermal snapshot at bring-up — the PA-heating baseline for the * session, same as the Kestrel InitWrite snapshot. Logged, never acted on: * the meter is a PA-bias tracking index, not a calibrated °C sensor, and