Forward rtl_fm stderr to Morse frontend diagnostic log

rtl_fm prints device info, tuning, and errors to stderr but the morse
route only logged these server-side. Now stderr lines are forwarded to
the morse queue as info events, displayed in a compact diagnostic log
below the scope canvas. After 10s with no audio data, the scope text
escalates to prompt the user to check the SDR log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-26 09:43:28 +00:00
parent c0c066904c
commit bfae73cabf
4 changed files with 70 additions and 4 deletions
+11 -1
View File
@@ -158,12 +158,17 @@ def start_morse() -> Response:
morse_active_device = None
return jsonify({'status': 'error', 'message': msg}), 500
# Monitor rtl_fm stderr
# Forward rtl_fm stderr to queue so frontend can display diagnostics
def monitor_stderr():
for line in rtl_process.stderr:
err_text = line.decode('utf-8', errors='replace').strip()
if err_text:
logger.debug(f"[rtl_fm/morse] {err_text}")
with contextlib.suppress(queue.Full):
app_module.morse_queue.put_nowait({
'type': 'info',
'text': f'[rtl_fm] {err_text}',
})
stderr_thread = threading.Thread(target=monitor_stderr)
stderr_thread.daemon = True
@@ -190,6 +195,11 @@ def start_morse() -> Response:
app_module.morse_process._decoder_thread = decoder_thread
app_module.morse_queue.put({'type': 'status', 'status': 'started'})
with contextlib.suppress(queue.Full):
app_module.morse_queue.put_nowait({
'type': 'info',
'text': f'[cmd] {full_cmd}',
})
return jsonify({
'status': 'started',
+47 -3
View File
@@ -23,6 +23,7 @@ var MorseMode = (function () {
var scopeThreshold = 0;
var scopeToneOn = false;
var scopeWaiting = false;
var waitingStart = 0; // timestamp when waiting began
// ---- Initialization ----
@@ -152,9 +153,13 @@ var MorseMode = (function () {
// Update scope data
var amps = msg.amplitudes || [];
if (msg.waiting && amps.length === 0 && scopeHistory.length === 0) {
scopeWaiting = true;
if (!scopeWaiting) {
scopeWaiting = true;
waitingStart = Date.now();
}
} else if (amps.length > 0) {
scopeWaiting = false;
waitingStart = 0;
}
for (var i = 0; i < amps.length; i++) {
scopeHistory.push(amps[i]);
@@ -178,6 +183,9 @@ var MorseMode = (function () {
disconnectSSE();
stopScope();
}
} else if (type === 'info') {
appendDiagLine(msg.text);
} else if (type === 'error') {
console.error('Morse error:', msg.text);
}
@@ -263,10 +271,14 @@ var MorseMode = (function () {
if (scopeHistory.length === 0) {
if (scopeWaiting) {
scopeCtx.fillStyle = '#556677';
var elapsed = waitingStart ? (Date.now() - waitingStart) / 1000 : 0;
var waitText = elapsed > 10
? 'No audio data \u2014 check SDR log below'
: 'Awaiting SDR data\u2026';
scopeCtx.fillStyle = elapsed > 10 ? '#887744' : '#556677';
scopeCtx.font = '12px monospace';
scopeCtx.textAlign = 'center';
scopeCtx.fillText('Awaiting SDR data\u2026', w / 2, h / 2);
scopeCtx.fillText(waitText, w / 2, h / 2);
scopeCtx.textAlign = 'start';
}
scopeAnim = requestAnimationFrame(draw);
@@ -371,6 +383,30 @@ var MorseMode = (function () {
URL.revokeObjectURL(url);
}
// ---- Diagnostic log ----
function appendDiagLine(text) {
var log = document.getElementById('morseDiagLog');
if (!log) return;
log.style.display = 'block';
var line = document.createElement('div');
line.textContent = text;
log.appendChild(line);
// Limit to 20 entries
while (log.children.length > 20) {
log.removeChild(log.firstChild);
}
log.scrollTop = log.scrollHeight;
}
function clearDiagLog() {
var log = document.getElementById('morseDiagLog');
if (log) {
log.innerHTML = '';
log.style.display = 'none';
}
}
// ---- UI ----
function updateUI(running) {
@@ -398,6 +434,14 @@ var MorseMode = (function () {
var scopeStatus = document.getElementById('morseScopeStatusLabel');
if (scopeStatus) scopeStatus.textContent = running ? 'ACTIVE' : 'IDLE';
if (scopeStatus) scopeStatus.style.color = running ? '#0f0' : '#444';
// Diagnostic log: clear on start, hide on stop
if (running) {
clearDiagLog();
} else {
var diagLog = document.getElementById('morseDiagLog');
if (diagLog) diagLog.style.display = 'none';
}
}
function setFreq(mhz) {
+7
View File
@@ -3085,6 +3085,11 @@
</div>
</div>
<div id="morseDiagLog" style="display: none; margin-bottom: 8px; max-height: 60px; overflow-y: auto;
background: #080812; border: 1px solid #1a1a2e; border-radius: 4px; padding: 4px 8px;
font-family: var(--font-mono); font-size: 10px; color: #556677; line-height: 1.6;">
</div>
<!-- Morse Decoded Output -->
<div id="morseOutputPanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 6px; padding: 8px 10px;">
@@ -4172,6 +4177,8 @@
const morseOutputPanel = document.getElementById('morseOutputPanel');
if (morseScopePanel && mode !== 'morse') morseScopePanel.style.display = 'none';
if (morseOutputPanel && mode !== 'morse') morseOutputPanel.style.display = 'none';
const morseDiagLog = document.getElementById('morseDiagLog');
if (morseDiagLog && mode !== 'morse') morseDiagLog.style.display = 'none';
// Update output panel title based on mode
const outputTitle = document.getElementById('outputTitle');
+5
View File
@@ -278,6 +278,7 @@ def morse_decoder_thread(
CHUNK = 4096 # bytes per read (2048 samples at 16-bit mono)
SCOPE_INTERVAL = 0.1 # scope updates at ~10 Hz
last_scope = time.monotonic()
waiting_since: float | None = None
decoder = MorseDecoder(
sample_rate=sample_rate,
@@ -293,6 +294,8 @@ def morse_decoder_thread(
if not ready:
# No data from SDR — emit diagnostic heartbeat
now = time.monotonic()
if waiting_since is None:
waiting_since = now
if now - last_scope >= SCOPE_INTERVAL:
last_scope = now
with contextlib.suppress(queue.Full):
@@ -302,12 +305,14 @@ def morse_decoder_thread(
'threshold': 0,
'tone_on': False,
'waiting': True,
'waiting_seconds': round(now - waiting_since, 1),
})
continue
data = os.read(fd, CHUNK)
if not data:
break
waiting_since = None
events = decoder.process_block(data)