From 0bf8341b6c8b6dbd930ea66dc83d780b48591e4e Mon Sep 17 00:00:00 2001 From: Smittix Date: Thu, 26 Feb 2026 08:43:51 +0000 Subject: [PATCH] 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 --- routes/morse.py | 24 +++++++++++++++ static/js/modes/morse.js | 9 +++++- templates/partials/modes/morse.html | 3 +- utils/morse.py | 3 ++ utils/sdr/rtlsdr.py | 48 +++++++++++++++++------------ 5 files changed, 65 insertions(+), 22 deletions(-) diff --git a/routes/morse.py b/routes/morse.py index c30a2d4..dae772c 100644 --- a/routes/morse.py +++ b/routes/morse.py @@ -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: diff --git a/static/js/modes/morse.js b/static/js/modes/morse.js index b594bf6..6498db4 100644 --- a/static/js/modes/morse.js +++ b/static/js/modes/morse.js @@ -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 ---- diff --git a/templates/partials/modes/morse.html b/templates/partials/modes/morse.html index b3bb0f0..07e8f32 100644 --- a/templates/partials/modes/morse.html +++ b/templates/partials/modes/morse.html @@ -12,7 +12,8 @@

Frequency

- + + Enter frequency in MHz (e.g., 7.030 for 40m CW)
diff --git a/utils/morse.py b/utils/morse.py index cd354f3..1a957e5 100644 --- a/utils/morse.py +++ b/utils/morse.py @@ -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'}) diff --git a/utils/sdr/rtlsdr.py b/utils/sdr/rtlsdr.py index 1e68c35..36f27d3 100644 --- a/utils/sdr/rtlsdr.py +++ b/utils/sdr/rtlsdr.py @@ -14,16 +14,16 @@ from typing import Optional from .base import CommandBuilder, SDRCapabilities, SDRDevice, SDRType from utils.dependencies import get_tool_path -logger = logging.getLogger('intercept.sdr.rtlsdr') - - -def _rtl_fm_demod_mode(modulation: str) -> str: - """Map app/UI modulation names to rtl_fm demod tokens.""" - mod = str(modulation or '').lower().strip() - return 'wbfm' if mod == 'wfm' else mod - - -def _get_dump1090_bias_t_flag(dump1090_path: str) -> Optional[str]: +logger = logging.getLogger('intercept.sdr.rtlsdr') + + +def _rtl_fm_demod_mode(modulation: str) -> str: + """Map app/UI modulation names to rtl_fm demod tokens.""" + mod = str(modulation or '').lower().strip() + return 'wbfm' if mod == 'wfm' else mod + + +def _get_dump1090_bias_t_flag(dump1090_path: str) -> Optional[str]: """Detect the correct bias-t flag for the installed dump1090 variant. Different dump1090 forks use different flags: @@ -86,22 +86,27 @@ 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) - cmd = [ - rtl_fm_path, - '-d', self._get_device_arg(device), - '-f', f'{frequency_mhz}M', - '-M', demod_mode, - '-s', str(sample_rate), - ] + rtl_fm_path = get_tool_path('rtl_fm') or 'rtl_fm' + demod_mode = _rtl_fm_demod_mode(modulation) + cmd = [ + rtl_fm_path, + '-d', self._get_device_arg(device), + '-f', f'{frequency_mhz}M', + '-M', demod_mode, + '-s', str(sample_rate), + ] if gain is not None and gain > 0: cmd.extend(['-g', str(gain)]) @@ -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'])