mirror of
https://github.com/smittix/intercept.git
synced 2026-07-05 16:18:12 -07:00
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:
@@ -172,7 +172,9 @@ Instead of running command-line tools manually, INTERCEPT handles the process ma
|
|||||||
- **Message export** to CSV/JSON
|
- **Message export** to CSV/JSON
|
||||||
- **Signal activity meter** and waterfall display
|
- **Signal activity meter** and waterfall display
|
||||||
- **Message logging** to file with timestamps
|
- **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**
|
- **Configurable gain and PPM correction**
|
||||||
- **Device intelligence** dashboard with tracking
|
- **Device intelligence** dashboard with tracking
|
||||||
- **Disclaimer acceptance** on first use
|
- **Disclaimer acceptance** on first use
|
||||||
@@ -183,7 +185,10 @@ Instead of running command-line tools manually, INTERCEPT handles the process ma
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Hardware
|
### 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)
|
- WiFi adapter capable of monitor mode (for WiFi features)
|
||||||
- Bluetooth adapter (for Bluetooth 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:
|
Install the tools for the features you need:
|
||||||
|
|
||||||
|
#### Core SDR Tools
|
||||||
|
|
||||||
| Tool | macOS | Ubuntu/Debian | Purpose |
|
| Tool | macOS | Ubuntu/Debian | Purpose |
|
||||||
|------|-------|---------------|---------|
|
|------|-------|---------------|---------|
|
||||||
| rtl-sdr | `brew install rtl-sdr` | `sudo apt install rtl-sdr` | Required for all SDR features |
|
| rtl-sdr | `brew install librtlsdr` | `sudo apt install rtl-sdr` | RTL-SDR support |
|
||||||
| multimon-ng | `Use MacPorts for now sudo ports install multimon-ng` | `sudo apt install multimon-ng` | Pager decoding |
|
| 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 |
|
| 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 |
|
| 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 |
|
| aircrack-ng | `brew install aircrack-ng` | `sudo apt install aircrack-ng` | WiFi reconnaissance |
|
||||||
| bluez | Built-in (limited) | `sudo apt install bluez bluetooth` | Bluetooth scanning |
|
| 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
|
### 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
|
```bash
|
||||||
git clone https://github.com/smittix/intercept.git
|
git clone https://github.com/smittix/intercept.git
|
||||||
cd intercept
|
cd intercept
|
||||||
@@ -221,10 +250,30 @@ pip install -r requirements.txt
|
|||||||
sudo python3 intercept.py
|
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.
|
Open `http://localhost:5050` in your browser.
|
||||||
|
|
||||||
> **Note:** Running as root/sudo is recommended for full functionality (monitor mode, raw sockets, etc.)
|
> **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
|
### Command-line options
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -241,11 +290,12 @@ python3 intercept.py --help
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Pager Mode
|
### Pager Mode
|
||||||
1. **Select Device** - Choose your RTL-SDR device from the dropdown
|
1. **Select Hardware** - Choose your SDR type (RTL-SDR, LimeSDR, or HackRF)
|
||||||
2. **Set Frequency** - Enter a frequency in MHz or use a preset
|
2. **Select Device** - Choose your SDR device from the dropdown
|
||||||
3. **Choose Protocols** - Select which protocols to decode (POCSAG/FLEX)
|
3. **Set Frequency** - Enter a frequency in MHz or use a preset
|
||||||
4. **Adjust Settings** - Set gain, squelch, and PPM correction as needed
|
4. **Choose Protocols** - Select which protocols to decode (POCSAG/FLEX)
|
||||||
5. **Start Decoding** - Click the green "Start Decoding" button
|
5. **Adjust Settings** - Set gain, squelch, and PPM correction as needed
|
||||||
|
6. **Start Decoding** - Click the green "Start Decoding" button
|
||||||
|
|
||||||
### WiFi Mode
|
### WiFi Mode
|
||||||
1. **Select Interface** - Choose a WiFi adapter capable of monitor 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
|
4. **View Devices** - Devices appear with name, address, and classification
|
||||||
|
|
||||||
### Aircraft Mode
|
### Aircraft Mode
|
||||||
1. **Check Tools** - Ensure dump1090 or rtl_adsb is installed
|
1. **Select Hardware** - Choose your SDR type (RTL-SDR uses dump1090, others use readsb)
|
||||||
2. **Set Location** - Enter observer coordinates or click "Use GPS Location"
|
2. **Check Tools** - Ensure dump1090 or readsb is installed
|
||||||
3. **Start Tracking** - Click "Start Tracking" to begin ADS-B reception
|
3. **Set Location** - Enter observer coordinates or click "Use GPS Location"
|
||||||
4. **View Map** - Aircraft appear on the interactive Leaflet map
|
4. **Start Tracking** - Click "Start Tracking" to begin ADS-B reception
|
||||||
5. **Click Aircraft** - Click markers for detailed information
|
5. **View Map** - Aircraft appear on the interactive Leaflet map
|
||||||
6. **Display Options** - Toggle callsigns, altitude, trails, range rings, clustering
|
6. **Click Aircraft** - Click markers for detailed information
|
||||||
7. **Filter Aircraft** - Use dropdown to show all, military, civil, or emergency only
|
7. **Display Options** - Toggle callsigns, altitude, trails, range rings, clustering
|
||||||
8. **Full Dashboard** - Click "Full Screen Dashboard" for dedicated radar view
|
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
|
### Satellite Mode
|
||||||
1. **Set Location** - Enter observer coordinates or click "Use My Location"
|
1. **Set Location** - Enter observer coordinates or click "Use My Location"
|
||||||
@@ -291,10 +342,46 @@ python3 intercept.py --help
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### No devices found
|
### Python/pip installation issues
|
||||||
- Ensure your RTL-SDR is plugged in
|
|
||||||
- Check `rtl_test` works from command line
|
**"externally-managed-environment" error (Ubuntu 23.04+, Debian 12+):**
|
||||||
- On Linux, you may need to blacklist the DVB-T driver
|
```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
|
### No messages appearing
|
||||||
- Verify the frequency is correct for your area
|
- Verify the frequency is correct for your area
|
||||||
@@ -309,7 +396,13 @@ python3 intercept.py --help
|
|||||||
|
|
||||||
### Device busy error
|
### Device busy error
|
||||||
- Click "Kill All Processes" to stop any stale processes
|
- 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)
|
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
|
## Acknowledgments
|
||||||
|
|
||||||
- [rtl-sdr](https://osmocom.org/projects/rtl-sdr/wiki) - RTL-SDR drivers
|
- [rtl-sdr](https://osmocom.org/projects/rtl-sdr/wiki) - RTL-SDR drivers
|
||||||
- [multimon-ng](https://github.com/EliasOenal/multimon-ng) - Multi-protocol pager decoder
|
- [multimon-ng](https://github.com/EliasOenal/multimon-ng) - Multi-protocol pager decoder
|
||||||
- [rtl_433](https://github.com/merbanan/rtl_433) - 433MHz sensor decoder
|
- [rtl_433](https://github.com/merbanan/rtl_433) - 433MHz sensor decoder
|
||||||
- [dump1090](https://github.com/flightaware/dump1090) - ADS-B decoder for aircraft tracking
|
- [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
|
- [aircrack-ng](https://www.aircrack-ng.org/) - WiFi security auditing tools
|
||||||
- [BlueZ](http://www.bluez.org/) - Official Linux Bluetooth protocol stack
|
- [BlueZ](http://www.bluez.org/) - Official Linux Bluetooth protocol stack
|
||||||
- [Leaflet.js](https://leafletjs.com/) - Interactive maps for aircraft tracking
|
- [Leaflet.js](https://leafletjs.com/) - Interactive maps for aircraft tracking
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ from typing import Any
|
|||||||
from flask import Flask, render_template, jsonify, send_file, Response, request
|
from flask import Flask, render_template, jsonify, send_file, Response, request
|
||||||
|
|
||||||
from utils.dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES
|
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
|
# Create Flask app
|
||||||
@@ -105,7 +106,7 @@ def index() -> str:
|
|||||||
'multimon': check_tool('multimon-ng'),
|
'multimon': check_tool('multimon-ng'),
|
||||||
'rtl_433': check_tool('rtl_433')
|
'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)
|
return render_template('index.html', tools=tools, devices=devices)
|
||||||
|
|
||||||
|
|
||||||
@@ -116,7 +117,9 @@ def favicon() -> Response:
|
|||||||
|
|
||||||
@app.route('/devices')
|
@app.route('/devices')
|
||||||
def get_devices() -> Response:
|
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')
|
@app.route('/dependencies')
|
||||||
|
|||||||
+3
-9
@@ -3,18 +3,12 @@ TLE_SATELLITES = {
|
|||||||
'ISS': ('ISS (ZARYA)',
|
'ISS': ('ISS (ZARYA)',
|
||||||
'1 25544U 98067A 24001.00000000 .00000000 00000-0 00000-0 0 0000',
|
'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'),
|
'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)',
|
'NOAA-20': ('NOAA 20 (JPSS-1)',
|
||||||
'1 43013U 17073A 24001.00000000 .00000-0 00000-0 00000-0 0 0000',
|
'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'),
|
'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',
|
'METEOR-M2': ('METEOR-M 2',
|
||||||
'1 40069U 14037A 24001.00000000 .00000-0 00000-0 00000-0 0 0000',
|
'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'),
|
'2 40069 98.5400 0.0000 0005000 0.0000 0.0000 14.21000000000000'),
|
||||||
|
|||||||
+36
-5
@@ -18,6 +18,7 @@ import app as app_module
|
|||||||
from utils.logging import adsb_logger as logger
|
from utils.logging import adsb_logger as logger
|
||||||
from utils.validation import validate_device_index, validate_gain
|
from utils.validation import validate_device_index, validate_gain
|
||||||
from utils.sse import format_sse
|
from utils.sse import format_sse
|
||||||
|
from utils.sdr import SDRFactory, SDRType
|
||||||
|
|
||||||
adsb_bp = Blueprint('adsb', __name__, url_prefix='/adsb')
|
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)
|
# Common installation paths for dump1090 (when not in PATH)
|
||||||
DUMP1090_PATHS = [
|
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',
|
||||||
'/usr/local/bin/dump1090-fa',
|
'/usr/local/bin/dump1090-fa',
|
||||||
'/usr/local/bin/dump1090-mutability',
|
'/usr/local/bin/dump1090-mutability',
|
||||||
|
# Linux system paths
|
||||||
'/usr/bin/dump1090',
|
'/usr/bin/dump1090',
|
||||||
'/usr/bin/dump1090-fa',
|
'/usr/bin/dump1090-fa',
|
||||||
'/usr/bin/dump1090-mutability',
|
'/usr/bin/dump1090-mutability',
|
||||||
@@ -240,11 +247,23 @@ def start_adsb():
|
|||||||
thread.start()
|
thread.start()
|
||||||
return jsonify({'status': 'started', 'message': 'Connected to existing dump1090 service'})
|
return jsonify({'status': 'started', 'message': 'Connected to existing dump1090 service'})
|
||||||
|
|
||||||
# No existing service, need to start dump1090 ourselves
|
# Get SDR type from request
|
||||||
dump1090_path = find_dump1090()
|
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:
|
# For RTL-SDR, use dump1090. For other hardware, need readsb with SoapySDR
|
||||||
return jsonify({'status': 'error', 'message': 'dump1090 not found. Install dump1090/dump1090-fa or ensure it is in /usr/local/bin/'})
|
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
|
# Kill any stale app-started process
|
||||||
if app_module.adsb_process:
|
if app_module.adsb_process:
|
||||||
@@ -255,7 +274,19 @@ def start_adsb():
|
|||||||
pass
|
pass
|
||||||
app_module.adsb_process = None
|
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:
|
try:
|
||||||
app_module.adsb_process = subprocess.Popen(
|
app_module.adsb_process = subprocess.Popen(
|
||||||
|
|||||||
+38
-13
@@ -23,6 +23,7 @@ import app as app_module
|
|||||||
from utils.logging import iridium_logger as logger
|
from utils.logging import iridium_logger as logger
|
||||||
from utils.validation import validate_frequency, validate_device_index, validate_gain
|
from utils.validation import validate_frequency, validate_device_index, validate_gain
|
||||||
from utils.sse import format_sse
|
from utils.sse import format_sse
|
||||||
|
from utils.sdr import SDRFactory, SDRType
|
||||||
|
|
||||||
iridium_bp = Blueprint('iridium', __name__, url_prefix='/iridium')
|
iridium_bp = Blueprint('iridium', __name__, url_prefix='/iridium')
|
||||||
|
|
||||||
@@ -103,21 +104,45 @@ def start_iridium():
|
|||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
return jsonify({'status': 'error', 'message': 'Invalid sample rate format'}), 400
|
return jsonify({'status': 'error', 'message': 'Invalid sample rate format'}), 400
|
||||||
|
|
||||||
if not shutil.which('iridium-extractor') and not shutil.which('rtl_fm'):
|
# Get SDR type from request
|
||||||
return jsonify({
|
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
||||||
'status': 'error',
|
try:
|
||||||
'message': 'Iridium tools not found. Requires rtl_fm or iridium-extractor.'
|
sdr_type = SDRType(sdr_type_str)
|
||||||
}), 503
|
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:
|
try:
|
||||||
cmd = [
|
# Create device object and build command via abstraction layer
|
||||||
'rtl_fm',
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
||||||
'-f', f'{float(freq)}M',
|
builder = SDRFactory.get_builder(sdr_type)
|
||||||
'-g', str(gain),
|
|
||||||
'-s', sample_rate,
|
# Parse sample rate
|
||||||
'-d', str(device),
|
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(
|
app_module.satellite_process = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
|
|||||||
+20
-17
@@ -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.validation import validate_frequency, validate_device_index, validate_gain, validate_ppm
|
||||||
from utils.sse import format_sse
|
from utils.sse import format_sse
|
||||||
from utils.process import safe_terminate, register_process
|
from utils.process import safe_terminate, register_process
|
||||||
|
from utils.sdr import SDRFactory, SDRType, SDRValidationError
|
||||||
|
|
||||||
pager_bp = Blueprint('pager', __name__)
|
pager_bp = Blueprint('pager', __name__)
|
||||||
|
|
||||||
@@ -201,25 +202,27 @@ def start_decoding() -> Response:
|
|||||||
elif proto == 'FLEX':
|
elif proto == 'FLEX':
|
||||||
decoders.extend(['-a', 'FLEX'])
|
decoders.extend(['-a', 'FLEX'])
|
||||||
|
|
||||||
# Build rtl_fm command
|
# Get SDR type and build command via abstraction layer
|
||||||
rtl_cmd = [
|
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
||||||
'rtl_fm',
|
try:
|
||||||
'-d', str(device),
|
sdr_type = SDRType(sdr_type_str)
|
||||||
'-f', f'{freq}M',
|
except ValueError:
|
||||||
'-M', 'fm',
|
sdr_type = SDRType.RTL_SDR
|
||||||
'-s', '22050',
|
|
||||||
]
|
|
||||||
|
|
||||||
if gain and gain != '0':
|
# Create device object and get command builder
|
||||||
rtl_cmd.extend(['-g', str(gain)])
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
||||||
|
builder = SDRFactory.get_builder(sdr_type)
|
||||||
|
|
||||||
if ppm and ppm != '0':
|
# Build FM demodulation command
|
||||||
rtl_cmd.extend(['-p', str(ppm)])
|
rtl_cmd = builder.build_fm_demod_command(
|
||||||
|
device=sdr_device,
|
||||||
if squelch and squelch != '0':
|
frequency_mhz=freq,
|
||||||
rtl_cmd.extend(['-l', str(squelch)])
|
sample_rate=22050,
|
||||||
|
gain=float(gain) if gain and gain != '0' else None,
|
||||||
rtl_cmd.append('-')
|
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', '-']
|
multimon_cmd = ['multimon-ng', '-t', 'raw'] + decoders + ['-f', 'alpha', '-']
|
||||||
|
|
||||||
|
|||||||
+17
-11
@@ -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.validation import validate_frequency, validate_device_index, validate_gain, validate_ppm
|
||||||
from utils.sse import format_sse
|
from utils.sse import format_sse
|
||||||
from utils.process import safe_terminate, register_process
|
from utils.process import safe_terminate, register_process
|
||||||
|
from utils.sdr import SDRFactory, SDRType
|
||||||
|
|
||||||
sensor_bp = Blueprint('sensor', __name__)
|
sensor_bp = Blueprint('sensor', __name__)
|
||||||
|
|
||||||
@@ -82,19 +83,24 @@ def start_sensor() -> Response:
|
|||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Build rtl_433 command
|
# Get SDR type and build command via abstraction layer
|
||||||
cmd = [
|
sdr_type_str = data.get('sdr_type', 'rtlsdr')
|
||||||
'rtl_433',
|
try:
|
||||||
'-d', str(device),
|
sdr_type = SDRType(sdr_type_str)
|
||||||
'-f', f'{freq}M',
|
except ValueError:
|
||||||
'-F', 'json'
|
sdr_type = SDRType.RTL_SDR
|
||||||
]
|
|
||||||
|
|
||||||
if gain and gain != 0:
|
# Create device object and get command builder
|
||||||
cmd.extend(['-g', str(int(gain))])
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
||||||
|
builder = SDRFactory.get_builder(sdr_type)
|
||||||
|
|
||||||
if ppm and ppm != 0:
|
# Build ISM band decoder command
|
||||||
cmd.extend(['-p', str(ppm)])
|
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)
|
full_cmd = ' '.join(cmd)
|
||||||
logger.info(f"Running: {full_cmd}")
|
logger.info(f"Running: {full_cmd}")
|
||||||
|
|||||||
@@ -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 "$@"
|
||||||
+82
-12
@@ -3105,18 +3105,33 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section" id="rtlDeviceSection">
|
<div class="section" id="rtlDeviceSection">
|
||||||
<h3>RTL-SDR Device</h3>
|
<h3>SDR Device</h3>
|
||||||
<div class="form-group">
|
<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">
|
<select id="deviceSelect">
|
||||||
{% if devices %}
|
{% if devices %}
|
||||||
{% for device in 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 %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<option value="0">No devices found</option>
|
<option value="0">No devices found</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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%;">
|
<button class="preset-btn" onclick="refreshDevices()" style="width: 100%;">
|
||||||
Refresh Devices
|
Refresh Devices
|
||||||
</button>
|
</button>
|
||||||
@@ -4730,7 +4745,8 @@
|
|||||||
frequency: freq,
|
frequency: freq,
|
||||||
gain: gain,
|
gain: gain,
|
||||||
ppm: ppm,
|
ppm: ppm,
|
||||||
device: device
|
device: device,
|
||||||
|
sdr_type: getSelectedSDRType()
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch('/start_sensor', {
|
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() {
|
function refreshDevices() {
|
||||||
fetch('/devices')
|
fetch('/devices')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(devices => {
|
.then(devices => {
|
||||||
|
// Store full device list with SDR type info
|
||||||
|
currentDeviceList = devices;
|
||||||
deviceList = devices;
|
deviceList = devices;
|
||||||
const select = document.getElementById('deviceSelect');
|
|
||||||
if (devices.length === 0) {
|
// Auto-select SDR type if devices found
|
||||||
select.innerHTML = '<option value="0">No devices found</option>';
|
if (devices.length > 0) {
|
||||||
} else {
|
const firstType = devices[0].sdr_type || 'rtlsdr';
|
||||||
select.innerHTML = devices.map(d =>
|
document.getElementById('sdrTypeSelect').value = firstType;
|
||||||
`<option value="${d.index}">${d.index}: ${d.name}</option>`
|
|
||||||
).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
return document.getElementById('deviceSelect').value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelectedSDRType() {
|
||||||
|
return document.getElementById('sdrTypeSelect').value;
|
||||||
|
}
|
||||||
|
|
||||||
function getSelectedProtocols() {
|
function getSelectedProtocols() {
|
||||||
const protocols = [];
|
const protocols = [];
|
||||||
if (document.getElementById('proto_pocsag512').checked) protocols.push('POCSAG512');
|
if (document.getElementById('proto_pocsag512').checked) protocols.push('POCSAG512');
|
||||||
@@ -5187,6 +5254,7 @@
|
|||||||
squelch: squelch,
|
squelch: squelch,
|
||||||
ppm: ppm,
|
ppm: ppm,
|
||||||
device: device,
|
device: device,
|
||||||
|
sdr_type: getSelectedSDRType(),
|
||||||
protocols: protocols
|
protocols: protocols
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -8759,11 +8827,12 @@
|
|||||||
function startAdsbScan() {
|
function startAdsbScan() {
|
||||||
const gain = document.getElementById('adsbGain').value;
|
const gain = document.getElementById('adsbGain').value;
|
||||||
const device = getSelectedDevice();
|
const device = getSelectedDevice();
|
||||||
|
const sdr_type = getSelectedSDRType();
|
||||||
|
|
||||||
fetch('/adsb/start', {
|
fetch('/adsb/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ gain, device })
|
body: JSON.stringify({ gain, device, sdr_type })
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -9759,11 +9828,12 @@
|
|||||||
const gain = document.getElementById('iridiumGain').value;
|
const gain = document.getElementById('iridiumGain').value;
|
||||||
const sampleRate = document.getElementById('iridiumSampleRate').value;
|
const sampleRate = document.getElementById('iridiumSampleRate').value;
|
||||||
const device = getSelectedDevice();
|
const device = getSelectedDevice();
|
||||||
|
const sdr_type = getSelectedSDRType();
|
||||||
|
|
||||||
fetch('/iridium/start', {
|
fetch('/iridium/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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}")
|
||||||
Reference in New Issue
Block a user