Jaguar2: per-packet TX power via TX-descriptor TXPWR_OFSET (radiotap-driven)#194
Merged
Conversation
The forum note ("driver already does per-rate, per-packet txpower") is right
about the mechanism — each packet's RATE selects its TXAGC row, which devourer
already does — but the 8822B/8821C silicon also has a genuine per-PACKET power
knob the vendor uses only for phydm dynamic tracking and devourer didn't wire:
the TX-descriptor TXPWR_OFSET field (+0x14[30:28]), a hardware LUT applied
per-frame on top of the rate's power at ZERO USB cost (a bitfield in the
descriptor send_packet already builds). Vendor LUT: 0=none, 1=-3, 2=-7, 3=-11,
4=+3, 5=+6 dB (PHYDM_OFFSET_* / phydm_dynamictxpower.h).
Measured first (tests/txpkt_pwr_ofset_onair.sh, chip-RSSI ground): the field
moves on-air power standalone — 5/5 LUT steps correct direction, negative rungs
tracking dB nearly exactly (-3->-4, -7->-8, -11->-13 raw), positive rungs
ceiling-clipped by the near-field RSSI. So it's real, not a phydm-gated no-op.
Wired two ways:
- send_packet honours a per-frame radiotap DBM_TX_POWER field as a dB delta,
quantized to the LUT (jaguar2::txpkt_pwr_step_for_db, unit-tested in
tests/txpkt_pwr_selftest.cpp) — the adaptive-link per-packet integration
point (wfb-style injection sets it per frame; the vendor skips this field so
there's no absolute-dBm convention to violate).
- RtlJaguar2Device::SetTxPacketPowerStep sets a session default for rate-less
frames (also the measurement knob, DEVOURER_TX_PKT_OFSET in txdemo).
Both paths on-air-validated identical (+6->rssi 101, -11->rssi 83 via either
the descriptor default or the radiotap field). offset 0 keeps the descriptor
byte-identical. The classic 8812AU has no such field — its per-packet power IS
rate selection, already supported; 8822C's 2-bit TXPWR_OFSET_TYPE left unwired.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
josephnef
added a commit
that referenced
this pull request
Jul 6, 2026
…#196) ## Why From `/tmp/adaptive-link-recipes.md` (the OpenIPC-FPV "Adaptive Link" topic): the **#1 hand-tuning chore** is the universal law **"higher MCS ⇒ lower max clean power"** — the PA garbles dense QAM first, so each MCS has a power ceiling above which video breaks. The whole ecosystem hand-calibrates this per adapter ("raise power until garbage, back off 5"; e.g. Johhn's 8812AU: 58@MCS0/1 → 30@MCS5) and bakes it into per-MCS power tables (`wlan_adapters.yaml`). This automates it, reusing the diagnostics this session already shipped. ## What `tests/per_mcs_power_ceiling.sh` — for each MCS, sweep the flat TXAGC index (`DEVOURER_TX_PWR`) while a second devourer adapter reports per-frame stats (`DEVOURER_STREAM_OUT`). The ceiling per MCS is the highest power that still **delivers** cleanly — the point just before frames garble. Output is the `mcs → safe-power` table + the law-check. **The metric matters, and the measurement corrected my first guess:** the EVM *knee* (PA compression) turned out to be nearly the same power for *every* MCS, so keying on it gave a flat table. What actually differs per MCS is the decode threshold — a denser constellation garbles at lower power — so **frame-delivery collapse**, not the EVM knee, is the right signal. ## Validation (8812AU, 8×8 sweep, chip-RSSI ground — no SDR) Per-MCS delivery (frames/cell) shows the cliff directly: | MCS | ceiling idx | note | |---|---|---| | 0–3 | **56** | deliver across the whole range | | 4 | **44** | 0 frames at idx56 | | 5–7 | **32** | delivery collapses to ~0 at idx44 | `RESULT ceiling MCS0=56 → top=32 (drop 24 index steps ≈ 12 dB); 0 law inversions → LAW REPRODUCED.` This matches the community's known 8812AU shape. Chip-RSSI is the sensor (not the B210 — its front end limits on near-field frames, measured earlier this session). Fixed near-field geometry is valid: the delivery cliff is the PA's clean limit regardless of distance. ## Scope Tooling only — no library change. The table feeds the runtime controls already shipped: `SetTxPowerOffsetQdb` (per-rate baseline, #188) + the Jaguar2 per-packet offset (#194). A runtime per-MCS *clamp* was considered and rejected: per-MCS TXAGC is individually addressable only on Jaguar1; J2/J3 group rates, so it'd be invasive for little gain. The calibration table is the valuable artifact. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
From the forum: "no need to manually set txpower for adaptive link, driver already supports TX power per rate, per packet" (gist).
That's right about the mechanism — each packet's rate selects its TXAGC row, which devourer already does (radiotap rate per-packet → the per-rate table, now runtime-shiftable via #188). The gist also confirms the vendor skips
IEEE80211_RADIOTAP_DBM_TX_POWERon injection, so there's no absolute-dBm per-packet path in the vendor either.But the 8822B/8821C silicon has a genuine per-packet power knob that devourer didn't wire and the vendor uses only for phydm dynamic tracking: the TX-descriptor
TXPWR_OFSETfield (+0x14[30:28]) — a hardware LUT applied per-frame on top of the rate's power, at zero USB cost (a bitfield in the descriptorsend_packetalready builds). Vendor LUT (PHYDM_OFFSET_*):0=none,1=−3,2=−7,3=−11,4=+3,5=+6 dB.Measured first
Before wiring anything (
tests/txpkt_pwr_ofset_onair.sh, chip-RSSI ground station), confirmed the field moves on-air power standalone (no phydm dynamic-txpwr enable needed):5/5 correct direction, negative rungs tracking dB nearly exactly. Real, not a gated no-op.
What
send_packethonours a per-frame radiotapDBM_TX_POWERas a dB delta, quantized to the LUT (jaguar2::txpkt_pwr_step_for_db, unit-tested) — the adaptive-link per-packet integration point (wfb-style injection sets it per frame).RtlJaguar2Device::SetTxPacketPowerStep(step)— a session default for rate-less frames (also the measurement knob,DEVOURER_TX_PKT_OFSETin txdemo).Both paths on-air-validated identical (
+6→rssi 101,−11→rssi 83, whether via the descriptor default or the radiotap field).offset 0keeps the descriptor byte-identical.Scope / honesty
TXPWR_OFSET_TYPEis a different variant, left unwired (and 8822E unwires it in the vendor).Validation
ctest):tests/txpkt_pwr_selftest.cpp— the dB→LUT quantizer (nearest-rung, ties, clamps). Gated on Jaguar2; added to the mingw target list.tests/txpkt_pwr_ofset_onair.sh— the descriptor LUT sweep (5/5) and the radiotap per-packet path (2/2, identical to the descriptor path).Composes with the runtime per-rate
SetTxPowerOffsetQdb(#188): that sets the operating point, this trims per-frame. No new library env vars (the demo-sideDEVOURER_TX_PKT_OFSET/DEVOURER_TX_PKT_PWR_DBare measurement drivers).🤖 Generated with Claude Code