From 33a360b48390380958a9eb7cf4fed97b190d6dc8 Mon Sep 17 00:00:00 2001 From: Smittix Date: Thu, 26 Feb 2026 21:05:25 +0000 Subject: [PATCH] morse: fix startup race and stuck noise floor in Goertzel decoder Filter decoder-thread 'stopped' status events that race with the route lifecycle, causing the frontend to drop back to idle on first start. Pull noise floor upward using adjacent-frequency Goertzel reference when warmup calibration runs before AGC converges, preventing permanent tone-on with zero character decodes. Co-Authored-By: Claude Opus 4.6 --- routes/morse.py | 19 ++++++++++++++++++- utils/morse.py | 5 +++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/routes/morse.py b/routes/morse.py index 83338c2..5491979 100644 --- a/routes/morse.py +++ b/routes/morse.py @@ -32,6 +32,23 @@ from utils.validation import ( morse_bp = Blueprint('morse', __name__) + +class _FilteredQueue: + """Suppress decoder-thread 'stopped' events that race with route lifecycle.""" + + def __init__(self, inner: queue.Queue) -> None: + self._inner = inner + + def put_nowait(self, item: Any) -> None: + if isinstance(item, dict) and item.get('type') == 'status' and item.get('status') == 'stopped': + return + self._inner.put_nowait(item) + + def put(self, item: Any, **kwargs: Any) -> None: + if isinstance(item, dict) and item.get('type') == 'status' and item.get('status') == 'stopped': + return + self._inner.put(item, **kwargs) + # Track which device is being used morse_active_device: int | None = None @@ -495,7 +512,7 @@ def start_morse() -> Response: target=morse_decoder_thread, kwargs={ 'rtl_stdout': rtl_process.stdout, - 'output_queue': app_module.morse_queue, + 'output_queue': _FilteredQueue(app_module.morse_queue), 'stop_event': stop_event, 'sample_rate': sample_rate, 'tone_freq': tone_freq, diff --git a/utils/morse.py b/utils/morse.py index 58c03dc..6606d18 100644 --- a/utils/morse.py +++ b/utils/morse.py @@ -433,6 +433,11 @@ class MorseDecoder: self._signal_peak = max(self._signal_peak, self._noise_floor * 1.05) + # Prevent noise floor from staying stuck below actual ambient noise + # (occurs when warmup calibration runs before AGC converges) + if noise_ref > self._noise_floor * 1.5: + self._noise_floor += settle_alpha * 0.5 * (noise_ref - self._noise_floor) + if self.threshold_mode == 'manual': self._threshold = max(0.0, self.manual_threshold) else: