mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Fix Morse decoder scope events not reaching frontend
Replace blocking rtl_stdout.read() with select()+os.read() so the decoder thread emits diagnostic heartbeat scope events when rtl_fm produces no PCM data (common in direct sampling mode). Add waiting-state rendering in the scope canvas and hide the generic placeholder/status bar for morse mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ var MorseMode = (function () {
|
||||
var SCOPE_HISTORY_LEN = 300;
|
||||
var scopeThreshold = 0;
|
||||
var scopeToneOn = false;
|
||||
var scopeWaiting = false;
|
||||
|
||||
// ---- Initialization ----
|
||||
|
||||
@@ -150,6 +151,11 @@ var MorseMode = (function () {
|
||||
if (type === 'scope') {
|
||||
// Update scope data
|
||||
var amps = msg.amplitudes || [];
|
||||
if (msg.waiting && amps.length === 0 && scopeHistory.length === 0) {
|
||||
scopeWaiting = true;
|
||||
} else if (amps.length > 0) {
|
||||
scopeWaiting = false;
|
||||
}
|
||||
for (var i = 0; i < amps.length; i++) {
|
||||
scopeHistory.push(amps[i]);
|
||||
if (scopeHistory.length > SCOPE_HISTORY_LEN) {
|
||||
@@ -238,6 +244,7 @@ var MorseMode = (function () {
|
||||
scopeCtx = canvas.getContext('2d');
|
||||
scopeCtx.scale(dpr, dpr);
|
||||
scopeHistory = [];
|
||||
scopeWaiting = false;
|
||||
|
||||
var toneLabel = document.getElementById('morseScopeToneLabel');
|
||||
var threshLabel = document.getElementById('morseScopeThreshLabel');
|
||||
@@ -255,6 +262,13 @@ var MorseMode = (function () {
|
||||
if (threshLabel) threshLabel.textContent = scopeThreshold > 0 ? Math.round(scopeThreshold) : '--';
|
||||
|
||||
if (scopeHistory.length === 0) {
|
||||
if (scopeWaiting) {
|
||||
scopeCtx.fillStyle = '#556677';
|
||||
scopeCtx.font = '12px monospace';
|
||||
scopeCtx.textAlign = 'center';
|
||||
scopeCtx.fillText('Awaiting SDR data\u2026', w / 2, h / 2);
|
||||
scopeCtx.textAlign = 'start';
|
||||
}
|
||||
scopeAnim = requestAnimationFrame(draw);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4256,8 +4256,8 @@
|
||||
// Hide output console for modes with their own visualizations
|
||||
const outputEl = document.getElementById('output');
|
||||
const statusBar = document.querySelector('.status-bar');
|
||||
if (outputEl) outputEl.style.display = (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'aprs' || mode === 'wifi' || mode === 'bluetooth' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'bt_locate' || mode === 'waterfall') ? 'none' : 'block';
|
||||
if (statusBar) statusBar.style.display = (mode === 'satellite' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'waterfall') ? 'none' : 'flex';
|
||||
if (outputEl) outputEl.style.display = (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'aprs' || mode === 'wifi' || mode === 'bluetooth' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'bt_locate' || mode === 'waterfall' || mode === 'morse') ? 'none' : 'block';
|
||||
if (statusBar) statusBar.style.display = (mode === 'satellite' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'waterfall' || mode === 'morse') ? 'none' : 'flex';
|
||||
|
||||
// Restore sidebar when leaving Meshtastic mode (user may have collapsed it)
|
||||
if (mode !== 'meshtastic') {
|
||||
|
||||
@@ -327,6 +327,48 @@ class TestMorseDecoderThread:
|
||||
t.join(timeout=5)
|
||||
assert not t.is_alive(), "Thread should finish after reading all data"
|
||||
|
||||
def test_thread_heartbeat_on_no_data(self):
|
||||
"""When rtl_fm produces no data, thread should emit waiting scope events."""
|
||||
import os as _os
|
||||
stop = threading.Event()
|
||||
q = queue.Queue(maxsize=100)
|
||||
|
||||
# Create a pipe that never gets written to (simulates rtl_fm with no output)
|
||||
read_fd, write_fd = _os.pipe()
|
||||
read_file = _os.fdopen(read_fd, 'rb', 0)
|
||||
|
||||
t = threading.Thread(
|
||||
target=morse_decoder_thread,
|
||||
args=(read_file, q, stop),
|
||||
)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
# Wait up to 5 seconds for at least one heartbeat event
|
||||
events = []
|
||||
import time as _time
|
||||
deadline = _time.monotonic() + 5.0
|
||||
while _time.monotonic() < deadline:
|
||||
try:
|
||||
ev = q.get(timeout=0.5)
|
||||
events.append(ev)
|
||||
if ev.get('waiting'):
|
||||
break
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
stop.set()
|
||||
_os.close(write_fd)
|
||||
read_file.close()
|
||||
t.join(timeout=3)
|
||||
|
||||
waiting_events = [e for e in events if e.get('type') == 'scope' and e.get('waiting')]
|
||||
assert len(waiting_events) >= 1, f"Expected waiting heartbeat events, got {events}"
|
||||
ev = waiting_events[0]
|
||||
assert ev['amplitudes'] == []
|
||||
assert ev['threshold'] == 0
|
||||
assert ev['tone_on'] is False
|
||||
|
||||
def test_thread_produces_events(self):
|
||||
"""Thread should push character events to the queue."""
|
||||
import io
|
||||
|
||||
@@ -7,7 +7,9 @@ from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import math
|
||||
import os
|
||||
import queue
|
||||
import select
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
@@ -284,8 +286,26 @@ def morse_decoder_thread(
|
||||
)
|
||||
|
||||
try:
|
||||
fd = rtl_stdout.fileno()
|
||||
|
||||
while not stop_event.is_set():
|
||||
data = rtl_stdout.read(CHUNK)
|
||||
ready, _, _ = select.select([fd], [], [], 2.0)
|
||||
if not ready:
|
||||
# No data from SDR — emit diagnostic heartbeat
|
||||
now = time.monotonic()
|
||||
if now - last_scope >= SCOPE_INTERVAL:
|
||||
last_scope = now
|
||||
with contextlib.suppress(queue.Full):
|
||||
output_queue.put_nowait({
|
||||
'type': 'scope',
|
||||
'amplitudes': [],
|
||||
'threshold': 0,
|
||||
'tone_on': False,
|
||||
'waiting': True,
|
||||
})
|
||||
continue
|
||||
|
||||
data = os.read(fd, CHUNK)
|
||||
if not data:
|
||||
break
|
||||
|
||||
|
||||
Reference in New Issue
Block a user