Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ gate any more (an HT-MCS radiotap is honoured unconditionally):
(Programmatic equivalent: `dev->SetTxMode(devourer::TxMode{...})`;
`dev->ClearTxMode()` reverts to the built-in default.)

`SvcTxDemo` is a per-packet-rate showcase: it reads length-prefixed HEVC NALs
from stdin, classifies each by `temporal_id` / IRAP-or-parameter-set criticality
(`txdemo/svc_tx_demo/svc_tx.h`), and injects each at the PHY rate its SVC-T layer
deserves — a per-layer unequal-error-protection ladder (robust MCS for base/IDR,
fast MCS for enhancement), since the radiotap is honoured per-packet. The ladder
is `DEVOURER_SVC_LADDER="CRIT=<spec>;T0=<spec>;T1=<spec>;..."` where each `<spec>`
is a `DEVOURER_TX_RATE` string; unset uses the built-in default. On-air check:
`tests/gen_svc_nals.py` (synthetic 1:4:8:16 layer mix) + `tests/svc_uep_onair.sh`.

`WiFiDriverTxDemo` also honours a TX-gain ramp + duty knob for thermal /
TX-power characterisation (drives `RtlJaguarDevice::SetTxPowerOverride` +
`ApplyTxPower`):
Expand Down
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ target_link_libraries(StreamTxDemo PUBLIC WiFiDriver PRIVATE PkgConfig::libusb)
# stream_stdin.h (shared binary-stdin framing) lives in txdemo/.
target_include_directories(StreamTxDemo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/txdemo)

# SVC-T UEP injector: maps each HEVC NAL's temporal_id -> a robust/fast TxMode
# (svc_tx.h), so base/IDR layers fly robust and enhancement layers ride fast.
add_executable(SvcTxDemo
txdemo/svc_tx_demo/main.cpp
)
target_link_libraries(SvcTxDemo PUBLIC WiFiDriver PRIVATE PkgConfig::libusb)
target_include_directories(SvcTxDemo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/txdemo)

# Stream-link DUPLEX: one binary, one chip, both directions. Combines the RX
# loop from WiFiDriverDemo and the stdin-driven TX from StreamTxDemo —
# stdin = length-prefixed PSDU bodies, stdout = <devourer-stream> lines.
Expand Down
20 changes: 12 additions & 8 deletions src/RadiotapBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,13 @@ std::vector<uint8_t> build_stream_radiotap(const TxMode& cfg) {
}
}

TxMode parse_tx_mode_env() {
TxMode parse_tx_mode_str(const std::string& spec) {
TxMode cfg; /* defaults: legacy 6M, 20 MHz, no SGI/LDPC/STBC */

const char* raw = std::getenv("DEVOURER_TX_RATE");
if (raw == nullptr || *raw == '\0') {
if (spec.empty()) {
return cfg;
}
const std::string s = to_upper_stripped(raw);
const std::string s = to_upper_stripped(spec.c_str());

/* Split on '/': first token = rate, rest = bandwidth (numeric) or modifier
* flags (SGI / LDPC / STBC). */
Expand All @@ -221,8 +220,8 @@ TxMode parse_tx_mode_env() {
}
if (tokens.empty() || tokens[0].empty() || !parse_rate_token(tokens[0], &cfg)) {
std::fprintf(stderr,
"<tx-mode>warning: unrecognised DEVOURER_TX_RATE=%s, "
"falling back to 6M legacy\n", raw);
"<tx-mode>warning: unrecognised TX rate '%s', "
"falling back to 6M legacy\n", spec.c_str());
return TxMode{};
}

Expand All @@ -235,10 +234,15 @@ TxMode parse_tx_mode_env() {
cfg.bw_mhz = static_cast<uint8_t>(std::atoi(t.c_str()));
else if (!t.empty())
std::fprintf(stderr,
"<tx-mode>warning: ignoring unrecognised DEVOURER_TX_RATE "
"token '%s'\n", t.c_str());
"<tx-mode>warning: ignoring unrecognised TX-mode token "
"'%s'\n", t.c_str());
}
return cfg;
}

TxMode parse_tx_mode_env() {
const char* raw = std::getenv("DEVOURER_TX_RATE");
return parse_tx_mode_str(raw ? raw : "");
}

} // namespace devourer
8 changes: 6 additions & 2 deletions src/RadiotapBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#define RADIOTAP_BUILDER_H

#include <cstdint>
#include <string>
#include <vector>

#include "TxMode.h"
Expand All @@ -26,11 +27,14 @@ namespace devourer {
* well-formed radiotap header — no 802.11 frame body. */
std::vector<uint8_t> build_stream_radiotap(const TxMode& mode);

/* Parse DEVOURER_TX_RATE into a TxMode. Single slash-separated string:
/* Parse a TX-mode spec string into a TxMode. Single slash-separated string:
* <rate>[/<bw>][/SGI][/LDPC][/STBC] (case-insensitive)
* <rate> : 6M|9M|12M|18M|24M|36M|48M|54M | MCS0..MCS31 | VHT1SS_MCS0..VHT4SS_MCS9
* <bw> : 20|40|80|160 (default 20)
* Unset or unrecognised falls back to 6M legacy. */
* Empty or unrecognised falls back to 6M legacy. */
TxMode parse_tx_mode_str(const std::string& spec);

/* parse_tx_mode_str applied to the DEVOURER_TX_RATE environment variable. */
TxMode parse_tx_mode_env();

} // namespace devourer
Expand Down
39 changes: 39 additions & 0 deletions tests/gen_svc_nals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Generate length-prefixed synthetic HEVC NAL units with controlled
temporal_ids, to exercise SvcTxDemo's TID -> TxMode UEP mapping on air.

