feat: add rtl_tcp remote SDR support to weather satellite decoder

Extends the rtl_tcp support (added in c1339b6 for APRS, Morse, DSC) to
the weather satellite mode. When a remote SDR host is provided, SatDump
uses --source rtltcp instead of --source rtlsdr, local device claiming
is skipped, and the frontend sends rtl_tcp params via getRemoteSDRConfig().

Closes #166

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-03-01 16:55:04 +00:00
parent 2de592f798
commit bdeb32e723
5 changed files with 213 additions and 39 deletions

View File

@@ -136,6 +136,68 @@ class TestWeatherSatDecoder:
assert 'noaa_apt' in cmd
assert '--bias' in cmd
@patch('subprocess.Popen')
@patch('pty.openpty')
@patch('utils.weather_sat.register_process')
def test_start_rtl_tcp_uses_rtltcp_source(self, mock_register, mock_pty, mock_popen):
"""start() with rtl_tcp should use --source rtltcp instead of rtlsdr."""
with patch('shutil.which', return_value='/usr/bin/satdump'):
mock_pty.return_value = (10, 11)
mock_process = MagicMock()
mock_process.poll.return_value = None
mock_popen.return_value = mock_process
decoder = WeatherSatDecoder()
callback = MagicMock()
decoder.set_callback(callback)
success, error_msg = decoder.start(
satellite='NOAA-18',
device_index=0,
gain=40.0,
rtl_tcp_host='192.168.1.100',
rtl_tcp_port=1234,
)
assert success is True
assert error_msg is None
mock_popen.assert_called_once()
cmd = mock_popen.call_args[0][0]
assert '--source' in cmd
source_idx = cmd.index('--source')
assert cmd[source_idx + 1] == 'rtltcp'
assert '--ip_address' in cmd
assert '192.168.1.100' in cmd
assert '--port' in cmd
assert '1234' in cmd
# Should NOT have --source_id for remote
assert '--source_id' not in cmd
@patch('subprocess.Popen')
@patch('pty.openpty')
@patch('utils.weather_sat.register_process')
def test_start_rtl_tcp_skips_device_resolve(self, mock_register, mock_pty, mock_popen):
"""start() with rtl_tcp should skip _resolve_device_id."""
with patch('shutil.which', return_value='/usr/bin/satdump'), \
patch('utils.weather_sat.WeatherSatDecoder._resolve_device_id') as mock_resolve:
mock_pty.return_value = (10, 11)
mock_process = MagicMock()
mock_process.poll.return_value = None
mock_popen.return_value = mock_process
decoder = WeatherSatDecoder()
success, _ = decoder.start(
satellite='NOAA-18',
device_index=0,
gain=40.0,
rtl_tcp_host='10.0.0.1',
)
assert success is True
mock_resolve.assert_not_called()
@patch('subprocess.Popen')
@patch('pty.openpty')
def test_start_already_running(self, mock_pty, mock_popen):

View File

@@ -226,6 +226,68 @@ class TestWeatherSatRoutes:
assert data['error_type'] == 'DEVICE_BUSY'
assert 'Device busy' in data['message']
def test_start_capture_rtl_tcp_success(self, client):
"""POST /weather-sat/start with rtl_tcp remote SDR."""
with patch('routes.weather_sat.is_weather_sat_available', return_value=True), \
patch('routes.weather_sat.get_weather_sat_decoder') as mock_get, \
patch('app.claim_sdr_device') as mock_claim:
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_decoder.start.return_value = (True, None)
mock_get.return_value = mock_decoder
payload = {
'satellite': 'NOAA-18',
'device': 0,
'gain': 40.0,
'rtl_tcp_host': '192.168.1.100',
'rtl_tcp_port': 1234,
}
response = client.post(
'/weather-sat/start',
data=json.dumps(payload),
content_type='application/json'
)
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'started'
# Device claim should NOT be called for remote SDR
mock_claim.assert_not_called()
# Verify rtl_tcp params passed to decoder
mock_decoder.start.assert_called_once()
call_kwargs = mock_decoder.start.call_args
assert call_kwargs[1]['rtl_tcp_host'] == '192.168.1.100'
assert call_kwargs[1]['rtl_tcp_port'] == 1234
def test_start_capture_rtl_tcp_invalid_host(self, client):
"""POST /weather-sat/start with invalid rtl_tcp host."""
with patch('routes.weather_sat.is_weather_sat_available', return_value=True), \
patch('routes.weather_sat.get_weather_sat_decoder') as mock_get:
mock_decoder = MagicMock()
mock_decoder.is_running = False
mock_get.return_value = mock_decoder
payload = {
'satellite': 'NOAA-18',
'rtl_tcp_host': 'not a valid host!@#',
}
response = client.post(
'/weather-sat/start',
data=json.dumps(payload),
content_type='application/json'
)
assert response.status_code == 400
data = response.get_json()
assert data['status'] == 'error'
def test_start_capture_start_failure(self, client):
"""POST /weather-sat/start when decoder.start() fails."""
with patch('routes.weather_sat.is_weather_sat_available', return_value=True), \