morse: add multimon decoder alias fallback and clear stale idle scope

This commit is contained in:
Smittix
2026-02-26 17:43:23 +00:00
parent bee468f870
commit b656da17dd
2 changed files with 279 additions and 228 deletions
+44 -4
View File
@@ -114,6 +114,29 @@ def _queue_morse_event(payload: dict[str, Any]) -> None:
app_module.morse_queue.put_nowait(payload) app_module.morse_queue.put_nowait(payload)
def _resolve_multimon_morse_modes(multimon_path: str) -> list[str]:
preferred = ['MORSE_CW', 'MORSE']
discovered: list[str] = []
try:
result = subprocess.run(
[multimon_path, '-h'],
capture_output=True,
text=True,
timeout=2,
check=False,
)
blob = f'{result.stdout}\n{result.stderr}'.upper()
for mode in preferred:
if mode in blob and mode not in discovered:
discovered.append(mode)
except Exception:
pass
if not discovered:
return preferred
return discovered
def _parse_multimon_morse_text(line: str) -> str | None: def _parse_multimon_morse_text(line: str) -> str | None:
cleaned = str(line or '').strip() cleaned = str(line or '').strip()
if not cleaned: if not cleaned:
@@ -601,7 +624,7 @@ def start_morse() -> Response:
_set_state(MORSE_IDLE, 'Idle') _set_state(MORSE_IDLE, 'Idle')
return jsonify({'status': 'error', 'message': msg}), 400 return jsonify({'status': 'error', 'message': msg}), 400
multimon_cmd = [multimon_path, '-t', 'raw', '-a', 'MORSE_CW', '-f', 'alpha', '-'] multimon_decoder_modes = _resolve_multimon_morse_modes(multimon_path)
def _build_rtl_cmd(device_index: int, direct_sampling_mode: int | None) -> list[str]: def _build_rtl_cmd(device_index: int, direct_sampling_mode: int | None) -> list[str]:
sdr_device = SDRFactory.create_default_device(sdr_type, index=device_index) sdr_device = SDRFactory.create_default_device(sdr_type, index=device_index)
@@ -645,6 +668,7 @@ def start_morse() -> Response:
'active_device': active_device_index, 'active_device': active_device_index,
'device_serial': str(device_catalog.get(active_device_index, {}).get('serial') or 'Unknown'), 'device_serial': str(device_catalog.get(active_device_index, {}).get('serial') or 'Unknown'),
'candidate_devices': list(candidate_device_indices), 'candidate_devices': list(candidate_device_indices),
'multimon_decoder_modes': list(multimon_decoder_modes),
} }
active_rtl_process: subprocess.Popen[bytes] | None = None active_rtl_process: subprocess.Popen[bytes] | None = None
@@ -706,6 +730,15 @@ def start_morse() -> Response:
try: try:
startup_succeeded = False startup_succeeded = False
for decoder_pos, decoder_mode in enumerate(multimon_decoder_modes, start=1):
multimon_cmd = [multimon_path, '-t', 'raw', '-a', decoder_mode, '-f', 'alpha', '-']
runtime_config['multimon_decoder'] = decoder_mode
if decoder_pos > 1:
_queue_morse_event({
'type': 'info',
'text': f'[morse] retrying with multimon decoder {decoder_mode}',
})
for device_pos, candidate_device_index in enumerate(candidate_device_indices, start=1): for device_pos, candidate_device_index in enumerate(candidate_device_indices, start=1):
if candidate_device_index != active_device_index: if candidate_device_index != active_device_index:
prev_device = active_device_index prev_device = active_device_index
@@ -752,7 +785,8 @@ def start_morse() -> Response:
direct_mode_label = direct_sampling_mode if direct_sampling_mode is not None else 'none' direct_mode_label = direct_sampling_mode if direct_sampling_mode is not None else 'none'
full_cmd = ' '.join(rtl_cmd) + ' | ' + ' '.join(multimon_cmd) full_cmd = ' '.join(rtl_cmd) + ' | ' + ' '.join(multimon_cmd)
logger.info( logger.info(
'Morse decoder attempt device=%s (%s/%s) direct_mode=%s (%s/%s): %s', 'Morse decoder attempt decoder=%s device=%s (%s/%s) direct_mode=%s (%s/%s): %s',
decoder_mode,
active_device_index, active_device_index,
device_pos, device_pos,
len(candidate_device_indices), len(candidate_device_indices),
@@ -871,10 +905,12 @@ def start_morse() -> Response:
startup_error = 'No PCM samples received within startup timeout' startup_error = 'No PCM samples received within startup timeout'
if stderr_lines: if stderr_lines:
startup_error = f'{startup_error}; stderr: {stderr_lines[-1]}' startup_error = f'{startup_error}; stderr: {stderr_lines[-1]}'
is_last_decoder = decoder_pos == len(multimon_decoder_modes)
is_last_device = device_pos == len(candidate_device_indices) is_last_device = device_pos == len(candidate_device_indices)
is_last_attempt = attempt_index == len(direct_sampling_attempts) is_last_attempt = attempt_index == len(direct_sampling_attempts)
if ( if (
is_last_device is_last_decoder
and is_last_device
and is_last_attempt and is_last_attempt
and rtl_process.poll() is None and rtl_process.poll() is None
and multimon_process.poll() is None and multimon_process.poll() is None
@@ -899,6 +935,7 @@ def start_morse() -> Response:
) )
runtime_config['command'] = full_cmd runtime_config['command'] = full_cmd
runtime_config['active_device'] = active_device_index runtime_config['active_device'] = active_device_index
runtime_config['multimon_decoder'] = decoder_mode
active_rtl_process = rtl_process active_rtl_process = rtl_process
active_multimon_process = multimon_process active_multimon_process = multimon_process
@@ -912,7 +949,7 @@ def start_morse() -> Response:
break break
attempt_errors.append( attempt_errors.append(
f'{_device_label(active_device_index)} ' f'{_device_label(active_device_index)} decoder={decoder_mode} '
f'attempt {attempt_index}/{len(direct_sampling_attempts)} ' f'attempt {attempt_index}/{len(direct_sampling_attempts)} '
f'(source=rtl_fm direct_mode={direct_mode_label}): {startup_error}' f'(source=rtl_fm direct_mode={direct_mode_label}): {startup_error}'
) )
@@ -955,6 +992,9 @@ def start_morse() -> Response:
'timestamp': time.strftime('%H:%M:%S'), 'timestamp': time.strftime('%H:%M:%S'),
}) })
if startup_succeeded:
break
if ( if (
active_rtl_process is None active_rtl_process is None
or active_multimon_process is None or active_multimon_process is None
+11
View File
@@ -832,6 +832,17 @@ var MorseMode = (function () {
} }
function stopScope() { function stopScope() {
var canvas = el('morseScopeCanvas');
if (canvas) {
var ctx = canvas.getContext('2d');
if (ctx) {
var w = canvas.clientWidth || canvas.width || 1;
var h = canvas.clientHeight || 80;
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = '#050510';
ctx.fillRect(0, 0, w, h);
}
}
if (scopeAnim) { if (scopeAnim) {
cancelAnimationFrame(scopeAnim); cancelAnimationFrame(scopeAnim);
scopeAnim = null; scopeAnim = null;