Skip to content

Jaguar2: per-packet TX power via TX-descriptor TXPWR_OFSET (radiotap-driven)#194

Merged
josephnef merged 1 commit into
masterfrom
per-packet-txpower
Jul 6, 2026
Merged

Jaguar2: per-packet TX power via TX-descriptor TXPWR_OFSET (radiotap-driven)#194
josephnef merged 1 commit into
masterfrom
per-packet-txpower

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

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_POWER on 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_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 (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):

step nominal measured Δ (raw≈dB)
1 −3 −4
2 −7 −8
3 −11 −13
4 +3 +3
5 +6 +5 (RSSI ceiling-clipped near-field)

5/5 correct direction, negative rungs tracking dB nearly exactly. Real, not a gated no-op.

What

  • send_packet honours a per-frame radiotap DBM_TX_POWER as 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_OFSET in txdemo).

Both paths on-air-validated identical (+6→rssi 101, −11→rssi 83, whether via the descriptor default or the radiotap field). offset 0 keeps the descriptor byte-identical.

Scope / honesty

  • The classic 8812AU has no such descriptor field — its per-packet power is rate selection, already supported. So for the most common wfb chip, the forum note is literally true and nothing new is needed.
  • Jaguar3's 2-bit TXPWR_OFSET_TYPE is a different variant, left unwired (and 8822E unwires it in the vendor).

Validation

  • Unit (ctest): tests/txpkt_pwr_selftest.cpp — the dB→LUT quantizer (nearest-rung, ties, clamps). Gated on Jaguar2; added to the mingw target list.
  • On-air: 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-side DEVOURER_TX_PKT_OFSET / DEVOURER_TX_PKT_PWR_DB are measurement drivers).

🤖 Generated with Claude Code

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 josephnef merged commit c0f7609 into master Jul 6, 2026
15 checks passed
@josephnef josephnef deleted the per-packet-txpower branch July 6, 2026 06:39
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>
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.

1 participant