fix: HackRF users get misleading RTL-SDR error in rtlamr/sstv/weather-sat modes

Several modes didn't pass sdr_type to claim_sdr_device(), defaulting to
'rtlsdr' and triggering an rtl_test USB probe that fails for HackRF with
a confusing "check that the RTL-SDR is connected" message.

- Add sdr_type to frontend start requests for rtlamr, weather-sat, sstv-general
- Read sdr_type in backend routes and pass to claim/release_sdr_device()
- Add early guard returning clear "not yet supported" error for non-RTL-SDR
  hardware in modes that are hardcoded to RTL-SDR tools
- Make probe_rtlsdr_device error message device-type-agnostic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-03-04 07:52:13 +00:00
parent 325dafacbc
commit 8d91c200a5
8 changed files with 60 additions and 22 deletions

View File

@@ -29,6 +29,7 @@ rtl_tcp_lock = threading.Lock()
# Track which device is being used # Track which device is being used
rtlamr_active_device: int | None = None rtlamr_active_device: int | None = None
rtlamr_active_sdr_type: str = 'rtlsdr'
def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None: def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
@@ -62,7 +63,7 @@ def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
except Exception as e: except Exception as e:
app_module.rtlamr_queue.put({'type': 'error', 'text': str(e)}) app_module.rtlamr_queue.put({'type': 'error', 'text': str(e)})
finally: finally:
global rtl_tcp_process, rtlamr_active_device global rtl_tcp_process, rtlamr_active_device, rtlamr_active_sdr_type
# Ensure rtlamr process is terminated # Ensure rtlamr process is terminated
try: try:
process.terminate() process.terminate()
@@ -91,19 +92,26 @@ def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
app_module.rtlamr_process = None app_module.rtlamr_process = None
# Release SDR device # Release SDR device
if rtlamr_active_device is not None: if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device) app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type)
rtlamr_active_device = None rtlamr_active_device = None
@rtlamr_bp.route('/start_rtlamr', methods=['POST']) @rtlamr_bp.route('/start_rtlamr', methods=['POST'])
def start_rtlamr() -> Response: def start_rtlamr() -> Response:
global rtl_tcp_process, rtlamr_active_device global rtl_tcp_process, rtlamr_active_device, rtlamr_active_sdr_type
with app_module.rtlamr_lock: with app_module.rtlamr_lock:
if app_module.rtlamr_process: if app_module.rtlamr_process:
return jsonify({'status': 'error', 'message': 'RTLAMR already running'}), 409 return jsonify({'status': 'error', 'message': 'RTLAMR already running'}), 409
data = request.json or {} data = request.json or {}
sdr_type_str = data.get('sdr_type', 'rtlsdr')
if sdr_type_str != 'rtlsdr':
return jsonify({
'status': 'error',
'message': f'{sdr_type_str.replace("_", " ").title()} is not yet supported for this mode. Please use an RTL-SDR device.'
}), 400
# Validate inputs # Validate inputs
try: try:
@@ -116,7 +124,7 @@ def start_rtlamr() -> Response:
# Check if device is available # Check if device is available
device_int = int(device) device_int = int(device)
error = app_module.claim_sdr_device(device_int, 'rtlamr') error = app_module.claim_sdr_device(device_int, 'rtlamr', sdr_type_str)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -125,6 +133,7 @@ def start_rtlamr() -> Response:
}), 409 }), 409
rtlamr_active_device = device_int rtlamr_active_device = device_int
rtlamr_active_sdr_type = sdr_type_str
# Clear queue # Clear queue
while not app_module.rtlamr_queue.empty(): while not app_module.rtlamr_queue.empty():
@@ -170,7 +179,7 @@ def start_rtlamr() -> Response:
logger.error(f"Failed to start rtl_tcp: {e}") logger.error(f"Failed to start rtl_tcp: {e}")
# Release SDR device on rtl_tcp failure # Release SDR device on rtl_tcp failure
if rtlamr_active_device is not None: if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device) app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type)
rtlamr_active_device = None rtlamr_active_device = None
return jsonify({'status': 'error', 'message': f'Failed to start rtl_tcp: {e}'}), 500 return jsonify({'status': 'error', 'message': f'Failed to start rtl_tcp: {e}'}), 500
@@ -242,7 +251,7 @@ def start_rtlamr() -> Response:
rtl_tcp_process.wait(timeout=2) rtl_tcp_process.wait(timeout=2)
rtl_tcp_process = None rtl_tcp_process = None
if rtlamr_active_device is not None: if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device) app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type)
rtlamr_active_device = None rtlamr_active_device = None
return jsonify({'status': 'error', 'message': 'rtlamr not found. Install from https://github.com/bemasher/rtlamr'}) return jsonify({'status': 'error', 'message': 'rtlamr not found. Install from https://github.com/bemasher/rtlamr'})
except Exception as e: except Exception as e:
@@ -253,14 +262,14 @@ def start_rtlamr() -> Response:
rtl_tcp_process.wait(timeout=2) rtl_tcp_process.wait(timeout=2)
rtl_tcp_process = None rtl_tcp_process = None
if rtlamr_active_device is not None: if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device) app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type)
rtlamr_active_device = None rtlamr_active_device = None
return jsonify({'status': 'error', 'message': str(e)}) return jsonify({'status': 'error', 'message': str(e)})
@rtlamr_bp.route('/stop_rtlamr', methods=['POST']) @rtlamr_bp.route('/stop_rtlamr', methods=['POST'])
def stop_rtlamr() -> Response: def stop_rtlamr() -> Response:
global rtl_tcp_process, rtlamr_active_device global rtl_tcp_process, rtlamr_active_device, rtlamr_active_sdr_type
# Grab process refs inside locks, clear state, then terminate outside # Grab process refs inside locks, clear state, then terminate outside
rtlamr_proc = None rtlamr_proc = None
@@ -293,7 +302,7 @@ def stop_rtlamr() -> Response:
# Release device from registry # Release device from registry
if rtlamr_active_device is not None: if rtlamr_active_device is not None:
app_module.release_sdr_device(rtlamr_active_device) app_module.release_sdr_device(rtlamr_active_device, rtlamr_active_sdr_type)
rtlamr_active_device = None rtlamr_active_device = None
return jsonify({'status': 'stopped'}) return jsonify({'status': 'stopped'})

