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
+40 -4
View File
@@ -29,6 +29,9 @@ from utils.dependencies import get_tool_path
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:
"""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'])
def start_decoding() -> Response:
global pager_active_device
with app_module.process_lock:
if app_module.current_process:
return jsonify({'status': 'error', 'message': 'Already running'}), 409
@@ -178,10 +183,29 @@ def start_decoding() -> Response:
except (ValueError, TypeError):
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
valid_protocols = ['POCSAG512', 'POCSAG1200', 'POCSAG2400', 'FLEX']
protocols = data.get('protocols', valid_protocols)
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
protocols = [p for p in protocols if p in valid_protocols]
if not protocols:
@@ -213,10 +237,6 @@ def start_decoding() -> Response:
except ValueError:
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:
# Validate and create network device
try:
@@ -302,13 +322,23 @@ def start_decoding() -> Response:
return jsonify({'status': 'started', 'command': full_cmd})
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}'})
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)})
@pager_bp.route('/stop', methods=['POST'])
def stop_decoding() -> Response:
global pager_active_device
with app_module.process_lock:
if app_module.current_process:
# Kill rtl_fm process first
@@ -337,6 +367,12 @@ def stop_decoding() -> Response:
app_module.current_process.kill()
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': 'not_running'})