Fix weather satellite mode returning false success on SatDump startup failure

Add synchronous startup verification after Popen() — sleep 0.5s and poll
the process before returning to the caller. If SatDump exits immediately
(missing device, bad args), raise RuntimeError with the actual error
message instead of returning status: 'started'. Keep a shorter (2s) async
backup check for slower failures.

Also fix --source_id handling: omit the flag entirely when no serial number
is found instead of passing "0" which SatDump may reject. Change start()
and start_from_file() to return (bool, str|None) tuples so error messages
propagate through to the HTTP response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-25 21:49:16 +00:00
parent a50f77629c
commit 935b7a4d9d
7 changed files with 197 additions and 147 deletions

View File

@@ -73,9 +73,10 @@ class TestWeatherSatDecoder:
callback = MagicMock()
decoder.set_callback(callback)
success = decoder.start(satellite='NOAA-18', device_index=0, gain=40.0)
success, error_msg = decoder.start(satellite='NOAA-18', device_index=0, gain=40.0)
assert success is False
assert error_msg is not None
callback.assert_called()
progress = callback.call_args[0][0]
assert progress.status == 'error'
@@ -88,7 +89,7 @@ class TestWeatherSatDecoder:
callback = MagicMock()
decoder.set_callback(callback)
success = decoder.start(satellite='FAKE-SAT', device_index=0, gain=40.0)
success, error_msg = decoder.start(satellite='FAKE-SAT', device_index=0, gain=40.0)
assert success is False
callback.assert_called()
@@ -113,7 +114,7 @@ class TestWeatherSatDecoder:
callback = MagicMock()
decoder.set_callback(callback)
success = decoder.start(
success, error_msg = decoder.start(
satellite='NOAA-18',
device_index=0,
gain=40.0,
@@ -121,6 +122,7 @@ class TestWeatherSatDecoder:
)
assert success is True
assert error_msg is None
assert decoder.is_running is True
assert decoder.current_satellite == 'NOAA-18'
assert decoder.current_frequency == 137.9125
@@ -143,9 +145,10 @@ class TestWeatherSatDecoder:
decoder = WeatherSatDecoder()
decoder._running = True
success = decoder.start(satellite='NOAA-18', device_index=0, gain=40.0)
success, error_msg = decoder.start(satellite='NOAA-18', device_index=0, gain=40.0)
assert success is True
assert error_msg is None
mock_popen.assert_not_called()
@patch('subprocess.Popen')
@@ -160,9 +163,10 @@ class TestWeatherSatDecoder:
callback = MagicMock()
decoder.set_callback(callback)
success = decoder.start(satellite='NOAA-18', device_index=0, gain=40.0)
success, error_msg = decoder.start(satellite='NOAA-18', device_index=0, gain=40.0)
assert success is False
assert error_msg is not None
assert decoder.is_running is False
callback.assert_called()
progress = callback.call_args[0][0]
@@ -175,12 +179,13 @@ class TestWeatherSatDecoder:
callback = MagicMock()
decoder.set_callback(callback)
success = decoder.start_from_file(
success, error_msg = decoder.start_from_file(
satellite='NOAA-18',
input_file='data/test.wav',
)
assert success is False
assert error_msg is not None
callback.assert_called()
@patch('subprocess.Popen')
@@ -200,19 +205,21 @@ class TestWeatherSatDecoder:
mock_pty.return_value = (10, 11)
mock_process = MagicMock()
mock_process.poll.return_value = None # Process still running
mock_popen.return_value = mock_process
decoder = WeatherSatDecoder()
callback = MagicMock()
decoder.set_callback(callback)
success = decoder.start_from_file(
success, error_msg = decoder.start_from_file(
satellite='NOAA-18',
input_file='data/test.wav',
sample_rate=1000000,
)
assert success is True
assert error_msg is None
assert decoder.is_running is True
assert decoder.current_satellite == 'NOAA-18'
@@ -236,7 +243,7 @@ class TestWeatherSatDecoder:
callback = MagicMock()
decoder.set_callback(callback)
success = decoder.start_from_file(
success, error_msg = decoder.start_from_file(
satellite='NOAA-18',
input_file='/etc/passwd',
)
@@ -259,7 +266,7 @@ class TestWeatherSatDecoder:
callback = MagicMock()
decoder.set_callback(callback)
success = decoder.start_from_file(
success, error_msg = decoder.start_from_file(
satellite='NOAA-18',
input_file='data/missing.wav',
)
@@ -426,12 +433,12 @@ class TestWeatherSatDecoder:
@patch('subprocess.run')
def test_resolve_device_id_fallback(self, mock_run):
"""_resolve_device_id() should fall back to index string."""
"""_resolve_device_id() should return None when no serial found."""
mock_run.side_effect = FileNotFoundError
serial = WeatherSatDecoder._resolve_device_id(0)
assert serial == '0'
assert serial is None
def test_parse_product_name_rgb(self):
"""_parse_product_name() should identify RGB composite."""

View File

@@ -106,13 +106,14 @@ class TestWeatherSatDecoderRegressions:
mock_resolve.return_value = resolved
decoder = WeatherSatDecoder(output_dir=tmp_path / 'weather_sat_out')
success = decoder.start_from_file(
success, error_msg = decoder.start_from_file(
satellite='METEOR-M2-3',
input_file='data/weather_sat/samples/sample.wav',
sample_rate=1_000_000,
)
assert success is True
assert error_msg is None
assert decoder.device_index == -1
mock_start.assert_called_once()

View File

@@ -73,7 +73,7 @@ class TestWeatherSatRoutes:
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start.return_value = True
mock_decoder.start.return_value = (True, None)
mock_get.return_value = mock_decoder
payload = {
@@ -233,7 +233,7 @@ class TestWeatherSatRoutes:
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start.return_value = False
mock_decoder.start.return_value = (False, 'SatDump exited immediately (code 1)')
mock_get.return_value = mock_decoder
payload = {'satellite': 'NOAA-18'}
@@ -246,7 +246,7 @@ class TestWeatherSatRoutes:
assert response.status_code == 500
data = response.get_json()
assert data['status'] == 'error'
assert 'Failed to start capture' in data['message']
assert 'SatDump exited immediately' in data['message']
def test_test_decode_success(self, client):
"""POST /weather-sat/test-decode successfully starts file decode."""
@@ -262,7 +262,7 @@ class TestWeatherSatRoutes:
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start_from_file.return_value = True
mock_decoder.start_from_file.return_value = (True, None)
mock_get.return_value = mock_decoder
payload = {

View File

@@ -546,7 +546,7 @@ class TestWeatherSatScheduler:
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start.return_value = True
mock_decoder.start.return_value = (True, None)
mock_get.return_value = mock_decoder
mock_timer_instance = MagicMock()
@@ -590,7 +590,7 @@ class TestWeatherSatScheduler:
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start.return_value = False
mock_decoder.start.return_value = (False, 'Start failed')
mock_get.return_value = mock_decoder
pass_data = {
@@ -798,7 +798,7 @@ class TestSchedulerIntegration:
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start.return_value = True
mock_decoder.start.return_value = (True, None)
mock_get_decoder.return_value = mock_decoder
scheduler = WeatherSatScheduler()