Fix SSTV decoder thread lifecycle and VIS detection reliability

Three bugs preventing the live SSTV pipeline from working:

1. Race condition: self._running was set AFTER starting the decode
   thread, so the thread checked the flag, found it False, and exited
   immediately without ever processing audio.

2. Ghost running state: when the decode thread exited (e.g. rtl_fm
   died), self._running stayed True. The decoder reported as running
   but was dead, and subsequent start() calls returned without doing
   anything - permanently stuck until app restart.

3. VIS detection fragility: unclassifiable windows at tone transition
   boundaries (mixed energy from two tones) caused the state machine
   to reset from LEADER/BREAK states back to IDLE, dropping valid
   VIS headers on real signals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-07 10:33:08 +00:00
parent ef7d8cca9f
commit 28891f4709
2 changed files with 30 additions and 2 deletions

View File

@@ -283,8 +283,10 @@ class SSTVDecoder:
try:
freq_hz = self._get_doppler_corrected_freq_hz()
self._current_tuned_freq_hz = freq_hz
self._start_pipeline(freq_hz)
# Set _running BEFORE starting the pipeline so the decode
# thread sees it as True on its first loop iteration.
self._running = True
self._start_pipeline(freq_hz)
# Start Doppler tracking thread if enabled
if self._doppler_enabled:
@@ -306,6 +308,7 @@ class SSTVDecoder:
return True
except Exception as e:
self._running = False
logger.error(f"Failed to start SSTV decoder: {e}")
self._emit_progress(DecodeProgress(
status='error',
@@ -433,7 +436,26 @@ class SSTVDecoder:
break
time.sleep(0.1)
logger.info("Audio decode thread stopped")
# Clean up if the thread exits while we thought we were running.
# This prevents a "ghost running" state where is_running is True
# but the thread has already died (e.g. rtl_fm exited).
with self._lock:
was_running = self._running
self._running = False
if was_running and self._rtl_process:
with contextlib.suppress(Exception):
self._rtl_process.terminate()
self._rtl_process.wait(timeout=2)
self._rtl_process = None
if was_running:
logger.warning("Audio decode thread stopped unexpectedly")
self._emit_progress(DecodeProgress(
status='error',
message='Decode pipeline stopped unexpectedly'
))
else:
logger.info("Audio decode thread stopped")
def _save_decoded_image(self, decoder: SSTVImageDecoder,
mode_name: str | None) -> None:

View File

@@ -193,6 +193,8 @@ class VISDetector:
# Transition to BREAK; this window counts as break window 1
self._tone_counter = 1
self._state = VISState.BREAK
elif tone is None:
pass # Ambiguous window at tone boundary — stay in state
else:
self._tone_counter = 0
self._state = VISState.IDLE
@@ -207,6 +209,8 @@ class VISDetector:
# Transition to LEADER_2; this window counts
self._tone_counter = 1
self._state = VISState.LEADER_2
elif tone is None:
pass # Ambiguous window at tone boundary — stay in state
else:
self._tone_counter = 0
self._state = VISState.IDLE
@@ -227,6 +231,8 @@ class VISDetector:
self._data_bits = []
self._bit_accumulator = []
self._state = VISState.DATA_BITS
elif tone is None:
pass # Ambiguous window at tone boundary — stay in state
else:
self._tone_counter = 0
self._state = VISState.IDLE