Improve waterfall error handling and SDR tool path resolution

- Add pre-flight check for I/Q capture binary before spawning process
- Capture stderr from I/Q process for better error diagnostics
- Sync effective span value back to UI when backend adjusts it
- Use get_tool_path('rx_sdr') in Airspy, HackRF, LimeSDR, and SDRPlay
  command builders to support custom install locations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-25 18:32:14 +00:00
parent 3f7430d114
commit dbf76a4e84
6 changed files with 39 additions and 6 deletions

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import json
import queue
import shutil
import socket
import subprocess
import threading
@@ -546,6 +547,16 @@ def init_waterfall_websocket(app: Flask):
}))
continue
# Pre-flight: check the capture binary exists
if not shutil.which(iq_cmd[0]):
app_module.release_sdr_device(device_index)
claimed_device = None
ws.send(json.dumps({
'status': 'error',
'message': f'Required tool "{iq_cmd[0]}" not found. Install SoapySDR tools (rx_sdr).',
}))
continue
# Spawn I/Q capture process (retry to handle USB release lag)
max_attempts = 3 if was_restarting else 1
try:
@@ -558,7 +569,7 @@ def init_waterfall_websocket(app: Flask):
iq_process = subprocess.Popen(
iq_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
stderr=subprocess.PIPE,
bufsize=0,
)
register_process(iq_process)
@@ -566,17 +577,23 @@ def init_waterfall_websocket(app: Flask):
# Brief check that process started
time.sleep(0.3)
if iq_process.poll() is not None:
stderr_out = ''
if iq_process.stderr:
with suppress(Exception):
stderr_out = iq_process.stderr.read().decode('utf-8', errors='replace').strip()
unregister_process(iq_process)
iq_process = None
if attempt < max_attempts - 1:
logger.info(
f"I/Q process exited immediately, "
f"retrying ({attempt + 1}/{max_attempts})..."
+ (f" stderr: {stderr_out}" if stderr_out else "")
)
time.sleep(0.5)
continue
detail = f": {stderr_out}" if stderr_out else ""
raise RuntimeError(
"I/Q capture process exited immediately"
f"I/Q capture process exited immediately{detail}"
)
break # Process started successfully
except Exception as e:

View File

@@ -2515,6 +2515,10 @@ const Waterfall = (function () {
_endMhz = msg.end_freq;
_drawFreqAxis();
}
if (Number.isFinite(msg.effective_span_mhz)) {
const spanEl = document.getElementById('wfSpanMhz');
if (spanEl) spanEl.value = msg.effective_span_mhz;
}
_setStatus(`Streaming ${_startMhz.toFixed(4)} - ${_endMhz.toFixed(4)} MHz`);
_setVisualStatus('RUNNING');
if (_monitoring) {

View File

@@ -10,6 +10,8 @@ from __future__ import annotations
from typing import Optional
from utils.dependencies import get_tool_path
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
@@ -203,8 +205,9 @@ class AirspyCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
freq_hz = int(frequency_mhz * 1e6)
rx_sdr_path = get_tool_path('rx_sdr') or 'rx_sdr'
cmd = [
'rx_sdr',
rx_sdr_path,
'-d', device_str,
'-f', str(freq_hz),
'-s', str(sample_rate),

View File

@@ -9,6 +9,8 @@ from __future__ import annotations
from typing import Optional
from utils.dependencies import get_tool_path
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
@@ -203,8 +205,9 @@ class HackRFCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
freq_hz = int(frequency_mhz * 1e6)
rx_sdr_path = get_tool_path('rx_sdr') or 'rx_sdr'
cmd = [
'rx_sdr',
rx_sdr_path,
'-d', device_str,
'-f', str(freq_hz),
'-s', str(sample_rate),

View File

@@ -9,6 +9,8 @@ from __future__ import annotations
from typing import Optional
from utils.dependencies import get_tool_path
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
@@ -181,8 +183,9 @@ class LimeSDRCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
freq_hz = int(frequency_mhz * 1e6)
rx_sdr_path = get_tool_path('rx_sdr') or 'rx_sdr'
cmd = [
'rx_sdr',
rx_sdr_path,
'-d', device_str,
'-f', str(freq_hz),
'-s', str(sample_rate),

View File

@@ -9,6 +9,8 @@ from __future__ import annotations
from typing import Optional
from utils.dependencies import get_tool_path
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
@@ -181,8 +183,9 @@ class SDRPlayCommandBuilder(CommandBuilder):
device_str = self._build_device_string(device)
freq_hz = int(frequency_mhz * 1e6)
rx_sdr_path = get_tool_path('rx_sdr') or 'rx_sdr'
cmd = [
'rx_sdr',
rx_sdr_path,
'-d', device_str,
'-f', str(freq_hz),
'-s', str(sample_rate),