diff --git a/routes/waterfall_websocket.py b/routes/waterfall_websocket.py index 260c044..de31227 100644 --- a/routes/waterfall_websocket.py +++ b/routes/waterfall_websocket.py @@ -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: diff --git a/static/js/modes/waterfall.js b/static/js/modes/waterfall.js index a5b4f8c..0f3a05a 100644 --- a/static/js/modes/waterfall.js +++ b/static/js/modes/waterfall.js @@ -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) { diff --git a/utils/sdr/airspy.py b/utils/sdr/airspy.py index 875f4cb..a8c7539 100644 --- a/utils/sdr/airspy.py +++ b/utils/sdr/airspy.py @@ -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), diff --git a/utils/sdr/hackrf.py b/utils/sdr/hackrf.py index 63a5fd6..1eb7c3c 100644 --- a/utils/sdr/hackrf.py +++ b/utils/sdr/hackrf.py @@ -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), diff --git a/utils/sdr/limesdr.py b/utils/sdr/limesdr.py index 3dcd8d2..d41a8d4 100644 --- a/utils/sdr/limesdr.py +++ b/utils/sdr/limesdr.py @@ -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), diff --git a/utils/sdr/sdrplay.py b/utils/sdr/sdrplay.py index 79df27c..c4aa1e6 100644 --- a/utils/sdr/sdrplay.py +++ b/utils/sdr/sdrplay.py @@ -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),