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:
Smittix
2026-02-02 21:05:21 +00:00
parent 36f8349bc7
commit a76dfde02d
8 changed files with 275 additions and 51 deletions

50
app.py
View File

@@ -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})

View File

@@ -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'})

View File

@@ -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

View File

@@ -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

View File

@@ -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'})

View File

@@ -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'})

View File

@@ -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'})

View File

@@ -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'})