style: apply ruff-format to entire codebase

First-time run of ruff-format via pre-commit hook normalises quote
style, trailing commas, and whitespace across 188 Python files.
No logic changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-07-05 14:48:11 +01:00
parent 82e64104fe
commit 96172ca593
189 changed files with 19883 additions and 19552 deletions
+37 -46
View File
@@ -133,25 +133,20 @@ class SDRFactory:
for sdr_type in cls._builders:
caps = cls.get_capabilities(sdr_type)
capabilities[sdr_type.value] = {
'name': sdr_type.name.replace('_', ' '),
'freq_min_mhz': caps.freq_min_mhz,
'freq_max_mhz': caps.freq_max_mhz,
'gain_min': caps.gain_min,
'gain_max': caps.gain_max,
'sample_rates': caps.sample_rates,
'supports_bias_t': caps.supports_bias_t,
'supports_ppm': caps.supports_ppm,
'tx_capable': caps.tx_capable,
"name": sdr_type.name.replace("_", " "),
"freq_min_mhz": caps.freq_min_mhz,
"freq_max_mhz": caps.freq_max_mhz,
"gain_min": caps.gain_min,
"gain_max": caps.gain_max,
"sample_rates": caps.sample_rates,
"supports_bias_t": caps.supports_bias_t,
"supports_ppm": caps.supports_ppm,
"tx_capable": caps.tx_capable,
}
return capabilities
@classmethod
def create_default_device(
cls,
sdr_type: SDRType,
index: int = 0,
serial: str = 'N/A'
) -> SDRDevice:
def create_default_device(cls, sdr_type: SDRType, index: int = 0, serial: str = "N/A") -> SDRDevice:
"""
Create a default device object for a given SDR type.
@@ -170,18 +165,14 @@ class SDRFactory:
return SDRDevice(
sdr_type=sdr_type,
index=index,
name=f'{sdr_type.name.replace("_", " ")} Device {index}',
name=f"{sdr_type.name.replace('_', ' ')} Device {index}",
serial=serial,
driver=sdr_type.value,
capabilities=caps
capabilities=caps,
)
@classmethod
def create_network_device(
cls,
host: str,
port: int = 1234
) -> SDRDevice:
def create_network_device(cls, host: str, port: int = 1234) -> SDRDevice:
"""
Create a network device for rtl_tcp connection.
@@ -196,40 +187,40 @@ class SDRFactory:
return SDRDevice(
sdr_type=SDRType.RTL_SDR,
index=0,
name=f'{host}:{port}',
serial='rtl_tcp',
driver='rtl_tcp',
name=f"{host}:{port}",
serial="rtl_tcp",
driver="rtl_tcp",
capabilities=caps,
rtl_tcp_host=host,
rtl_tcp_port=port
rtl_tcp_port=port,
)
# Export commonly used items at package level
__all__ = [
# Factory
'SDRFactory',
"SDRFactory",
# Types and classes
'SDRType',
'SDRDevice',
'SDRCapabilities',
'CommandBuilder',
"SDRType",
"SDRDevice",
"SDRCapabilities",
"CommandBuilder",
# Builders
'RTLSDRCommandBuilder',
'LimeSDRCommandBuilder',
'HackRFCommandBuilder',
'AirspyCommandBuilder',
'SDRPlayCommandBuilder',
"RTLSDRCommandBuilder",
"LimeSDRCommandBuilder",
"HackRFCommandBuilder",
"AirspyCommandBuilder",
"SDRPlayCommandBuilder",
# Validation
'SDRValidationError',
'validate_frequency',
'validate_gain',
'validate_sample_rate',
'validate_ppm',
'validate_device_index',
'validate_squelch',
'get_capabilities_for_type',
"SDRValidationError",
"validate_frequency",
"validate_gain",
"validate_sample_rate",
"validate_ppm",
"validate_device_index",
"validate_squelch",
"get_capabilities_for_type",
# Device probing
'probe_rtlsdr_device',
'invalidate_device_cache',
"probe_rtlsdr_device",
"invalidate_device_cache",
]
+58 -63
View File
@@ -20,22 +20,22 @@ class AirspyCommandBuilder(CommandBuilder):
# HF+ has different range but same interface
CAPABILITIES = SDRCapabilities(
sdr_type=SDRType.AIRSPY,
freq_min_mhz=24.0, # 24 MHz (HF+ goes lower)
freq_max_mhz=1800.0, # 1.8 GHz
freq_min_mhz=24.0, # 24 MHz (HF+ goes lower)
freq_max_mhz=1800.0, # 1.8 GHz
gain_min=0.0,
gain_max=45.0, # LNA (0-15) + Mixer (0-15) + VGA (0-15)
gain_max=45.0, # LNA (0-15) + Mixer (0-15) + VGA (0-15)
sample_rates=[2500000, 3000000, 6000000, 10000000],
supports_bias_t=True,
supports_ppm=False, # Airspy has TCXO, no PPM needed
tx_capable=False
supports_ppm=False, # Airspy has TCXO, no PPM needed
tx_capable=False,
)
def _build_device_string(self, device: SDRDevice) -> str:
"""Build SoapySDR device string for Airspy."""
driver = device.driver if device.driver in ('airspy', 'airspyhf') else 'airspy'
if device.serial and device.serial != 'N/A':
return f'driver={driver},serial={device.serial}'
return f'driver={driver}'
driver = device.driver if device.driver in ("airspy", "airspyhf") else "airspy"
if device.serial and device.serial != "N/A":
return f"driver={driver},serial={device.serial}"
return f"driver={driver}"
def _format_gain(self, gain: float) -> str:
"""
@@ -49,12 +49,12 @@ class AirspyCommandBuilder(CommandBuilder):
This distributes the requested gain across stages.
"""
if gain <= 15:
return f'LNA={int(gain)},MIX=0,VGA=0'
return f"LNA={int(gain)},MIX=0,VGA=0"
elif gain <= 30:
return f'LNA=15,MIX={int(gain - 15)},VGA=0'
return f"LNA=15,MIX={int(gain - 15)},VGA=0"
else:
vga = min(15, int(gain - 30))
return f'LNA=15,MIX=15,VGA={vga}'
return f"LNA=15,MIX=15,VGA={vga}"
def build_fm_demod_command(
self,
@@ -65,7 +65,7 @@ class AirspyCommandBuilder(CommandBuilder):
ppm: int | None = None,
modulation: str = "fm",
squelch: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build SoapySDR rx_fm command for FM demodulation.
@@ -74,35 +74,34 @@ class AirspyCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
rx_fm_path = get_tool_path('rx_fm') or 'rx_fm'
rx_fm_path = get_tool_path("rx_fm") or "rx_fm"
cmd = [
rx_fm_path,
'-d', device_str,
'-f', f'{frequency_mhz}M',
'-M', modulation,
'-s', str(sample_rate),
"-d",
device_str,
"-f",
f"{frequency_mhz}M",
"-M",
modulation,
"-s",
str(sample_rate),
]
if gain is not None and gain > 0:
cmd.extend(['-g', self._format_gain(gain)])
cmd.extend(["-g", self._format_gain(gain)])
if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)])
cmd.extend(["-l", str(squelch)])
if bias_t:
cmd.extend(['-T'])
cmd.extend(["-T"])
# Output to stdout
cmd.append('-')
cmd.append("-")
return cmd
def build_adsb_command(
self,
device: SDRDevice,
gain: float | None = None,
bias_t: bool = False
) -> list[str]:
def build_adsb_command(self, device: SDRDevice, gain: float | None = None, bias_t: bool = False) -> list[str]:
"""
Build dump1090/readsb command with SoapySDR support for ADS-B decoding.
@@ -110,19 +109,13 @@ class AirspyCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
cmd = [
'readsb',
'--net',
'--device-type', 'soapysdr',
'--device', device_str,
'--quiet'
]
cmd = ["readsb", "--net", "--device-type", "soapysdr", "--device", device_str, "--quiet"]
if gain is not None:
cmd.extend(['--gain', str(int(gain))])
cmd.extend(["--gain", str(int(gain))])
if bias_t:
cmd.extend(['--enable-bias-t'])
cmd.extend(["--enable-bias-t"])
return cmd
@@ -132,7 +125,7 @@ class AirspyCommandBuilder(CommandBuilder):
frequency_mhz: float = 433.92,
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build rtl_433 command with SoapySDR support for ISM band decoding.
@@ -141,18 +134,13 @@ class AirspyCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
cmd = [
'rtl_433',
'-d', device_str,
'-f', f'{frequency_mhz}M',
'-F', 'json'
]
cmd = ["rtl_433", "-d", device_str, "-f", f"{frequency_mhz}M", "-F", "json"]
if gain is not None and gain > 0:
cmd.extend(['-g', str(int(gain))])
cmd.extend(["-g", str(int(gain))])
if bias_t:
cmd.extend(['-T'])
cmd.extend(["-T"])
return cmd
@@ -173,21 +161,24 @@ class AirspyCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
cmd = [
'AIS-catcher',
'-d', f'soapysdr -d {device_str}',
'-S', str(tcp_port),
'-o', '5',
'-q',
"AIS-catcher",
"-d",
f"soapysdr -d {device_str}",
"-S",
str(tcp_port),
"-o",
"5",
"-q",
]
if gain is not None and gain > 0:
cmd.extend(['-gr', 'tuner', str(int(gain))])
cmd.extend(["-gr", "tuner", str(int(gain))])
if bias_t:
cmd.extend(['-gr', 'biastee', '1'])
cmd.extend(["-gr", "biastee", "1"])
if udp_host and udp_port:
cmd.extend(['-u', udp_host, str(udp_port)])
cmd.extend(["-u", udp_host, str(udp_port)])
return cmd
@@ -199,7 +190,7 @@ class AirspyCommandBuilder(CommandBuilder):
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False,
output_format: str = 'cu8',
output_format: str = "cu8",
) -> list[str]:
"""
Build rx_sdr command for raw I/Q capture with Airspy.
@@ -209,23 +200,27 @@ class AirspyCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
freq_hz = int(frequency_mhz * 1e6)
rx_sdr_path = get_tool_path('rx_sdr') or 'rx_sdr'
rx_sdr_path = get_tool_path("rx_sdr") or "rx_sdr"
cmd = [
rx_sdr_path,
'-d', device_str,
'-f', str(freq_hz),
'-s', str(sample_rate),
'-F', 'CU8',
"-d",
device_str,
"-f",
str(freq_hz),
"-s",
str(sample_rate),
"-F",
"CU8",
]
if gain is not None and gain > 0:
cmd.extend(['-g', self._format_gain(gain)])
cmd.extend(["-g", self._format_gain(gain)])
if bias_t:
cmd.append('-T')
cmd.append("-T")
# Output to stdout
cmd.append('-')
cmd.append("-")
return cmd
+40 -44
View File
@@ -14,6 +14,7 @@ from enum import Enum
class SDRType(Enum):
"""Supported SDR hardware types."""
RTL_SDR = "rtlsdr"
LIME_SDR = "limesdr"
HACKRF = "hackrf"
@@ -27,29 +28,31 @@ class SDRType(Enum):
@dataclass
class SDRCapabilities:
"""Hardware capabilities for an SDR device."""
sdr_type: SDRType
freq_min_mhz: float # Minimum frequency in MHz
freq_max_mhz: float # Maximum frequency in MHz
gain_min: float # Minimum gain in dB
gain_max: float # Maximum gain in dB
freq_min_mhz: float # Minimum frequency in MHz
freq_max_mhz: float # Maximum frequency in MHz
gain_min: float # Minimum gain in dB
gain_max: float # Maximum gain in dB
sample_rates: list[int] = field(default_factory=list) # Supported sample rates
supports_bias_t: bool = False # Bias-T support
supports_ppm: bool = True # PPM correction support
tx_capable: bool = False # Can transmit
supports_bias_t: bool = False # Bias-T support
supports_ppm: bool = True # PPM correction support
tx_capable: bool = False # Can transmit
supports_iq_capture: bool = False # Raw I/Q sample capture
@dataclass
class SDRDevice:
"""Detected SDR device."""
sdr_type: SDRType
index: int
name: str
serial: str
driver: str # e.g., "rtlsdr", "lime", "hackrf"
driver: str # e.g., "rtlsdr", "lime", "hackrf"
capabilities: SDRCapabilities
rtl_tcp_host: str | None = None # Remote rtl_tcp server host
rtl_tcp_port: int | None = None # Remote rtl_tcp server port
rtl_tcp_host: str | None = None # Remote rtl_tcp server host
rtl_tcp_port: int | None = None # Remote rtl_tcp server port
@property
def is_network(self) -> bool:
@@ -59,26 +62,26 @@ class SDRDevice:
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
result = {
'index': self.index,
'name': self.name,
'serial': self.serial,
'sdr_type': self.sdr_type.value,
'driver': self.driver,
'is_network': self.is_network,
'capabilities': {
'freq_min_mhz': self.capabilities.freq_min_mhz,
'freq_max_mhz': self.capabilities.freq_max_mhz,
'gain_min': self.capabilities.gain_min,
'gain_max': self.capabilities.gain_max,
'sample_rates': self.capabilities.sample_rates,
'supports_bias_t': self.capabilities.supports_bias_t,
'supports_ppm': self.capabilities.supports_ppm,
'tx_capable': self.capabilities.tx_capable,
}
"index": self.index,
"name": self.name,
"serial": self.serial,
"sdr_type": self.sdr_type.value,
"driver": self.driver,
"is_network": self.is_network,
"capabilities": {
"freq_min_mhz": self.capabilities.freq_min_mhz,
"freq_max_mhz": self.capabilities.freq_max_mhz,
"gain_min": self.capabilities.gain_min,
"gain_max": self.capabilities.gain_max,
"sample_rates": self.capabilities.sample_rates,
"supports_bias_t": self.capabilities.supports_bias_t,
"supports_ppm": self.capabilities.supports_ppm,
"tx_capable": self.capabilities.tx_capable,
},
}
if self.is_network:
result['rtl_tcp_host'] = self.rtl_tcp_host
result['rtl_tcp_port'] = self.rtl_tcp_port
result["rtl_tcp_host"] = self.rtl_tcp_host
result["rtl_tcp_port"] = self.rtl_tcp_port
return result
@@ -95,7 +98,7 @@ class CommandBuilder(ABC):
ppm: int | None = None,
modulation: str = "fm",
squelch: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build FM demodulation command (for pager decoding).
@@ -116,12 +119,7 @@ class CommandBuilder(ABC):
pass
@abstractmethod
def build_adsb_command(
self,
device: SDRDevice,
gain: float | None = None,
bias_t: bool = False
) -> list[str]:
def build_adsb_command(self, device: SDRDevice, gain: float | None = None, bias_t: bool = False) -> list[str]:
"""
Build ADS-B decoder command.
@@ -142,7 +140,7 @@ class CommandBuilder(ABC):
frequency_mhz: float = 433.92,
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build ISM band decoder command (433MHz sensors).
@@ -198,7 +196,7 @@ class CommandBuilder(ABC):
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False,
output_format: str = 'cu8',
output_format: str = "cu8",
) -> list[str]:
"""
Build raw I/Q capture command for streaming samples to stdout.
@@ -222,17 +220,15 @@ class CommandBuilder(ABC):
NotImplementedError: If the SDR type does not support I/Q capture.
"""
if not device.capabilities.supports_iq_capture:
supported = ', '.join(
t.value for t in SDRType
supported = ", ".join(
t.value
for t in SDRType
if t == SDRType.RTL_SDR # known IQ-capable types
)
raise ValueError(
f"{device.sdr_type.value} does not support raw I/Q capture. "
f"Supported devices: {supported}"
f"{device.sdr_type.value} does not support raw I/Q capture. Supported devices: {supported}"
)
raise NotImplementedError(
f"{self.__class__.__name__} does not support raw I/Q capture"
)
raise NotImplementedError(f"{self.__class__.__name__} does not support raw I/Q capture")
@classmethod
@abstractmethod
+115 -122
View File
@@ -39,7 +39,8 @@ def _hackrf_probe_blocked() -> bool:
"""Return True when probing HackRF would interfere with an active stream."""
try:
from utils.subghz import get_subghz_manager
return get_subghz_manager().active_mode in {'rx', 'decode', 'tx', 'sweep'}
return get_subghz_manager().active_mode in {"rx", "decode", "tx", "sweep"}
except Exception:
return False
@@ -75,20 +76,20 @@ def _get_capabilities_for_type(sdr_type: SDRType) -> SDRCapabilities:
sample_rates=[2048000],
supports_bias_t=False,
supports_ppm=False,
tx_capable=False
tx_capable=False,
)
def _driver_to_sdr_type(driver: str) -> SDRType | None:
"""Map SoapySDR driver name to SDRType."""
mapping = {
'rtlsdr': SDRType.RTL_SDR,
'lime': SDRType.LIME_SDR,
'limesdr': SDRType.LIME_SDR,
'hackrf': SDRType.HACKRF,
'airspy': SDRType.AIRSPY,
'airspyhf': SDRType.AIRSPY, # Airspy HF+ uses same builder
'sdrplay': SDRType.SDRPLAY,
"rtlsdr": SDRType.RTL_SDR,
"lime": SDRType.LIME_SDR,
"limesdr": SDRType.LIME_SDR,
"hackrf": SDRType.HACKRF,
"airspy": SDRType.AIRSPY,
"airspyhf": SDRType.AIRSPY, # Airspy HF+ uses same builder
"sdrplay": SDRType.SDRPLAY,
# Future support
# 'uhd': SDRType.USRP,
# 'bladerf': SDRType.BLADE_RF,
@@ -105,7 +106,7 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
"""
devices: list[SDRDevice] = []
rtl_test_path = get_tool_path('rtl_test')
rtl_test_path = get_tool_path("rtl_test")
if not rtl_test_path:
logger.debug("rtl_test not found, skipping RTL-SDR detection")
return devices
@@ -113,21 +114,22 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
try:
import os
import platform
env = os.environ.copy()
if platform.system() == 'Darwin':
lib_paths = ['/usr/local/lib', '/opt/homebrew/lib']
current_ld = env.get('DYLD_LIBRARY_PATH', '')
env['DYLD_LIBRARY_PATH'] = ':'.join(lib_paths + [current_ld] if current_ld else lib_paths)
if platform.system() == "Darwin":
lib_paths = ["/usr/local/lib", "/opt/homebrew/lib"]
current_ld = env.get("DYLD_LIBRARY_PATH", "")
env["DYLD_LIBRARY_PATH"] = ":".join(lib_paths + [current_ld] if current_ld else lib_paths)
try:
result = subprocess.run(
[rtl_test_path, '-t'],
[rtl_test_path, "-t"],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
encoding="utf-8",
errors="replace",
timeout=5,
env=env
env=env,
)
except subprocess.TimeoutExpired:
logger.warning("rtl_test timed out after 5s")
@@ -137,37 +139,41 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
# Parse device info from rtl_test output
# Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001"
# Require a non-empty serial to avoid matching malformed lines like "SN:".
device_pattern = r'(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$'
device_pattern = r"(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$"
from .rtlsdr import RTLSDRCommandBuilder
for line in output.split('\n'):
for line in output.split("\n"):
line = line.strip()
match = re.match(device_pattern, line)
if match:
devices.append(SDRDevice(
sdr_type=SDRType.RTL_SDR,
index=int(match.group(1)),
name=match.group(2).strip().rstrip(','),
serial=match.group(3),
driver='rtlsdr',
capabilities=RTLSDRCommandBuilder.CAPABILITIES
))
devices.append(
SDRDevice(
sdr_type=SDRType.RTL_SDR,
index=int(match.group(1)),
name=match.group(2).strip().rstrip(","),
serial=match.group(3),
driver="rtlsdr",
capabilities=RTLSDRCommandBuilder.CAPABILITIES,
)
)
# Fallback: if we found devices but couldn't parse details
if not devices:
found_match = re.search(r'Found (\d+) device', output)
found_match = re.search(r"Found (\d+) device", output)
if found_match:
count = int(found_match.group(1))
for i in range(count):
devices.append(SDRDevice(
sdr_type=SDRType.RTL_SDR,
index=i,
name=f'RTL-SDR Device {i}',
serial='Unknown',
driver='rtlsdr',
capabilities=RTLSDRCommandBuilder.CAPABILITIES
))
devices.append(
SDRDevice(
sdr_type=SDRType.RTL_SDR,
index=i,
name=f"RTL-SDR Device {i}",
serial="Unknown",
driver="rtlsdr",
capabilities=RTLSDRCommandBuilder.CAPABILITIES,
)
)
except subprocess.TimeoutExpired:
logger.warning("rtl_test timed out")
@@ -180,7 +186,7 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
def _find_soapy_util() -> str | None:
"""Find SoapySDR utility command (name varies by distribution)."""
# Try different command names used across distributions
for cmd in ['SoapySDRUtil', 'soapy_sdr_util', 'soapysdr-util']:
for cmd in ["SoapySDRUtil", "soapy_sdr_util", "soapysdr-util"]:
tool_path = get_tool_path(cmd)
if tool_path:
return tool_path
@@ -199,26 +205,27 @@ def _get_soapy_env() -> dict:
"""
import os
import platform
env = os.environ.copy()
if platform.system() == 'Darwin':
if platform.system() == "Darwin":
# Homebrew paths for Apple Silicon and Intel Macs
homebrew_paths = ['/opt/homebrew', '/usr/local']
homebrew_paths = ["/opt/homebrew", "/usr/local"]
lib_paths = []
for base in homebrew_paths:
lib_path = f'{base}/lib'
lib_path = f"{base}/lib"
if os.path.isdir(lib_path):
lib_paths.append(lib_path)
if lib_paths:
current_dyld = env.get('DYLD_LIBRARY_PATH', '')
env['DYLD_LIBRARY_PATH'] = ':'.join(lib_paths + ([current_dyld] if current_dyld else []))
current_dyld = env.get("DYLD_LIBRARY_PATH", "")
env["DYLD_LIBRARY_PATH"] = ":".join(lib_paths + ([current_dyld] if current_dyld else []))
# Set SOAPY_SDR_ROOT if we found Homebrew installation
for base in homebrew_paths:
if os.path.isdir(f'{base}/lib/SoapySDR'):
env['SOAPY_SDR_ROOT'] = base
if os.path.isdir(f"{base}/lib/SoapySDR"):
env["SOAPY_SDR_ROOT"] = base
break
return env
@@ -244,13 +251,7 @@ def detect_soapy_devices(skip_types: set[SDRType] | None = None) -> list[SDRDevi
try:
# Use macOS-aware environment to find Homebrew-installed modules
env = _get_soapy_env()
result = subprocess.run(
[soapy_cmd, '--find'],
capture_output=True,
text=True,
timeout=10,
env=env
)
result = subprocess.run([soapy_cmd, "--find"], capture_output=True, text=True, timeout=10, env=env)
# Parse SoapySDR output
# Format varies but typically includes lines like:
@@ -261,25 +262,25 @@ def detect_soapy_devices(skip_types: set[SDRType] | None = None) -> list[SDRDevi
current_device: dict = {}
device_counts: dict[SDRType, int] = {}
for line in result.stdout.split('\n'):
for line in result.stdout.split("\n"):
line = line.strip()
# Start of new device block
if line.startswith('Found device'):
if current_device.get('driver'):
if line.startswith("Found device"):
if current_device.get("driver"):
_add_soapy_device(devices, current_device, device_counts, skip_types)
current_device = {}
continue
# Parse key = value pairs
if ' = ' in line:
key, value = line.split(' = ', 1)
if " = " in line:
key, value = line.split(" = ", 1)
key = key.strip()
value = value.strip()
current_device[key] = value
# Don't forget the last device
if current_device.get('driver'):
if current_device.get("driver"):
_add_soapy_device(devices, current_device, device_counts, skip_types)
except subprocess.TimeoutExpired:
@@ -291,13 +292,10 @@ def detect_soapy_devices(skip_types: set[SDRType] | None = None) -> list[SDRDevi
def _add_soapy_device(
devices: list[SDRDevice],
device_info: dict,
device_counts: dict[SDRType, int],
skip_types: set[SDRType]
devices: list[SDRDevice], device_info: dict, device_counts: dict[SDRType, int], skip_types: set[SDRType]
) -> None:
"""Add a device from SoapySDR detection to the list."""
driver = device_info.get('driver', '').lower()
driver = device_info.get("driver", "").lower()
sdr_type = _driver_to_sdr_type(driver)
if not sdr_type:
@@ -316,14 +314,16 @@ def _add_soapy_device(
index = device_counts[sdr_type]
device_counts[sdr_type] += 1
devices.append(SDRDevice(
sdr_type=sdr_type,
index=index,
name=device_info.get('label', device_info.get('driver', 'Unknown')),
serial=device_info.get('serial', 'N/A'),
driver=driver,
capabilities=_get_capabilities_for_type(sdr_type)
))
devices.append(
SDRDevice(
sdr_type=sdr_type,
index=index,
name=device_info.get("label", device_info.get("driver", "Unknown")),
serial=device_info.get("serial", "N/A"),
driver=driver,
capabilities=_get_capabilities_for_type(sdr_type),
)
)
def detect_hackrf_devices() -> list[SDRDevice]:
@@ -345,19 +345,14 @@ def detect_hackrf_devices() -> list[SDRDevice]:
devices: list[SDRDevice] = []
hackrf_info_path = get_tool_path('hackrf_info')
hackrf_info_path = get_tool_path("hackrf_info")
if not hackrf_info_path:
_hackrf_cache = devices
_hackrf_cache_ts = now
return devices
try:
result = subprocess.run(
[hackrf_info_path],
capture_output=True,
text=True,
timeout=5
)
result = subprocess.run([hackrf_info_path], capture_output=True, text=True, timeout=5)
# Combine stdout + stderr: newer firmware may print to stderr,
# and hackrf_info may exit non-zero when device is briefly busy
@@ -369,46 +364,50 @@ def detect_hackrf_devices() -> list[SDRDevice]:
from .hackrf import HackRFCommandBuilder
serial_pattern = re.compile(
r'^\s*Serial\s+number:\s*(.+)$',
r"^\s*Serial\s+number:\s*(.+)$",
re.IGNORECASE | re.MULTILINE,
)
board_pattern = re.compile(
r'Board\s+ID\s+Number:\s*\d+\s*\(([^)]+)\)',
r"Board\s+ID\s+Number:\s*\d+\s*\(([^)]+)\)",
re.IGNORECASE,
)
serials_found = []
for raw in serial_pattern.findall(output):
# Normalise legacy formats like "0x1234 5678" to plain hex.
serial = re.sub(r'0x', '', raw, flags=re.IGNORECASE)
serial = re.sub(r'[^0-9A-Fa-f]', '', serial)
serial = re.sub(r"0x", "", raw, flags=re.IGNORECASE)
serial = re.sub(r"[^0-9A-Fa-f]", "", serial)
if serial:
serials_found.append(serial)
boards_found = board_pattern.findall(output)
for i, serial in enumerate(serials_found):
board_name = boards_found[i] if i < len(boards_found) else 'HackRF'
devices.append(SDRDevice(
sdr_type=SDRType.HACKRF,
index=i,
name=board_name,
serial=serial,
driver='hackrf',
capabilities=HackRFCommandBuilder.CAPABILITIES
))
board_name = boards_found[i] if i < len(boards_found) else "HackRF"
devices.append(
SDRDevice(
sdr_type=SDRType.HACKRF,
index=i,
name=board_name,
serial=serial,
driver="hackrf",
capabilities=HackRFCommandBuilder.CAPABILITIES,
)
)
# Fallback: check if any HackRF found without serial
if not devices and re.search(r'Found\s+HackRF', output, re.IGNORECASE):
if not devices and re.search(r"Found\s+HackRF", output, re.IGNORECASE):
board_match = board_pattern.search(output)
board_name = board_match.group(1) if board_match else 'HackRF'
devices.append(SDRDevice(
sdr_type=SDRType.HACKRF,
index=0,
name=board_name,
serial='Unknown',
driver='hackrf',
capabilities=HackRFCommandBuilder.CAPABILITIES
))
board_name = board_match.group(1) if board_match else "HackRF"
devices.append(
SDRDevice(
sdr_type=SDRType.HACKRF,
index=0,
name=board_name,
serial="Unknown",
driver="hackrf",
capabilities=HackRFCommandBuilder.CAPABILITIES,
)
)
except Exception as e:
logger.debug(f"HackRF detection error: {e}")
@@ -432,7 +431,7 @@ def probe_rtlsdr_device(device_index: int) -> str | None:
An error message string if the device cannot be opened,
or ``None`` if the device is available.
"""
rtl_test_path = get_tool_path('rtl_test')
rtl_test_path = get_tool_path("rtl_test")
if not rtl_test_path:
# Can't probe without rtl_test — let the caller proceed and
# surface errors from the actual decoder process instead.
@@ -441,20 +440,19 @@ def probe_rtlsdr_device(device_index: int) -> str | None:
try:
import os
import platform
env = os.environ.copy()
if platform.system() == 'Darwin':
lib_paths = ['/usr/local/lib', '/opt/homebrew/lib']
current_ld = env.get('DYLD_LIBRARY_PATH', '')
env['DYLD_LIBRARY_PATH'] = ':'.join(
lib_paths + [current_ld] if current_ld else lib_paths
)
if platform.system() == "Darwin":
lib_paths = ["/usr/local/lib", "/opt/homebrew/lib"]
current_ld = env.get("DYLD_LIBRARY_PATH", "")
env["DYLD_LIBRARY_PATH"] = ":".join(lib_paths + [current_ld] if current_ld else lib_paths)
# Use Popen with early termination instead of run() with full timeout.
# rtl_test prints device info to stderr quickly, then keeps running
# its test loop. We kill it as soon as we see success or failure.
proc = subprocess.Popen(
[rtl_test_path, '-d', str(device_index), '-t'],
[rtl_test_path, "-d", str(device_index), "-t"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
@@ -462,6 +460,7 @@ def probe_rtlsdr_device(device_index: int) -> str | None:
)
import select
error_found = False
device_found = False
deadline = time.monotonic() + 3.0
@@ -472,22 +471,20 @@ def probe_rtlsdr_device(device_index: int) -> str | None:
if remaining <= 0:
break
# Wait for stderr output with timeout
ready, _, _ = select.select(
[proc.stderr], [], [], min(remaining, 0.1)
)
ready, _, _ = select.select([proc.stderr], [], [], min(remaining, 0.1))
if ready:
line = proc.stderr.readline()
if not line:
break # EOF — process closed stderr
# Check for no-device messages first (before success check,
# since "No supported devices found" also contains "Found" + "device")
if 'no supported devices' in line.lower() or 'no matching devices' in line.lower():
if "no supported devices" in line.lower() or "no matching devices" in line.lower():
error_found = True
break
if 'usb_claim_interface' in line or 'Failed to open' in line:
if "usb_claim_interface" in line or "Failed to open" in line:
error_found = True
break
if 'Found' in line and 'device' in line.lower():
if "Found" in line and "device" in line.lower():
# Device opened successfully — no need to wait longer
device_found = True
break
@@ -506,13 +503,10 @@ def probe_rtlsdr_device(device_index: int) -> str | None:
time.sleep(0.5)
if error_found:
logger.warning(
f"RTL-SDR device {device_index} USB probe failed: "
f"device busy or unavailable"
)
logger.warning(f"RTL-SDR device {device_index} USB probe failed: device busy or unavailable")
return (
f'SDR device {device_index} is not available — '
f'check that the SDR device is connected and not in use by another process.'
f"SDR device {device_index} is not available — "
f"check that the SDR device is connected and not in use by another process."
)
except Exception as e:
@@ -589,4 +583,3 @@ def invalidate_device_cache() -> None:
global _all_devices_cache, _all_devices_cache_ts
_all_devices_cache = []
_all_devices_cache_ts = 0.0
+53 -58
View File
@@ -17,21 +17,21 @@ class HackRFCommandBuilder(CommandBuilder):
CAPABILITIES = SDRCapabilities(
sdr_type=SDRType.HACKRF,
freq_min_mhz=1.0, # 1 MHz
freq_max_mhz=6000.0, # 6 GHz
freq_min_mhz=1.0, # 1 MHz
freq_max_mhz=6000.0, # 6 GHz
gain_min=0.0,
gain_max=102.0, # LNA (0-40) + VGA (0-62)
gain_max=102.0, # LNA (0-40) + VGA (0-62)
sample_rates=[2000000, 4000000, 8000000, 10000000, 20000000],
supports_bias_t=True,
supports_ppm=False,
tx_capable=True
tx_capable=True,
)
def _build_device_string(self, device: SDRDevice) -> str:
"""Build SoapySDR device string for HackRF."""
if device.serial and device.serial != 'N/A':
return f'driver=hackrf,serial={device.serial}'
return 'driver=hackrf'
if device.serial and device.serial != "N/A":
return f"driver=hackrf,serial={device.serial}"
return "driver=hackrf"
def _split_gain(self, gain: float) -> tuple[int, int]:
"""
@@ -61,7 +61,7 @@ class HackRFCommandBuilder(CommandBuilder):
ppm: int | None = None,
modulation: str = "fm",
squelch: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build SoapySDR rx_fm command for FM demodulation.
@@ -70,36 +70,35 @@ class HackRFCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
rx_fm_path = get_tool_path('rx_fm') or 'rx_fm'
rx_fm_path = get_tool_path("rx_fm") or "rx_fm"
cmd = [
rx_fm_path,
'-d', device_str,
'-f', f'{frequency_mhz}M',
'-M', modulation,
'-s', str(sample_rate),
"-d",
device_str,
"-f",
f"{frequency_mhz}M",
"-M",
modulation,
"-s",
str(sample_rate),
]
if gain is not None and gain > 0:
lna, vga = self._split_gain(gain)
cmd.extend(['-g', f'LNA={lna},VGA={vga}'])
cmd.extend(["-g", f"LNA={lna},VGA={vga}"])
if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)])
cmd.extend(["-l", str(squelch)])
if bias_t:
cmd.extend(['-T'])
cmd.extend(["-T"])
# Output to stdout
cmd.append('-')
cmd.append("-")
return cmd
def build_adsb_command(
self,
device: SDRDevice,
gain: float | None = None,
bias_t: bool = False
) -> list[str]:
def build_adsb_command(self, device: SDRDevice, gain: float | None = None, bias_t: bool = False) -> list[str]:
"""
Build dump1090/readsb command with SoapySDR support for ADS-B decoding.
@@ -107,19 +106,13 @@ class HackRFCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
cmd = [
'readsb',
'--net',
'--device-type', 'soapysdr',
'--device', device_str,
'--quiet'
]
cmd = ["readsb", "--net", "--device-type", "soapysdr", "--device", device_str, "--quiet"]
if gain is not None:
cmd.extend(['--gain', str(int(gain))])
cmd.extend(["--gain", str(int(gain))])
if bias_t:
cmd.extend(['--enable-bias-t'])
cmd.extend(["--enable-bias-t"])
return cmd
@@ -129,7 +122,7 @@ class HackRFCommandBuilder(CommandBuilder):
frequency_mhz: float = 433.92,
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build rtl_433 command with SoapySDR support for ISM band decoding.
@@ -142,17 +135,12 @@ class HackRFCommandBuilder(CommandBuilder):
# Build device string with optional bias-t setting
device_str = self._build_device_string(device)
if bias_t:
device_str = f'{device_str},bias_t=1'
device_str = f"{device_str},bias_t=1"
cmd = [
'rtl_433',
'-d', device_str,
'-f', f'{frequency_mhz}M',
'-F', 'json'
]
cmd = ["rtl_433", "-d", device_str, "-f", f"{frequency_mhz}M", "-F", "json"]
if gain is not None and gain > 0:
cmd.extend(['-g', str(int(gain))])
cmd.extend(["-g", str(int(gain))])
return cmd
@@ -173,21 +161,24 @@ class HackRFCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
cmd = [
'AIS-catcher',
'-d', f'soapysdr -d {device_str}',
'-S', str(tcp_port),
'-o', '5',
'-q',
"AIS-catcher",
"-d",
f"soapysdr -d {device_str}",
"-S",
str(tcp_port),
"-o",
"5",
"-q",
]
if gain is not None and gain > 0:
cmd.extend(['-gr', 'tuner', str(int(gain))])
cmd.extend(["-gr", "tuner", str(int(gain))])
if bias_t:
cmd.extend(['-gr', 'biastee', '1'])
cmd.extend(["-gr", "biastee", "1"])
if udp_host and udp_port:
cmd.extend(['-u', udp_host, str(udp_port)])
cmd.extend(["-u", udp_host, str(udp_port)])
return cmd
@@ -199,7 +190,7 @@ class HackRFCommandBuilder(CommandBuilder):
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False,
output_format: str = 'cu8',
output_format: str = "cu8",
) -> list[str]:
"""
Build rx_sdr command for raw I/Q capture with HackRF.
@@ -209,24 +200,28 @@ class HackRFCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
freq_hz = int(frequency_mhz * 1e6)
rx_sdr_path = get_tool_path('rx_sdr') or 'rx_sdr'
rx_sdr_path = get_tool_path("rx_sdr") or "rx_sdr"
cmd = [
rx_sdr_path,
'-d', device_str,
'-f', str(freq_hz),
'-s', str(sample_rate),
'-F', 'CU8',
"-d",
device_str,
"-f",
str(freq_hz),
"-s",
str(sample_rate),
"-F",
"CU8",
]
if gain is not None and gain > 0:
lna, vga = self._split_gain(gain)
cmd.extend(['-g', f'LNA={lna},VGA={vga}'])
cmd.extend(["-g", f"LNA={lna},VGA={vga}"])
if bias_t:
cmd.append('-T')
cmd.append("-T")
# Output to stdout
cmd.append('-')
cmd.append("-")
return cmd
+50 -55
View File
@@ -17,21 +17,21 @@ class LimeSDRCommandBuilder(CommandBuilder):
CAPABILITIES = SDRCapabilities(
sdr_type=SDRType.LIME_SDR,
freq_min_mhz=0.1, # 100 kHz
freq_max_mhz=3800.0, # 3.8 GHz
freq_min_mhz=0.1, # 100 kHz
freq_max_mhz=3800.0, # 3.8 GHz
gain_min=0.0,
gain_max=73.0, # Combined LNA + TIA + PGA
gain_max=73.0, # Combined LNA + TIA + PGA
sample_rates=[1000000, 2000000, 4000000, 8000000, 10000000, 20000000],
supports_bias_t=False,
supports_ppm=False, # Uses TCXO, no PPM correction needed
tx_capable=True
supports_ppm=False, # Uses TCXO, no PPM correction needed
tx_capable=True,
)
def _build_device_string(self, device: SDRDevice) -> str:
"""Build SoapySDR device string for LimeSDR."""
if device.serial and device.serial != 'N/A':
return f'driver=lime,serial={device.serial}'
return 'driver=lime'
if device.serial and device.serial != "N/A":
return f"driver=lime,serial={device.serial}"
return "driver=lime"
def build_fm_demod_command(
self,
@@ -42,7 +42,7 @@ class LimeSDRCommandBuilder(CommandBuilder):
ppm: int | None = None,
modulation: str = "fm",
squelch: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build SoapySDR rx_fm command for FM demodulation.
@@ -52,33 +52,32 @@ class LimeSDRCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
rx_fm_path = get_tool_path('rx_fm') or 'rx_fm'
rx_fm_path = get_tool_path("rx_fm") or "rx_fm"
cmd = [
rx_fm_path,
'-d', device_str,
'-f', f'{frequency_mhz}M',
'-M', modulation,
'-s', str(sample_rate),
"-d",
device_str,
"-f",
f"{frequency_mhz}M",
"-M",
modulation,
"-s",
str(sample_rate),
]
if gain is not None and gain > 0:
# LimeSDR gain is applied to LNAH element
cmd.extend(['-g', f'LNAH={int(gain)}'])
cmd.extend(["-g", f"LNAH={int(gain)}"])
if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)])
cmd.extend(["-l", str(squelch)])
# Output to stdout
cmd.append('-')
cmd.append("-")
return cmd
def build_adsb_command(
self,
device: SDRDevice,
gain: float | None = None,
bias_t: bool = False
) -> list[str]:
def build_adsb_command(self, device: SDRDevice, gain: float | None = None, bias_t: bool = False) -> list[str]:
"""
Build dump1090 command with SoapySDR support for ADS-B decoding.
@@ -89,16 +88,10 @@ class LimeSDRCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
# Try readsb first (better SoapySDR support), fallback to dump1090
cmd = [
'readsb',
'--net',
'--device-type', 'soapysdr',
'--device', device_str,
'--quiet'
]
cmd = ["readsb", "--net", "--device-type", "soapysdr", "--device", device_str, "--quiet"]
if gain is not None:
cmd.extend(['--gain', str(int(gain))])
cmd.extend(["--gain", str(int(gain))])
return cmd
@@ -108,7 +101,7 @@ class LimeSDRCommandBuilder(CommandBuilder):
frequency_mhz: float = 433.92,
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build rtl_433 command with SoapySDR support for ISM band decoding.
@@ -118,20 +111,15 @@ class LimeSDRCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
cmd = [
'rtl_433',
'-d', device_str,
'-f', f'{frequency_mhz}M',
'-F', 'json'
]
cmd = ["rtl_433", "-d", device_str, "-f", f"{frequency_mhz}M", "-F", "json"]
if gain is not None and gain > 0:
cmd.extend(['-g', str(int(gain))])
cmd.extend(["-g", str(int(gain))])
# PPM not typically needed for LimeSDR (TCXO)
# but include if specified
if ppm is not None and ppm != 0:
cmd.extend(['-p', str(ppm)])
cmd.extend(["-p", str(ppm)])
return cmd
@@ -153,18 +141,21 @@ class LimeSDRCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
cmd = [
'AIS-catcher',
'-d', f'soapysdr -d {device_str}',
'-S', str(tcp_port),
'-o', '5',
'-q',
"AIS-catcher",
"-d",
f"soapysdr -d {device_str}",
"-S",
str(tcp_port),
"-o",
"5",
"-q",
]
if gain is not None and gain > 0:
cmd.extend(['-gr', 'tuner', str(int(gain))])
cmd.extend(["-gr", "tuner", str(int(gain))])
if udp_host and udp_port:
cmd.extend(['-u', udp_host, str(udp_port)])
cmd.extend(["-u", udp_host, str(udp_port)])
return cmd
@@ -176,7 +167,7 @@ class LimeSDRCommandBuilder(CommandBuilder):
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False,
output_format: str = 'cu8',
output_format: str = "cu8",
) -> list[str]:
"""
Build rx_sdr command for raw I/Q capture with LimeSDR.
@@ -187,20 +178,24 @@ class LimeSDRCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
freq_hz = int(frequency_mhz * 1e6)
rx_sdr_path = get_tool_path('rx_sdr') or 'rx_sdr'
rx_sdr_path = get_tool_path("rx_sdr") or "rx_sdr"
cmd = [
rx_sdr_path,
'-d', device_str,
'-f', str(freq_hz),
'-s', str(sample_rate),
'-F', 'CU8',
"-d",
device_str,
"-f",
str(freq_hz),
"-s",
str(sample_rate),
"-F",
"CU8",
]
if gain is not None and gain > 0:
cmd.extend(['-g', f'LNAH={int(gain)}'])
cmd.extend(["-g", f"LNAH={int(gain)}"])
# Output to stdout
cmd.append('-')
cmd.append("-")
return cmd
+62 -85
View File
@@ -15,13 +15,13 @@ from utils.dependencies import get_tool_path
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
logger = logging.getLogger('intercept.sdr.rtlsdr')
logger = logging.getLogger("intercept.sdr.rtlsdr")
def _rtl_fm_demod_mode(modulation: str) -> str:
"""Map app/UI modulation names to rtl_fm demod tokens."""
mod = str(modulation or '').lower().strip()
return 'wbfm' if mod == 'wfm' else mod
mod = str(modulation or "").lower().strip()
return "wbfm" if mod == "wfm" else mod
def _rtl_tool_supports_bias_t(tool_path: str) -> bool:
@@ -31,16 +31,11 @@ def _rtl_tool_supports_bias_t(tool_path: str) -> bool:
rtl-sdr packages shipped by most distros.
"""
try:
result = subprocess.run(
[tool_path, '--help'],
capture_output=True,
text=True,
timeout=5
)
result = subprocess.run([tool_path, "--help"], capture_output=True, text=True, timeout=5)
help_text = result.stdout + result.stderr
# Match "-T" as a CLI flag (e.g. "[-T]" or "-T enable bias"),
# not as part of "DVB-T" or similar text.
return bool(re.search(r'(?<!\w)-T\b', help_text))
return bool(re.search(r"(?<!\w)-T\b", help_text))
except Exception as e:
logger.warning(f"Could not detect bias-t support for {tool_path}: {e}")
return False
@@ -54,13 +49,10 @@ def enable_bias_t_via_rtl_biast(device_index: int = 0) -> bool:
Returns True if bias-t was enabled successfully.
"""
rtl_biast_path = get_tool_path('rtl_biast') or 'rtl_biast'
rtl_biast_path = get_tool_path("rtl_biast") or "rtl_biast"
try:
result = subprocess.run(
[rtl_biast_path, '-b', '1', '-d', str(device_index)],
capture_output=True,
text=True,
timeout=5
[rtl_biast_path, "-b", "1", "-d", str(device_index)], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
logger.info(f"Bias-t enabled via rtl_biast on device {device_index}")
@@ -83,13 +75,10 @@ def disable_bias_t_via_rtl_biast(device_index: int = 0) -> bool:
Returns True if bias-t was disabled successfully.
"""
rtl_biast_path = get_tool_path('rtl_biast') or 'rtl_biast'
rtl_biast_path = get_tool_path("rtl_biast") or "rtl_biast"
try:
result = subprocess.run(
[rtl_biast_path, '-b', '0', '-d', str(device_index)],
capture_output=True,
text=True,
timeout=5
[rtl_biast_path, "-b", "0", "-d", str(device_index)], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
logger.info(f"Bias-t disabled via rtl_biast on device {device_index}")
@@ -114,17 +103,12 @@ def _get_dump1090_bias_t_flag(dump1090_path: str) -> str | None:
Returns the correct flag string or None if bias-t is not supported.
"""
try:
result = subprocess.run(
[dump1090_path, '--help'],
capture_output=True,
text=True,
timeout=5
)
result = subprocess.run([dump1090_path, "--help"], capture_output=True, text=True, timeout=5)
help_text = result.stdout + result.stderr
# Check for dump1090-fa/readsb style flag (no hyphen)
if '--enable-biast' in help_text:
return '--enable-biast'
if "--enable-biast" in help_text:
return "--enable-biast"
# No bias-t support found
return None
@@ -146,7 +130,7 @@ class RTLSDRCommandBuilder(CommandBuilder):
supports_bias_t=True,
supports_ppm=True,
tx_capable=False,
supports_iq_capture=True
supports_iq_capture=True,
)
def _get_device_arg(self, device: SDRDevice) -> str:
@@ -180,50 +164,49 @@ class RTLSDRCommandBuilder(CommandBuilder):
direct_sampling: Enable direct sampling mode (0=off, 1=I-branch,
2=Q-branch). Use 2 for HF reception below 24 MHz.
"""
rtl_fm_path = get_tool_path('rtl_fm') or 'rtl_fm'
rtl_fm_path = get_tool_path("rtl_fm") or "rtl_fm"
demod_mode = _rtl_fm_demod_mode(modulation)
cmd = [
rtl_fm_path,
'-d', self._get_device_arg(device),
'-f', f'{frequency_mhz}M',
'-M', demod_mode,
'-s', str(sample_rate),
"-d",
self._get_device_arg(device),
"-f",
f"{frequency_mhz}M",
"-M",
demod_mode,
"-s",
str(sample_rate),
]
if gain is not None and gain > 0:
cmd.extend(['-g', str(gain)])
cmd.extend(["-g", str(gain)])
if ppm is not None and ppm != 0:
cmd.extend(['-p', str(ppm)])
cmd.extend(["-p", str(ppm)])
if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)])
cmd.extend(["-l", str(squelch)])
if direct_sampling is not None:
# Older rtl_fm builds (common in Docker/distro packages) don't
# support -D; they use -E direct / -E direct2 instead.
if direct_sampling == 1:
cmd.extend(['-E', 'direct'])
cmd.extend(["-E", "direct"])
elif direct_sampling == 2:
cmd.extend(['-E', 'direct2'])
cmd.extend(["-E", "direct2"])
if bias_t:
if _rtl_tool_supports_bias_t(rtl_fm_path):
cmd.append('-T')
cmd.append("-T")
else:
logger.warning("Bias-t requested but rtl_fm does not support -T (RTL-SDR Blog drivers required).")
# Output to stdout for piping
cmd.append('-')
cmd.append("-")
return cmd
def build_adsb_command(
self,
device: SDRDevice,
gain: float | None = None,
bias_t: bool = False
) -> list[str]:
def build_adsb_command(self, device: SDRDevice, gain: float | None = None, bias_t: bool = False) -> list[str]:
"""
Build dump1090 command for ADS-B decoding.
@@ -239,16 +222,11 @@ class RTLSDRCommandBuilder(CommandBuilder):
"connect to its SBS output (port 30003)."
)
dump1090_path = get_tool_path('dump1090') or 'dump1090'
cmd = [
dump1090_path,
'--net',
'--device-index', str(device.index),
'--quiet'
]
dump1090_path = get_tool_path("dump1090") or "dump1090"
cmd = [dump1090_path, "--net", "--device-index", str(device.index), "--quiet"]
if gain is not None:
cmd.extend(['--gain', str(int(gain))])
cmd.extend(["--gain", str(int(gain))])
if bias_t:
bias_t_flag = _get_dump1090_bias_t_flag(dump1090_path)
@@ -271,7 +249,7 @@ class RTLSDRCommandBuilder(CommandBuilder):
frequency_mhz: float = 433.92,
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build rtl_433 command for ISM band sensor decoding.
@@ -281,27 +259,22 @@ class RTLSDRCommandBuilder(CommandBuilder):
Note: rtl_433's -T flag is for timeout, NOT bias-t.
Bias-t is enabled via the device string suffix :biast=1
"""
rtl_433_path = get_tool_path('rtl_433') or 'rtl_433'
rtl_433_path = get_tool_path("rtl_433") or "rtl_433"
# Build device argument with optional bias-t suffix
# rtl_433 uses :biast=1 suffix on device string, not -T flag
# (-T is timeout in rtl_433)
device_arg = self._get_device_arg(device)
if bias_t:
device_arg = f'{device_arg}:biast=1'
device_arg = f"{device_arg}:biast=1"
cmd = [
rtl_433_path,
'-d', device_arg,
'-f', f'{frequency_mhz}M',
'-F', 'json'
]
cmd = [rtl_433_path, "-d", device_arg, "-f", f"{frequency_mhz}M", "-F", "json"]
if gain is not None and gain > 0:
cmd.extend(['-g', str(int(gain))])
cmd.extend(["-g", str(int(gain))])
if ppm is not None and ppm != 0:
cmd.extend(['-p', str(ppm)])
cmd.extend(["-p", str(ppm)])
return cmd
@@ -322,25 +295,27 @@ class RTLSDRCommandBuilder(CommandBuilder):
"""
if device.is_network:
raise ValueError(
"AIS-catcher does not support rtl_tcp. "
"For remote AIS, run AIS-catcher on the remote machine."
"AIS-catcher does not support rtl_tcp. For remote AIS, run AIS-catcher on the remote machine."
)
cmd = [
'AIS-catcher',
f'-d:{device.index}', # Device index (colon format required)
'-S', str(tcp_port), 'JSON_FULL', 'on', # TCP server with full JSON output
'-q', # Quiet mode (less console output)
"AIS-catcher",
f"-d:{device.index}", # Device index (colon format required)
"-S",
str(tcp_port),
"JSON_FULL",
"on", # TCP server with full JSON output
"-q", # Quiet mode (less console output)
]
if gain is not None and gain > 0:
cmd.extend(['-gr', 'TUNER', str(int(gain))])
cmd.extend(["-gr", "TUNER", str(int(gain))])
if bias_t:
cmd.extend(['-gr', 'BIASTEE', 'on'])
cmd.extend(["-gr", "BIASTEE", "on"])
if udp_host and udp_port:
cmd.extend(['-u', udp_host, str(udp_port)])
cmd.extend(["-u", udp_host, str(udp_port)])
return cmd
@@ -352,37 +327,40 @@ class RTLSDRCommandBuilder(CommandBuilder):
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False,
output_format: str = 'cu8',
output_format: str = "cu8",
) -> list[str]:
"""
Build rtl_sdr command for raw I/Q capture.
Outputs unsigned 8-bit I/Q pairs to stdout for waterfall display.
"""
rtl_sdr_path = get_tool_path('rtl_sdr') or 'rtl_sdr'
rtl_sdr_path = get_tool_path("rtl_sdr") or "rtl_sdr"
freq_hz = int(frequency_mhz * 1e6)
cmd = [
rtl_sdr_path,
'-d', self._get_device_arg(device),
'-f', str(freq_hz),
'-s', str(sample_rate),
"-d",
self._get_device_arg(device),
"-f",
str(freq_hz),
"-s",
str(sample_rate),
]
if gain is not None and gain > 0:
cmd.extend(['-g', str(gain)])
cmd.extend(["-g", str(gain)])
if ppm is not None and ppm != 0:
cmd.extend(['-p', str(ppm)])
cmd.extend(["-p", str(ppm)])
if bias_t:
if _rtl_tool_supports_bias_t(rtl_sdr_path):
cmd.append('-T')
cmd.append("-T")
else:
logger.warning("Bias-t requested but rtl_sdr does not support -T (RTL-SDR Blog drivers required).")
# Output to stdout
cmd.append('-')
cmd.append("-")
return cmd
@@ -394,4 +372,3 @@ class RTLSDRCommandBuilder(CommandBuilder):
def get_sdr_type(cls) -> SDRType:
"""Return SDR type."""
return SDRType.RTL_SDR
+54 -59
View File
@@ -18,21 +18,21 @@ class SDRPlayCommandBuilder(CommandBuilder):
# SDRPlay RSP capabilities (RSPdx, RSP1A, RSPduo, etc.)
CAPABILITIES = SDRCapabilities(
sdr_type=SDRType.SDRPLAY,
freq_min_mhz=0.001, # 1 kHz
freq_max_mhz=2000.0, # 2 GHz
freq_min_mhz=0.001, # 1 kHz
freq_max_mhz=2000.0, # 2 GHz
gain_min=0.0,
gain_max=59.0, # IFGR range
gain_max=59.0, # IFGR range
sample_rates=[62500, 96000, 125000, 192000, 250000, 384000, 500000, 1000000, 2000000],
supports_bias_t=True,
supports_ppm=False, # SDRPlay has TCXO, no PPM needed
tx_capable=False
supports_ppm=False, # SDRPlay has TCXO, no PPM needed
tx_capable=False,
)
def _build_device_string(self, device: SDRDevice) -> str:
"""Build SoapySDR device string for SDRPlay."""
if device.serial and device.serial != 'N/A':
return f'driver=sdrplay,serial={device.serial}'
return 'driver=sdrplay'
if device.serial and device.serial != "N/A":
return f"driver=sdrplay,serial={device.serial}"
return "driver=sdrplay"
def build_fm_demod_command(
self,
@@ -43,7 +43,7 @@ class SDRPlayCommandBuilder(CommandBuilder):
ppm: int | None = None,
modulation: str = "fm",
squelch: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build SoapySDR rx_fm command for FM demodulation.
@@ -52,35 +52,34 @@ class SDRPlayCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
rx_fm_path = get_tool_path('rx_fm') or 'rx_fm'
rx_fm_path = get_tool_path("rx_fm") or "rx_fm"
cmd = [
rx_fm_path,
'-d', device_str,
'-f', f'{frequency_mhz}M',
'-M', modulation,
'-s', str(sample_rate),
"-d",
device_str,
"-f",
f"{frequency_mhz}M",
"-M",
modulation,
"-s",
str(sample_rate),
]
if gain is not None and gain > 0:
cmd.extend(['-g', f'IFGR={int(gain)}'])
cmd.extend(["-g", f"IFGR={int(gain)}"])
if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)])
cmd.extend(["-l", str(squelch)])
if bias_t:
cmd.extend(['-T'])
cmd.extend(["-T"])
# Output to stdout
cmd.append('-')
cmd.append("-")
return cmd
def build_adsb_command(
self,
device: SDRDevice,
gain: float | None = None,
bias_t: bool = False
) -> list[str]:
def build_adsb_command(self, device: SDRDevice, gain: float | None = None, bias_t: bool = False) -> list[str]:
"""
Build dump1090/readsb command with SoapySDR support for ADS-B decoding.
@@ -88,19 +87,13 @@ class SDRPlayCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
cmd = [
'readsb',
'--net',
'--device-type', 'soapysdr',
'--device', device_str,
'--quiet'
]
cmd = ["readsb", "--net", "--device-type", "soapysdr", "--device", device_str, "--quiet"]
if gain is not None:
cmd.extend(['--gain', str(int(gain))])
cmd.extend(["--gain", str(int(gain))])
if bias_t:
cmd.extend(['--enable-bias-t'])
cmd.extend(["--enable-bias-t"])
return cmd
@@ -110,7 +103,7 @@ class SDRPlayCommandBuilder(CommandBuilder):
frequency_mhz: float = 433.92,
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False
bias_t: bool = False,
) -> list[str]:
"""
Build rtl_433 command with SoapySDR support for ISM band decoding.
@@ -119,18 +112,13 @@ class SDRPlayCommandBuilder(CommandBuilder):
"""
device_str = self._build_device_string(device)
cmd = [
'rtl_433',
'-d', device_str,
'-f', f'{frequency_mhz}M',
'-F', 'json'
]
cmd = ["rtl_433", "-d", device_str, "-f", f"{frequency_mhz}M", "-F", "json"]
if gain is not None and gain > 0:
cmd.extend(['-g', str(int(gain))])
cmd.extend(["-g", str(int(gain))])
if bias_t:
cmd.extend(['-T'])
cmd.extend(["-T"])
return cmd
@@ -151,21 +139,24 @@ class SDRPlayCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
cmd = [
'AIS-catcher',
'-d', f'soapysdr -d {device_str}',
'-S', str(tcp_port),
'-o', '5',
'-q',
"AIS-catcher",
"-d",
f"soapysdr -d {device_str}",
"-S",
str(tcp_port),
"-o",
"5",
"-q",
]
if gain is not None and gain > 0:
cmd.extend(['-gr', 'tuner', str(int(gain))])
cmd.extend(["-gr", "tuner", str(int(gain))])
if bias_t:
cmd.extend(['-gr', 'biastee', '1'])
cmd.extend(["-gr", "biastee", "1"])
if udp_host and udp_port:
cmd.extend(['-u', udp_host, str(udp_port)])
cmd.extend(["-u", udp_host, str(udp_port)])
return cmd
@@ -177,7 +168,7 @@ class SDRPlayCommandBuilder(CommandBuilder):
gain: float | None = None,
ppm: int | None = None,
bias_t: bool = False,
output_format: str = 'cu8',
output_format: str = "cu8",
) -> list[str]:
"""
Build rx_sdr command for raw I/Q capture with SDRPlay.
@@ -187,23 +178,27 @@ class SDRPlayCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
freq_hz = int(frequency_mhz * 1e6)
rx_sdr_path = get_tool_path('rx_sdr') or 'rx_sdr'
rx_sdr_path = get_tool_path("rx_sdr") or "rx_sdr"
cmd = [
rx_sdr_path,
'-d', device_str,
'-f', str(freq_hz),
'-s', str(sample_rate),
'-F', 'CU8',
"-d",
device_str,
"-f",
str(freq_hz),
"-s",
str(sample_rate),
"-F",
"CU8",
]
if gain is not None and gain > 0:
cmd.extend(['-g', f'IFGR={int(gain)}'])
cmd.extend(["-g", f"IFGR={int(gain)}"])
if bias_t:
cmd.append('-T')
cmd.append("-T")
# Output to stdout
cmd.append('-')
cmd.append("-")
return cmd
+12 -35
View File
@@ -12,13 +12,12 @@ from .base import SDRCapabilities, SDRDevice, SDRType
class SDRValidationError(ValueError):
"""Raised when SDR parameter validation fails."""
pass
def validate_frequency(
freq_mhz: float,
device: Optional[SDRDevice] = None,
capabilities: Optional[SDRCapabilities] = None
freq_mhz: float, device: Optional[SDRDevice] = None, capabilities: Optional[SDRCapabilities] = None
) -> float:
"""
Validate frequency against device capabilities.
@@ -41,11 +40,7 @@ def validate_frequency(
else:
# Default RTL-SDR range for backwards compatibility
caps = SDRCapabilities(
sdr_type=SDRType.RTL_SDR,
freq_min_mhz=24.0,
freq_max_mhz=1766.0,
gain_min=0.0,
gain_max=50.0
sdr_type=SDRType.RTL_SDR, freq_min_mhz=24.0, freq_max_mhz=1766.0, gain_min=0.0, gain_max=50.0
)
if not caps.freq_min_mhz <= freq_mhz <= caps.freq_max_mhz:
@@ -58,9 +53,7 @@ def validate_frequency(
def validate_gain(
gain: float,
device: Optional[SDRDevice] = None,
capabilities: Optional[SDRCapabilities] = None
gain: float, device: Optional[SDRDevice] = None, capabilities: Optional[SDRCapabilities] = None
) -> float:
"""
Validate gain against device capabilities.
@@ -83,11 +76,7 @@ def validate_gain(
else:
# Default range for backwards compatibility
caps = SDRCapabilities(
sdr_type=SDRType.RTL_SDR,
freq_min_mhz=24.0,
freq_max_mhz=1766.0,
gain_min=0.0,
gain_max=50.0
sdr_type=SDRType.RTL_SDR, freq_min_mhz=24.0, freq_max_mhz=1766.0, gain_min=0.0, gain_max=50.0
)
# Allow 0 for auto gain
@@ -96,8 +85,7 @@ def validate_gain(
if not caps.gain_min <= gain <= caps.gain_max:
raise SDRValidationError(
f"Gain {gain} dB out of range for {caps.sdr_type.value}. "
f"Valid range: {caps.gain_min}-{caps.gain_max} dB"
f"Gain {gain} dB out of range for {caps.sdr_type.value}. Valid range: {caps.gain_min}-{caps.gain_max} dB"
)
return gain
@@ -107,7 +95,7 @@ def validate_sample_rate(
rate: int,
device: Optional[SDRDevice] = None,
capabilities: Optional[SDRCapabilities] = None,
snap_to_nearest: bool = True
snap_to_nearest: bool = True,
) -> int:
"""
Validate sample rate against device capabilities.
@@ -143,16 +131,11 @@ def validate_sample_rate(
return closest
raise SDRValidationError(
f"Sample rate {rate} Hz not supported by {caps.sdr_type.value}. "
f"Valid rates: {caps.sample_rates}"
f"Sample rate {rate} Hz not supported by {caps.sdr_type.value}. Valid rates: {caps.sample_rates}"
)
def validate_ppm(
ppm: int,
device: Optional[SDRDevice] = None,
capabilities: Optional[SDRCapabilities] = None
) -> int:
def validate_ppm(ppm: int, device: Optional[SDRDevice] = None, capabilities: Optional[SDRCapabilities] = None) -> int:
"""
Validate PPM frequency correction.
@@ -183,9 +166,7 @@ def validate_ppm(
# Standard PPM range
if not -1000 <= ppm <= 1000:
raise SDRValidationError(
f"PPM correction {ppm} out of range. Valid range: -1000 to 1000"
)
raise SDRValidationError(f"PPM correction {ppm} out of range. Valid range: -1000 to 1000")
return ppm
@@ -204,9 +185,7 @@ def validate_device_index(index: int) -> int:
SDRValidationError: If index is out of range
"""
if not 0 <= index <= 255:
raise SDRValidationError(
f"Device index {index} out of range. Valid range: 0-255"
)
raise SDRValidationError(f"Device index {index} out of range. Valid range: 0-255")
return index
@@ -224,9 +203,7 @@ def validate_squelch(squelch: int) -> int:
SDRValidationError: If squelch is out of range
"""
if not 0 <= squelch <= 1000:
raise SDRValidationError(
f"Squelch {squelch} out of range. Valid range: 0-1000"
)
raise SDRValidationError(f"Squelch {squelch} out of range. Valid range: 0-1000")
return squelch