diff --git a/CHANGELOG.md b/CHANGELOG.md index d480dc6..e6815e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to iNTERCEPT will be documented in this file. +## [2.31.0] - 2026-07-07 + +### Added +- **BladeRF support** — bladeRF 2.0 micro (47 MHz – 6 GHz) and bladeRF x40/x115 (300 MHz – 3.8 GHz) detected and usable via SoapyBladeRF. TX capable. Supports FM demod, ADS-B, ISM, AIS, and I/Q capture. +- **HydraSDR RFOne support** — Detected and usable via SoapyHydraSDR (24 MHz – 1800 MHz, RX only, 10 MHz instantaneous bandwidth). Supports FM demod, ADS-B, ISM, AIS, and I/Q capture. + +--- + ## [2.30.0] - 2026-07-07 ### Added diff --git a/config.py b/config.py index b12ce53..db6cd33 100644 --- a/config.py +++ b/config.py @@ -7,10 +7,18 @@ import os import sys # Application version -VERSION = "2.30.0" +VERSION = "2.31.0" # Changelog - latest release notes (shown on welcome screen) CHANGELOG = [ + { + "version": "2.31.0", + "date": "July 2026", + "highlights": [ + "Feat: BladeRF support — bladeRF 2.0 micro and x40/x115 detected and usable via SoapyBladeRF (47 MHz – 6 GHz, TX capable)", + "Feat: HydraSDR RFOne support — detected and usable via SoapyHydraSDR (24 MHz – 1800 MHz, RX only)", + ], + }, { "version": "2.30.0", "date": "July 2026", diff --git a/pyproject.toml b/pyproject.toml index 3b744fe..a120663 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "intercept" -version = "2.30.0" +version = "2.31.0" description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth" readme = "README.md" requires-python = ">=3.9" diff --git a/utils/sdr/__init__.py b/utils/sdr/__init__.py index 20ef0eb..a265e82 100644 --- a/utils/sdr/__init__.py +++ b/utils/sdr/__init__.py @@ -27,8 +27,10 @@ from typing import Optional from .airspy import AirspyCommandBuilder from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType +from .bladerf import BladeRFCommandBuilder from .detection import detect_all_devices, invalidate_device_cache, probe_rtlsdr_device from .hackrf import HackRFCommandBuilder +from .hydrasdr import HydraSDRCommandBuilder from .limesdr import LimeSDRCommandBuilder from .rtlsdr import RTLSDRCommandBuilder from .sdrplay import SDRPlayCommandBuilder @@ -55,6 +57,8 @@ class SDRFactory: SDRType.AIRSPY: AirspyCommandBuilder, SDRType.SDRPLAY: SDRPlayCommandBuilder, SDRType.USRP: USRPCommandBuilder, + SDRType.BLADE_RF: BladeRFCommandBuilder, + SDRType.HYDRA_SDR: HydraSDRCommandBuilder, } @classmethod @@ -214,6 +218,8 @@ __all__ = [ "AirspyCommandBuilder", "SDRPlayCommandBuilder", "USRPCommandBuilder", + "BladeRFCommandBuilder", + "HydraSDRCommandBuilder", # Validation "SDRValidationError", "validate_frequency", diff --git a/utils/sdr/base.py b/utils/sdr/base.py index 4b4236b..01c51f4 100644 --- a/utils/sdr/base.py +++ b/utils/sdr/base.py @@ -21,8 +21,8 @@ class SDRType(Enum): AIRSPY = "airspy" SDRPLAY = "sdrplay" USRP = "usrp" - # Future support - # BLADE_RF = "bladerf" + BLADE_RF = "bladerf" + HYDRA_SDR = "hydrasdr" @dataclass diff --git a/utils/sdr/bladerf.py b/utils/sdr/bladerf.py new file mode 100644 index 0000000..c282437 --- /dev/null +++ b/utils/sdr/bladerf.py @@ -0,0 +1,153 @@ +""" +BladeRF command builder implementation. + +Uses SoapySDR (via SoapyBladeRF) for all signal processing tasks. +Targets bladeRF 2.0 micro (47 MHz – 6 GHz) but is compatible with +bladeRF x40/x115 (300 MHz – 3.8 GHz) — the SoapySDR layer handles +per-device frequency clamping transparently. + +Requires: libbladeRF, SoapySDR, SoapyBladeRF module installed. +""" + +from __future__ import annotations + +from utils.dependencies import get_tool_path + +from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType + + +class BladeRFCommandBuilder(CommandBuilder): + """BladeRF command builder using SoapySDR / SoapyBladeRF.""" + + CAPABILITIES = SDRCapabilities( + sdr_type=SDRType.BLADE_RF, + freq_min_mhz=47.0, # bladeRF 2.0 micro lower bound + freq_max_mhz=6000.0, + gain_min=0.0, + gain_max=66.0, # LNA (0/3/6) + RXVGA1 (5-30) + RXVGA2 (0-30) + sample_rates=[1000000, 2000000, 4000000, 8000000, 10000000, 20000000, 40000000], + supports_bias_t=False, + supports_ppm=False, + tx_capable=True, + supports_iq_capture=True, + ) + + def _build_device_string(self, device: SDRDevice) -> str: + """Build SoapySDR device string for BladeRF.""" + if device.serial and device.serial not in ("N/A", "Unknown"): + return f"driver=bladerf,serial={device.serial}" + return "driver=bladerf" + + def build_fm_demod_command( + self, + device: SDRDevice, + frequency_mhz: float, + sample_rate: int = 22050, + gain: float | None = None, + ppm: int | None = None, + modulation: str = "fm", + squelch: int | None = None, + bias_t: bool = False, + ) -> list[str]: + device_str = self._build_device_string(device) + 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), + ] + if gain is not None and gain > 0: + cmd.extend(["-g", str(int(gain))]) + if squelch is not None and squelch > 0: + cmd.extend(["-l", str(squelch)]) + cmd.append("-") + return cmd + + def build_adsb_command(self, device: SDRDevice, gain: float | None = None, bias_t: bool = False) -> list[str]: + 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))]) + return cmd + + def build_ism_command( + self, + device: SDRDevice, + frequency_mhz: float = 433.92, + gain: float | None = None, + ppm: int | None = None, + bias_t: bool = False, + ) -> list[str]: + 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))]) + return cmd + + def build_ais_command( + self, + device: SDRDevice, + gain: float | None = None, + bias_t: bool = False, + tcp_port: int = 10110, + udp_host: str | None = None, + udp_port: int | None = None, + ) -> list[str]: + device_str = self._build_device_string(device) + cmd = [ + "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))]) + if udp_host and udp_port: + cmd.extend(["-u", udp_host, str(udp_port)]) + return cmd + + def build_iq_capture_command( + self, + device: SDRDevice, + frequency_mhz: float, + sample_rate: int = 2048000, + gain: float | None = None, + ppm: int | None = None, + bias_t: bool = False, + output_format: str = "cu8", + ) -> list[str]: + device_str = self._build_device_string(device) + freq_hz = int(frequency_mhz * 1e6) + 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", + ] + if gain is not None and gain > 0: + cmd.extend(["-g", str(int(gain))]) + cmd.append("-") + return cmd + + def get_capabilities(self) -> SDRCapabilities: + return self.CAPABILITIES + + @classmethod + def get_sdr_type(cls) -> SDRType: + return SDRType.BLADE_RF diff --git a/utils/sdr/detection.py b/utils/sdr/detection.py index e6f4c5e..ba43e16 100644 --- a/utils/sdr/detection.py +++ b/utils/sdr/detection.py @@ -49,7 +49,9 @@ def _get_capabilities_for_type(sdr_type: SDRType) -> SDRCapabilities: """Get default capabilities for an SDR type.""" # Import here to avoid circular imports from .airspy import AirspyCommandBuilder + from .bladerf import BladeRFCommandBuilder from .hackrf import HackRFCommandBuilder + from .hydrasdr import HydraSDRCommandBuilder from .limesdr import LimeSDRCommandBuilder from .rtlsdr import RTLSDRCommandBuilder from .sdrplay import SDRPlayCommandBuilder @@ -62,6 +64,8 @@ def _get_capabilities_for_type(sdr_type: SDRType) -> SDRCapabilities: SDRType.AIRSPY: AirspyCommandBuilder, SDRType.SDRPLAY: SDRPlayCommandBuilder, SDRType.USRP: USRPCommandBuilder, + SDRType.BLADE_RF: BladeRFCommandBuilder, + SDRType.HYDRA_SDR: HydraSDRCommandBuilder, } builder_class = builders.get(sdr_type) @@ -93,8 +97,8 @@ def _driver_to_sdr_type(driver: str) -> SDRType | None: "airspyhf": SDRType.AIRSPY, # Airspy HF+ uses same builder "sdrplay": SDRType.SDRPLAY, "uhd": SDRType.USRP, - # Future support - # 'bladerf': SDRType.BLADE_RF, + "bladerf": SDRType.BLADE_RF, + "hydrasdr": SDRType.HYDRA_SDR, } return mapping.get(driver.lower()) diff --git a/utils/sdr/hydrasdr.py b/utils/sdr/hydrasdr.py new file mode 100644 index 0000000..04aabab --- /dev/null +++ b/utils/sdr/hydrasdr.py @@ -0,0 +1,157 @@ +""" +HydraSDR RFOne command builder implementation. + +Uses SoapySDR (via SoapyHydraSDR plugin) for all signal processing tasks. +The RFOne is an RX-only receiver covering 24 MHz to 1800 MHz with a +10 MHz instantaneous bandwidth. + +Gain is a linear scale 0–21 (not dB); the SoapyHydraSDR plugin maps this +to the hardware's RF gain stages. + +Requires: rfone_host library, SoapySDR, SoapyHydraSDR module installed. +Official: https://github.com/hydrasdr/SoapyHydraSDR +""" + +from __future__ import annotations + +from utils.dependencies import get_tool_path + +from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType + + +class HydraSDRCommandBuilder(CommandBuilder): + """HydraSDR RFOne command builder using SoapySDR / SoapyHydraSDR.""" + + CAPABILITIES = SDRCapabilities( + sdr_type=SDRType.HYDRA_SDR, + freq_min_mhz=24.0, + freq_max_mhz=1800.0, + gain_min=0.0, + gain_max=21.0, # linear scale; recommended starting point: 12 + sample_rates=[2500000, 5000000, 10000000], + supports_bias_t=False, + supports_ppm=False, + tx_capable=False, + supports_iq_capture=True, + ) + + def _build_device_string(self, device: SDRDevice) -> str: + """Build SoapySDR device string for HydraSDR RFOne.""" + if device.serial and device.serial not in ("N/A", "Unknown"): + return f"driver=hydrasdr,serial={device.serial}" + return "driver=hydrasdr" + + def build_fm_demod_command( + self, + device: SDRDevice, + frequency_mhz: float, + sample_rate: int = 22050, + gain: float | None = None, + ppm: int | None = None, + modulation: str = "fm", + squelch: int | None = None, + bias_t: bool = False, + ) -> list[str]: + device_str = self._build_device_string(device) + 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), + ] + if gain is not None and gain > 0: + cmd.extend(["-g", str(int(gain))]) + if squelch is not None and squelch > 0: + cmd.extend(["-l", str(squelch)]) + cmd.append("-") + return cmd + + def build_adsb_command(self, device: SDRDevice, gain: float | None = None, bias_t: bool = False) -> list[str]: + # 1090 MHz is within the RFOne's range (24–1800 MHz) + 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))]) + return cmd + + def build_ism_command( + self, + device: SDRDevice, + frequency_mhz: float = 433.92, + gain: float | None = None, + ppm: int | None = None, + bias_t: bool = False, + ) -> list[str]: + 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))]) + return cmd + + def build_ais_command( + self, + device: SDRDevice, + gain: float | None = None, + bias_t: bool = False, + tcp_port: int = 10110, + udp_host: str | None = None, + udp_port: int | None = None, + ) -> list[str]: + device_str = self._build_device_string(device) + cmd = [ + "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))]) + if udp_host and udp_port: + cmd.extend(["-u", udp_host, str(udp_port)]) + return cmd + + def build_iq_capture_command( + self, + device: SDRDevice, + frequency_mhz: float, + sample_rate: int = 2048000, + gain: float | None = None, + ppm: int | None = None, + bias_t: bool = False, + output_format: str = "cu8", + ) -> list[str]: + device_str = self._build_device_string(device) + freq_hz = int(frequency_mhz * 1e6) + 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", + ] + if gain is not None and gain > 0: + cmd.extend(["-g", str(int(gain))]) + cmd.append("-") + return cmd + + def get_capabilities(self) -> SDRCapabilities: + return self.CAPABILITIES + + @classmethod + def get_sdr_type(cls) -> SDRType: + return SDRType.HYDRA_SDR