Output (stdout, binary): <u32_le len><len bytes of NAL> ...

HEVC NAL header (2 bytes): byte0 = (nal_type << 1), byte1 = (tid + 1).
Per GOP we emit a dyadic 3-temporal-layer hierarchical-P pattern plus an IDR:
1 IDR (critical) + 4 T0 + 8 T1 + 16 T2 (ratio 1:4:8:16)
so the witness rate histogram should be MCS0:MCS1:MCS4:MCS7 ~= 1:4:8:16
(the default_policy ladder in svc_tx.h).

python3 tests/gen_svc_nals.py [GOPS] [PAYLOAD] | DEVOURER_PID=0x8812 ... ./build/SvcTxDemo
"""
import struct
import sys

GOPS = int(sys.argv[1]) if len(sys.argv) > 1 else 6
PAYLOAD = int(sys.argv[2]) if len(sys.argv) > 2 else 200

IDR_W_RADL = 19 # IRAP -> critical
TRAIL_R = 1 # referenced trailing picture


def nal(nal_type: int, tid: int) -> bytes:
body = bytes([(nal_type << 1) & 0xFF, (tid + 1) & 0x07]) + bytes(PAYLOAD)
return struct.pack("<I", len(body)) + body


out = sys.stdout.buffer
for _ in range(GOPS):
out.write(nal(IDR_W_RADL, 0)) # critical
for _ in range(4):
out.write(nal(TRAIL_R, 0)) # T0 base
for _ in range(8):
out.write(nal(TRAIL_R, 1)) # T1
for _ in range(16):
out.write(nal(TRAIL_R, 2)) # T2
out.flush()
46 changes: 46 additions & 0 deletions tests/svc_uep_onair.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# On-air verification of SvcTxDemo's TID -> TxMode UEP mapping.
#
# Synthetic HEVC NALs (tests/gen_svc_nals.py, ratio 1:4:8:16 = IDR:T0:T1:T2) are
# injected by the 8812; the 8814 kernel monitor witness decodes the per-frame
# MCS. The decoded histogram should track the default_policy ladder
# (critical=MCS0, T0=MCS1, T1=MCS4, T2=MCS7) in the same 1:4:8:16 proportion.
#
# ch6 (2.4 GHz, low current) avoids the USB Vbus-sag gotcha; a fresh power-cycle
# is taken first. Run: sudo bash tests/svc_uep_onair.sh
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SA=57:42:75:05:d6:00
TX=9-2; WIT=4-2.3.2
KILL(){ sudo pkill -9 SvcTxDemo 2>/dev/null; sudo pkill -9 -f gen_svc_nals 2>/dev/null; }
trap KILL EXIT

echo "=== fresh rail (power-cycle hub tree) ==="
sudo modprobe -r rtw88_8812au rtw88_8814au rtw88_8821au 2>/dev/null
for hp in "9 2" "4-2.3 2" "9-1 4"; do sudo uhubctl -a off -l ${hp% *} -p ${hp#* } >/dev/null 2>&1; done; sleep 18
for hp in "9 2" "4-2.3 2" "9-1 4"; do sudo uhubctl -a on -l ${hp% *} -p ${hp#* } >/dev/null 2>&1; done; sleep 9
sudo modprobe rtw88_8812au rtw88_8814au rtw88_8821au 2>/dev/null

# 8814 kernel monitor witness @ ch6
for i in /sys/bus/usb/devices/$WIT/$WIT:*; do printf '%s' "$(basename "$i")"|sudo tee /sys/bus/usb/drivers_probe>/dev/null 2>&1; done; sleep 4
W=$(basename "$(readlink -f /sys/bus/usb/devices/$WIT/*:1.0/net/* 2>/dev/null)")
sudo ip link set "$W" down 2>/dev/null; sudo iw dev "$W" set monitor none 2>/dev/null
sudo ip link set "$W" up; sudo iw dev "$W" set channel 6 2>/dev/null
echo "witness=$W $(iw dev "$W" info 2>/dev/null|grep -oE 'channel [0-9]+')"

# free the 8812 for devourer, inject the SVC stream
for i in /sys/bus/usb/devices/$TX/$TX:*; do ifc=$(basename "$i"); drv=$(readlink -f "$i/driver" 2>/dev/null); [ -n "$drv" ]&&echo "$ifc"|sudo tee "$drv/unbind">/dev/null 2>&1; done; sleep 1
# Witness-friendly ladder: all 20 MHz, distinct MCS, no LDPC/STBC — so a plain
# 20 MHz monitor decodes every rung. (The realistic default_policy stacks
# 40 MHz/LDPC/STBC on the rungs, which a 20 MHz sniffer can't fully decode;
# that ladder is for real links, not this histogram check.)
python3 "$ROOT/tests/gen_svc_nals.py" 12 | \
sudo env DEVOURER_VID=0x0bda DEVOURER_PID=0x8812 DEVOURER_CHANNEL=6 \
DEVOURER_SVC_LADDER="CRIT=MCS0;T0=MCS1;T1=MCS4;T2=MCS7" \
"$ROOT/build/SvcTxDemo" --gap-us 600 >/tmp/svc.log 2>&1 &
sleep 9
echo "=== SvcTxDemo policy + per-TID counters ==="
grep -E "SVC UEP policy| T[0-9] ->|<svc>" /tmp/svc.log | tail -6
echo "=== witness decoded MCS histogram (expect MCS0:MCS1:MCS4:MCS7 ~ 1:4:8:16) ==="
sudo timeout 6 tcpdump -i "$W" -e -nn "ether src $SA" 2>/dev/null | grep -oE "MCS [0-9]+" | sort -V | uniq -c
KILL
Loading
Loading