diff --git a/utils/sdr/__init__.py b/utils/sdr/__init__.py index 249735d..358dffe 100644 --- a/utils/sdr/__init__.py +++ b/utils/sdr/__init__.py @@ -31,6 +31,7 @@ from .rtlsdr import RTLSDRCommandBuilder from .limesdr import LimeSDRCommandBuilder from .hackrf import HackRFCommandBuilder from .airspy import AirspyCommandBuilder +from .sdrplay import SDRPlayCommandBuilder from .validation import ( SDRValidationError, validate_frequency, @@ -51,6 +52,7 @@ class SDRFactory: SDRType.LIME_SDR: LimeSDRCommandBuilder, SDRType.HACKRF: HackRFCommandBuilder, SDRType.AIRSPY: AirspyCommandBuilder, + SDRType.SDRPLAY: SDRPlayCommandBuilder, } @classmethod @@ -217,6 +219,7 @@ __all__ = [ 'LimeSDRCommandBuilder', 'HackRFCommandBuilder', 'AirspyCommandBuilder', + 'SDRPlayCommandBuilder', # Validation 'SDRValidationError', 'validate_frequency', diff --git a/utils/sdr/base.py b/utils/sdr/base.py index 5755dd0..7b43c2f 100644 --- a/utils/sdr/base.py +++ b/utils/sdr/base.py @@ -19,6 +19,7 @@ class SDRType(Enum): LIME_SDR = "limesdr" HACKRF = "hackrf" AIRSPY = "airspy" + SDRPLAY = "sdrplay" # Future support # USRP = "usrp" # BLADE_RF = "bladerf" diff --git a/utils/sdr/detection.py b/utils/sdr/detection.py index 95fba91..aaafb94 100644 --- a/utils/sdr/detection.py +++ b/utils/sdr/detection.py @@ -29,12 +29,14 @@ def _get_capabilities_for_type(sdr_type: SDRType) -> SDRCapabilities: from .limesdr import LimeSDRCommandBuilder from .hackrf import HackRFCommandBuilder from .airspy import AirspyCommandBuilder + from .sdrplay import SDRPlayCommandBuilder builders = { SDRType.RTL_SDR: RTLSDRCommandBuilder, SDRType.LIME_SDR: LimeSDRCommandBuilder, SDRType.HACKRF: HackRFCommandBuilder, SDRType.AIRSPY: AirspyCommandBuilder, + SDRType.SDRPLAY: SDRPlayCommandBuilder, } builder_class = builders.get(sdr_type) @@ -64,6 +66,7 @@ def _driver_to_sdr_type(driver: str) -> Optional[SDRType]: '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, diff --git a/utils/sdr/sdrplay.py b/utils/sdr/sdrplay.py new file mode 100644 index 0000000..3143dc0 --- /dev/null +++ b/utils/sdr/sdrplay.py @@ -0,0 +1,143 @@ +""" +SDRPlay command builder implementation. + +Uses SoapySDR-based tools for FM demodulation and signal capture. +SDRPlay RSP devices support 1 kHz to 2 GHz frequency range. +""" + +from __future__ import annotations + +from typing import Optional + +from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType + + +class SDRPlayCommandBuilder(CommandBuilder): + """SDRPlay command builder using SoapySDR tools.""" + + # 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 + gain_min=0.0, + 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 + ) + + 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' + + def build_fm_demod_command( + self, + device: SDRDevice, + frequency_mhz: float, + sample_rate: int = 22050, + gain: Optional[float] = None, + ppm: Optional[int] = None, + modulation: str = "fm", + squelch: Optional[int] = None, + bias_t: bool = False + ) -> list[str]: + """ + Build SoapySDR rx_fm command for FM demodulation. + + For pager decoding with SDRPlay. + """ + device_str = self._build_device_string(device) + + cmd = [ + 'rx_fm', + '-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)}']) + + if squelch is not None and squelch > 0: + cmd.extend(['-l', str(squelch)]) + + if bias_t: + cmd.extend(['-T']) + + # Output to stdout + cmd.append('-') + + return cmd + + def build_adsb_command( + self, + device: SDRDevice, + gain: Optional[float] = None, + bias_t: bool = False + ) -> list[str]: + """ + Build dump1090/readsb command with SoapySDR support for ADS-B decoding. + + Uses readsb which has better SoapySDR support. + """ + device_str = self._build_device_string(device) + + cmd = [ + 'readsb', + '--net', + '--device-type', 'soapysdr', + '--device', device_str, + '--quiet' + ] + + if gain is not None: + cmd.extend(['--gain', str(int(gain))]) + + if bias_t: + cmd.extend(['--enable-bias-t']) + + return cmd + + def build_ism_command( + self, + device: SDRDevice, + frequency_mhz: float = 433.92, + gain: Optional[float] = None, + ppm: Optional[int] = None, + bias_t: bool = False + ) -> list[str]: + """ + Build rtl_433 command with SoapySDR support for ISM band decoding. + + rtl_433 has native SoapySDR support via -d flag. + """ + device_str = self._build_device_string(device) + + 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))]) + + if bias_t: + cmd.extend(['-T']) + + return cmd + + def get_capabilities(self) -> SDRCapabilities: + """Return SDRPlay capabilities.""" + return self.CAPABILITIES + + @classmethod + def get_sdr_type(cls) -> SDRType: + """Return SDR type.""" + return SDRType.SDRPLAY