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 subprocess
import threading
import time
from typing import Any
from flask import Blueprint, Response, jsonify, request
@@ -113,6 +114,9 @@ def start_morse() -> Response:
sample_rate = 8000
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(
device=sdr_device,
frequency_mhz=freq,
@@ -121,6 +125,7 @@ def start_morse() -> Response:
ppm=int(ppm) if ppm and ppm != '0' else None,
modulation='usb',
bias_t=bias_t,
direct_sampling=direct_sampling,
)
full_cmd = ' '.join(rtl_cmd)
@@ -134,6 +139,25 @@ def start_morse() -> Response:
)
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
def monitor_stderr():
for line in rtl_process.stderr:

View File

@@ -107,7 +107,14 @@ var MorseMode = (function () {
disconnectSSE();
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 ----

View File

@@ -12,7 +12,8 @@
<h3>Frequency</h3>
<div class="form-group">
<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 class="form-group">
<label>Band Presets</label>

View File

@@ -274,3 +274,6 @@ def morse_decoder_thread(
for event in decoder.flush():
with contextlib.suppress(queue.Full):
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,
modulation: str = "fm",
squelch: Optional[int] = None,
bias_t: bool = False
bias_t: bool = False,
direct_sampling: Optional[int] = None,
) -> list[str]:
"""
Build rtl_fm command for FM demodulation.
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'
demod_mode = _rtl_fm_demod_mode(modulation)
@@ -112,6 +117,9 @@ class RTLSDRCommandBuilder(CommandBuilder):
if squelch is not None and squelch > 0:
cmd.extend(['-l', str(squelch)])
if direct_sampling is not None:
cmd.extend(['-D', str(direct_sampling)])
if bias_t:
cmd.extend(['-T'])