Switch Morse startup to IQ-first and harden timeout handling

This commit is contained in:
Smittix
2026-02-26 13:44:04 +00:00
parent 146bca4b37
commit d3b737c19b
3 changed files with 55 additions and 25 deletions

View File

@@ -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 = ''

View File

@@ -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 };
});
}

View File

@@ -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