mirror of
https://github.com/smittix/intercept.git
synced 2026-07-29 11:08:10 -07:00
Fix APRS rtl_fm startup failure and SDR device conflicts (#122)
Add SDR device reservation to prevent conflicts with other modes, and capture rtl_fm stderr so actual error messages are reported to the user instead of a generic exit code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+59
-5
@@ -13,7 +13,7 @@ import tempfile
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from subprocess import DEVNULL, PIPE, STDOUT
|
from subprocess import PIPE, STDOUT
|
||||||
from typing import Generator, Optional
|
from typing import Generator, Optional
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request, Response
|
from flask import Blueprint, jsonify, request, Response
|
||||||
@@ -31,6 +31,9 @@ from utils.constants import (
|
|||||||
|
|
||||||
aprs_bp = Blueprint('aprs', __name__, url_prefix='/aprs')
|
aprs_bp = Blueprint('aprs', __name__, url_prefix='/aprs')
|
||||||
|
|
||||||
|
# Track which SDR device is being used
|
||||||
|
aprs_active_device: int | None = None
|
||||||
|
|
||||||
# APRS frequencies by region (MHz)
|
# APRS frequencies by region (MHz)
|
||||||
APRS_FREQUENCIES = {
|
APRS_FREQUENCIES = {
|
||||||
'north_america': '144.390',
|
'north_america': '144.390',
|
||||||
@@ -1301,7 +1304,7 @@ def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subproces
|
|||||||
|
|
||||||
This function reads from the decoder's stdout (text mode, line-buffered).
|
This function reads from the decoder's stdout (text mode, line-buffered).
|
||||||
The decoder's stderr is merged into stdout (STDOUT) to avoid deadlocks.
|
The decoder's stderr is merged into stdout (STDOUT) to avoid deadlocks.
|
||||||
rtl_fm's stderr is sent to DEVNULL for the same reason.
|
rtl_fm's stderr is captured via PIPE with a monitor thread.
|
||||||
|
|
||||||
Outputs two types of messages to the queue:
|
Outputs two types of messages to the queue:
|
||||||
- type='aprs': Decoded APRS packets
|
- type='aprs': Decoded APRS packets
|
||||||
@@ -1383,6 +1386,7 @@ def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subproces
|
|||||||
logger.error(f"APRS stream error: {e}")
|
logger.error(f"APRS stream error: {e}")
|
||||||
app_module.aprs_queue.put({'type': 'error', 'message': str(e)})
|
app_module.aprs_queue.put({'type': 'error', 'message': str(e)})
|
||||||
finally:
|
finally:
|
||||||
|
global aprs_active_device
|
||||||
app_module.aprs_queue.put({'type': 'status', 'status': 'stopped'})
|
app_module.aprs_queue.put({'type': 'status', 'status': 'stopped'})
|
||||||
# Cleanup processes
|
# Cleanup processes
|
||||||
for proc in [rtl_process, decoder_process]:
|
for proc in [rtl_process, decoder_process]:
|
||||||
@@ -1394,6 +1398,10 @@ def stream_aprs_output(rtl_process: subprocess.Popen, decoder_process: subproces
|
|||||||
proc.kill()
|
proc.kill()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# Release SDR device
|
||||||
|
if aprs_active_device is not None:
|
||||||
|
app_module.release_sdr_device(aprs_active_device)
|
||||||
|
aprs_active_device = None
|
||||||
|
|
||||||
|
|
||||||
@aprs_bp.route('/tools')
|
@aprs_bp.route('/tools')
|
||||||
@@ -1441,6 +1449,7 @@ def get_stations() -> Response:
|
|||||||
def start_aprs() -> Response:
|
def start_aprs() -> Response:
|
||||||
"""Start APRS decoder."""
|
"""Start APRS decoder."""
|
||||||
global aprs_packet_count, aprs_station_count, aprs_last_packet_time, aprs_stations
|
global aprs_packet_count, aprs_station_count, aprs_last_packet_time, aprs_stations
|
||||||
|
global aprs_active_device
|
||||||
|
|
||||||
with app_module.aprs_lock:
|
with app_module.aprs_lock:
|
||||||
if app_module.aprs_process and app_module.aprs_process.poll() is None:
|
if app_module.aprs_process and app_module.aprs_process.poll() is None:
|
||||||
@@ -1477,6 +1486,16 @@ def start_aprs() -> Response:
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
# Reserve SDR device to prevent conflicts with other modes
|
||||||
|
error = app_module.reserve_sdr_device(device, 'APRS')
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error
|
||||||
|
}), 409
|
||||||
|
aprs_active_device = device
|
||||||
|
|
||||||
# Get frequency for region
|
# Get frequency for region
|
||||||
region = data.get('region', 'north_america')
|
region = data.get('region', 'north_america')
|
||||||
frequency = APRS_FREQUENCIES.get(region, '144.390')
|
frequency = APRS_FREQUENCIES.get(region, '144.390')
|
||||||
@@ -1552,15 +1571,25 @@ def start_aprs() -> Response:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Start rtl_fm with stdout piped to decoder.
|
# Start rtl_fm with stdout piped to decoder.
|
||||||
# stderr goes to DEVNULL to prevent blocking (rtl_fm logs to stderr).
|
# stderr is captured via PIPE so errors are reported to the user.
|
||||||
# NOTE: RTL-SDR Blog V4 may show offset-tuned frequency in logs - this is normal.
|
# NOTE: RTL-SDR Blog V4 may show offset-tuned frequency in logs - this is normal.
|
||||||
rtl_process = subprocess.Popen(
|
rtl_process = subprocess.Popen(
|
||||||
rtl_cmd,
|
rtl_cmd,
|
||||||
stdout=PIPE,
|
stdout=PIPE,
|
||||||
stderr=DEVNULL,
|
stderr=PIPE,
|
||||||
start_new_session=True
|
start_new_session=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Start a thread to monitor rtl_fm stderr for errors
|
||||||
|
def monitor_rtl_stderr():
|
||||||
|
for line in rtl_process.stderr:
|
||||||
|
err_text = line.decode('utf-8', errors='replace').strip()
|
||||||
|
if err_text:
|
||||||
|
logger.debug(f"[RTL_FM] {err_text}")
|
||||||
|
|
||||||
|
rtl_stderr_thread = threading.Thread(target=monitor_rtl_stderr, daemon=True)
|
||||||
|
rtl_stderr_thread.start()
|
||||||
|
|
||||||
# Start decoder with stdin wired to rtl_fm's stdout.
|
# Start decoder with stdin wired to rtl_fm's stdout.
|
||||||
# Use text mode with line buffering for reliable line-by-line reading.
|
# Use text mode with line buffering for reliable line-by-line reading.
|
||||||
# Merge stderr into stdout to avoid blocking on unbuffered stderr.
|
# Merge stderr into stdout to avoid blocking on unbuffered stderr.
|
||||||
@@ -1582,13 +1611,25 @@ def start_aprs() -> Response:
|
|||||||
time.sleep(PROCESS_START_WAIT)
|
time.sleep(PROCESS_START_WAIT)
|
||||||
|
|
||||||
if rtl_process.poll() is not None:
|
if rtl_process.poll() is not None:
|
||||||
# rtl_fm exited early - something went wrong
|
# rtl_fm exited early - capture stderr for diagnostics
|
||||||
|
stderr_output = ''
|
||||||
|
try:
|
||||||
|
remaining = rtl_process.stderr.read()
|
||||||
|
if remaining:
|
||||||
|
stderr_output = remaining.decode('utf-8', errors='replace').strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
error_msg = f'rtl_fm failed to start (exit code {rtl_process.returncode})'
|
error_msg = f'rtl_fm failed to start (exit code {rtl_process.returncode})'
|
||||||
|
if stderr_output:
|
||||||
|
error_msg += f': {stderr_output[:200]}'
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
try:
|
try:
|
||||||
decoder_process.kill()
|
decoder_process.kill()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
if aprs_active_device is not None:
|
||||||
|
app_module.release_sdr_device(aprs_active_device)
|
||||||
|
aprs_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': error_msg}), 500
|
return jsonify({'status': 'error', 'message': error_msg}), 500
|
||||||
|
|
||||||
if decoder_process.poll() is not None:
|
if decoder_process.poll() is not None:
|
||||||
@@ -1602,6 +1643,9 @@ def start_aprs() -> Response:
|
|||||||
rtl_process.kill()
|
rtl_process.kill()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
if aprs_active_device is not None:
|
||||||
|
app_module.release_sdr_device(aprs_active_device)
|
||||||
|
aprs_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': error_msg}), 500
|
return jsonify({'status': 'error', 'message': error_msg}), 500
|
||||||
|
|
||||||
# Store references for status checks and cleanup
|
# Store references for status checks and cleanup
|
||||||
@@ -1626,12 +1670,17 @@ def start_aprs() -> Response:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to start APRS decoder: {e}")
|
logger.error(f"Failed to start APRS decoder: {e}")
|
||||||
|
if aprs_active_device is not None:
|
||||||
|
app_module.release_sdr_device(aprs_active_device)
|
||||||
|
aprs_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@aprs_bp.route('/stop', methods=['POST'])
|
@aprs_bp.route('/stop', methods=['POST'])
|
||||||
def stop_aprs() -> Response:
|
def stop_aprs() -> Response:
|
||||||
"""Stop APRS decoder."""
|
"""Stop APRS decoder."""
|
||||||
|
global aprs_active_device
|
||||||
|
|
||||||
with app_module.aprs_lock:
|
with app_module.aprs_lock:
|
||||||
processes_to_stop = []
|
processes_to_stop = []
|
||||||
|
|
||||||
@@ -1660,6 +1709,11 @@ def stop_aprs() -> Response:
|
|||||||
if hasattr(app_module, 'aprs_rtl_process'):
|
if hasattr(app_module, 'aprs_rtl_process'):
|
||||||
app_module.aprs_rtl_process = None
|
app_module.aprs_rtl_process = None
|
||||||
|
|
||||||
|
# Release SDR device
|
||||||
|
if aprs_active_device is not None:
|
||||||
|
app_module.release_sdr_device(aprs_active_device)
|
||||||
|
aprs_active_device = None
|
||||||
|
|
||||||
return jsonify({'status': 'stopped'})
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user