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

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

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

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

View File

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

View File

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

View File

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

View File

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