Add FixedAttenuator class and udev rules for RF test bench

FixedAttenuator supports --attenuator fixed:XX for non-programmable
inline SMA pads (set_db is a no-op returning the fixed value).
Udev rules grant non-root USB access for NanoVNA and HMC472A.
This commit is contained in:
Ryan Malloy 2026-02-20 10:56:36 -07:00
parent 3d2cd477b2
commit a12a394099
2 changed files with 1010 additions and 950 deletions

View File

@ -219,6 +219,30 @@ def _make_mock_skywalker(verbose=False):
return sw
class FixedAttenuator:
"""Stand-in for a fixed (non-programmable) inline attenuator.
Reports the declared fixed attenuation for every set_db() call.
Used when testing with a fixed pad instead of the HMC472A.
"""
def __init__(self, fixed_db: float = 20.0):
self._fixed_db = fixed_db
def status(self) -> dict:
return {"attenuation_db": self._fixed_db, "step": 0,
"version": "fixed-pad", "note": "non-programmable"}
def set_db(self, attenuation_db: float) -> dict:
# Can't change a fixed pad — just return what it is
return self.status()
def config(self) -> dict:
return {"db_min": self._fixed_db, "db_max": self._fixed_db,
"db_step": 0, "version": "fixed-pad",
"hostname": f"fixed-{self._fixed_db:.1f}dB"}
class MockHMC472A:
"""Mock attenuator for testing without hardware."""
@ -745,7 +769,8 @@ hardware setup:
help="Path loss calibration CSV (NanoVNA S21 sweep)")
parser.add_argument("--attenuator", type=str, default="auto",
help="HMC472A connection: 'auto' (USB then HTTP), "
"/dev/ttyACMx (USB serial), or http://host (REST) "
"/dev/ttyACMx (USB serial), http://host (REST), "
"or 'fixed:20' for a non-programmable pad "
"(default: auto)")
parser.add_argument("--nanovna", choices=["auto", "manual"],
default="auto",
@ -794,11 +819,22 @@ hardware setup:
def _connect_attenuator(target: str):
"""Connect to HMC472A via auto-detect, USB serial, or HTTP REST.
"""Connect to HMC472A via auto-detect, USB serial, HTTP REST, or fixed pad.
Args:
target: "auto", a serial port path (/dev/ttyACM*), or an HTTP URL.
target: "auto", "fixed:XX" (dB), /dev/ttyACM* (USB), or http://... (REST)
"""
# Fixed attenuator mode (non-programmable inline pad)
if target.startswith("fixed:"):
try:
fixed_db = float(target.split(":", 1)[1])
except ValueError:
print(f"HMC472A: invalid fixed value '{target}' (use fixed:20)")
sys.exit(1)
atten = FixedAttenuator(fixed_db)
print(f"HMC472A: fixed {fixed_db:.1f} dB pad (non-programmable)")
return atten
# Auto-detect: try USB serial first, fall back to HTTP
if target == "auto":
port = detect_hmc472a_serial()

View File

@ -0,0 +1,24 @@
# RF Test Bench udev rules
# Install: sudo cp udev/99-rf-testbench.rules /etc/udev/rules.d/ && sudo udevadm control --reload-rules
#
# Provides non-root access and stable /dev symlinks for:
# /dev/skywalker1 - Genpix SkyWalker-1 DVB-S receiver (USB bulk device, not serial)
# /dev/nanovna - NanoVNA-H vector network analyzer (ttyACM)
# /dev/attenuator - HMC472A digital attenuator on ESP32-S3 (ttyACM)
# --- Genpix SkyWalker-1 (09c0:0203) ---
# Custom firmware: Product="SkyWalker-1 Custom", Serial="0001"
# Stock firmware: Product="Genpix SkyWalker-1", Serial="00857"
# Kernel dvb_usb_gp8psk driver blacklisted in /etc/modprobe.d/blacklist-gp8psk.conf
SUBSYSTEM=="usb", ATTR{idVendor}=="09c0", ATTR{idProduct}=="0203", MODE="0666", SYMLINK+="skywalker1"
# Cypress FX2 bare/unprogrammed (04b4:8613) - for recovery/development
SUBSYSTEM=="usb", ATTR{idVendor}=="04b4", ATTR{idProduct}=="8613", MODE="0666"
# --- NanoVNA-H (0483:5740) ---
# Match on model to avoid hitting other STM32 CDC devices
SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", ATTRS{product}=="NanoVNA-H", MODE="0666", SYMLINK+="nanovna"
# --- HMC472A attenuator on ESP32-S3 native USB CDC ---
# Espressif VID=303a, PID=1001 (USB JTAG/serial), match on product string
SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{product}=="hmc472a-attenuator", MODE="0666", SYMLINK+="attenuator"