View File

@@ -57,6 +57,7 @@ _timescale_lock = threading.Lock()
# Track which device is being used # Track which device is being used
sstv_active_device: int | None = None sstv_active_device: int | None = None
sstv_active_sdr_type: str = 'rtlsdr'
def _progress_callback(data: dict) -> None: def _progress_callback(data: dict) -> None:
@@ -154,6 +155,14 @@ def start_decoder():
# Get parameters # Get parameters
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
sdr_type_str = data.get('sdr_type', 'rtlsdr')
if sdr_type_str != 'rtlsdr':
return jsonify({
'status': 'error',
'message': f'{sdr_type_str.replace("_", " ").title()} is not yet supported for this mode. Please use an RTL-SDR device.'
}), 400
frequency = data.get('frequency', ISS_SSTV_FREQ) frequency = data.get('frequency', ISS_SSTV_FREQ)
modulation = str(data.get('modulation', ISS_SSTV_MODULATION)).strip().lower() modulation = str(data.get('modulation', ISS_SSTV_MODULATION)).strip().lower()
device_index = data.get('device', 0) device_index = data.get('device', 0)
@@ -209,9 +218,9 @@ def start_decoder():
longitude = None longitude = None
# Claim SDR device # Claim SDR device
global sstv_active_device global sstv_active_device, sstv_active_sdr_type
device_int = int(device_index) device_int = int(device_index)
error = app_module.claim_sdr_device(device_int, 'sstv') error = app_module.claim_sdr_device(device_int, 'sstv', sdr_type_str)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -231,6 +240,7 @@ def start_decoder():
if success: if success:
sstv_active_device = device_int sstv_active_device = device_int
sstv_active_sdr_type = sdr_type_str
result = { result = {
'status': 'started', 'status': 'started',
@@ -247,7 +257,7 @@ def start_decoder():
return jsonify(result) return jsonify(result)
else: else:
# Release device on failure # Release device on failure
app_module.release_sdr_device(device_int) app_module.release_sdr_device(device_int, sdr_type_str)
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': 'Failed to start decoder' 'message': 'Failed to start decoder'
@@ -262,13 +272,13 @@ def stop_decoder():
Returns: Returns:
JSON confirmation. JSON confirmation.
""" """
global sstv_active_device global sstv_active_device, sstv_active_sdr_type
decoder = get_sstv_decoder() decoder = get_sstv_decoder()
decoder.stop() decoder.stop()
# Release device from registry # Release device from registry
if sstv_active_device is not None: if sstv_active_device is not None:
app_module.release_sdr_device(sstv_active_device) app_module.release_sdr_device(sstv_active_device, sstv_active_sdr_type)
sstv_active_device = None sstv_active_device = None
return jsonify({'status': 'stopped'}) return jsonify({'status': 'stopped'})

View File

@@ -30,6 +30,7 @@ _sstv_general_queue: queue.Queue = queue.Queue(maxsize=100)
# Track which device is being used # Track which device is being used
_sstv_general_active_device: int | None = None _sstv_general_active_device: int | None = None
_sstv_general_active_sdr_type: str = 'rtlsdr'
# Predefined SSTV frequencies # Predefined SSTV frequencies
SSTV_FREQUENCIES = [ SSTV_FREQUENCIES = [
@@ -119,6 +120,14 @@ def start_decoder():
break break
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
sdr_type_str = data.get('sdr_type', 'rtlsdr')
if sdr_type_str != 'rtlsdr':
return jsonify({
'status': 'error',
'message': f'{sdr_type_str.replace("_", " ").title()} is not yet supported for this mode. Please use an RTL-SDR device.'
}), 400
frequency = data.get('frequency') frequency = data.get('frequency')
modulation = data.get('modulation') modulation = data.get('modulation')
device_index = data.get('device', 0) device_index = data.get('device', 0)
@@ -155,9 +164,9 @@ def start_decoder():
}), 400 }), 400
# Claim SDR device # Claim SDR device
global _sstv_general_active_device global _sstv_general_active_device, _sstv_general_active_sdr_type
device_int = int(device_index) device_int = int(device_index)
error = app_module.claim_sdr_device(device_int, 'sstv_general') error = app_module.claim_sdr_device(device_int, 'sstv_general', sdr_type_str)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -175,6 +184,7 @@ def start_decoder():
if success: if success:
_sstv_general_active_device = device_int _sstv_general_active_device = device_int
_sstv_general_active_sdr_type = sdr_type_str
return jsonify({ return jsonify({
'status': 'started', 'status': 'started',
'frequency': frequency, 'frequency': frequency,
@@ -182,7 +192,7 @@ def start_decoder():
'device': device_index, 'device': device_index,
}) })
else: else:
app_module.release_sdr_device(device_int) app_module.release_sdr_device(device_int, sdr_type_str)
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
'message': 'Failed to start decoder', 'message': 'Failed to start decoder',
@@ -192,12 +202,12 @@ def start_decoder():
@sstv_general_bp.route('/stop', methods=['POST']) @sstv_general_bp.route('/stop', methods=['POST'])
def stop_decoder(): def stop_decoder():
"""Stop general SSTV decoder.""" """Stop general SSTV decoder."""
global _sstv_general_active_device global _sstv_general_active_device, _sstv_general_active_sdr_type
decoder = get_general_sstv_decoder() decoder = get_general_sstv_decoder()
decoder.stop() decoder.stop()
if _sstv_general_active_device is not None: if _sstv_general_active_device is not None:
app_module.release_sdr_device(_sstv_general_active_device) app_module.release_sdr_device(_sstv_general_active_device, _sstv_general_active_sdr_type)
_sstv_general_active_device = None _sstv_general_active_device = None
return jsonify({'status': 'stopped'}) return jsonify({'status': 'stopped'})

