mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 14:50:00 -07:00
Add SDR device registry to prevent decoder conflicts
Implements centralized tracking of SDR device allocation to prevent multiple decoders from trying to use the same device simultaneously. - Add sdr_device_registry with claim/release/status functions in app.py - Update all SDR-based routes to claim devices on start and release on stop - Return HTTP 409 with DEVICE_BUSY error when device is already in use - Clear registry on /killall - Skip device claims for remote connections (rtl_tcp, remote SBS) Fixes #100 Fixes #101 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
50
app.py
50
app.py
@@ -216,6 +216,52 @@ cleanup_manager.register(adsb_aircraft)
|
|||||||
cleanup_manager.register(ais_vessels)
|
cleanup_manager.register(ais_vessels)
|
||||||
cleanup_manager.register(dsc_messages)
|
cleanup_manager.register(dsc_messages)
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# SDR DEVICE REGISTRY
|
||||||
|
# ============================================
|
||||||
|
# Tracks which mode is using which SDR device to prevent conflicts
|
||||||
|
# Key: device_index (int), Value: mode_name (str)
|
||||||
|
sdr_device_registry: dict[int, str] = {}
|
||||||
|
sdr_device_registry_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def claim_sdr_device(device_index: int, mode_name: str) -> str | None:
|
||||||
|
"""Claim an SDR device for a mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_index: The SDR device index to claim
|
||||||
|
mode_name: Name of the mode claiming the device (e.g., 'sensor', 'rtlamr')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Error message if device is in use, None if successfully claimed
|
||||||
|
"""
|
||||||
|
with sdr_device_registry_lock:
|
||||||
|
if device_index in sdr_device_registry:
|
||||||
|
in_use_by = sdr_device_registry[device_index]
|
||||||
|
return f'SDR device {device_index} is in use by {in_use_by}. Stop {in_use_by} first or use a different device.'
|
||||||
|
sdr_device_registry[device_index] = mode_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def release_sdr_device(device_index: int) -> None:
|
||||||
|
"""Release an SDR device from the registry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_index: The SDR device index to release
|
||||||
|
"""
|
||||||
|
with sdr_device_registry_lock:
|
||||||
|
sdr_device_registry.pop(device_index, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_sdr_device_status() -> dict[int, str]:
|
||||||
|
"""Get current SDR device allocations.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping device indices to mode names
|
||||||
|
"""
|
||||||
|
with sdr_device_registry_lock:
|
||||||
|
return dict(sdr_device_registry)
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# MAIN ROUTES
|
# MAIN ROUTES
|
||||||
@@ -629,6 +675,10 @@ def kill_all() -> Response:
|
|||||||
dsc_process = None
|
dsc_process = None
|
||||||
dsc_rtl_process = None
|
dsc_rtl_process = None
|
||||||
|
|
||||||
|
# Clear SDR device registry
|
||||||
|
with sdr_device_registry_lock:
|
||||||
|
sdr_device_registry.clear()
|
||||||
|
|
||||||
return jsonify({'status': 'killed', 'processes': killed})
|
return jsonify({'status': 'killed', 'processes': killed})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ DEFAULT_ACARS_FREQUENCIES = [
|
|||||||
acars_message_count = 0
|
acars_message_count = 0
|
||||||
acars_last_message_time = None
|
acars_last_message_time = None
|
||||||
|
|
||||||
|
# Track which device is being used
|
||||||
|
acars_active_device: int | None = None
|
||||||
|
|
||||||
|
|
||||||
def find_acarsdec():
|
def find_acarsdec():
|
||||||
"""Find acarsdec binary."""
|
"""Find acarsdec binary."""
|
||||||
@@ -175,7 +178,7 @@ def acars_status() -> Response:
|
|||||||
@acars_bp.route('/start', methods=['POST'])
|
@acars_bp.route('/start', methods=['POST'])
|
||||||
def start_acars() -> Response:
|
def start_acars() -> Response:
|
||||||
"""Start ACARS decoder."""
|
"""Start ACARS decoder."""
|
||||||
global acars_message_count, acars_last_message_time
|
global acars_message_count, acars_last_message_time, acars_active_device
|
||||||
|
|
||||||
with app_module.acars_lock:
|
with app_module.acars_lock:
|
||||||
if app_module.acars_process and app_module.acars_process.poll() is None:
|
if app_module.acars_process and app_module.acars_process.poll() is None:
|
||||||
@@ -202,6 +205,18 @@ def start_acars() -> 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
|
||||||
|
|
||||||
|
# Check if device is available
|
||||||
|
device_int = int(device)
|
||||||
|
error = app_module.claim_sdr_device(device_int, 'acars')
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error
|
||||||
|
}), 409
|
||||||
|
|
||||||
|
acars_active_device = device_int
|
||||||
|
|
||||||
# Get frequencies - use provided or defaults
|
# Get frequencies - use provided or defaults
|
||||||
frequencies = data.get('frequencies', DEFAULT_ACARS_FREQUENCIES)
|
frequencies = data.get('frequencies', DEFAULT_ACARS_FREQUENCIES)
|
||||||
if isinstance(frequencies, str):
|
if isinstance(frequencies, str):
|
||||||
@@ -282,7 +297,10 @@ def start_acars() -> Response:
|
|||||||
time.sleep(PROCESS_START_WAIT)
|
time.sleep(PROCESS_START_WAIT)
|
||||||
|
|
||||||
if process.poll() is not None:
|
if process.poll() is not None:
|
||||||
# Process died
|
# Process died - release device
|
||||||
|
if acars_active_device is not None:
|
||||||
|
app_module.release_sdr_device(acars_active_device)
|
||||||
|
acars_active_device = None
|
||||||
stderr = ''
|
stderr = ''
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
stderr = process.stderr.read().decode('utf-8', errors='replace')
|
stderr = process.stderr.read().decode('utf-8', errors='replace')
|
||||||
@@ -310,6 +328,10 @@ def start_acars() -> Response:
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Release device on failure
|
||||||
|
if acars_active_device is not None:
|
||||||
|
app_module.release_sdr_device(acars_active_device)
|
||||||
|
acars_active_device = None
|
||||||
logger.error(f"Failed to start ACARS decoder: {e}")
|
logger.error(f"Failed to start ACARS decoder: {e}")
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
@@ -317,6 +339,8 @@ def start_acars() -> Response:
|
|||||||
@acars_bp.route('/stop', methods=['POST'])
|
@acars_bp.route('/stop', methods=['POST'])
|
||||||
def stop_acars() -> Response:
|
def stop_acars() -> Response:
|
||||||
"""Stop ACARS decoder."""
|
"""Stop ACARS decoder."""
|
||||||
|
global acars_active_device
|
||||||
|
|
||||||
with app_module.acars_lock:
|
with app_module.acars_lock:
|
||||||
if not app_module.acars_process:
|
if not app_module.acars_process:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -334,6 +358,11 @@ def stop_acars() -> Response:
|
|||||||
|
|
||||||
app_module.acars_process = None
|
app_module.acars_process = None
|
||||||
|
|
||||||
|
# Release device from registry
|
||||||
|
if acars_active_device is not None:
|
||||||
|
app_module.release_sdr_device(acars_active_device)
|
||||||
|
acars_active_device = None
|
||||||
|
|
||||||
return jsonify({'status': 'stopped'})
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -686,6 +686,16 @@ def start_adsb():
|
|||||||
app_module.adsb_process = None
|
app_module.adsb_process = None
|
||||||
logger.info("Killed stale ADS-B process")
|
logger.info("Killed stale ADS-B process")
|
||||||
|
|
||||||
|
# Check if device is available before starting local dump1090
|
||||||
|
device_int = int(device)
|
||||||
|
error = app_module.claim_sdr_device(device_int, 'adsb')
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error
|
||||||
|
}), 409
|
||||||
|
|
||||||
# Create device object and build command via abstraction layer
|
# Create device object and build command via abstraction layer
|
||||||
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
||||||
builder = SDRFactory.get_builder(sdr_type)
|
builder = SDRFactory.get_builder(sdr_type)
|
||||||
@@ -714,7 +724,8 @@ def start_adsb():
|
|||||||
time.sleep(DUMP1090_START_WAIT)
|
time.sleep(DUMP1090_START_WAIT)
|
||||||
|
|
||||||
if app_module.adsb_process.poll() is not None:
|
if app_module.adsb_process.poll() is not None:
|
||||||
# Process exited - try to get error message
|
# Process exited - release device and get error message
|
||||||
|
app_module.release_sdr_device(device_int)
|
||||||
stderr_output = ''
|
stderr_output = ''
|
||||||
if app_module.adsb_process.stderr:
|
if app_module.adsb_process.stderr:
|
||||||
try:
|
try:
|
||||||
@@ -752,6 +763,8 @@ def start_adsb():
|
|||||||
'session': session
|
'session': session
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Release device on failure
|
||||||
|
app_module.release_sdr_device(device_int)
|
||||||
return jsonify({'status': 'error', 'message': str(e)})
|
return jsonify({'status': 'error', 'message': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@@ -779,6 +792,11 @@ def stop_adsb():
|
|||||||
pass
|
pass
|
||||||
app_module.adsb_process = None
|
app_module.adsb_process = None
|
||||||
logger.info("ADS-B process stopped")
|
logger.info("ADS-B process stopped")
|
||||||
|
|
||||||
|
# Release device from registry
|
||||||
|
if adsb_active_device is not None:
|
||||||
|
app_module.release_sdr_device(adsb_active_device)
|
||||||
|
|
||||||
adsb_using_service = False
|
adsb_using_service = False
|
||||||
adsb_active_device = None
|
adsb_active_device = None
|
||||||
|
|
||||||
|
|||||||
@@ -370,6 +370,16 @@ def start_ais():
|
|||||||
app_module.ais_process = None
|
app_module.ais_process = None
|
||||||
logger.info("Killed existing AIS process")
|
logger.info("Killed existing AIS process")
|
||||||
|
|
||||||
|
# Check if device is available
|
||||||
|
device_int = int(device)
|
||||||
|
error = app_module.claim_sdr_device(device_int, 'ais')
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error
|
||||||
|
}), 409
|
||||||
|
|
||||||
# Build command using SDR abstraction
|
# Build command using SDR abstraction
|
||||||
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
|
||||||
builder = SDRFactory.get_builder(sdr_type)
|
builder = SDRFactory.get_builder(sdr_type)
|
||||||
@@ -400,6 +410,8 @@ def start_ais():
|
|||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
|
|
||||||
if app_module.ais_process.poll() is not None:
|
if app_module.ais_process.poll() is not None:
|
||||||
|
# Release device on failure
|
||||||
|
app_module.release_sdr_device(device_int)
|
||||||
stderr_output = ''
|
stderr_output = ''
|
||||||
if app_module.ais_process.stderr:
|
if app_module.ais_process.stderr:
|
||||||
try:
|
try:
|
||||||
@@ -425,6 +437,8 @@ def start_ais():
|
|||||||
'port': tcp_port
|
'port': tcp_port
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Release device on failure
|
||||||
|
app_module.release_sdr_device(device_int)
|
||||||
logger.error(f"Failed to start AIS-catcher: {e}")
|
logger.error(f"Failed to start AIS-catcher: {e}")
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
@@ -448,6 +462,11 @@ def stop_ais():
|
|||||||
pass
|
pass
|
||||||
app_module.ais_process = None
|
app_module.ais_process = None
|
||||||
logger.info("AIS process stopped")
|
logger.info("AIS process stopped")
|
||||||
|
|
||||||
|
# Release device from registry
|
||||||
|
if ais_active_device is not None:
|
||||||
|
app_module.release_sdr_device(ais_active_device)
|
||||||
|
|
||||||
ais_running = False
|
ais_running = False
|
||||||
ais_active_device = None
|
ais_active_device = None
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ dsc_bp = Blueprint('dsc', __name__, url_prefix='/dsc')
|
|||||||
# Module state (track if running independent of process state)
|
# Module state (track if running independent of process state)
|
||||||
dsc_running = False
|
dsc_running = False
|
||||||
|
|
||||||
|
# Track which device is being used
|
||||||
|
dsc_active_device: int | None = None
|
||||||
|
|
||||||
|
|
||||||
def _get_dsc_decoder_path() -> str | None:
|
def _get_dsc_decoder_path() -> str | None:
|
||||||
"""Get path to DSC decoder."""
|
"""Get path to DSC decoder."""
|
||||||
@@ -309,21 +312,18 @@ def start_decoding() -> Response:
|
|||||||
'message': str(e)
|
'message': str(e)
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# Check if device is in use by AIS
|
# Check if device is available using centralized registry
|
||||||
try:
|
global dsc_active_device
|
||||||
from routes import ais as ais_module
|
device_int = int(device)
|
||||||
if hasattr(ais_module, 'ais_running') and ais_module.ais_running:
|
error = app_module.claim_sdr_device(device_int, 'dsc')
|
||||||
# AIS is running - check if same device
|
if error:
|
||||||
if hasattr(ais_module, 'ais_device') and str(ais_module.ais_device) == str(device):
|
return jsonify({
|
||||||
return jsonify({
|
'status': 'error',
|
||||||
'status': 'error',
|
'error_type': 'DEVICE_BUSY',
|
||||||
'error_type': 'DEVICE_BUSY',
|
'message': error
|
||||||
'message': f'SDR device {device} is in use by AIS tracking',
|
}), 409
|
||||||
'suggestion': 'Use a different SDR device or stop AIS tracking first',
|
|
||||||
'in_use_by': 'ais'
|
dsc_active_device = device_int
|
||||||
}), 409
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Clear queue
|
# Clear queue
|
||||||
while not app_module.dsc_queue.empty():
|
while not app_module.dsc_queue.empty():
|
||||||
@@ -408,11 +408,19 @@ def start_decoding() -> Response:
|
|||||||
})
|
})
|
||||||
|
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
|
# Release device on failure
|
||||||
|
if dsc_active_device is not None:
|
||||||
|
app_module.release_sdr_device(dsc_active_device)
|
||||||
|
dsc_active_device = None
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': f'Tool not found: {e.filename}'
|
'message': f'Tool not found: {e.filename}'
|
||||||
}), 400
|
}), 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Release device on failure
|
||||||
|
if dsc_active_device is not None:
|
||||||
|
app_module.release_sdr_device(dsc_active_device)
|
||||||
|
dsc_active_device = None
|
||||||
logger.error(f"Failed to start DSC decoder: {e}")
|
logger.error(f"Failed to start DSC decoder: {e}")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
@@ -423,7 +431,7 @@ def start_decoding() -> Response:
|
|||||||
@dsc_bp.route('/stop', methods=['POST'])
|
@dsc_bp.route('/stop', methods=['POST'])
|
||||||
def stop_decoding() -> Response:
|
def stop_decoding() -> Response:
|
||||||
"""Stop DSC decoder."""
|
"""Stop DSC decoder."""
|
||||||
global dsc_running
|
global dsc_running, dsc_active_device
|
||||||
|
|
||||||
with app_module.dsc_lock:
|
with app_module.dsc_lock:
|
||||||
if not app_module.dsc_process:
|
if not app_module.dsc_process:
|
||||||
@@ -460,6 +468,11 @@ def stop_decoding() -> Response:
|
|||||||
app_module.dsc_process = None
|
app_module.dsc_process = None
|
||||||
app_module.dsc_rtl_process = None
|
app_module.dsc_rtl_process = None
|
||||||
|
|
||||||
|
# Release device from registry
|
||||||
|
if dsc_active_device is not None:
|
||||||
|
app_module.release_sdr_device(dsc_active_device)
|
||||||
|
dsc_active_device = None
|
||||||
|
|
||||||
return jsonify({'status': 'stopped'})
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ from utils.dependencies import get_tool_path
|
|||||||
|
|
||||||
pager_bp = Blueprint('pager', __name__)
|
pager_bp = Blueprint('pager', __name__)
|
||||||
|
|
||||||
|
# Track which device is being used
|
||||||
|
pager_active_device: int | None = None
|
||||||
|
|
||||||
|
|
||||||
def parse_multimon_output(line: str) -> dict[str, str] | None:
|
def parse_multimon_output(line: str) -> dict[str, str] | None:
|
||||||
"""Parse multimon-ng output line."""
|
"""Parse multimon-ng output line."""
|
||||||
@@ -155,6 +158,8 @@ def stream_decoder(master_fd: int, process: subprocess.Popen[bytes]) -> None:
|
|||||||
|
|
||||||
@pager_bp.route('/start', methods=['POST'])
|
@pager_bp.route('/start', methods=['POST'])
|
||||||
def start_decoding() -> Response:
|
def start_decoding() -> Response:
|
||||||
|
global pager_active_device
|
||||||
|
|
||||||
with app_module.process_lock:
|
with app_module.process_lock:
|
||||||
if app_module.current_process:
|
if app_module.current_process:
|
||||||
return jsonify({'status': 'error', 'message': 'Already running'}), 409
|
return jsonify({'status': 'error', 'message': 'Already running'}), 409
|
||||||
@@ -178,10 +183,29 @@ def start_decoding() -> Response:
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return jsonify({'status': 'error', 'message': 'Invalid squelch value'}), 400
|
return jsonify({'status': 'error', 'message': 'Invalid squelch value'}), 400
|
||||||
|
|
||||||
|
# Check for rtl_tcp (remote SDR) connection
|
||||||
|
rtl_tcp_host = data.get('rtl_tcp_host')
|
||||||
|
rtl_tcp_port = data.get('rtl_tcp_port', 1234)
|
||||||
|
|
||||||
|
# Claim local device if not using remote rtl_tcp
|
||||||
|
if not rtl_tcp_host:
|
||||||
|
device_int = int(device)
|
||||||
|
error = app_module.claim_sdr_device(device_int, 'pager')
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error
|
||||||
|
}), 409
|
||||||
|
pager_active_device = device_int
|
||||||
|
|
||||||
# Validate protocols
|
# Validate protocols
|
||||||
valid_protocols = ['POCSAG512', 'POCSAG1200', 'POCSAG2400', 'FLEX']
|
valid_protocols = ['POCSAG512', 'POCSAG1200', 'POCSAG2400', 'FLEX']
|
||||||
protocols = data.get('protocols', valid_protocols)
|
protocols = data.get('protocols', valid_protocols)
|
||||||
if not isinstance(protocols, list):
|
if not isinstance(protocols, list):
|
||||||
|
if pager_active_device is not None:
|
||||||
|
app_module.release_sdr_device(pager_active_device)
|
||||||
|
pager_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': 'Protocols must be a list'}), 400
|
return jsonify({'status': 'error', 'message': 'Protocols must be a list'}), 400
|
||||||
protocols = [p for p in protocols if p in valid_protocols]
|
protocols = [p for p in protocols if p in valid_protocols]
|
||||||
if not protocols:
|
if not protocols:
|
||||||
@@ -213,10 +237,6 @@ def start_decoding() -> Response:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
sdr_type = SDRType.RTL_SDR
|
sdr_type = SDRType.RTL_SDR
|
||||||
|
|
||||||
# Check for rtl_tcp (remote SDR) connection
|
|
||||||
rtl_tcp_host = data.get('rtl_tcp_host')
|
|
||||||
rtl_tcp_port = data.get('rtl_tcp_port', 1234)
|
|
||||||
|
|
||||||
if rtl_tcp_host:
|
if rtl_tcp_host:
|
||||||
# Validate and create network device
|
# Validate and create network device
|
||||||
try:
|
try:
|
||||||
@@ -302,13 +322,23 @@ def start_decoding() -> Response:
|
|||||||
return jsonify({'status': 'started', 'command': full_cmd})
|
return jsonify({'status': 'started', 'command': full_cmd})
|
||||||
|
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
|
# Release device on failure
|
||||||
|
if pager_active_device is not None:
|
||||||
|
app_module.release_sdr_device(pager_active_device)
|
||||||
|
pager_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': f'Tool not found: {e.filename}'})
|
return jsonify({'status': 'error', 'message': f'Tool not found: {e.filename}'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Release device on failure
|
||||||
|
if pager_active_device is not None:
|
||||||
|
app_module.release_sdr_device(pager_active_device)
|
||||||
|
pager_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': str(e)})
|
return jsonify({'status': 'error', 'message': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@pager_bp.route('/stop', methods=['POST'])
|
@pager_bp.route('/stop', methods=['POST'])
|
||||||
def stop_decoding() -> Response:
|
def stop_decoding() -> Response:
|
||||||
|
global pager_active_device
|
||||||
|
|
||||||
with app_module.process_lock:
|
with app_module.process_lock:
|
||||||
if app_module.current_process:
|
if app_module.current_process:
|
||||||
# Kill rtl_fm process first
|
# Kill rtl_fm process first
|
||||||
@@ -337,6 +367,12 @@ def stop_decoding() -> Response:
|
|||||||
app_module.current_process.kill()
|
app_module.current_process.kill()
|
||||||
|
|
||||||
app_module.current_process = None
|
app_module.current_process = None
|
||||||
|
|
||||||
|
# Release device from registry
|
||||||
|
if pager_active_device is not None:
|
||||||
|
app_module.release_sdr_device(pager_active_device)
|
||||||
|
pager_active_device = None
|
||||||
|
|
||||||
return jsonify({'status': 'stopped'})
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
return jsonify({'status': 'not_running'})
|
return jsonify({'status': 'not_running'})
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ rtlamr_bp = Blueprint('rtlamr', __name__)
|
|||||||
rtl_tcp_process = None
|
rtl_tcp_process = None
|
||||||
rtl_tcp_lock = threading.Lock()
|
rtl_tcp_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Track which device is being used
|
||||||
|
rtlamr_active_device: int | None = None
|
||||||
|
|
||||||
|
|
||||||
def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
|
def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
|
||||||
"""Stream rtlamr JSON output to queue."""
|
"""Stream rtlamr JSON output to queue."""
|
||||||
@@ -66,7 +69,7 @@ def stream_rtlamr_output(process: subprocess.Popen[bytes]) -> None:
|
|||||||
|
|
||||||
@rtlamr_bp.route('/start_rtlamr', methods=['POST'])
|
@rtlamr_bp.route('/start_rtlamr', methods=['POST'])
|
||||||
def start_rtlamr() -> Response:
|
def start_rtlamr() -> Response:
|
||||||
global rtl_tcp_process
|
global rtl_tcp_process, rtlamr_active_device
|
||||||
|
|
||||||
with app_module.rtlamr_lock:
|
with app_module.rtlamr_lock:
|
||||||
if app_module.rtlamr_process:
|
if app_module.rtlamr_process:
|
||||||
@@ -83,6 +86,18 @@ def start_rtlamr() -> 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
|
||||||
|
|
||||||
|
# Check if device is available
|
||||||
|
device_int = int(device)
|
||||||
|
error = app_module.claim_sdr_device(device_int, 'rtlamr')
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error
|
||||||
|
}), 409
|
||||||
|
|
||||||
|
rtlamr_active_device = device_int
|
||||||
|
|
||||||
# Clear queue
|
# Clear queue
|
||||||
while not app_module.rtlamr_queue.empty():
|
while not app_module.rtlamr_queue.empty():
|
||||||
try:
|
try:
|
||||||
@@ -182,26 +197,32 @@ def start_rtlamr() -> Response:
|
|||||||
return jsonify({'status': 'started', 'command': full_cmd})
|
return jsonify({'status': 'started', 'command': full_cmd})
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# If rtlamr fails, clean up rtl_tcp
|
# If rtlamr fails, clean up rtl_tcp and release device
|
||||||
with rtl_tcp_lock:
|
with rtl_tcp_lock:
|
||||||
if rtl_tcp_process:
|
if rtl_tcp_process:
|
||||||
rtl_tcp_process.terminate()
|
rtl_tcp_process.terminate()
|
||||||
rtl_tcp_process.wait(timeout=2)
|
rtl_tcp_process.wait(timeout=2)
|
||||||
rtl_tcp_process = None
|
rtl_tcp_process = None
|
||||||
|
if rtlamr_active_device is not None:
|
||||||
|
app_module.release_sdr_device(rtlamr_active_device)
|
||||||
|
rtlamr_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': 'rtlamr not found. Install from https://github.com/bemasher/rtlamr'})
|
return jsonify({'status': 'error', 'message': 'rtlamr not found. Install from https://github.com/bemasher/rtlamr'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If rtlamr fails, clean up rtl_tcp
|
# If rtlamr fails, clean up rtl_tcp and release device
|
||||||
with rtl_tcp_lock:
|
with rtl_tcp_lock:
|
||||||
if rtl_tcp_process:
|
if rtl_tcp_process:
|
||||||
rtl_tcp_process.terminate()
|
rtl_tcp_process.terminate()
|
||||||
rtl_tcp_process.wait(timeout=2)
|
rtl_tcp_process.wait(timeout=2)
|
||||||
rtl_tcp_process = None
|
rtl_tcp_process = None
|
||||||
|
if rtlamr_active_device is not None:
|
||||||
|
app_module.release_sdr_device(rtlamr_active_device)
|
||||||
|
rtlamr_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': str(e)})
|
return jsonify({'status': 'error', 'message': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@rtlamr_bp.route('/stop_rtlamr', methods=['POST'])
|
@rtlamr_bp.route('/stop_rtlamr', methods=['POST'])
|
||||||
def stop_rtlamr() -> Response:
|
def stop_rtlamr() -> Response:
|
||||||
global rtl_tcp_process
|
global rtl_tcp_process, rtlamr_active_device
|
||||||
|
|
||||||
with app_module.rtlamr_lock:
|
with app_module.rtlamr_lock:
|
||||||
if app_module.rtlamr_process:
|
if app_module.rtlamr_process:
|
||||||
@@ -223,6 +244,11 @@ def stop_rtlamr() -> Response:
|
|||||||
rtl_tcp_process = None
|
rtl_tcp_process = None
|
||||||
logger.info("rtl_tcp stopped")
|
logger.info("rtl_tcp stopped")
|
||||||
|
|
||||||
|
# Release device from registry
|
||||||
|
if rtlamr_active_device is not None:
|
||||||
|
app_module.release_sdr_device(rtlamr_active_device)
|
||||||
|
rtlamr_active_device = None
|
||||||
|
|
||||||
return jsonify({'status': 'stopped'})
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ from utils.sdr import SDRFactory, SDRType
|
|||||||
|
|
||||||
sensor_bp = Blueprint('sensor', __name__)
|
sensor_bp = Blueprint('sensor', __name__)
|
||||||
|
|
||||||
|
# Track which device is being used
|
||||||
|
sensor_active_device: int | None = None
|
||||||
|
|
||||||
|
|
||||||
def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
|
def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
|
||||||
"""Stream rtl_433 JSON output to queue."""
|
"""Stream rtl_433 JSON output to queue."""
|
||||||
@@ -64,6 +67,8 @@ def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
|
|||||||
|
|
||||||
@sensor_bp.route('/start_sensor', methods=['POST'])
|
@sensor_bp.route('/start_sensor', methods=['POST'])
|
||||||
def start_sensor() -> Response:
|
def start_sensor() -> Response:
|
||||||
|
global sensor_active_device
|
||||||
|
|
||||||
with app_module.sensor_lock:
|
with app_module.sensor_lock:
|
||||||
if app_module.sensor_process:
|
if app_module.sensor_process:
|
||||||
return jsonify({'status': 'error', 'message': 'Sensor already running'}), 409
|
return jsonify({'status': 'error', 'message': 'Sensor already running'}), 409
|
||||||
@@ -79,6 +84,22 @@ def start_sensor() -> 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
|
||||||
|
|
||||||
|
# Check for rtl_tcp (remote SDR) connection
|
||||||
|
rtl_tcp_host = data.get('rtl_tcp_host')
|
||||||
|
rtl_tcp_port = data.get('rtl_tcp_port', 1234)
|
||||||
|
|
||||||
|
# Claim local device if not using remote rtl_tcp
|
||||||
|
if not rtl_tcp_host:
|
||||||
|
device_int = int(device)
|
||||||
|
error = app_module.claim_sdr_device(device_int, 'sensor')
|
||||||
|
if error:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error_type': 'DEVICE_BUSY',
|
||||||
|
'message': error
|
||||||
|
}), 409
|
||||||
|
sensor_active_device = device_int
|
||||||
|
|
||||||
# Clear queue
|
# Clear queue
|
||||||
while not app_module.sensor_queue.empty():
|
while not app_module.sensor_queue.empty():
|
||||||
try:
|
try:
|
||||||
@@ -93,10 +114,6 @@ def start_sensor() -> Response:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
sdr_type = SDRType.RTL_SDR
|
sdr_type = SDRType.RTL_SDR
|
||||||
|
|
||||||
# Check for rtl_tcp (remote SDR) connection
|
|
||||||
rtl_tcp_host = data.get('rtl_tcp_host')
|
|
||||||
rtl_tcp_port = data.get('rtl_tcp_port', 1234)
|
|
||||||
|
|
||||||
if rtl_tcp_host:
|
if rtl_tcp_host:
|
||||||
# Validate and create network device
|
# Validate and create network device
|
||||||
try:
|
try:
|
||||||
@@ -155,13 +172,23 @@ def start_sensor() -> Response:
|
|||||||
return jsonify({'status': 'started', 'command': full_cmd})
|
return jsonify({'status': 'started', 'command': full_cmd})
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
# Release device on failure
|
||||||
|
if sensor_active_device is not None:
|
||||||
|
app_module.release_sdr_device(sensor_active_device)
|
||||||
|
sensor_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': 'rtl_433 not found. Install with: brew install rtl_433'})
|
return jsonify({'status': 'error', 'message': 'rtl_433 not found. Install with: brew install rtl_433'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Release device on failure
|
||||||
|
if sensor_active_device is not None:
|
||||||
|
app_module.release_sdr_device(sensor_active_device)
|
||||||
|
sensor_active_device = None
|
||||||
return jsonify({'status': 'error', 'message': str(e)})
|
return jsonify({'status': 'error', 'message': str(e)})
|
||||||
|
|
||||||
|
|
||||||
@sensor_bp.route('/stop_sensor', methods=['POST'])
|
@sensor_bp.route('/stop_sensor', methods=['POST'])
|
||||||
def stop_sensor() -> Response:
|
def stop_sensor() -> Response:
|
||||||
|
global sensor_active_device
|
||||||
|
|
||||||
with app_module.sensor_lock:
|
with app_module.sensor_lock:
|
||||||
if app_module.sensor_process:
|
if app_module.sensor_process:
|
||||||
app_module.sensor_process.terminate()
|
app_module.sensor_process.terminate()
|
||||||
@@ -170,6 +197,12 @@ def stop_sensor() -> Response:
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
app_module.sensor_process.kill()
|
app_module.sensor_process.kill()
|
||||||
app_module.sensor_process = None
|
app_module.sensor_process = None
|
||||||
|
|
||||||
|
# Release device from registry
|
||||||
|
if sensor_active_device is not None:
|
||||||
|
app_module.release_sdr_device(sensor_active_device)
|
||||||
|
sensor_active_device = None
|
||||||
|
|
||||||
return jsonify({'status': 'stopped'})
|
return jsonify({'status': 'stopped'})
|
||||||
|
|
||||||
return jsonify({'status': 'not_running'})
|
return jsonify({'status': 'not_running'})
|
||||||
|
|||||||
Reference in New Issue
Block a user