Fix WeFax error detection and surface errors in strip UI

rtl_fm subprocess failures (missing tool, no SDR hardware) were silent —
add tool-path check and post-spawn health check in _start_pipeline(),
show errors prominently in the strip status bar (red text + red dot),
and include error detail in scheduler skip events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-24 15:53:57 +00:00
parent e8056a1d66
commit d976956d2b
3 changed files with 44 additions and 3 deletions
+17 -2
View File
@@ -192,11 +192,15 @@ var WeFax = (function () {
setStripFreq(freqKhz); setStripFreq(freqKhz);
connectSSE(); connectSSE();
} else { } else {
setStatus('Error: ' + (data.message || 'unknown')); var errMsg = data.message || 'unknown error';
setStatus('Error: ' + errMsg);
showStripError(errMsg);
} }
}) })
.catch(function (err) { .catch(function (err) {
setStatus('Error: ' + err.message); var errMsg = err.message || 'Network error';
setStatus('Error: ' + errMsg);
showStripError(errMsg);
}); });
} }
@@ -323,6 +327,7 @@ var WeFax = (function () {
if (data.status === 'error') { if (data.status === 'error') {
state.running = false; state.running = false;
updateButtons(false); updateButtons(false);
showStripError(data.message || 'Decode error');
} }
if (data.status === 'stopped') { if (data.status === 'stopped') {
@@ -680,6 +685,16 @@ var WeFax = (function () {
if (el) el.textContent = String(khz); if (el) el.textContent = String(khz);
} }
function showStripError(msg) {
var statusEl = document.getElementById('wefaxStripStatus');
if (statusEl) {
statusEl.textContent = 'Error: ' + msg;
statusEl.style.color = '#ff4444';
}
var dot = document.getElementById('wefaxStripDot');
if (dot) dot.className = 'wefax-strip-dot error';
}
function flashStartError() { function flashStartError() {
setStatus('Select a station and frequency first'); setStatus('Select a station and frequency first');
+26 -1
View File
@@ -26,6 +26,7 @@ from typing import Callable
import numpy as np import numpy as np
from utils.dependencies import get_tool_path
from utils.logging import get_logger from utils.logging import get_logger
logger = get_logger('intercept.wefax') logger = get_logger('intercept.wefax')
@@ -235,11 +236,17 @@ class WeFaxDecoder:
self._direct_sampling = True self._direct_sampling = True
self._output_dir.mkdir(parents=True, exist_ok=True) self._output_dir.mkdir(parents=True, exist_ok=True)
self._last_error: str = ''
@property @property
def is_running(self) -> bool: def is_running(self) -> bool:
return self._running return self._running
@property
def last_error(self) -> str:
"""Last error message from a failed start() attempt."""
return self._last_error
def set_callback(self, callback: Callable[[dict], None]) -> None: def set_callback(self, callback: Callable[[dict], None]) -> None:
"""Set callback for progress updates (fed to SSE queue).""" """Set callback for progress updates (fed to SSE queue)."""
self._callback = callback self._callback = callback
@@ -283,6 +290,7 @@ class WeFaxDecoder:
try: try:
self._running = True self._running = True
self._last_error = ''
self._start_pipeline() self._start_pipeline()
logger.info( logger.info(
@@ -298,6 +306,7 @@ class WeFaxDecoder:
except Exception as e: except Exception as e:
self._running = False self._running = False
self._last_error = str(e)
logger.error(f"Failed to start WeFax decoder: {e}") logger.error(f"Failed to start WeFax decoder: {e}")
self._emit_progress(WeFaxProgress( self._emit_progress(WeFaxProgress(
status='error', status='error',
@@ -307,10 +316,14 @@ class WeFaxDecoder:
def _start_pipeline(self) -> None: def _start_pipeline(self) -> None:
"""Start rtl_fm subprocess in USB mode for WeFax.""" """Start rtl_fm subprocess in USB mode for WeFax."""
rtl_fm_path = get_tool_path('rtl_fm')
if not rtl_fm_path:
raise RuntimeError('rtl_fm not found')
freq_hz = int(self._frequency_khz * 1000) freq_hz = int(self._frequency_khz * 1000)
rtl_cmd = [ rtl_cmd = [
'rtl_fm', rtl_fm_path,
'-d', str(self._device_index), '-d', str(self._device_index),
'-f', str(freq_hz), '-f', str(freq_hz),
'-M', 'usb', '-M', 'usb',
@@ -332,6 +345,18 @@ class WeFaxDecoder:
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
) )
# Post-spawn health check — catch immediate failures
time.sleep(0.3)
if self._rtl_process.poll() is not None:
stderr_detail = ''
if self._rtl_process.stderr:
stderr_detail = self._rtl_process.stderr.read().decode(
errors='replace').strip()
rc = self._rtl_process.returncode
self._rtl_process = None
detail = stderr_detail.split('\n')[-1] if stderr_detail else f'exit code {rc}'
raise RuntimeError(f'rtl_fm failed: {detail}')
self._decode_thread = threading.Thread( self._decode_thread = threading.Thread(
target=self._decode_audio_stream, daemon=True) target=self._decode_audio_stream, daemon=True)
self._decode_thread.start() self._decode_thread.start()
+1
View File
@@ -406,6 +406,7 @@ class WeFaxScheduler:
'type': 'schedule_capture_skipped', 'type': 'schedule_capture_skipped',
'broadcast': sb.to_dict(), 'broadcast': sb.to_dict(),
'reason': 'start_failed', 'reason': 'start_failed',
'detail': decoder.last_error or 'unknown error',
}) })
def _stop_capture( def _stop_capture(