diff --git a/routes/morse.py b/routes/morse.py index b6a9834..6cd2049 100644 --- a/routes/morse.py +++ b/routes/morse.py @@ -354,8 +354,17 @@ def start_morse() -> Response: can_try_direct_sampling = bool(sdr_device.sdr_type == SDRType.RTL_SDR and freq < 24.0) if can_try_direct_sampling: - # Keep rtl_fm attempts first (cheap), then switch to IQ capture fallback. + # IQ-first strategy: avoid repeated rtl_fm/rtl_sdr handoffs that can + # leave the tuner in a bad state on some Linux builds. command_attempts: list[dict[str, Any]] = [ + { + 'source': 'iq', + 'direct_sampling_mode': 2, + }, + { + 'source': 'iq', + 'direct_sampling_mode': None, + }, { 'source': 'rtl_fm', 'use_direct_sampling': True, @@ -370,17 +379,13 @@ def start_morse() -> Response: 'add_resample_rate': True, 'add_dc_fast': True, }, - { - 'source': 'iq', - 'direct_sampling_mode': 2, - }, + ] + else: + command_attempts = [ { 'source': 'iq', 'direct_sampling_mode': None, }, - ] - else: - command_attempts = [ { 'source': 'rtl_fm', 'use_direct_sampling': False, @@ -388,10 +393,6 @@ def start_morse() -> Response: 'add_resample_rate': True, 'add_dc_fast': True, }, - { - 'source': 'iq', - 'direct_sampling_mode': None, - }, ] rtl_process: subprocess.Popen | None = None @@ -571,7 +572,7 @@ def start_morse() -> Response: ) decoder_thread.start() - startup_deadline = time.monotonic() + 1.2 + startup_deadline = time.monotonic() + (2.5 if source == 'iq' else 1.2) startup_ok = False startup_error = '' diff --git a/static/js/modes/morse.js b/static/js/modes/morse.js index ef23a66..7733946 100644 --- a/static/js/modes/morse.js +++ b/static/js/modes/morse.js @@ -8,7 +8,7 @@ var MorseMode = (function () { var SETTINGS_KEY = 'intercept.morse.settings.v3'; var STATUS_POLL_MS = 5000; var LOCAL_STOP_TIMEOUT_MS = 2200; - var START_TIMEOUT_MS = 20000; + var START_TIMEOUT_MS = 60000; var state = { initialized: false, @@ -324,12 +324,32 @@ var MorseMode = (function () { if (seq !== state.startSeq) { return { status: 'stale' }; } + var initialErrorMsg = String(err && err.message ? err.message : err); + if (initialErrorMsg === 'Request timed out while waiting for decoder startup') { + return fetch('/morse/status') + .then(function (r) { return parseJsonSafe(r); }) + .then(function (statusData) { + var statusError = statusData && (statusData.error || statusData.message); + var resolvedError = statusError ? String(statusError) : initialErrorMsg; + setLifecycle('error'); + setStatusText('Start failed'); + appendDiagLine('[start] failed: ' + resolvedError); + notifyError('Failed to start Morse decoder: ' + resolvedError); + return { status: 'error', message: resolvedError }; + }) + .catch(function () { + setLifecycle('error'); + setStatusText('Start failed'); + appendDiagLine('[start] failed: ' + initialErrorMsg); + notifyError('Failed to start Morse decoder: ' + initialErrorMsg); + return { status: 'error', message: initialErrorMsg }; + }); + } setLifecycle('error'); - var errorMsg = String(err && err.message ? err.message : err); setStatusText('Start failed'); - appendDiagLine('[start] failed: ' + errorMsg); - notifyError('Failed to start Morse decoder: ' + errorMsg); - return { status: 'error', message: errorMsg }; + appendDiagLine('[start] failed: ' + initialErrorMsg); + notifyError('Failed to start Morse decoder: ' + initialErrorMsg); + return { status: 'error', message: initialErrorMsg }; }); } diff --git a/tests/test_morse.py b/tests/test_morse.py index 95adff8..8d48b88 100644 --- a/tests/test_morse.py +++ b/tests/test_morse.py @@ -266,6 +266,13 @@ class TestMorseLifecycleRoutes: def build_fm_demod_command(self, **kwargs): return ['rtl_fm', '-f', '14060000'] + def build_iq_capture_command(self, **kwargs): + cmd = ['rtl_sdr', '-f', '14060000', '-s', '250000'] + if kwargs.get('gain') is not None: + cmd.extend(['-g', str(kwargs['gain'])]) + cmd.append('-') + return cmd + monkeypatch.setattr(morse_routes.SDRFactory, 'create_default_device', staticmethod(lambda sdr_type, index: DummyDevice())) monkeypatch.setattr(morse_routes.SDRFactory, 'get_builder', staticmethod(lambda sdr_type: DummyBuilder())) monkeypatch.setattr(morse_routes.time, 'sleep', lambda _secs: None) @@ -337,6 +344,10 @@ class TestMorseLifecycleRoutes: cmd.append('-') return cmd + def build_iq_capture_command(self, **kwargs): + cmd = ['rtl_sdr', '-f', '14.0593M', '-s', '250000', '-'] + return cmd + monkeypatch.setattr(morse_routes.SDRFactory, 'create_default_device', staticmethod(lambda sdr_type, index: DummyDevice())) monkeypatch.setattr(morse_routes.SDRFactory, 'get_builder', staticmethod(lambda sdr_type: DummyBuilder())) @@ -378,13 +389,11 @@ class TestMorseLifecycleRoutes: assert start_resp.status_code == 200 assert start_resp.get_json()['status'] == 'started' assert len(popen_cmds) >= 2 - assert '-E' in popen_cmds[0] and 'direct2' in popen_cmds[0] - assert '-r' in popen_cmds[0] - assert '-A' in popen_cmds[0] - assert '-E' in popen_cmds[1] and 'direct2' not in popen_cmds[1] - assert '-r' in popen_cmds[1] - assert '-A' in popen_cmds[1] - assert 'dc' in popen_cmds[1] + assert popen_cmds[0][0] == 'rtl_sdr' + assert '-D' in popen_cmds[0] + assert '2' in popen_cmds[0] + assert popen_cmds[1][0] == 'rtl_sdr' + assert '-D' not in popen_cmds[1] stop_resp = client.post('/morse/stop') assert stop_resp.status_code == 200