View File

@@ -136,6 +136,13 @@ def start_capture():
}) })
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
sdr_type_str = data.get('sdr_type', 'rtlsdr')
if sdr_type_str != 'rtlsdr':
return jsonify({
'status': 'error',
'message': f'{sdr_type_str.replace("_", " ").title()} is not yet supported for this mode. Please use an RTL-SDR device.'
}), 400
# Validate satellite # Validate satellite
satellite = data.get('satellite') satellite = data.get('satellite')
@@ -173,7 +180,7 @@ def start_capture():
if not rtl_tcp_host: if not rtl_tcp_host:
try: try:
import app as app_module import app as app_module
error = app_module.claim_sdr_device(device_index, 'weather_sat') error = app_module.claim_sdr_device(device_index, 'weather_sat', sdr_type_str)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',

View File

@@ -108,7 +108,7 @@ const SSTVGeneral = (function() {
const response = await fetch('/sstv-general/start', { const response = await fetch('/sstv-general/start', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ frequency, modulation, device }) body: JSON.stringify({ frequency, modulation, device, sdr_type: typeof getSelectedSDRType === 'function' ? getSelectedSDRType() : 'rtlsdr' })
}); });
const data = await response.json(); const data = await response.json();

View File

@@ -258,6 +258,7 @@ const WeatherSat = (function() {
device, device,
gain, gain,
bias_t: biasT, bias_t: biasT,
sdr_type: typeof getSelectedSDRType === 'function' ? getSelectedSDRType() : 'rtlsdr',
}; };
// Add rtl_tcp params if using remote SDR // Add rtl_tcp params if using remote SDR

View File

@@ -5329,6 +5329,7 @@
gain: gain, gain: gain,
ppm: ppm, ppm: ppm,
device: device, device: device,
sdr_type: getSelectedSDRType(),
msgtype: msgtype, msgtype: msgtype,
filterid: filterid, filterid: filterid,
unique: unique, unique: unique,

View File

@@ -493,7 +493,7 @@ def probe_rtlsdr_device(device_index: int) -> str | None:
) )
return ( return (
f'SDR device {device_index} is not available — ' f'SDR device {device_index} is not available — '
f'check that the RTL-SDR is connected and not in use by another process.' f'check that the SDR device is connected and not in use by another process.'
) )
except Exception as e: except Exception as e: