mirror of
https://github.com/smittix/intercept.git
synced 2026-07-06 00:28:12 -07:00
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:
+37
-46
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user