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

@@ -86,12 +86,17 @@ 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)
@@ -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'])