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'])