Fix Morse mode HF reception, stop button, and UX guidance

Enable direct sampling (-D 2) for RTL-SDR at HF frequencies below 24 MHz
so rtl_fm can actually receive CW signals. Add startup health check to
detect immediate rtl_fm failures. Push stopped status event from decoder
thread on EOF so the frontend auto-resets. Add frequency placeholder and
help text. Fix stop button silently swallowing errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-26 08:43:51 +00:00
parent 2ec458aa14
commit 0bf8341b6c
5 changed files with 65 additions and 22 deletions

View File

@@ -6,6 +6,7 @@ import contextlib
import queue import queue
import subprocess import subprocess
import threading import threading
import time
from typing import Any from typing import Any
from flask import Blueprint, Response, jsonify, request from flask import Blueprint, Response, jsonify, request
@@ -113,6 +114,9 @@ def start_morse() -> Response:
sample_rate = 8000 sample_rate = 8000
bias_t = data.get('bias_t', False) bias_t = data.get('bias_t', False)
# RTL-SDR needs direct sampling mode for HF frequencies below 24 MHz
direct_sampling = 2 if freq < 24.0 else None
rtl_cmd = builder.build_fm_demod_command( rtl_cmd = builder.build_fm_demod_command(
device=sdr_device, device=sdr_device,
frequency_mhz=freq, frequency_mhz=freq,
@@ -121,6 +125,7 @@ def start_morse() -> Response:
ppm=int(ppm) if ppm and ppm != '0' else None, ppm=int(ppm) if ppm and ppm != '0' else None,
modulation='usb', modulation='usb',
bias_t=bias_t, bias_t=bias_t,
direct_sampling=direct_sampling,
) )
full_cmd = ' '.join(rtl_cmd) full_cmd = ' '.join(rtl_cmd)
@@ -134,6 +139,25 @@ def start_morse() -> Response:
) )
register_process(rtl_process) register_process(rtl_process)
# Detect immediate startup failure (e.g. device busy, no device)
time.sleep(0.35)
if rtl_process.poll() is not None:
stderr_text = ''
try:
if rtl_process.stderr:
stderr_text = rtl_process.stderr.read().decode(
'utf-8', errors='replace'
).strip()
except Exception:
stderr_text = ''
msg = stderr_text or f'rtl_fm exited immediately (code {rtl_process.returncode})'
logger.error(f"Morse rtl_fm startup failed: {msg}")
unregister_process(rtl_process)
if morse_active_device is not None:
app_module.release_sdr_device(morse_active_device)
morse_active_device = None
return jsonify({'status': 'error', 'message': msg}), 500
# Monitor rtl_fm stderr # Monitor rtl_fm stderr
def monitor_stderr(): def monitor_stderr():
for line in rtl_process.stderr: for line in rtl_process.stderr:

View File

@@ -107,7 +107,14 @@ var MorseMode = (function () {
disconnectSSE(); disconnectSSE();
stopScope(); stopScope();
}) })
.catch(function () {}); .catch(function (err) {
console.error('Morse stop request failed:', err);
// Reset UI regardless so the user isn't stuck
state.running = false;
updateUI(false);
disconnectSSE();
stopScope();
});
} }
// ---- SSE ---- // ---- SSE ----

View File

@@ -12,7 +12,8 @@
<h3>Frequency</h3> <h3>Frequency</h3>
<div class="form-group"> <div class="form-group">
<label>Frequency (MHz)</label> <label>Frequency (MHz)</label>
<input type="number" id="morseFrequency" value="14.060" step="0.001" min="1" max="30"> <input type="number" id="morseFrequency" value="14.060" step="0.001" min="1" max="30" placeholder="e.g., 14.060">
<span class="help-text" style="font-size: 10px; color: var(--text-dim); margin-top: 2px; display: block;">Enter frequency in MHz (e.g., 7.030 for 40m CW)</span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Band Presets</label> <label>Band Presets</label>

View File

@@ -274,3 +274,6 @@ def morse_decoder_thread(
for event in decoder.flush(): for event in decoder.flush():
with contextlib.suppress(queue.Full): with contextlib.suppress(queue.Full):
output_queue.put_nowait(event) output_queue.put_nowait(event)
# Notify frontend that the decoder has stopped (e.g. rtl_fm died)
with contextlib.suppress(queue.Full):
output_queue.put_nowait({'type': 'status', 'status': 'stopped'})

View File

@@ -14,16 +14,16 @@ from typing import Optional
from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType
from utils.dependencies import get_tool_path from utils.dependencies import get_tool_path
logger = logging.getLogger('intercept.sdr.rtlsdr') logger = logging.getLogger('intercept.sdr.rtlsdr')
def _rtl_fm_demod_mode(modulation: str) -> str: def _rtl_fm_demod_mode(modulation: str) -> str:
"""Map app/UI modulation names to rtl_fm demod tokens.""" """Map app/UI modulation names to rtl_fm demod tokens."""
mod = str(modulation or '').lower().strip() mod = str(modulation or '').lower().strip()
return 'wbfm' if mod == 'wfm' else mod return 'wbfm' if mod == 'wfm' else mod
def _get_dump1090_bias_t_flag(dump1090_path: str) -> Optional[str]: def _get_dump1090_bias_t_flag(dump1090_path: str) -> Optional[str]:
"""Detect the correct bias-t flag for the installed dump1090 variant. """Detect the correct bias-t flag for the installed dump1090 variant.
Different dump1090 forks use different flags: Different dump1090 forks use different flags:
@@ -86,22 +86,27 @@ class RTLSDRCommandBuilder(CommandBuilder):
ppm: Optional[int] = None, ppm: Optional[int] = None,
modulation: str = "fm", modulation: str = "fm",
squelch: Optional[int] = None, squelch: Optional[int] = None,
bias_t: bool = False bias_t: bool = False,
direct_sampling: Optional[int] = None,
) -> list[str]: ) -> list[str]:
""" """
Build rtl_fm command for FM demodulation. Build rtl_fm command for FM demodulation.
Used for pager decoding. Supports local devices and rtl_tcp connections. Used for pager decoding. Supports local devices and rtl_tcp connections.
Args:
direct_sampling: Enable direct sampling mode (0=off, 1=I-branch,
2=Q-branch). Use 2 for HF reception below 24 MHz.
""" """
rtl_fm_path = get_tool_path('rtl_fm') or 'rtl_fm' rtl_fm_path = get_tool_path('rtl_fm') or 'rtl_fm'
demod_mode = _rtl_fm_demod_mode(modulation) demod_mode = _rtl_fm_demod_mode(modulation)
cmd = [ cmd = [
rtl_fm_path, rtl_fm_path,
'-d', self._get_device_arg(device), '-d', self._get_device_arg(device),
'-f', f'{frequency_mhz}M', '-f', f'{frequency_mhz}M',
'-M', demod_mode, '-M', demod_mode,
'-s', str(sample_rate), '-s', str(sample_rate),
] ]
if gain is not None and gain > 0: if gain is not None and gain > 0:
cmd.extend(['-g', str(gain)]) cmd.extend(['-g', str(gain)])
@@ -112,6 +117,9 @@ class RTLSDRCommandBuilder(CommandBuilder):
if squelch is not None and squelch > 0: if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)]) cmd.extend(['-l', str(squelch)])
if direct_sampling is not None:
cmd.extend(['-D', str(direct_sampling)])
if bias_t: if bias_t:
cmd.extend(['-T']) cmd.extend(['-T'])