Add multi-SDR hardware support (LimeSDR, HackRF) and setup script

- Add SDR hardware abstraction layer (utils/sdr/) with support for:
  - RTL-SDR (existing, using native rtl_* tools)
  - LimeSDR (via SoapySDR)
  - HackRF (via SoapySDR)
- Add hardware type selector to UI with capabilities display
- Add automatic device detection across all supported hardware
- Add hardware-specific parameter validation (frequency/gain ranges)
- Add setup.sh script for automated dependency installation
- Update README with multi-SDR docs, installation guide, troubleshooting
- Add SoapySDR/LimeSDR/HackRF to dependency definitions
- Fix dump1090 detection for Homebrew on Apple Silicon Macs
- Remove defunct NOAA-15/18/19 satellites, add NOAA-21

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-01-02 14:23:51 +00:00
parent 3437a2fc0a
commit 5ed9674e1f
17 changed files with 1957 additions and 92 deletions

151
README.md
View File

@@ -172,7 +172,9 @@ Instead of running command-line tools manually, INTERCEPT handles the process ma
- **Message export** to CSV/JSON
- **Signal activity meter** and waterfall display
- **Message logging** to file with timestamps
- **RTL-SDR device detection** and selection
- **Multi-SDR hardware support** - RTL-SDR, LimeSDR, HackRF
- **Automatic device detection** across all supported hardware
- **Hardware-specific validation** - frequency/gain ranges per device type
- **Configurable gain and PPM correction**
- **Device intelligence** dashboard with tracking
- **Disclaimer acceptance** on first use
@@ -183,7 +185,10 @@ Instead of running command-line tools manually, INTERCEPT handles the process ma
## Requirements
### Hardware
- RTL-SDR compatible dongle (RTL2832U based)
- **SDR Device** (one of the following):
- RTL-SDR compatible dongle (RTL2832U based) - most common, budget-friendly
- LimeSDR / LimeSDR Mini - wider frequency range (100 kHz - 3.8 GHz)
- HackRF One - ultra-wide frequency range (1 MHz - 6 GHz)
- WiFi adapter capable of monitor mode (for WiFi features)
- Bluetooth adapter (for Bluetooth features)
@@ -203,17 +208,41 @@ Instead of running command-line tools manually, INTERCEPT handles the process ma
Install the tools for the features you need:
#### Core SDR Tools
| Tool | macOS | Ubuntu/Debian | Purpose |
|------|-------|---------------|---------|
| rtl-sdr | `brew install rtl-sdr` | `sudo apt install rtl-sdr` | Required for all SDR features |
| multimon-ng | `Use MacPorts for now sudo ports install multimon-ng` | `sudo apt install multimon-ng` | Pager decoding |
| rtl-sdr | `brew install librtlsdr` | `sudo apt install rtl-sdr` | RTL-SDR support |
| multimon-ng | `sudo port install multimon-ng` | `sudo apt install multimon-ng` | Pager decoding |
| rtl_433 | `brew install rtl_433` | `sudo apt install rtl-433` | 433MHz sensors |
| dump1090 | `brew install dump1090-mutability` | `sudo apt install dump1090-mutability` | ADS-B aircraft |
| aircrack-ng | `brew install aircrack-ng` | `sudo apt install aircrack-ng` | WiFi reconnaissance |
| bluez | Built-in (limited) | `sudo apt install bluez bluetooth` | Bluetooth scanning |
#### Additional SDR Hardware (Optional)
For LimeSDR or HackRF support, install SoapySDR and the appropriate driver:
| Tool | macOS | Ubuntu/Debian | Purpose |
|------|-------|---------------|---------|
| SoapySDR | `brew install soapysdr` | `sudo apt install soapysdr-tools` | Universal SDR abstraction |
| LimeSDR | `brew install limesuite soapylms7` | `sudo apt install limesuite soapysdr-module-lms7` | LimeSDR support |
| HackRF | `brew install hackrf soapyhackrf` | `sudo apt install hackrf soapysdr-module-hackrf` | HackRF support |
| readsb | Build from source | Build from source | ADS-B with SoapySDR |
> **Note:** RTL-SDR works out of the box. LimeSDR and HackRF require SoapySDR plus the hardware-specific driver.
### Install and run
**Option 1: Automated setup (recommended)**
```bash
git clone https://github.com/smittix/intercept.git
cd intercept
./setup.sh # Installs Python deps and checks for external tools
sudo python3 intercept.py
```
**Option 2: Manual setup**
```bash
git clone https://github.com/smittix/intercept.git
cd intercept
@@ -221,10 +250,30 @@ pip install -r requirements.txt
sudo python3 intercept.py
```
**Option 3: Using a virtual environment**
```bash
git clone https://github.com/smittix/intercept.git
cd intercept
python3 -m venv venv
source venv/bin/activate # On Linux/macOS
pip install -r requirements.txt
sudo venv/bin/python intercept.py
```
Open `http://localhost:5050` in your browser.
> **Note:** Running as root/sudo is recommended for full functionality (monitor mode, raw sockets, etc.)
### Check dependencies
Run the built-in dependency checker to see what's installed and what's missing:
```bash
python3 intercept.py --check-deps
```
This will show the status of all required tools and provide installation instructions for your platform.
### Command-line options
```
@@ -241,11 +290,12 @@ python3 intercept.py --help
## Usage
### Pager Mode
1. **Select Device** - Choose your RTL-SDR device from the dropdown
2. **Set Frequency** - Enter a frequency in MHz or use a preset
3. **Choose Protocols** - Select which protocols to decode (POCSAG/FLEX)
4. **Adjust Settings** - Set gain, squelch, and PPM correction as needed
5. **Start Decoding** - Click the green "Start Decoding" button
1. **Select Hardware** - Choose your SDR type (RTL-SDR, LimeSDR, or HackRF)
2. **Select Device** - Choose your SDR device from the dropdown
3. **Set Frequency** - Enter a frequency in MHz or use a preset
4. **Choose Protocols** - Select which protocols to decode (POCSAG/FLEX)
5. **Adjust Settings** - Set gain, squelch, and PPM correction as needed
6. **Start Decoding** - Click the green "Start Decoding" button
### WiFi Mode
1. **Select Interface** - Choose a WiFi adapter capable of monitor mode
@@ -262,14 +312,15 @@ python3 intercept.py --help
4. **View Devices** - Devices appear with name, address, and classification
### Aircraft Mode
1. **Check Tools** - Ensure dump1090 or rtl_adsb is installed
2. **Set Location** - Enter observer coordinates or click "Use GPS Location"
3. **Start Tracking** - Click "Start Tracking" to begin ADS-B reception
4. **View Map** - Aircraft appear on the interactive Leaflet map
5. **Click Aircraft** - Click markers for detailed information
6. **Display Options** - Toggle callsigns, altitude, trails, range rings, clustering
7. **Filter Aircraft** - Use dropdown to show all, military, civil, or emergency only
8. **Full Dashboard** - Click "Full Screen Dashboard" for dedicated radar view
1. **Select Hardware** - Choose your SDR type (RTL-SDR uses dump1090, others use readsb)
2. **Check Tools** - Ensure dump1090 or readsb is installed
3. **Set Location** - Enter observer coordinates or click "Use GPS Location"
4. **Start Tracking** - Click "Start Tracking" to begin ADS-B reception
5. **View Map** - Aircraft appear on the interactive Leaflet map
6. **Click Aircraft** - Click markers for detailed information
7. **Display Options** - Toggle callsigns, altitude, trails, range rings, clustering
8. **Filter Aircraft** - Use dropdown to show all, military, civil, or emergency only
9. **Full Dashboard** - Click "Full Screen Dashboard" for dedicated radar view
### Satellite Mode
1. **Set Location** - Enter observer coordinates or click "Use My Location"
@@ -291,10 +342,46 @@ python3 intercept.py --help
## Troubleshooting
### No devices found
- Ensure your RTL-SDR is plugged in
- Check `rtl_test` works from command line
- On Linux, you may need to blacklist the DVB-T driver
### Python/pip installation issues
**"externally-managed-environment" error (Ubuntu 23.04+, Debian 12+):**
```bash
# Option 1: Use a virtual environment (recommended)
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Option 2: Use pipx for isolated install
pipx install flask skyfield
# Option 3: Override the restriction (not recommended)
pip install -r requirements.txt --break-system-packages
```
**"pip: command not found":**
```bash
# Ubuntu/Debian
sudo apt install python3-pip
# macOS
python3 -m ensurepip --upgrade
```
**Permission denied errors:**
```bash
# Install to user directory instead
pip install --user -r requirements.txt
```
### No SDR devices found
- Ensure your SDR device is plugged in
- Check `rtl_test` (RTL-SDR) or `SoapySDRUtil --find` (LimeSDR/HackRF)
- On Linux, you may need udev rules (see setup.sh output)
- On Linux, blacklist the DVB-T driver:
```bash
echo "blacklist dvb_usb_rtl28xxu" | sudo tee /etc/modprobe.d/blacklist-rtl.conf
sudo modprobe -r dvb_usb_rtl28xxu
```
### No messages appearing
- Verify the frequency is correct for your area
@@ -309,7 +396,13 @@ python3 intercept.py --help
### Device busy error
- Click "Kill All Processes" to stop any stale processes
- Unplug and replug the RTL-SDR device
- Unplug and replug the SDR device
- Check if another application is using the device: `lsof | grep rtl`
### LimeSDR/HackRF not detected
- Ensure SoapySDR is installed: `SoapySDRUtil --info`
- Check the driver module is loaded: `SoapySDRUtil --find`
- Verify permissions (may need udev rules or run as root)
---
@@ -337,12 +430,26 @@ MIT License - see [LICENSE](LICENSE) for details.
Created by **smittix** - [GitHub](https://github.com/smittix)
## Supported SDR Hardware
| Hardware | Frequency Range | Gain Range | TX | Notes |
|----------|-----------------|------------|-----|-------|
| **RTL-SDR** | 24 - 1766 MHz | 0 - 50 dB | No | Most common, budget-friendly (~$25) |
| **LimeSDR** | 0.1 - 3800 MHz | 0 - 73 dB | Yes | Wide range, requires SoapySDR |
| **HackRF** | 1 - 6000 MHz | 0 - 62 dB | Yes | Ultra-wide range, requires SoapySDR |
The application automatically detects connected devices and shows hardware-specific capabilities (frequency limits, gain ranges) in the UI.
---
## Acknowledgments
- [rtl-sdr](https://osmocom.org/projects/rtl-sdr/wiki) - RTL-SDR drivers
- [multimon-ng](https://github.com/EliasOenal/multimon-ng) - Multi-protocol pager decoder
- [rtl_433](https://github.com/merbanan/rtl_433) - 433MHz sensor decoder
- [dump1090](https://github.com/flightaware/dump1090) - ADS-B decoder for aircraft tracking
- [SoapySDR](https://github.com/pothosware/SoapySDR) - Universal SDR abstraction layer
- [LimeSuite](https://github.com/myriadrf/LimeSuite) - LimeSDR driver and tools
- [aircrack-ng](https://www.aircrack-ng.org/) - WiFi security auditing tools
- [BlueZ](http://www.bluez.org/) - Official Linux Bluetooth protocol stack
- [Leaflet.js](https://leafletjs.com/) - Interactive maps for aircraft tracking

9
app.py
View File

@@ -26,7 +26,8 @@ from typing import Any
from flask import Flask, render_template, jsonify, send_file, Response, request
from utils.dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES
from utils.process import detect_devices, cleanup_stale_processes
from utils.process import cleanup_stale_processes
from utils.sdr import SDRFactory
# Create Flask app
@@ -105,7 +106,7 @@ def index() -> str:
'multimon': check_tool('multimon-ng'),
'rtl_433': check_tool('rtl_433')
}
devices = detect_devices()
devices = [d.to_dict() for d in SDRFactory.detect_devices()]
return render_template('index.html', tools=tools, devices=devices)
@@ -116,7 +117,9 @@ def favicon() -> Response:
@app.route('/devices')
def get_devices() -> Response:
return jsonify(detect_devices())
"""Get all detected SDR devices with hardware type info."""
devices = SDRFactory.detect_devices()
return jsonify([d.to_dict() for d in devices])
@app.route('/dependencies')

View File

@@ -3,18 +3,12 @@ TLE_SATELLITES = {
'ISS': ('ISS (ZARYA)',
'1 25544U 98067A 24001.00000000 .00000000 00000-0 00000-0 0 0000',
'2 25544 51.6400 0.0000 0000000 0.0000 0.0000 15.50000000000000'),
'NOAA-15': ('NOAA 15',
'1 25338U 98030A 24001.00000000 .00000-0 00000-0 00000-0 0 0000',
'2 25338 98.7300 0.0000 0010000 0.0000 0.0000 14.26000000000000'),
'NOAA-18': ('NOAA 18',
'1 28654U 05018A 24001.00000000 .00000-0 00000-0 00000-0 0 0000',
'2 28654 98.8800 0.0000 0014000 0.0000 0.0000 14.12000000000000'),
'NOAA-19': ('NOAA 19',
'1 33591U 09005A 24001.00000000 .00000-0 00000-0 00000-0 0 0000',
'2 33591 99.1900 0.0000 0014000 0.0000 0.0000 14.12000000000000'),
'NOAA-20': ('NOAA 20 (JPSS-1)',
'1 43013U 17073A 24001.00000000 .00000-0 00000-0 00000-0 0 0000',
'2 43013 98.7400 0.0000 0001000 0.0000 0.0000 14.19000000000000'),
'NOAA-21': ('NOAA 21 (JPSS-2)',
'1 54234U 22150A 24001.00000000 .00000-0 00000-0 00000-0 0 0000',
'2 54234 98.7100 0.0000 0001000 0.0000 0.0000 14.19000000000000'),
'METEOR-M2': ('METEOR-M 2',
'1 40069U 14037A 24001.00000000 .00000-0 00000-0 00000-0 0 0000',
'2 40069 98.5400 0.0000 0005000 0.0000 0.0000 14.21000000000000'),

View File

@@ -18,6 +18,7 @@ import app as app_module
from utils.logging import adsb_logger as logger
from utils.validation import validate_device_index, validate_gain
from utils.sse import format_sse
from utils.sdr import SDRFactory, SDRType
adsb_bp = Blueprint('adsb', __name__, url_prefix='/adsb')
@@ -29,9 +30,15 @@ adsb_last_message_time = None
# Common installation paths for dump1090 (when not in PATH)
DUMP1090_PATHS = [
# Homebrew on Apple Silicon (M1/M2/M3)
'/opt/homebrew/bin/dump1090',
'/opt/homebrew/bin/dump1090-fa',
'/opt/homebrew/bin/dump1090-mutability',
# Homebrew on Intel Mac
'/usr/local/bin/dump1090',
'/usr/local/bin/dump1090-fa',
'/usr/local/bin/dump1090-mutability',
# Linux system paths
'/usr/bin/dump1090',
'/usr/bin/dump1090-fa',
'/usr/bin/dump1090-mutability',
@@ -240,11 +247,23 @@ def start_adsb():
thread.start()
return jsonify({'status': 'started', 'message': 'Connected to existing dump1090 service'})
# No existing service, need to start dump1090 ourselves
dump1090_path = find_dump1090()
# Get SDR type from request
sdr_type_str = data.get('sdr_type', 'rtlsdr')
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
if not dump1090_path:
return jsonify({'status': 'error', 'message': 'dump1090 not found. Install dump1090/dump1090-fa or ensure it is in /usr/local/bin/'})
# For RTL-SDR, use dump1090. For other hardware, need readsb with SoapySDR
if sdr_type == SDRType.RTL_SDR:
dump1090_path = find_dump1090()
if not dump1090_path:
return jsonify({'status': 'error', 'message': 'dump1090 not found. Install dump1090/dump1090-fa or ensure it is in /usr/local/bin/'})
else:
# For LimeSDR/HackRF, check for readsb (dump1090 with SoapySDR support)
dump1090_path = shutil.which('readsb') or find_dump1090()
if not dump1090_path:
return jsonify({'status': 'error', 'message': f'readsb or dump1090 not found for {sdr_type.value}. Install readsb with SoapySDR support.'})
# Kill any stale app-started process
if app_module.adsb_process:
@@ -255,7 +274,19 @@ def start_adsb():
pass
app_module.adsb_process = None
cmd = [dump1090_path, '--net', '--gain', str(gain), '--device-index', str(device), '--quiet']
# Create device object and build command via abstraction layer
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
builder = SDRFactory.get_builder(sdr_type)
# Build ADS-B decoder command
cmd = builder.build_adsb_command(
device=sdr_device,
gain=float(gain)
)
# For RTL-SDR, ensure we use the found dump1090 path
if sdr_type == SDRType.RTL_SDR:
cmd[0] = dump1090_path
try:
app_module.adsb_process = subprocess.Popen(

View File

@@ -23,6 +23,7 @@ import app as app_module
from utils.logging import iridium_logger as logger
from utils.validation import validate_frequency, validate_device_index, validate_gain
from utils.sse import format_sse
from utils.sdr import SDRFactory, SDRType
iridium_bp = Blueprint('iridium', __name__, url_prefix='/iridium')
@@ -103,21 +104,45 @@ def start_iridium():
except (ValueError, AttributeError):
return jsonify({'status': 'error', 'message': 'Invalid sample rate format'}), 400
if not shutil.which('iridium-extractor') and not shutil.which('rtl_fm'):
return jsonify({
'status': 'error',
'message': 'Iridium tools not found. Requires rtl_fm or iridium-extractor.'
}), 503
# Get SDR type from request
sdr_type_str = data.get('sdr_type', 'rtlsdr')
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
# Check for required tools based on SDR type
if sdr_type == SDRType.RTL_SDR:
if not shutil.which('iridium-extractor') and not shutil.which('rtl_fm'):
return jsonify({
'status': 'error',
'message': 'Iridium tools not found. Requires rtl_fm or iridium-extractor.'
}), 503
else:
if not shutil.which('rx_fm'):
return jsonify({
'status': 'error',
'message': f'rx_fm not found for {sdr_type.value}. Install SoapySDR tools.'
}), 503
try:
cmd = [
'rtl_fm',
'-f', f'{float(freq)}M',
'-g', str(gain),
'-s', sample_rate,
'-d', str(device),
'-'
]
# Create device object and build command via abstraction layer
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
builder = SDRFactory.get_builder(sdr_type)
# Parse sample rate
sample_rate_hz = int(float(sample_rate))
# Build FM demodulation command
cmd = builder.build_fm_demod_command(
device=sdr_device,
frequency_mhz=float(freq),
sample_rate=sample_rate_hz,
gain=float(gain),
ppm=None,
modulation='fm',
squelch=None
)
app_module.satellite_process = subprocess.Popen(
cmd,

View File

@@ -21,6 +21,7 @@ from utils.logging import pager_logger as logger
from utils.validation import validate_frequency, validate_device_index, validate_gain, validate_ppm
from utils.sse import format_sse
from utils.process import safe_terminate, register_process
from utils.sdr import SDRFactory, SDRType, SDRValidationError
pager_bp = Blueprint('pager', __name__)
@@ -201,25 +202,27 @@ def start_decoding() -> Response:
elif proto == 'FLEX':
decoders.extend(['-a', 'FLEX'])
# Build rtl_fm command
rtl_cmd = [
'rtl_fm',
'-d', str(device),
'-f', f'{freq}M',
'-M', 'fm',
'-s', '22050',
]
# Get SDR type and build command via abstraction layer
sdr_type_str = data.get('sdr_type', 'rtlsdr')
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
if gain and gain != '0':
rtl_cmd.extend(['-g', str(gain)])
# Create device object and get command builder
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
builder = SDRFactory.get_builder(sdr_type)
if ppm and ppm != '0':
rtl_cmd.extend(['-p', str(ppm)])
if squelch and squelch != '0':
rtl_cmd.extend(['-l', str(squelch)])
rtl_cmd.append('-')
# Build FM demodulation command
rtl_cmd = builder.build_fm_demod_command(
device=sdr_device,
frequency_mhz=freq,
sample_rate=22050,
gain=float(gain) if gain and gain != '0' else None,
ppm=int(ppm) if ppm and ppm != '0' else None,
modulation='fm',
squelch=squelch if squelch and squelch != 0 else None
)
multimon_cmd = ['multimon-ng', '-t', 'raw'] + decoders + ['-f', 'alpha', '-']

View File

@@ -17,6 +17,7 @@ from utils.logging import sensor_logger as logger
from utils.validation import validate_frequency, validate_device_index, validate_gain, validate_ppm
from utils.sse import format_sse
from utils.process import safe_terminate, register_process
from utils.sdr import SDRFactory, SDRType
sensor_bp = Blueprint('sensor', __name__)
@@ -82,19 +83,24 @@ def start_sensor() -> Response:
except queue.Empty:
break
# Build rtl_433 command
cmd = [
'rtl_433',
'-d', str(device),
'-f', f'{freq}M',
'-F', 'json'
]
# Get SDR type and build command via abstraction layer
sdr_type_str = data.get('sdr_type', 'rtlsdr')
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
if gain and gain != 0:
cmd.extend(['-g', str(int(gain))])
# Create device object and get command builder
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
builder = SDRFactory.get_builder(sdr_type)
if ppm and ppm != 0:
cmd.extend(['-p', str(ppm)])
# Build ISM band decoder command
cmd = builder.build_ism_command(
device=sdr_device,
frequency_mhz=freq,
gain=float(gain) if gain and gain != 0 else None,
ppm=int(ppm) if ppm and ppm != 0 else None
)
full_cmd = ' '.join(cmd)
logger.info(f"Running: {full_cmd}")

247
setup.sh Executable file
View File

@@ -0,0 +1,247 @@
#!/bin/bash
#
# INTERCEPT Setup Script
# Installs Python dependencies and checks for external tools
#
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}"
echo " ___ _ _ _____ _____ ____ ____ _____ ____ _____ "
echo " |_ _| \\ | |_ _| ____| _ \\ / ___| ____| _ \\_ _|"
echo " | || \\| | | | | _| | |_) | | | _| | |_) || | "
echo " | || |\\ | | | | |___| _ <| |___| |___| __/ | | "
echo " |___|_| \\_| |_| |_____|_| \\_\\\\____|_____|_| |_| "
echo -e "${NC}"
echo "Signal Intelligence Platform - Setup Script"
echo "============================================"
echo ""
# Detect OS
detect_os() {
if [[ "$OSTYPE" == "darwin"* ]]; then
OS="macos"
PKG_MANAGER="brew"
elif [[ -f /etc/debian_version ]]; then
OS="debian"
PKG_MANAGER="apt"
elif [[ -f /etc/redhat-release ]]; then
OS="redhat"
PKG_MANAGER="dnf"
elif [[ -f /etc/arch-release ]]; then
OS="arch"
PKG_MANAGER="pacman"
else
OS="unknown"
PKG_MANAGER="unknown"
fi
echo -e "${BLUE}Detected OS:${NC} $OS (package manager: $PKG_MANAGER)"
}
# Check if a command exists
check_cmd() {
command -v "$1" &> /dev/null
}
# Install Python dependencies
install_python_deps() {
echo ""
echo -e "${BLUE}[1/3] Installing Python dependencies...${NC}"
if ! check_cmd python3; then
echo -e "${RED}Error: Python 3 is not installed${NC}"
echo "Please install Python 3.7 or later"
exit 1
fi
PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
echo "Python version: $PYTHON_VERSION"
if check_cmd pip3; then
pip3 install -r requirements.txt
elif check_cmd pip; then
pip install -r requirements.txt
else
python3 -m pip install -r requirements.txt
fi
echo -e "${GREEN}Python dependencies installed successfully${NC}"
}
# Check external tools
check_tools() {
echo ""
echo -e "${BLUE}[2/3] Checking external tools...${NC}"
echo ""
MISSING_TOOLS=()
# Core SDR tools
echo "Core SDR Tools:"
check_tool "rtl_fm" "RTL-SDR FM demodulator"
check_tool "rtl_test" "RTL-SDR device detection"
check_tool "multimon-ng" "Pager decoder"
check_tool "rtl_433" "433MHz sensor decoder"
check_tool "dump1090" "ADS-B decoder"
echo ""
echo "Additional SDR Hardware (optional):"
check_tool "SoapySDRUtil" "SoapySDR (for LimeSDR/HackRF)"
check_tool "LimeUtil" "LimeSDR tools"
check_tool "hackrf_info" "HackRF tools"
echo ""
echo "WiFi Tools:"
check_tool "airmon-ng" "WiFi monitor mode"
check_tool "airodump-ng" "WiFi scanner"
echo ""
echo "Bluetooth Tools:"
check_tool "bluetoothctl" "Bluetooth controller"
check_tool "hcitool" "Bluetooth HCI tool"
if [ ${#MISSING_TOOLS[@]} -gt 0 ]; then
echo ""
echo -e "${YELLOW}Some tools are missing. See installation instructions below.${NC}"
fi
}
check_tool() {
local cmd=$1
local desc=$2
if check_cmd "$cmd"; then
echo -e " ${GREEN}${NC} $cmd - $desc"
else
echo -e " ${RED}${NC} $cmd - $desc ${YELLOW}(not found)${NC}"
MISSING_TOOLS+=("$cmd")
fi
}
# Show installation instructions
show_install_instructions() {
echo ""
echo -e "${BLUE}[3/3] Installation instructions for missing tools${NC}"
echo ""
if [ ${#MISSING_TOOLS[@]} -eq 0 ]; then
echo -e "${GREEN}All tools are installed!${NC}"
return
fi
echo "Run the following commands to install missing tools:"
echo ""
if [[ "$OS" == "macos" ]]; then
echo -e "${YELLOW}macOS (Homebrew):${NC}"
echo ""
# Check if Homebrew is installed
if ! check_cmd brew; then
echo "First, install Homebrew:"
echo ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
echo ""
fi
echo "# Core SDR tools"
echo "brew install librtlsdr multimon-ng rtl_433 dump1090-mutability"
echo ""
echo "# LimeSDR support (optional)"
echo "brew install soapysdr limesuite soapylms7"
echo ""
echo "# HackRF support (optional)"
echo "brew install hackrf soapyhackrf"
echo ""
echo "# WiFi tools"
echo "brew install aircrack-ng"
elif [[ "$OS" == "debian" ]]; then
echo -e "${YELLOW}Ubuntu/Debian:${NC}"
echo ""
echo "# Core SDR tools"
echo "sudo apt update"
echo "sudo apt install rtl-sdr multimon-ng rtl-433 dump1090-mutability"
echo ""
echo "# LimeSDR support (optional)"
echo "sudo apt install soapysdr-tools limesuite soapysdr-module-lms7"
echo ""
echo "# HackRF support (optional)"
echo "sudo apt install hackrf soapysdr-module-hackrf"
echo ""
echo "# WiFi tools"
echo "sudo apt install aircrack-ng"
echo ""
echo "# Bluetooth tools"
echo "sudo apt install bluez bluetooth"
elif [[ "$OS" == "arch" ]]; then
echo -e "${YELLOW}Arch Linux:${NC}"
echo ""
echo "# Core SDR tools"
echo "sudo pacman -S rtl-sdr multimon-ng"
echo "yay -S rtl_433 dump1090"
echo ""
echo "# LimeSDR/HackRF support (optional)"
echo "sudo pacman -S soapysdr limesuite hackrf"
elif [[ "$OS" == "redhat" ]]; then
echo -e "${YELLOW}Fedora/RHEL:${NC}"
echo ""
echo "# Core SDR tools"
echo "sudo dnf install rtl-sdr"
echo "# multimon-ng, rtl_433, dump1090 may need to be built from source"
else
echo "Please install the following tools manually:"
for tool in "${MISSING_TOOLS[@]}"; do
echo " - $tool"
done
fi
}
# RTL-SDR udev rules (Linux only)
setup_udev_rules() {
if [[ "$OS" != "macos" ]] && [[ "$OS" != "unknown" ]]; then
echo ""
echo -e "${BLUE}RTL-SDR udev rules (Linux only):${NC}"
echo ""
echo "If your RTL-SDR is not detected, you may need to add udev rules:"
echo ""
echo "sudo bash -c 'cat > /etc/udev/rules.d/20-rtlsdr.rules << EOF"
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", MODE="0666"'
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2832", MODE="0666"'
echo "EOF'"
echo ""
echo "sudo udevadm control --reload-rules"
echo "sudo udevadm trigger"
echo ""
echo "Then unplug and replug your RTL-SDR device."
fi
}
# Main
main() {
detect_os
install_python_deps
check_tools
show_install_instructions
setup_udev_rules
echo ""
echo "============================================"
echo -e "${GREEN}Setup complete!${NC}"
echo ""
echo "To start INTERCEPT:"
echo " sudo python3 intercept.py"
echo ""
echo "Then open http://localhost:5050 in your browser"
echo ""
}
main "$@"

View File

@@ -3105,18 +3105,33 @@
</div>
<div class="section" id="rtlDeviceSection">
<h3>RTL-SDR Device</h3>
<h3>SDR Device</h3>
<div class="form-group">
<label style="font-size: 11px; color: #888; margin-bottom: 4px;">Hardware Type</label>
<select id="sdrTypeSelect" onchange="onSDRTypeChanged()">
<option value="rtlsdr">RTL-SDR</option>
<option value="limesdr">LimeSDR</option>
<option value="hackrf">HackRF</option>
</select>
</div>
<div class="form-group">
<label style="font-size: 11px; color: #888; margin-bottom: 4px;">Device</label>
<select id="deviceSelect">
{% if devices %}
{% for device in devices %}
<option value="{{ device.index }}">{{ device.index }}: {{ device.name }}</option>
<option value="{{ device.index }}" data-sdr-type="{{ device.sdr_type | default('rtlsdr') }}">{{ device.index }}: {{ device.name }}</option>
{% endfor %}
{% else %}
<option value="0">No devices found</option>
{% endif %}
</select>
</div>
<div id="deviceCapabilities" class="info-text" style="font-size: 11px; margin-bottom: 8px; padding: 6px; background: #0a0a1a; border-radius: 4px;">
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 8px;">
<span style="color: #888;">Freq:</span><span id="capFreqRange">24-1766 MHz</span>
<span style="color: #888;">Gain:</span><span id="capGainRange">0-50 dB</span>
</div>
</div>
<button class="preset-btn" onclick="refreshDevices()" style="width: 100%;">
Refresh Devices
</button>
@@ -4730,7 +4745,8 @@
frequency: freq,
gain: gain,
ppm: ppm,
device: device
device: device,
sdr_type: getSelectedSDRType()
};
fetch('/start_sensor', {
@@ -5139,19 +5155,66 @@
}
}
// SDR hardware capabilities
const sdrCapabilities = {
'rtlsdr': { name: 'RTL-SDR', freq_min: 24, freq_max: 1766, gain_min: 0, gain_max: 50 },
'limesdr': { name: 'LimeSDR', freq_min: 0.1, freq_max: 3800, gain_min: 0, gain_max: 73 },
'hackrf': { name: 'HackRF', freq_min: 1, freq_max: 6000, gain_min: 0, gain_max: 62 }
};
// Current device list with SDR type info
let currentDeviceList = [];
function onSDRTypeChanged() {
const sdrType = document.getElementById('sdrTypeSelect').value;
const select = document.getElementById('deviceSelect');
// Filter devices by selected SDR type
const filteredDevices = currentDeviceList.filter(d =>
(d.sdr_type || 'rtlsdr') === sdrType
);
if (filteredDevices.length === 0) {
select.innerHTML = `<option value="0">No ${sdrCapabilities[sdrType]?.name || sdrType} devices found</option>`;
} else {
select.innerHTML = filteredDevices.map(d =>
`<option value="${d.index}" data-sdr-type="${d.sdr_type || 'rtlsdr'}">${d.index}: ${d.name}</option>`
).join('');
}
// Update capabilities display
updateCapabilitiesDisplay(sdrType);
}
function updateCapabilitiesDisplay(sdrType) {
const caps = sdrCapabilities[sdrType];
if (caps) {
document.getElementById('capFreqRange').textContent = `${caps.freq_min}-${caps.freq_max} MHz`;
document.getElementById('capGainRange').textContent = `${caps.gain_min}-${caps.gain_max} dB`;
}
}
function refreshDevices() {
fetch('/devices')
.then(r => r.json())
.then(devices => {
// Store full device list with SDR type info
currentDeviceList = devices;
deviceList = devices;
const select = document.getElementById('deviceSelect');
if (devices.length === 0) {
select.innerHTML = '<option value="0">No devices found</option>';
} else {
select.innerHTML = devices.map(d =>
`<option value="${d.index}">${d.index}: ${d.name}</option>`
).join('');
// Auto-select SDR type if devices found
if (devices.length > 0) {
const firstType = devices[0].sdr_type || 'rtlsdr';
document.getElementById('sdrTypeSelect').value = firstType;
}
// Trigger filter update
onSDRTypeChanged();
})
.catch(err => {
console.error('Failed to refresh devices:', err);
const select = document.getElementById('deviceSelect');
select.innerHTML = '<option value="0">Error loading devices</option>';
});
}
@@ -5159,6 +5222,10 @@
return document.getElementById('deviceSelect').value;
}
function getSelectedSDRType() {
return document.getElementById('sdrTypeSelect').value;
}
function getSelectedProtocols() {
const protocols = [];
if (document.getElementById('proto_pocsag512').checked) protocols.push('POCSAG512');
@@ -5187,6 +5254,7 @@
squelch: squelch,
ppm: ppm,
device: device,
sdr_type: getSelectedSDRType(),
protocols: protocols
};
@@ -8759,11 +8827,12 @@
function startAdsbScan() {
const gain = document.getElementById('adsbGain').value;
const device = getSelectedDevice();
const sdr_type = getSelectedSDRType();
fetch('/adsb/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gain, device })
body: JSON.stringify({ gain, device, sdr_type })
})
.then(r => r.json())
.then(data => {
@@ -9759,11 +9828,12 @@
const gain = document.getElementById('iridiumGain').value;
const sampleRate = document.getElementById('iridiumSampleRate').value;
const device = getSelectedDevice();
const sdr_type = getSelectedSDRType();
fetch('/iridium/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ freq, gain, sampleRate, device })
body: JSON.stringify({ freq, gain, sampleRate, device, sdr_type })
})
.then(r => r.json())
.then(data => {

View File

@@ -200,6 +200,72 @@ TOOL_DEPENDENCIES = {
}
}
}
},
'sdr_hardware': {
'name': 'SDR Hardware Support',
'tools': {
'SoapySDRUtil': {
'required': False,
'description': 'Universal SDR abstraction (required for LimeSDR, HackRF)',
'install': {
'apt': 'sudo apt install soapysdr-tools',
'brew': 'brew install soapysdr',
'manual': 'https://github.com/pothosware/SoapySDR'
}
},
'rx_fm': {
'required': False,
'description': 'SoapySDR FM receiver (for non-RTL hardware)',
'install': {
'manual': 'Part of SoapySDR utilities or build from source'
}
},
'LimeUtil': {
'required': False,
'description': 'LimeSDR native utilities',
'install': {
'apt': 'sudo apt install limesuite',
'brew': 'brew install limesuite',
'manual': 'https://github.com/myriadrf/LimeSuite'
}
},
'SoapyLMS7': {
'required': False,
'description': 'SoapySDR plugin for LimeSDR',
'install': {
'apt': 'sudo apt install soapysdr-module-lms7',
'brew': 'brew install soapylms7',
'manual': 'https://github.com/myriadrf/LimeSuite'
}
},
'hackrf_info': {
'required': False,
'description': 'HackRF native utilities',
'install': {
'apt': 'sudo apt install hackrf',
'brew': 'brew install hackrf',
'manual': 'https://github.com/greatscottgadgets/hackrf'
}
},
'SoapyHackRF': {
'required': False,
'description': 'SoapySDR plugin for HackRF',
'install': {
'apt': 'sudo apt install soapysdr-module-hackrf',
'brew': 'brew install soapyhackrf',
'manual': 'https://github.com/pothosware/SoapyHackRF'
}
},
'readsb': {
'required': False,
'description': 'ADS-B decoder with SoapySDR support',
'install': {
'apt': 'Build from source with SoapySDR support',
'brew': 'Build from source with SoapySDR support',
'manual': 'https://github.com/wiedehopf/readsb'
}
}
}
}
}

196
utils/sdr/__init__.py Normal file
View File

@@ -0,0 +1,196 @@
"""
SDR Hardware Abstraction Layer.
This module provides a unified interface for multiple SDR hardware types
including RTL-SDR, LimeSDR, and HackRF. Use SDRFactory to detect devices
and get appropriate command builders.
Example usage:
from utils.sdr import SDRFactory, SDRType
# Detect all connected devices
devices = SDRFactory.detect_devices()
# Get a command builder for a specific device
builder = SDRFactory.get_builder_for_device(devices[0])
# Or get a builder by type
builder = SDRFactory.get_builder(SDRType.RTL_SDR)
# Build commands
cmd = builder.build_fm_demod_command(device, frequency_mhz=153.35)
"""
from typing import Optional
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
from .detection import detect_all_devices
from .rtlsdr import RTLSDRCommandBuilder
from .limesdr import LimeSDRCommandBuilder
from .hackrf import HackRFCommandBuilder
from .validation import (
SDRValidationError,
validate_frequency,
validate_gain,
validate_sample_rate,
validate_ppm,
validate_device_index,
validate_squelch,
get_capabilities_for_type,
)
class SDRFactory:
"""Factory for creating SDR command builders and detecting devices."""
_builders: dict[SDRType, type[CommandBuilder]] = {
SDRType.RTL_SDR: RTLSDRCommandBuilder,
SDRType.LIME_SDR: LimeSDRCommandBuilder,
SDRType.HACKRF: HackRFCommandBuilder,
}
@classmethod
def get_builder(cls, sdr_type: SDRType) -> CommandBuilder:
"""
Get a command builder for the specified SDR type.
Args:
sdr_type: The SDR hardware type
Returns:
CommandBuilder instance for the specified type
Raises:
ValueError: If the SDR type is not supported
"""
builder_class = cls._builders.get(sdr_type)
if not builder_class:
raise ValueError(f"Unsupported SDR type: {sdr_type}")
return builder_class()
@classmethod
def get_builder_for_device(cls, device: SDRDevice) -> CommandBuilder:
"""
Get a command builder for a specific device.
Args:
device: The SDR device
Returns:
CommandBuilder instance for the device's type
"""
return cls.get_builder(device.sdr_type)
@classmethod
def detect_devices(cls) -> list[SDRDevice]:
"""
Detect all available SDR devices.
Returns:
List of detected SDR devices
"""
return detect_all_devices()
@classmethod
def get_supported_types(cls) -> list[SDRType]:
"""
Get list of supported SDR types.
Returns:
List of supported SDRType values
"""
return list(cls._builders.keys())
@classmethod
def get_capabilities(cls, sdr_type: SDRType) -> SDRCapabilities:
"""
Get capabilities for an SDR type.
Args:
sdr_type: The SDR hardware type
Returns:
SDRCapabilities for the specified type
"""
builder = cls.get_builder(sdr_type)
return builder.get_capabilities()
@classmethod
def get_all_capabilities(cls) -> dict[str, dict]:
"""
Get capabilities for all supported SDR types.
Returns:
Dictionary mapping SDR type names to capability dicts
"""
capabilities = {}
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,
}
return capabilities
@classmethod
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.
Useful when device detection didn't provide full details but
you know the hardware type.
Args:
sdr_type: The SDR hardware type
index: Device index (default 0)
serial: Device serial (default 'N/A')
Returns:
SDRDevice with default capabilities for the type
"""
caps = cls.get_capabilities(sdr_type)
return SDRDevice(
sdr_type=sdr_type,
index=index,
name=f'{sdr_type.name.replace("_", " ")} Device {index}',
serial=serial,
driver=sdr_type.value,
capabilities=caps
)
# Export commonly used items at package level
__all__ = [
# Factory
'SDRFactory',
# Types and classes
'SDRType',
'SDRDevice',
'SDRCapabilities',
'CommandBuilder',
# Builders
'RTLSDRCommandBuilder',
'LimeSDRCommandBuilder',
'HackRFCommandBuilder',
# Validation
'SDRValidationError',
'validate_frequency',
'validate_gain',
'validate_sample_rate',
'validate_ppm',
'validate_device_index',
'validate_squelch',
'get_capabilities_for_type',
]

149
utils/sdr/base.py Normal file
View File

@@ -0,0 +1,149 @@
"""
Base classes and types for SDR hardware abstraction.
This module provides the core abstractions for supporting multiple SDR hardware
types (RTL-SDR, LimeSDR, HackRF, etc.) through a unified interface.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class SDRType(Enum):
"""Supported SDR hardware types."""
RTL_SDR = "rtlsdr"
LIME_SDR = "limesdr"
HACKRF = "hackrf"
# Future support
# USRP = "usrp"
# BLADE_RF = "bladerf"
@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
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
@dataclass
class SDRDevice:
"""Detected SDR device."""
sdr_type: SDRType
index: int
name: str
serial: str
driver: str # e.g., "rtlsdr", "lime", "hackrf"
capabilities: SDRCapabilities
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
'index': self.index,
'name': self.name,
'serial': self.serial,
'sdr_type': self.sdr_type.value,
'driver': self.driver,
'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,
}
}
class CommandBuilder(ABC):
"""Abstract base class for building SDR commands."""
@abstractmethod
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
) -> list[str]:
"""
Build FM demodulation command (for pager, iridium).
Args:
device: The SDR device to use
frequency_mhz: Center frequency in MHz
sample_rate: Audio sample rate (default 22050 for pager)
gain: Gain in dB (None for auto)
ppm: PPM frequency correction
modulation: Modulation type (fm, am, etc.)
squelch: Squelch level
Returns:
Command as list of strings for subprocess
"""
pass
@abstractmethod
def build_adsb_command(
self,
device: SDRDevice,
gain: Optional[float] = None
) -> list[str]:
"""
Build ADS-B decoder command.
Args:
device: The SDR device to use
gain: Gain in dB (None for auto)
Returns:
Command as list of strings for subprocess
"""
pass
@abstractmethod
def build_ism_command(
self,
device: SDRDevice,
frequency_mhz: float = 433.92,
gain: Optional[float] = None,
ppm: Optional[int] = None
) -> list[str]:
"""
Build ISM band decoder command (433MHz sensors).
Args:
device: The SDR device to use
frequency_mhz: Center frequency in MHz (default 433.92)
gain: Gain in dB (None for auto)
ppm: PPM frequency correction
Returns:
Command as list of strings for subprocess
"""
pass
@abstractmethod
def get_capabilities(self) -> SDRCapabilities:
"""Return hardware capabilities for this SDR type."""
pass
@classmethod
@abstractmethod
def get_sdr_type(cls) -> SDRType:
"""Return the SDR type this builder handles."""
pass

306
utils/sdr/detection.py Normal file
View File

@@ -0,0 +1,306 @@
"""
Multi-hardware SDR device detection.
Detects RTL-SDR devices via rtl_test and other SDR hardware via SoapySDR.
"""
import logging
import re
import shutil
import subprocess
from typing import Optional
from .base import SDRCapabilities, SDRDevice, SDRType
logger = logging.getLogger(__name__)
def _check_tool(name: str) -> bool:
"""Check if a tool is available in PATH."""
return shutil.which(name) is not None
def _get_capabilities_for_type(sdr_type: SDRType) -> SDRCapabilities:
"""Get default capabilities for an SDR type."""
# Import here to avoid circular imports
from .rtlsdr import RTLSDRCommandBuilder
from .limesdr import LimeSDRCommandBuilder
from .hackrf import HackRFCommandBuilder
builders = {
SDRType.RTL_SDR: RTLSDRCommandBuilder,
SDRType.LIME_SDR: LimeSDRCommandBuilder,
SDRType.HACKRF: HackRFCommandBuilder,
}
builder_class = builders.get(sdr_type)
if builder_class:
return builder_class.CAPABILITIES
# Fallback generic capabilities
return SDRCapabilities(
sdr_type=sdr_type,
freq_min_mhz=1.0,
freq_max_mhz=6000.0,
gain_min=0.0,
gain_max=50.0,
sample_rates=[2048000],
supports_bias_t=False,
supports_ppm=False,
tx_capable=False
)
def _driver_to_sdr_type(driver: str) -> Optional[SDRType]:
"""Map SoapySDR driver name to SDRType."""
mapping = {
'rtlsdr': SDRType.RTL_SDR,
'lime': SDRType.LIME_SDR,
'limesdr': SDRType.LIME_SDR,
'hackrf': SDRType.HACKRF,
# Future support
# 'uhd': SDRType.USRP,
# 'bladerf': SDRType.BLADE_RF,
}
return mapping.get(driver.lower())
def detect_rtlsdr_devices() -> list[SDRDevice]:
"""
Detect RTL-SDR devices using rtl_test.
This uses the native rtl_test tool for best compatibility with
existing RTL-SDR installations.
"""
devices: list[SDRDevice] = []
if not _check_tool('rtl_test'):
logger.debug("rtl_test not found, skipping RTL-SDR detection")
return devices
try:
result = subprocess.run(
['rtl_test', '-t'],
capture_output=True,
text=True,
timeout=5
)
output = result.stderr + result.stdout
# Parse device info from rtl_test output
# Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001"
device_pattern = r'(\d+):\s+(.+?)(?:,\s*SN:\s*(\S+))?$'
from .rtlsdr import RTLSDRCommandBuilder
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) or 'N/A',
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)
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
))
except subprocess.TimeoutExpired:
logger.warning("rtl_test timed out")
except Exception as e:
logger.debug(f"RTL-SDR detection error: {e}")
return devices
def detect_soapy_devices() -> list[SDRDevice]:
"""
Detect SDR devices via SoapySDR.
This detects LimeSDR, HackRF, USRP, BladeRF, and other SoapySDR-compatible
devices. RTL-SDR devices may also appear here but we prefer the native
detection for those.
"""
devices: list[SDRDevice] = []
if not _check_tool('SoapySDRUtil'):
logger.debug("SoapySDRUtil not found, skipping SoapySDR detection")
return devices
try:
result = subprocess.run(
['SoapySDRUtil', '--find'],
capture_output=True,
text=True,
timeout=10
)
# Parse SoapySDR output
# Format varies but typically includes lines like:
# " driver = lime"
# " serial = 0009060B00123456"
# " label = LimeSDR Mini [USB 3.0] 0009060B00123456"
current_device: dict = {}
device_counts: dict[SDRType, int] = {}
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'):
_add_soapy_device(devices, current_device, device_counts)
current_device = {}
continue
# Parse key = value pairs
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'):
_add_soapy_device(devices, current_device, device_counts)
except subprocess.TimeoutExpired:
logger.warning("SoapySDRUtil timed out")
except Exception as e:
logger.debug(f"SoapySDR detection error: {e}")
return devices
def _add_soapy_device(
devices: list[SDRDevice],
device_info: dict,
device_counts: dict[SDRType, int]
) -> None:
"""Add a device from SoapySDR detection to the list."""
driver = device_info.get('driver', '').lower()
sdr_type = _driver_to_sdr_type(driver)
if not sdr_type:
logger.debug(f"Unknown SoapySDR driver: {driver}")
return
# Skip RTL-SDR devices from SoapySDR (we use native detection)
if sdr_type == SDRType.RTL_SDR:
return
# Track device index per type
if sdr_type not in device_counts:
device_counts[sdr_type] = 0
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)
))
def detect_hackrf_devices() -> list[SDRDevice]:
"""
Detect HackRF devices using native hackrf_info tool.
Fallback for when SoapySDR is not available.
"""
devices: list[SDRDevice] = []
if not _check_tool('hackrf_info'):
return devices
try:
result = subprocess.run(
['hackrf_info'],
capture_output=True,
text=True,
timeout=5
)
# Parse hackrf_info output
# Look for "Serial number:" lines
serial_pattern = r'Serial number:\s*(\S+)'
from .hackrf import HackRFCommandBuilder
serials_found = re.findall(serial_pattern, result.stdout)
for i, serial in enumerate(serials_found):
devices.append(SDRDevice(
sdr_type=SDRType.HACKRF,
index=i,
name=f'HackRF One',
serial=serial,
driver='hackrf',
capabilities=HackRFCommandBuilder.CAPABILITIES
))
# Fallback: check if any HackRF found without serial
if not devices and 'Found HackRF' in result.stdout:
devices.append(SDRDevice(
sdr_type=SDRType.HACKRF,
index=0,
name='HackRF One',
serial='Unknown',
driver='hackrf',
capabilities=HackRFCommandBuilder.CAPABILITIES
))
except Exception as e:
logger.debug(f"HackRF detection error: {e}")
return devices
def detect_all_devices() -> list[SDRDevice]:
"""
Detect all connected SDR devices across all supported hardware types.
Returns a unified list of SDRDevice objects sorted by type and index.
"""
devices: list[SDRDevice] = []
# RTL-SDR via native tool (primary method)
devices.extend(detect_rtlsdr_devices())
# SoapySDR devices (LimeSDR, HackRF, etc.)
soapy_devices = detect_soapy_devices()
devices.extend(soapy_devices)
# Native HackRF detection (fallback if SoapySDR didn't find it)
hackrf_from_soapy = any(d.sdr_type == SDRType.HACKRF for d in soapy_devices)
if not hackrf_from_soapy:
devices.extend(detect_hackrf_devices())
# Sort by type name, then index
devices.sort(key=lambda d: (d.sdr_type.value, d.index))
logger.info(f"Detected {len(devices)} SDR device(s)")
for d in devices:
logger.debug(f" {d.sdr_type.value}:{d.index} - {d.name} (serial: {d.serial})")
return devices

148
utils/sdr/hackrf.py Normal file
View File

@@ -0,0 +1,148 @@
"""
HackRF command builder implementation.
Uses SoapySDR-based tools for FM demodulation and signal capture.
HackRF supports 1 MHz to 6 GHz frequency range.
"""
from typing import Optional
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
class HackRFCommandBuilder(CommandBuilder):
"""HackRF command builder using SoapySDR tools."""
CAPABILITIES = SDRCapabilities(
sdr_type=SDRType.HACKRF,
freq_min_mhz=1.0, # 1 MHz
freq_max_mhz=6000.0, # 6 GHz
gain_min=0.0,
gain_max=62.0, # LNA (0-40) + VGA (0-62)
sample_rates=[2000000, 4000000, 8000000, 10000000, 20000000],
supports_bias_t=True,
supports_ppm=False,
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 f'driver=hackrf'
def _split_gain(self, gain: float) -> tuple[int, int]:
"""
Split total gain into LNA and VGA components.
HackRF has two gain stages:
- LNA: 0-40 dB (RF amplifier)
- VGA: 0-62 dB (IF amplifier)
This function distributes the requested gain across both stages.
"""
if gain <= 40:
# All to LNA first
return int(gain), 0
else:
# Max out LNA, rest to VGA
lna = 40
vga = min(62, int(gain - 40))
return lna, vga
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
) -> list[str]:
"""
Build SoapySDR rx_fm command for FM demodulation.
For pager decoding and iridium capture with HackRF.
"""
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:
lna, vga = self._split_gain(gain)
cmd.extend(['-g', f'LNA={lna},VGA={vga}'])
if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)])
# Output to stdout
cmd.append('-')
return cmd
def build_adsb_command(
self,
device: SDRDevice,
gain: Optional[float] = None
) -> 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))])
return cmd
def build_ism_command(
self,
device: SDRDevice,
frequency_mhz: float = 433.92,
gain: Optional[float] = None,
ppm: Optional[int] = None
) -> 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))])
return cmd
def get_capabilities(self) -> SDRCapabilities:
"""Return HackRF capabilities."""
return self.CAPABILITIES
@classmethod
def get_sdr_type(cls) -> SDRType:
"""Return SDR type."""
return SDRType.HACKRF

136
utils/sdr/limesdr.py Normal file
View File

@@ -0,0 +1,136 @@
"""
LimeSDR command builder implementation.
Uses SoapySDR-based tools for FM demodulation and signal capture.
LimeSDR supports 100 kHz to 3.8 GHz frequency range.
"""
from typing import Optional
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
class LimeSDRCommandBuilder(CommandBuilder):
"""LimeSDR command builder using SoapySDR tools."""
CAPABILITIES = SDRCapabilities(
sdr_type=SDRType.LIME_SDR,
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
sample_rates=[1000000, 2000000, 4000000, 8000000, 10000000, 20000000],
supports_bias_t=False,
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 f'driver=lime'
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
) -> list[str]:
"""
Build SoapySDR rx_fm command for FM demodulation.
For pager decoding and iridium capture with LimeSDR.
"""
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:
# LimeSDR gain is applied to LNAH element
cmd.extend(['-g', f'LNAH={int(gain)}'])
if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)])
# Output to stdout
cmd.append('-')
return cmd
def build_adsb_command(
self,
device: SDRDevice,
gain: Optional[float] = None
) -> list[str]:
"""
Build dump1090 command with SoapySDR support for ADS-B decoding.
Uses dump1090 compiled with SoapySDR support, or readsb as alternative.
Note: Requires dump1090 with SoapySDR support or readsb.
"""
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'
]
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: Optional[float] = None,
ppm: Optional[int] = None
) -> 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))])
# PPM not typically needed for LimeSDR (TCXO)
# but include if specified
if ppm is not None and ppm != 0:
cmd.extend(['-p', str(ppm)])
return cmd
def get_capabilities(self) -> SDRCapabilities:
"""Return LimeSDR capabilities."""
return self.CAPABILITIES
@classmethod
def get_sdr_type(cls) -> SDRType:
"""Return SDR type."""
return SDRType.LIME_SDR

121
utils/sdr/rtlsdr.py Normal file
View File

@@ -0,0 +1,121 @@
"""
RTL-SDR command builder implementation.
Uses native rtl_* tools (rtl_fm, rtl_433) and dump1090 for maximum compatibility
with existing RTL-SDR installations. No SoapySDR dependency required.
"""
from typing import Optional
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
class RTLSDRCommandBuilder(CommandBuilder):
"""RTL-SDR command builder using native rtl_* tools."""
CAPABILITIES = SDRCapabilities(
sdr_type=SDRType.RTL_SDR,
freq_min_mhz=24.0,
freq_max_mhz=1766.0,
gain_min=0.0,
gain_max=49.6,
sample_rates=[250000, 1024000, 1800000, 2048000, 2400000],
supports_bias_t=True,
supports_ppm=True,
tx_capable=False
)
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
) -> list[str]:
"""
Build rtl_fm command for FM demodulation.
Used for pager decoding and iridium capture.
"""
cmd = [
'rtl_fm',
'-d', str(device.index),
'-f', f'{frequency_mhz}M',
'-M', modulation,
'-s', str(sample_rate),
]
if gain is not None and gain > 0:
cmd.extend(['-g', str(gain)])
if ppm is not None and ppm != 0:
cmd.extend(['-p', str(ppm)])
if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)])
# Output to stdout for piping
cmd.append('-')
return cmd
def build_adsb_command(
self,
device: SDRDevice,
gain: Optional[float] = None
) -> list[str]:
"""
Build dump1090 command for ADS-B decoding.
Uses dump1090 with network output for SBS data streaming.
"""
cmd = [
'dump1090',
'--net',
'--device-index', str(device.index),
'--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: Optional[float] = None,
ppm: Optional[int] = None
) -> list[str]:
"""
Build rtl_433 command for ISM band sensor decoding.
Outputs JSON for easy parsing.
"""
cmd = [
'rtl_433',
'-d', str(device.index),
'-f', f'{frequency_mhz}M',
'-F', 'json'
]
if gain is not None and gain > 0:
cmd.extend(['-g', str(int(gain))])
if ppm is not None and ppm != 0:
cmd.extend(['-p', str(ppm)])
return cmd
def get_capabilities(self) -> SDRCapabilities:
"""Return RTL-SDR capabilities."""
return self.CAPABILITIES
@classmethod
def get_sdr_type(cls) -> SDRType:
"""Return SDR type."""
return SDRType.RTL_SDR

257
utils/sdr/validation.py Normal file
View File

@@ -0,0 +1,257 @@
"""
Hardware-specific parameter validation for SDR devices.
Validates frequency, gain, sample rate, and other parameters against
the capabilities of specific SDR hardware.
"""
from typing import Optional
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
) -> float:
"""
Validate frequency against device capabilities.
Args:
freq_mhz: Frequency in MHz
device: SDR device (optional, takes precedence)
capabilities: SDR capabilities (used if device not provided)
Returns:
Validated frequency in MHz
Raises:
SDRValidationError: If frequency is out of range
"""
if device:
caps = device.capabilities
elif capabilities:
caps = capabilities
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
)
if not caps.freq_min_mhz <= freq_mhz <= caps.freq_max_mhz:
raise SDRValidationError(
f"Frequency {freq_mhz} MHz out of range for {caps.sdr_type.value}. "
f"Valid range: {caps.freq_min_mhz}-{caps.freq_max_mhz} MHz"
)
return freq_mhz
def validate_gain(
gain: float,
device: Optional[SDRDevice] = None,
capabilities: Optional[SDRCapabilities] = None
) -> float:
"""
Validate gain against device capabilities.
Args:
gain: Gain in dB
device: SDR device (optional, takes precedence)
capabilities: SDR capabilities (used if device not provided)
Returns:
Validated gain in dB
Raises:
SDRValidationError: If gain is out of range
"""
if device:
caps = device.capabilities
elif capabilities:
caps = capabilities
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
)
# Allow 0 for auto gain
if gain == 0:
return 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"
)
return gain
def validate_sample_rate(
rate: int,
device: Optional[SDRDevice] = None,
capabilities: Optional[SDRCapabilities] = None,
snap_to_nearest: bool = True
) -> int:
"""
Validate sample rate against device capabilities.
Args:
rate: Sample rate in Hz
device: SDR device (optional, takes precedence)
capabilities: SDR capabilities (used if device not provided)
snap_to_nearest: If True, return nearest valid rate instead of raising
Returns:
Validated sample rate in Hz
Raises:
SDRValidationError: If rate is invalid and snap_to_nearest is False
"""
if device:
caps = device.capabilities
elif capabilities:
caps = capabilities
else:
return rate # No validation without capabilities
if not caps.sample_rates:
return rate # No restrictions
if rate in caps.sample_rates:
return rate
if snap_to_nearest:
# Find closest valid rate
closest = min(caps.sample_rates, key=lambda x: abs(x - rate))
return closest
raise SDRValidationError(
f"Sample rate {rate} Hz not supported by {caps.sdr_type.value}. "
f"Valid rates: {caps.sample_rates}"
)
def validate_ppm(
ppm: int,
device: Optional[SDRDevice] = None,
capabilities: Optional[SDRCapabilities] = None
) -> int:
"""
Validate PPM frequency correction.
Args:
ppm: PPM correction value
device: SDR device (optional, takes precedence)
capabilities: SDR capabilities (used if device not provided)
Returns:
Validated PPM value
Raises:
SDRValidationError: If PPM is out of range or not supported
"""
if device:
caps = device.capabilities
elif capabilities:
caps = capabilities
else:
caps = None
# Check if device supports PPM
if caps and not caps.supports_ppm:
if ppm != 0:
# Warn but don't fail - some hardware just ignores PPM
pass
return 0 # Return 0 to indicate no correction
# Standard PPM range
if not -1000 <= ppm <= 1000:
raise SDRValidationError(
f"PPM correction {ppm} out of range. Valid range: -1000 to 1000"
)
return ppm
def validate_device_index(index: int) -> int:
"""
Validate device index.
Args:
index: Device index (0-255)
Returns:
Validated device index
Raises:
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"
)
return index
def validate_squelch(squelch: int) -> int:
"""
Validate squelch level.
Args:
squelch: Squelch level (0-1000, 0 = off)
Returns:
Validated squelch level
Raises:
SDRValidationError: If squelch is out of range
"""
if not 0 <= squelch <= 1000:
raise SDRValidationError(
f"Squelch {squelch} out of range. Valid range: 0-1000"
)
return squelch
def get_capabilities_for_type(sdr_type: SDRType) -> SDRCapabilities:
"""
Get default capabilities for an SDR type.
Args:
sdr_type: The SDR type
Returns:
SDRCapabilities for the specified type
"""
from .rtlsdr import RTLSDRCommandBuilder
from .limesdr import LimeSDRCommandBuilder
from .hackrf import HackRFCommandBuilder
builders = {
SDRType.RTL_SDR: RTLSDRCommandBuilder,
SDRType.LIME_SDR: LimeSDRCommandBuilder,
SDRType.HACKRF: HackRFCommandBuilder,
}
builder_class = builders.get(sdr_type)
if builder_class:
return builder_class.CAPABILITIES
raise SDRValidationError(f"Unknown SDR type: {sdr_type}")