Update SDR device claims

This commit is contained in:
Smittix
2026-02-10 18:07:52 +00:00
parent b4c47ed28b
commit 257de37dfe
9 changed files with 585 additions and 420 deletions
+38 -20
View File
@@ -236,12 +236,12 @@ cleanup_manager.register(deauth_alerts)
# SDR DEVICE REGISTRY # SDR DEVICE REGISTRY
# ============================================ # ============================================
# Tracks which mode is using which SDR device to prevent conflicts # Tracks which mode is using which SDR device to prevent conflicts
# Key: device_index (int), Value: mode_name (str) # Key: device_index (int), Value: {sdr_type: mode_name}
sdr_device_registry: dict[int, str] = {} sdr_device_registry: dict[int, dict[str, str]] = {}
sdr_device_registry_lock = threading.Lock() sdr_device_registry_lock = threading.Lock()
def claim_sdr_device(device_index: int, mode_name: str) -> str | None: def claim_sdr_device(device_index: int, mode_name: str, sdr_type: str = 'rtlsdr') -> str | None:
"""Claim an SDR device for a mode. """Claim an SDR device for a mode.
Checks the in-app registry first, then probes the USB device to Checks the in-app registry first, then probes the USB device to
@@ -251,46 +251,61 @@ def claim_sdr_device(device_index: int, mode_name: str) -> str | None:
Args: Args:
device_index: The SDR device index to claim device_index: The SDR device index to claim
mode_name: Name of the mode claiming the device (e.g., 'sensor', 'rtlamr') mode_name: Name of the mode claiming the device (e.g., 'sensor', 'rtlamr')
sdr_type: SDR hardware type (e.g., 'rtlsdr', 'hackrf')
Returns: Returns:
Error message if device is in use, None if successfully claimed Error message if device is in use, None if successfully claimed
""" """
sdr_type_key = str(sdr_type or 'rtlsdr').lower()
with sdr_device_registry_lock: with sdr_device_registry_lock:
if device_index in sdr_device_registry: device_entry = sdr_device_registry.get(device_index, {})
in_use_by = sdr_device_registry[device_index] if sdr_type_key in device_entry:
in_use_by = device_entry[sdr_type_key]
return f'SDR device {device_index} is in use by {in_use_by}. Stop {in_use_by} first or use a different device.' return f'SDR device {device_index} is in use by {in_use_by}. Stop {in_use_by} first or use a different device.'
# Probe the USB device to catch external processes holding the handle # Probe the USB device to catch external processes holding the handle
try: # Only relevant for RTL-SDR devices
from utils.sdr.detection import probe_rtlsdr_device if sdr_type_key == 'rtlsdr':
usb_error = probe_rtlsdr_device(device_index) try:
if usb_error: from utils.sdr.detection import probe_rtlsdr_device
return usb_error usb_error = probe_rtlsdr_device(device_index)
except Exception: if usb_error:
pass # If probe fails, let the caller proceed normally return usb_error
except Exception:
pass # If probe fails, let the caller proceed normally
sdr_device_registry[device_index] = mode_name if device_index not in sdr_device_registry:
sdr_device_registry[device_index] = {}
sdr_device_registry[device_index][sdr_type_key] = mode_name
return None return None
def release_sdr_device(device_index: int) -> None: def release_sdr_device(device_index: int, sdr_type: str = 'rtlsdr') -> None:
"""Release an SDR device from the registry. """Release an SDR device from the registry.
Args: Args:
device_index: The SDR device index to release device_index: The SDR device index to release
sdr_type: SDR hardware type (e.g., 'rtlsdr', 'hackrf')
""" """
sdr_type_key = str(sdr_type or 'rtlsdr').lower()
with sdr_device_registry_lock: with sdr_device_registry_lock:
sdr_device_registry.pop(device_index, None) entry = sdr_device_registry.get(device_index)
if not entry:
return
entry.pop(sdr_type_key, None)
if not entry:
sdr_device_registry.pop(device_index, None)
def get_sdr_device_status() -> dict[int, str]: def get_sdr_device_status() -> dict[int, dict[str, str]]:
"""Get current SDR device allocations. """Get current SDR device allocations.
Returns: Returns:
Dictionary mapping device indices to mode names Dictionary mapping device indices to {sdr_type: mode_name}
""" """
with sdr_device_registry_lock: with sdr_device_registry_lock:
return dict(sdr_device_registry) return {idx: dict(modes) for idx, modes in sdr_device_registry.items()}
# ============================================ # ============================================
@@ -396,8 +411,11 @@ def get_devices_status() -> Response:
result = [] result = []
for device in devices: for device in devices:
d = device.to_dict() d = device.to_dict()
d['in_use'] = device.index in registry sdr_type_key = device.sdr_type.value if hasattr(device.sdr_type, 'value') else str(device.sdr_type)
d['used_by'] = registry.get(device.index) sdr_type_key = str(sdr_type_key).lower()
device_registry = registry.get(device.index, {})
d['in_use'] = sdr_type_key in device_registry
d['used_by'] = device_registry.get(sdr_type_key)
result.append(d) result.append(d)
return jsonify(result) return jsonify(result)
+38 -13
View File
@@ -679,6 +679,7 @@ class ModeManager:
running_modes_detail[mode] = { running_modes_detail[mode] = {
'started_at': info.get('started_at'), 'started_at': info.get('started_at'),
'device': params.get('device', params.get('device_index', 0)), 'device': params.get('device', params.get('device_index', 0)),
'sdr_type': str(params.get('sdr_type', 'rtlsdr')).lower(),
} }
status = { status = {
@@ -698,20 +699,22 @@ class ModeManager:
# Modes that use RTL-SDR devices # Modes that use RTL-SDR devices
SDR_MODES = {'adsb', 'sensor', 'pager', 'ais', 'acars', 'dsc', 'rtlamr', 'listening_post'} SDR_MODES = {'adsb', 'sensor', 'pager', 'ais', 'acars', 'dsc', 'rtlamr', 'listening_post'}
def get_sdr_in_use(self, device: int = 0) -> str | None: def get_sdr_in_use(self, device: int = 0, sdr_type: str = 'rtlsdr') -> str | None:
"""Check if an SDR device is in use by another mode. """Check if an SDR device is in use by another mode.
Returns the mode name using the device, or None if available. Returns the mode name using the device, or None if available.
""" """
sdr_type_key = str(sdr_type or 'rtlsdr').lower()
for mode, info in self.running_modes.items(): for mode, info in self.running_modes.items():
if mode in self.SDR_MODES: if mode in self.SDR_MODES:
mode_device = info.get('params', {}).get('device', 0) mode_device = info.get('params', {}).get('device', 0)
mode_sdr_type = str(info.get('params', {}).get('sdr_type', 'rtlsdr')).lower()
# Normalize to int for comparison # Normalize to int for comparison
try: try:
mode_device = int(mode_device) mode_device = int(mode_device)
except (ValueError, TypeError): except (ValueError, TypeError):
mode_device = 0 mode_device = 0
if mode_device == device: if mode_device == device and mode_sdr_type == sdr_type_key:
return mode return mode
return None return None
@@ -731,7 +734,8 @@ class ModeManager:
device = int(device) device = int(device)
except (ValueError, TypeError): except (ValueError, TypeError):
device = 0 device = 0
in_use_by = self.get_sdr_in_use(device) sdr_type = str(params.get('sdr_type', 'rtlsdr')).lower()
in_use_by = self.get_sdr_in_use(device, sdr_type)
if in_use_by: if in_use_by:
return { return {
'status': 'error', 'status': 'error',
@@ -1100,6 +1104,11 @@ class ModeManager:
# Mode-specific cleanup # Mode-specific cleanup
if mode == 'adsb': if mode == 'adsb':
self.adsb_aircraft.clear() self.adsb_aircraft.clear()
if 'adsb_mlat' in self.output_threads:
thread = self.output_threads['adsb_mlat']
if thread and thread.is_alive():
thread.join(timeout=1)
del self.output_threads['adsb_mlat']
elif mode == 'wifi': elif mode == 'wifi':
self.wifi_networks.clear() self.wifi_networks.clear()
self.wifi_clients.clear() self.wifi_clients.clear()
@@ -1315,10 +1324,16 @@ class ModeManager:
sdr_type_str = params.get('sdr_type', 'rtlsdr') sdr_type_str = params.get('sdr_type', 'rtlsdr')
remote_sbs_host = params.get('remote_sbs_host') remote_sbs_host = params.get('remote_sbs_host')
remote_sbs_port = params.get('remote_sbs_port', 30003) remote_sbs_port = params.get('remote_sbs_port', 30003)
mlat_sbs_host = params.get('mlat_sbs_host')
mlat_sbs_port = params.get('mlat_sbs_port', 30105)
# If remote SBS host provided, just connect to it # If remote SBS host provided, just connect to it
if remote_sbs_host: if remote_sbs_host:
return self._start_adsb_sbs_connection(remote_sbs_host, remote_sbs_port) result = self._start_adsb_sbs_connection(remote_sbs_host, remote_sbs_port, source_tag='adsb', thread_name='adsb')
if mlat_sbs_host:
self._start_adsb_sbs_connection(mlat_sbs_host, mlat_sbs_port, source_tag='mlat', thread_name='adsb_mlat')
result['mlat_source'] = f'{mlat_sbs_host}:{mlat_sbs_port}'
return result
# Check if dump1090 already running on port 30003 # Check if dump1090 already running on port 30003
try: try:
@@ -1328,7 +1343,11 @@ class ModeManager:
sock.close() sock.close()
if result == 0: if result == 0:
logger.info("dump1090 already running, connecting to SBS port") logger.info("dump1090 already running, connecting to SBS port")
return self._start_adsb_sbs_connection('localhost', 30003) result = self._start_adsb_sbs_connection('localhost', 30003, source_tag='adsb', thread_name='adsb')
if mlat_sbs_host:
self._start_adsb_sbs_connection(mlat_sbs_host, mlat_sbs_port, source_tag='mlat', thread_name='adsb_mlat')
result['mlat_source'] = f'{mlat_sbs_host}:{mlat_sbs_port}'
return result
except Exception: except Exception:
pass pass
@@ -1385,7 +1404,11 @@ class ModeManager:
return {'status': 'error', 'message': f'dump1090 failed to start: {stderr[:200]}'} return {'status': 'error', 'message': f'dump1090 failed to start: {stderr[:200]}'}
# Connect to SBS port # Connect to SBS port
return self._start_adsb_sbs_connection('localhost', 30003) result = self._start_adsb_sbs_connection('localhost', 30003, source_tag='adsb', thread_name='adsb')
if mlat_sbs_host:
self._start_adsb_sbs_connection(mlat_sbs_host, mlat_sbs_port, source_tag='mlat', thread_name='adsb_mlat')
result['mlat_source'] = f'{mlat_sbs_host}:{mlat_sbs_port}'
return result
except FileNotFoundError: except FileNotFoundError:
return {'status': 'error', 'message': 'dump1090 not found'} return {'status': 'error', 'message': 'dump1090 not found'}
@@ -1414,15 +1437,15 @@ class ModeManager:
return path return path
return None return None
def _start_adsb_sbs_connection(self, host: str, port: int) -> dict: def _start_adsb_sbs_connection(self, host: str, port: int, *, source_tag: str = 'adsb', thread_name: str = 'adsb') -> dict:
"""Connect to SBS port and start parsing.""" """Connect to SBS port and start parsing."""
thread = threading.Thread( thread = threading.Thread(
target=self._adsb_sbs_reader, target=self._adsb_sbs_reader,
args=(host, port), args=(host, port, source_tag),
daemon=True daemon=True
) )
thread.start() thread.start()
self.output_threads['adsb'] = thread self.output_threads[thread_name] = thread
return { return {
'status': 'started', 'status': 'started',
@@ -1431,7 +1454,7 @@ class ModeManager:
'gps_enabled': gps_manager.is_running 'gps_enabled': gps_manager.is_running
} }
def _adsb_sbs_reader(self, host: str, port: int): def _adsb_sbs_reader(self, host: str, port: int, source_tag: str = 'adsb'):
"""Read and parse SBS data from dump1090.""" """Read and parse SBS data from dump1090."""
mode = 'adsb' mode = 'adsb'
stop_event = self.stop_events.get(mode) stop_event = self.stop_events.get(mode)
@@ -1443,7 +1466,7 @@ class ModeManager:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0) sock.settimeout(5.0)
sock.connect((host, port)) sock.connect((host, port))
logger.info(f"Connected to SBS at {host}:{port}") logger.info(f"Connected to SBS at {host}:{port} ({source_tag})")
retry_count = 0 retry_count = 0
buffer = "" buffer = ""
@@ -1458,7 +1481,7 @@ class ModeManager:
while '\n' in buffer: while '\n' in buffer:
line, buffer = buffer.split('\n', 1) line, buffer = buffer.split('\n', 1)
self._parse_sbs_line(line.strip()) self._parse_sbs_line(line.strip(), source_tag)
except socket.timeout: except socket.timeout:
continue continue
@@ -1475,7 +1498,7 @@ class ModeManager:
logger.info("ADS-B SBS reader stopped") logger.info("ADS-B SBS reader stopped")
def _parse_sbs_line(self, line: str): def _parse_sbs_line(self, line: str, source_tag: str = 'adsb'):
"""Parse SBS format line and update aircraft dict.""" """Parse SBS format line and update aircraft dict."""
if not line: if not line:
return return
@@ -1509,6 +1532,8 @@ class ModeManager:
if parts[14] and parts[15]: if parts[14] and parts[15]:
aircraft['lat'] = float(parts[14]) aircraft['lat'] = float(parts[14])
aircraft['lon'] = float(parts[15]) aircraft['lon'] = float(parts[15])
if source_tag:
aircraft['position_source'] = source_tag
elif msg_type == '4' and len(parts) > 16: elif msg_type == '4' and len(parts) > 16:
if parts[12]: if parts[12]:
+83 -25
View File
@@ -35,6 +35,9 @@ from config import (
ADSB_DB_USER, ADSB_DB_USER,
ADSB_AUTO_START, ADSB_AUTO_START,
ADSB_HISTORY_ENABLED, ADSB_HISTORY_ENABLED,
ADSB_MLAT_ENABLED,
ADSB_MLAT_SBS_HOST,
ADSB_MLAT_SBS_PORT,
SHARED_OBSERVER_LOCATION_ENABLED, SHARED_OBSERVER_LOCATION_ENABLED,
) )
from utils.logging import adsb_logger as logger from utils.logging import adsb_logger as logger
@@ -71,7 +74,10 @@ adsb_last_message_time = None
adsb_bytes_received = 0 adsb_bytes_received = 0
adsb_lines_received = 0 adsb_lines_received = 0
adsb_active_device = None # Track which device index is being used adsb_active_device = None # Track which device index is being used
adsb_active_sdr_type = None
_sbs_error_logged = False # Suppress repeated connection error logs _sbs_error_logged = False # Suppress repeated connection error logs
adsb_connected_sources: set[str] = set()
_adsb_connection_lock = threading.Lock()
# Track ICAOs already looked up in aircraft database (avoid repeated lookups) # Track ICAOs already looked up in aircraft database (avoid repeated lookups)
_looked_up_icaos: set[str] = set() _looked_up_icaos: set[str] = set()
@@ -318,7 +324,29 @@ def check_dump1090_service():
return None return None
def parse_sbs_stream(service_addr): def _reset_adsb_state() -> None:
global adsb_connected, adsb_messages_received, adsb_last_message_time, adsb_bytes_received, adsb_lines_received, _sbs_error_logged
adsb_connected = False
adsb_messages_received = 0
adsb_last_message_time = None
adsb_bytes_received = 0
adsb_lines_received = 0
_sbs_error_logged = False
with _adsb_connection_lock:
adsb_connected_sources.clear()
def _set_adsb_connected(source_key: str, connected: bool) -> None:
global adsb_connected
with _adsb_connection_lock:
if connected:
adsb_connected_sources.add(source_key)
else:
adsb_connected_sources.discard(source_key)
adsb_connected = bool(adsb_connected_sources)
def parse_sbs_stream(service_addr: str, source_tag: str | None = None):
"""Parse SBS format data from dump1090 SBS port.""" """Parse SBS format data from dump1090 SBS port."""
global adsb_using_service, adsb_connected, adsb_messages_received, adsb_last_message_time, adsb_bytes_received, adsb_lines_received, _sbs_error_logged global adsb_using_service, adsb_connected, adsb_messages_received, adsb_last_message_time, adsb_bytes_received, adsb_lines_received, _sbs_error_logged
@@ -327,26 +355,23 @@ def parse_sbs_stream(service_addr):
host, port = service_addr.split(':') host, port = service_addr.split(':')
port = int(port) port = int(port)
source_label = source_tag or 'adsb'
logger.info(f"SBS stream parser started, connecting to {host}:{port}") logger.info(f"SBS stream parser started ({source_label}), connecting to {host}:{port}")
adsb_connected = False
adsb_messages_received = 0
_sbs_error_logged = False
while adsb_using_service: while adsb_using_service:
try: try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(SBS_SOCKET_TIMEOUT) sock.settimeout(SBS_SOCKET_TIMEOUT)
sock.connect((host, port)) sock.connect((host, port))
adsb_connected = True _set_adsb_connected(service_addr, True)
_sbs_error_logged = False # Reset so we log next error _sbs_error_logged = False # Reset so we log next error
logger.info("Connected to SBS stream") logger.info(f"Connected to SBS stream ({source_label})")
buffer = "" buffer = ""
last_update = time.time() last_update = time.time()
pending_updates = set() pending_updates = set()
adsb_bytes_received = 0 local_lines_received = 0
adsb_lines_received = 0
while adsb_using_service: while adsb_using_service:
try: try:
@@ -364,13 +389,14 @@ def parse_sbs_stream(service_addr):
continue continue
adsb_lines_received += 1 adsb_lines_received += 1
local_lines_received += 1
# Log first few lines for debugging # Log first few lines for debugging
if adsb_lines_received <= 3: if local_lines_received <= 3:
logger.info(f"SBS line {adsb_lines_received}: {line[:100]}") logger.info(f"SBS line ({source_label}) {local_lines_received}: {line[:100]}")
parts = line.split(',') parts = line.split(',')
if len(parts) < 11 or parts[0] != 'MSG': if len(parts) < 11 or parts[0] != 'MSG':
if adsb_lines_received <= 5: if local_lines_received <= 5:
logger.debug(f"Skipping non-MSG line: {line[:50]}") logger.debug(f"Skipping non-MSG line: {line[:50]}")
continue continue
@@ -421,6 +447,8 @@ def parse_sbs_stream(service_addr):
try: try:
aircraft['lat'] = float(parts[14]) aircraft['lat'] = float(parts[14])
aircraft['lon'] = float(parts[15]) aircraft['lon'] = float(parts[15])
if source_label:
aircraft['position_source'] = source_label
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
@@ -494,18 +522,26 @@ def parse_sbs_stream(service_addr):
continue continue
sock.close() sock.close()
adsb_connected = False _set_adsb_connected(service_addr, False)
except OSError as e: except OSError as e:
adsb_connected = False _set_adsb_connected(service_addr, False)
if not _sbs_error_logged: if not _sbs_error_logged:
logger.warning(f"SBS connection error: {e}, reconnecting...") logger.warning(f"SBS connection error: {e}, reconnecting...")
_sbs_error_logged = True _sbs_error_logged = True
time.sleep(SBS_RECONNECT_DELAY) time.sleep(SBS_RECONNECT_DELAY)
adsb_connected = False _set_adsb_connected(service_addr, False)
logger.info("SBS stream parser stopped") logger.info("SBS stream parser stopped")
def _start_mlat_stream(host: str, port: int) -> str:
mlat_addr = f"{host}:{port}"
logger.info(f"Connecting to MLAT SBS at {mlat_addr}")
thread = threading.Thread(target=parse_sbs_stream, args=(mlat_addr, 'mlat'), daemon=True)
thread.start()
return mlat_addr
@adsb_bp.route('/tools') @adsb_bp.route('/tools')
def check_adsb_tools(): def check_adsb_tools():
"""Check for ADS-B decoding tools and hardware.""" """Check for ADS-B decoding tools and hardware."""
@@ -580,7 +616,7 @@ def adsb_session():
@adsb_bp.route('/start', methods=['POST']) @adsb_bp.route('/start', methods=['POST'])
def start_adsb(): def start_adsb():
"""Start ADS-B tracking.""" """Start ADS-B tracking."""
global adsb_using_service, adsb_active_device global adsb_using_service, adsb_active_device, adsb_active_sdr_type
with app_module.adsb_lock: with app_module.adsb_lock:
if adsb_using_service: if adsb_using_service:
@@ -601,10 +637,22 @@ def start_adsb():
device = validate_device_index(data.get('device', '0')) device = validate_device_index(data.get('device', '0'))
except ValueError as e: except ValueError as e:
return jsonify({'status': 'error', 'message': str(e)}), 400 return jsonify({'status': 'error', 'message': str(e)}), 400
_reset_adsb_state()
# Check for remote SBS connection (e.g., remote dump1090) # Check for remote SBS connection (e.g., remote dump1090)
remote_sbs_host = data.get('remote_sbs_host') remote_sbs_host = data.get('remote_sbs_host')
remote_sbs_port = data.get('remote_sbs_port', 30003) remote_sbs_port = data.get('remote_sbs_port', 30003)
mlat_sbs_host = (data.get('mlat_sbs_host') or '').strip()
mlat_sbs_port = data.get('mlat_sbs_port', ADSB_MLAT_SBS_PORT)
if not mlat_sbs_host and ADSB_MLAT_ENABLED and ADSB_MLAT_SBS_HOST:
mlat_sbs_host = ADSB_MLAT_SBS_HOST
mlat_sbs_port = ADSB_MLAT_SBS_PORT
if mlat_sbs_host:
try:
mlat_sbs_host = validate_rtl_tcp_host(mlat_sbs_host)
mlat_sbs_port = validate_rtl_tcp_port(mlat_sbs_port)
except ValueError as e:
return jsonify({'status': 'error', 'message': str(e)}), 400
if remote_sbs_host: if remote_sbs_host:
# Validate and connect to remote dump1090 SBS output # Validate and connect to remote dump1090 SBS output
@@ -617,8 +665,10 @@ def start_adsb():
remote_addr = f"{remote_sbs_host}:{remote_sbs_port}" remote_addr = f"{remote_sbs_host}:{remote_sbs_port}"
logger.info(f"Connecting to remote dump1090 SBS at {remote_addr}") logger.info(f"Connecting to remote dump1090 SBS at {remote_addr}")
adsb_using_service = True adsb_using_service = True
thread = threading.Thread(target=parse_sbs_stream, args=(remote_addr,), daemon=True) thread = threading.Thread(target=parse_sbs_stream, args=(remote_addr, 'adsb'), daemon=True)
thread.start() thread.start()
if mlat_sbs_host:
_start_mlat_stream(mlat_sbs_host, mlat_sbs_port)
session = _record_session_start( session = _record_session_start(
device_index=device, device_index=device,
sdr_type='remote', sdr_type='remote',
@@ -638,8 +688,10 @@ def start_adsb():
if existing_service: if existing_service:
logger.info(f"Found existing dump1090 service at {existing_service}") logger.info(f"Found existing dump1090 service at {existing_service}")
adsb_using_service = True adsb_using_service = True
thread = threading.Thread(target=parse_sbs_stream, args=(existing_service,), daemon=True) thread = threading.Thread(target=parse_sbs_stream, args=(existing_service, 'adsb'), daemon=True)
thread.start() thread.start()
if mlat_sbs_host:
_start_mlat_stream(mlat_sbs_host, mlat_sbs_port)
session = _record_session_start( session = _record_session_start(
device_index=device, device_index=device,
sdr_type='external', sdr_type='external',
@@ -689,7 +741,7 @@ def start_adsb():
# Check if device is available before starting local dump1090 # Check if device is available before starting local dump1090
device_int = int(device) device_int = int(device)
error = app_module.claim_sdr_device(device_int, 'adsb') error = app_module.claim_sdr_device(device_int, 'adsb', sdr_type.value)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -726,7 +778,7 @@ def start_adsb():
if app_module.adsb_process.poll() is not None: if app_module.adsb_process.poll() is not None:
# Process exited - release device and get error message # Process exited - release device and get error message
app_module.release_sdr_device(device_int) app_module.release_sdr_device(device_int, sdr_type.value)
stderr_output = '' stderr_output = ''
if app_module.adsb_process.stderr: if app_module.adsb_process.stderr:
try: try:
@@ -772,9 +824,12 @@ def start_adsb():
}) })
adsb_using_service = True adsb_using_service = True
adsb_active_device = device # Track which device is being used adsb_active_device = device # Track which device index is being used
thread = threading.Thread(target=parse_sbs_stream, args=(f'localhost:{ADSB_SBS_PORT}',), daemon=True) adsb_active_sdr_type = sdr_type.value
thread = threading.Thread(target=parse_sbs_stream, args=(f'localhost:{ADSB_SBS_PORT}', 'adsb'), daemon=True)
thread.start() thread.start()
if mlat_sbs_host:
_start_mlat_stream(mlat_sbs_host, mlat_sbs_port)
session = _record_session_start( session = _record_session_start(
device_index=device, device_index=device,
@@ -792,14 +847,14 @@ def start_adsb():
}) })
except Exception as e: except Exception as e:
# Release device on failure # Release device on failure
app_module.release_sdr_device(device_int) app_module.release_sdr_device(device_int, sdr_type.value)
return jsonify({'status': 'error', 'message': str(e)}) return jsonify({'status': 'error', 'message': str(e)})
@adsb_bp.route('/stop', methods=['POST']) @adsb_bp.route('/stop', methods=['POST'])
def stop_adsb(): def stop_adsb():
"""Stop ADS-B tracking.""" """Stop ADS-B tracking."""
global adsb_using_service, adsb_active_device global adsb_using_service, adsb_active_device, adsb_active_sdr_type
data = request.json or {} data = request.json or {}
stop_source = data.get('source') stop_source = data.get('source')
stopped_by = request.remote_addr stopped_by = request.remote_addr
@@ -823,10 +878,12 @@ def stop_adsb():
# Release device from registry # Release device from registry
if adsb_active_device is not None: if adsb_active_device is not None:
app_module.release_sdr_device(adsb_active_device) app_module.release_sdr_device(adsb_active_device, adsb_active_sdr_type or 'rtlsdr')
adsb_using_service = False adsb_using_service = False
adsb_active_device = None adsb_active_device = None
adsb_active_sdr_type = None
_reset_adsb_state()
app_module.adsb_aircraft.clear() app_module.adsb_aircraft.clear()
_looked_up_icaos.clear() _looked_up_icaos.clear()
@@ -868,6 +925,7 @@ def adsb_dashboard():
'adsb_dashboard.html', 'adsb_dashboard.html',
shared_observer_location=SHARED_OBSERVER_LOCATION_ENABLED, shared_observer_location=SHARED_OBSERVER_LOCATION_ENABLED,
adsb_auto_start=ADSB_AUTO_START, adsb_auto_start=ADSB_AUTO_START,
adsb_mlat_enabled=ADSB_MLAT_ENABLED,
) )
+8 -5
View File
@@ -44,6 +44,7 @@ ais_connected = False
ais_messages_received = 0 ais_messages_received = 0
ais_last_message_time = None ais_last_message_time = None
ais_active_device = None ais_active_device = None
ais_active_sdr_type = None
_ais_error_logged = True _ais_error_logged = True
# Common installation paths for AIS-catcher # Common installation paths for AIS-catcher
@@ -326,7 +327,7 @@ def ais_status():
@ais_bp.route('/start', methods=['POST']) @ais_bp.route('/start', methods=['POST'])
def start_ais(): def start_ais():
"""Start AIS tracking.""" """Start AIS tracking."""
global ais_running, ais_active_device global ais_running, ais_active_device, ais_active_sdr_type
with app_module.ais_lock: with app_module.ais_lock:
if ais_running: if ais_running:
@@ -373,7 +374,7 @@ def start_ais():
# Check if device is available # Check if device is available
device_int = int(device) device_int = int(device)
error = app_module.claim_sdr_device(device_int, 'ais') error = app_module.claim_sdr_device(device_int, 'ais', sdr_type.value)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -412,7 +413,7 @@ def start_ais():
if app_module.ais_process.poll() is not None: if app_module.ais_process.poll() is not None:
# Release device on failure # Release device on failure
app_module.release_sdr_device(device_int) app_module.release_sdr_device(device_int, sdr_type.value)
stderr_output = '' stderr_output = ''
if app_module.ais_process.stderr: if app_module.ais_process.stderr:
try: try:
@@ -426,6 +427,7 @@ def start_ais():
ais_running = True ais_running = True
ais_active_device = device ais_active_device = device
ais_active_sdr_type = sdr_type.value
# Start TCP parser thread # Start TCP parser thread
thread = threading.Thread(target=parse_ais_stream, args=(tcp_port,), daemon=True) thread = threading.Thread(target=parse_ais_stream, args=(tcp_port,), daemon=True)
@@ -439,7 +441,7 @@ def start_ais():
}) })
except Exception as e: except Exception as e:
# Release device on failure # Release device on failure
app_module.release_sdr_device(device_int) app_module.release_sdr_device(device_int, sdr_type.value)
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
@@ -466,10 +468,11 @@ def stop_ais():
# Release device from registry # Release device from registry
if ais_active_device is not None: if ais_active_device is not None:
app_module.release_sdr_device(ais_active_device) app_module.release_sdr_device(ais_active_device, ais_active_sdr_type or 'rtlsdr')
ais_running = False ais_running = False
ais_active_device = None ais_active_device = None
ais_active_sdr_type = None
app_module.ais_vessels.clear() app_module.ais_vessels.clear()
return jsonify({'status': 'stopped'}) return jsonify({'status': 'stopped'})
+34 -18
View File
@@ -51,7 +51,9 @@ scanner_lock = threading.Lock()
scanner_paused = False scanner_paused = False
scanner_current_freq = 0.0 scanner_current_freq = 0.0
scanner_active_device: Optional[int] = None scanner_active_device: Optional[int] = None
scanner_active_sdr_type: Optional[str] = None
listening_active_device: Optional[int] = None listening_active_device: Optional[int] = None
listening_active_sdr_type: Optional[str] = None
scanner_power_process: Optional[subprocess.Popen] = None scanner_power_process: Optional[subprocess.Popen] = None
scanner_config = { scanner_config = {
'start_freq': 88.0, 'start_freq': 88.0,
@@ -937,6 +939,7 @@ def check_tools() -> Response:
def start_scanner() -> Response: def start_scanner() -> Response:
"""Start the frequency scanner.""" """Start the frequency scanner."""
global scanner_thread, scanner_running, scanner_config, scanner_active_device, listening_active_device global scanner_thread, scanner_running, scanner_config, scanner_active_device, listening_active_device
global scanner_active_sdr_type, listening_active_sdr_type
with scanner_lock: with scanner_lock:
if scanner_running: if scanner_running:
@@ -1003,10 +1006,11 @@ def start_scanner() -> Response:
}), 503 }), 503
# Release listening device if active # Release listening device if active
if listening_active_device is not None: if listening_active_device is not None:
app_module.release_sdr_device(listening_active_device) app_module.release_sdr_device(listening_active_device, listening_active_sdr_type or 'rtlsdr')
listening_active_device = None listening_active_device = None
listening_active_sdr_type = None
# Claim device for scanner # Claim device for scanner
error = app_module.claim_sdr_device(scanner_config['device'], 'scanner') error = app_module.claim_sdr_device(scanner_config['device'], 'scanner', sdr_type)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -1014,6 +1018,7 @@ def start_scanner() -> Response:
'message': error 'message': error
}), 409 }), 409
scanner_active_device = scanner_config['device'] scanner_active_device = scanner_config['device']
scanner_active_sdr_type = sdr_type
scanner_running = True scanner_running = True
scanner_thread = threading.Thread(target=scanner_loop_power, daemon=True) scanner_thread = threading.Thread(target=scanner_loop_power, daemon=True)
scanner_thread.start() scanner_thread.start()
@@ -1031,9 +1036,10 @@ def start_scanner() -> Response:
'message': f'rx_fm not found. Install SoapySDR utilities for {sdr_type}.' 'message': f'rx_fm not found. Install SoapySDR utilities for {sdr_type}.'
}), 503 }), 503
if listening_active_device is not None: if listening_active_device is not None:
app_module.release_sdr_device(listening_active_device) app_module.release_sdr_device(listening_active_device, listening_active_sdr_type or 'rtlsdr')
listening_active_device = None listening_active_device = None
error = app_module.claim_sdr_device(scanner_config['device'], 'scanner') listening_active_sdr_type = None
error = app_module.claim_sdr_device(scanner_config['device'], 'scanner', sdr_type)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -1041,6 +1047,7 @@ def start_scanner() -> Response:
'message': error 'message': error
}), 409 }), 409
scanner_active_device = scanner_config['device'] scanner_active_device = scanner_config['device']
scanner_active_sdr_type = sdr_type
scanner_running = True scanner_running = True
scanner_thread = threading.Thread(target=scanner_loop, daemon=True) scanner_thread = threading.Thread(target=scanner_loop, daemon=True)
@@ -1055,7 +1062,7 @@ def start_scanner() -> Response:
@listening_post_bp.route('/scanner/stop', methods=['POST']) @listening_post_bp.route('/scanner/stop', methods=['POST'])
def stop_scanner() -> Response: def stop_scanner() -> Response:
"""Stop the frequency scanner.""" """Stop the frequency scanner."""
global scanner_running, scanner_active_device, scanner_power_process global scanner_running, scanner_active_device, scanner_power_process, scanner_active_sdr_type
scanner_running = False scanner_running = False
_stop_audio_stream() _stop_audio_stream()
@@ -1070,8 +1077,9 @@ def stop_scanner() -> Response:
pass pass
scanner_power_process = None scanner_power_process = None
if scanner_active_device is not None: if scanner_active_device is not None:
app_module.release_sdr_device(scanner_active_device) app_module.release_sdr_device(scanner_active_device, scanner_active_sdr_type or 'rtlsdr')
scanner_active_device = None scanner_active_device = None
scanner_active_sdr_type = None
return jsonify({'status': 'stopped'}) return jsonify({'status': 'stopped'})
@@ -1243,13 +1251,15 @@ def get_presets() -> Response:
def start_audio() -> Response: def start_audio() -> Response:
"""Start audio at specific frequency (manual mode).""" """Start audio at specific frequency (manual mode)."""
global scanner_running, scanner_active_device, listening_active_device, scanner_power_process, scanner_thread global scanner_running, scanner_active_device, listening_active_device, scanner_power_process, scanner_thread
global scanner_active_sdr_type, listening_active_sdr_type, waterfall_active_sdr_type
# Stop scanner if running # Stop scanner if running
if scanner_running: if scanner_running:
scanner_running = False scanner_running = False
if scanner_active_device is not None: if scanner_active_device is not None:
app_module.release_sdr_device(scanner_active_device) app_module.release_sdr_device(scanner_active_device, scanner_active_sdr_type or 'rtlsdr')
scanner_active_device = None scanner_active_device = None
scanner_active_sdr_type = None
if scanner_thread and scanner_thread.is_alive(): if scanner_thread and scanner_thread.is_alive():
try: try:
scanner_thread.join(timeout=2.0) scanner_thread.join(timeout=2.0)
@@ -1306,7 +1316,7 @@ def start_audio() -> Response:
scanner_config['sdr_type'] = sdr_type scanner_config['sdr_type'] = sdr_type
# Stop waterfall if it's using the same SDR (SSE path) # Stop waterfall if it's using the same SDR (SSE path)
if waterfall_running and waterfall_active_device == device: if waterfall_running and waterfall_active_device == device and waterfall_active_sdr_type == sdr_type:
_stop_waterfall_internal() _stop_waterfall_internal()
time.sleep(0.2) time.sleep(0.2)
@@ -1316,8 +1326,9 @@ def start_audio() -> Response:
# to give the USB device time to be fully released. # to give the USB device time to be fully released.
if listening_active_device is None or listening_active_device != device: if listening_active_device is None or listening_active_device != device:
if listening_active_device is not None: if listening_active_device is not None:
app_module.release_sdr_device(listening_active_device) app_module.release_sdr_device(listening_active_device, listening_active_sdr_type or 'rtlsdr')
listening_active_device = None listening_active_device = None
listening_active_sdr_type = None
error = None error = None
max_claim_attempts = 6 max_claim_attempts = 6
@@ -1326,10 +1337,10 @@ def start_audio() -> Response:
# attempt — the WebSocket handler may not have finished # attempt — the WebSocket handler may not have finished
# cleanup yet. # cleanup yet.
device_status = app_module.get_sdr_device_status() device_status = app_module.get_sdr_device_status()
if device_status.get(device) == 'waterfall': if device_status.get(device, {}).get(sdr_type) == 'waterfall':
app_module.release_sdr_device(device) app_module.release_sdr_device(device, sdr_type)
error = app_module.claim_sdr_device(device, 'listening') error = app_module.claim_sdr_device(device, 'listening', sdr_type)
if not error: if not error:
break break
if attempt < max_claim_attempts - 1: if attempt < max_claim_attempts - 1:
@@ -1346,6 +1357,7 @@ def start_audio() -> Response:
'message': error 'message': error
}), 409 }), 409
listening_active_device = device listening_active_device = device
listening_active_sdr_type = sdr_type
_start_audio_stream(frequency, modulation) _start_audio_stream(frequency, modulation)
@@ -1365,11 +1377,12 @@ def start_audio() -> Response:
@listening_post_bp.route('/audio/stop', methods=['POST']) @listening_post_bp.route('/audio/stop', methods=['POST'])
def stop_audio() -> Response: def stop_audio() -> Response:
"""Stop audio.""" """Stop audio."""
global listening_active_device global listening_active_device, listening_active_sdr_type
_stop_audio_stream() _stop_audio_stream()
if listening_active_device is not None: if listening_active_device is not None:
app_module.release_sdr_device(listening_active_device) app_module.release_sdr_device(listening_active_device, listening_active_sdr_type or 'rtlsdr')
listening_active_device = None listening_active_device = None
listening_active_sdr_type = None
return jsonify({'status': 'stopped'}) return jsonify({'status': 'stopped'})
@@ -1549,6 +1562,7 @@ waterfall_running = False
waterfall_lock = threading.Lock() waterfall_lock = threading.Lock()
waterfall_queue: queue.Queue = queue.Queue(maxsize=200) waterfall_queue: queue.Queue = queue.Queue(maxsize=200)
waterfall_active_device: Optional[int] = None waterfall_active_device: Optional[int] = None
waterfall_active_sdr_type: Optional[str] = None
waterfall_config = { waterfall_config = {
'start_freq': 88.0, 'start_freq': 88.0,
'end_freq': 108.0, 'end_freq': 108.0,
@@ -1725,7 +1739,7 @@ def _waterfall_loop():
def _stop_waterfall_internal() -> None: def _stop_waterfall_internal() -> None:
"""Stop the waterfall display and release resources.""" """Stop the waterfall display and release resources."""
global waterfall_running, waterfall_process, waterfall_active_device global waterfall_running, waterfall_process, waterfall_active_device, waterfall_active_sdr_type
waterfall_running = False waterfall_running = False
if waterfall_process and waterfall_process.poll() is None: if waterfall_process and waterfall_process.poll() is None:
@@ -1740,14 +1754,15 @@ def _stop_waterfall_internal() -> None:
waterfall_process = None waterfall_process = None
if waterfall_active_device is not None: if waterfall_active_device is not None:
app_module.release_sdr_device(waterfall_active_device) app_module.release_sdr_device(waterfall_active_device, waterfall_active_sdr_type or 'rtlsdr')
waterfall_active_device = None waterfall_active_device = None
waterfall_active_sdr_type = None
@listening_post_bp.route('/waterfall/start', methods=['POST']) @listening_post_bp.route('/waterfall/start', methods=['POST'])
def start_waterfall() -> Response: def start_waterfall() -> Response:
"""Start the waterfall/spectrogram display.""" """Start the waterfall/spectrogram display."""
global waterfall_thread, waterfall_running, waterfall_config, waterfall_active_device global waterfall_thread, waterfall_running, waterfall_config, waterfall_active_device, waterfall_active_sdr_type
with waterfall_lock: with waterfall_lock:
if waterfall_running: if waterfall_running:
@@ -1788,11 +1803,12 @@ def start_waterfall() -> Response:
pass pass
# Claim SDR device # Claim SDR device
error = app_module.claim_sdr_device(waterfall_config['device'], 'waterfall') error = app_module.claim_sdr_device(waterfall_config['device'], 'waterfall', 'rtlsdr')
if error: if error:
return jsonify({'status': 'error', 'error_type': 'DEVICE_BUSY', 'message': error}), 409 return jsonify({'status': 'error', 'error_type': 'DEVICE_BUSY', 'message': error}), 409
waterfall_active_device = waterfall_config['device'] waterfall_active_device = waterfall_config['device']
waterfall_active_sdr_type = 'rtlsdr'
waterfall_running = True waterfall_running = True
waterfall_thread = threading.Thread(target=_waterfall_loop, daemon=True) waterfall_thread = threading.Thread(target=_waterfall_loop, daemon=True)
waterfall_thread.start() waterfall_thread.start()
+23 -16
View File
@@ -34,6 +34,7 @@ pager_bp = Blueprint('pager', __name__)
# Track which device is being used # Track which device is being used
pager_active_device: int | None = None pager_active_device: int | None = None
pager_active_sdr_type: str | None = None
def parse_multimon_output(line: str) -> dict[str, str] | None: def parse_multimon_output(line: str) -> dict[str, str] | None:
@@ -205,7 +206,7 @@ def stream_decoder(master_fd: int, process: subprocess.Popen[bytes]) -> None:
except Exception as e: except Exception as e:
app_module.output_queue.put({'type': 'error', 'text': str(e)}) app_module.output_queue.put({'type': 'error', 'text': str(e)})
finally: finally:
global pager_active_device global pager_active_device, pager_active_sdr_type
try: try:
os.close(master_fd) os.close(master_fd)
except OSError: except OSError:
@@ -234,13 +235,14 @@ def stream_decoder(master_fd: int, process: subprocess.Popen[bytes]) -> None:
app_module.current_process = None app_module.current_process = None
# Release SDR device # Release SDR device
if pager_active_device is not None: if pager_active_device is not None:
app_module.release_sdr_device(pager_active_device) app_module.release_sdr_device(pager_active_device, pager_active_sdr_type or 'rtlsdr')
pager_active_device = None pager_active_device = None
pager_active_sdr_type = 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 global pager_active_device, pager_active_sdr_type
with app_module.process_lock: with app_module.process_lock:
if app_module.current_process: if app_module.current_process:
@@ -265,6 +267,13 @@ 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
# Get SDR type and build command via abstraction layer
sdr_type_str = data.get('sdr_type', 'rtlsdr')
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
# Check for rtl_tcp (remote SDR) connection # Check for rtl_tcp (remote SDR) connection
rtl_tcp_host = data.get('rtl_tcp_host') rtl_tcp_host = data.get('rtl_tcp_host')
rtl_tcp_port = data.get('rtl_tcp_port', 1234) rtl_tcp_port = data.get('rtl_tcp_port', 1234)
@@ -272,7 +281,7 @@ def start_decoding() -> Response:
# Claim local device if not using remote rtl_tcp # Claim local device if not using remote rtl_tcp
if not rtl_tcp_host: if not rtl_tcp_host:
device_int = int(device) device_int = int(device)
error = app_module.claim_sdr_device(device_int, 'pager') error = app_module.claim_sdr_device(device_int, 'pager', sdr_type.value)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -280,14 +289,16 @@ def start_decoding() -> Response:
'message': error 'message': error
}), 409 }), 409
pager_active_device = device_int pager_active_device = device_int
pager_active_sdr_type = sdr_type.value
# 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: if pager_active_device is not None:
app_module.release_sdr_device(pager_active_device) app_module.release_sdr_device(pager_active_device, pager_active_sdr_type or 'rtlsdr')
pager_active_device = None pager_active_device = None
pager_active_sdr_type = 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:
@@ -312,13 +323,6 @@ def start_decoding() -> Response:
elif proto == 'FLEX': elif proto == 'FLEX':
decoders.extend(['-a', 'FLEX']) decoders.extend(['-a', 'FLEX'])
# Get SDR type and build command via abstraction layer
sdr_type_str = data.get('sdr_type', 'rtlsdr')
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
if rtl_tcp_host: if rtl_tcp_host:
# Validate and create network device # Validate and create network device
try: try:
@@ -428,8 +432,9 @@ def start_decoding() -> Response:
pass pass
# Release device on failure # Release device on failure
if pager_active_device is not None: if pager_active_device is not None:
app_module.release_sdr_device(pager_active_device) app_module.release_sdr_device(pager_active_device, pager_active_sdr_type or 'rtlsdr')
pager_active_device = None pager_active_device = None
pager_active_sdr_type = 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:
# Kill orphaned rtl_fm process if it was started # Kill orphaned rtl_fm process if it was started
@@ -443,14 +448,15 @@ def start_decoding() -> Response:
pass pass
# Release device on failure # Release device on failure
if pager_active_device is not None: if pager_active_device is not None:
app_module.release_sdr_device(pager_active_device) app_module.release_sdr_device(pager_active_device, pager_active_sdr_type or 'rtlsdr')
pager_active_device = None pager_active_device = None
pager_active_sdr_type = 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 global pager_active_device, pager_active_sdr_type
with app_module.process_lock: with app_module.process_lock:
if app_module.current_process: if app_module.current_process:
@@ -487,8 +493,9 @@ def stop_decoding() -> Response:
# Release device from registry # Release device from registry
if pager_active_device is not None: if pager_active_device is not None:
app_module.release_sdr_device(pager_active_device) app_module.release_sdr_device(pager_active_device, pager_active_sdr_type or 'rtlsdr')
pager_active_device = None pager_active_device = None
pager_active_sdr_type = None
return jsonify({'status': 'stopped'}) return jsonify({'status': 'stopped'})
+21 -15
View File
@@ -27,6 +27,7 @@ sensor_bp = Blueprint('sensor', __name__)
# Track which device is being used # Track which device is being used
sensor_active_device: int | None = None sensor_active_device: int | None = None
sensor_active_sdr_type: str | None = None
def stream_sensor_output(process: subprocess.Popen[bytes]) -> None: def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
@@ -75,7 +76,7 @@ def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
except Exception as e: except Exception as e:
app_module.sensor_queue.put({'type': 'error', 'text': str(e)}) app_module.sensor_queue.put({'type': 'error', 'text': str(e)})
finally: finally:
global sensor_active_device global sensor_active_device, sensor_active_sdr_type
# Ensure process is terminated # Ensure process is terminated
try: try:
process.terminate() process.terminate()
@@ -91,8 +92,9 @@ def stream_sensor_output(process: subprocess.Popen[bytes]) -> None:
app_module.sensor_process = None app_module.sensor_process = None
# Release SDR device # Release SDR device
if sensor_active_device is not None: if sensor_active_device is not None:
app_module.release_sdr_device(sensor_active_device) app_module.release_sdr_device(sensor_active_device, sensor_active_sdr_type or 'rtlsdr')
sensor_active_device = None sensor_active_device = None
sensor_active_sdr_type = None
@sensor_bp.route('/sensor/status') @sensor_bp.route('/sensor/status')
@@ -105,7 +107,7 @@ def sensor_status() -> Response:
@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 global sensor_active_device, sensor_active_sdr_type
with app_module.sensor_lock: with app_module.sensor_lock:
if app_module.sensor_process: if app_module.sensor_process:
@@ -122,6 +124,13 @@ 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
# Get SDR type and build command via abstraction layer
sdr_type_str = data.get('sdr_type', 'rtlsdr')
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
# Check for rtl_tcp (remote SDR) connection # Check for rtl_tcp (remote SDR) connection
rtl_tcp_host = data.get('rtl_tcp_host') rtl_tcp_host = data.get('rtl_tcp_host')
rtl_tcp_port = data.get('rtl_tcp_port', 1234) rtl_tcp_port = data.get('rtl_tcp_port', 1234)
@@ -129,7 +138,7 @@ def start_sensor() -> Response:
# Claim local device if not using remote rtl_tcp # Claim local device if not using remote rtl_tcp
if not rtl_tcp_host: if not rtl_tcp_host:
device_int = int(device) device_int = int(device)
error = app_module.claim_sdr_device(device_int, 'sensor') error = app_module.claim_sdr_device(device_int, 'sensor', sdr_type.value)
if error: if error:
return jsonify({ return jsonify({
'status': 'error', 'status': 'error',
@@ -137,6 +146,7 @@ def start_sensor() -> Response:
'message': error 'message': error
}), 409 }), 409
sensor_active_device = device_int sensor_active_device = device_int
sensor_active_sdr_type = sdr_type.value
# Clear queue # Clear queue
while not app_module.sensor_queue.empty(): while not app_module.sensor_queue.empty():
@@ -145,13 +155,6 @@ def start_sensor() -> Response:
except queue.Empty: except queue.Empty:
break break
# Get SDR type and build command via abstraction layer
sdr_type_str = data.get('sdr_type', 'rtlsdr')
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
sdr_type = SDRType.RTL_SDR
if rtl_tcp_host: if rtl_tcp_host:
# Validate and create network device # Validate and create network device
try: try:
@@ -217,20 +220,22 @@ def start_sensor() -> Response:
except FileNotFoundError: except FileNotFoundError:
# Release device on failure # Release device on failure
if sensor_active_device is not None: if sensor_active_device is not None:
app_module.release_sdr_device(sensor_active_device) app_module.release_sdr_device(sensor_active_device, sensor_active_sdr_type or 'rtlsdr')
sensor_active_device = None sensor_active_device = None
sensor_active_sdr_type = 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 # Release device on failure
if sensor_active_device is not None: if sensor_active_device is not None:
app_module.release_sdr_device(sensor_active_device) app_module.release_sdr_device(sensor_active_device, sensor_active_sdr_type or 'rtlsdr')
sensor_active_device = None sensor_active_device = None
sensor_active_sdr_type = 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 global sensor_active_device, sensor_active_sdr_type
with app_module.sensor_lock: with app_module.sensor_lock:
if app_module.sensor_process: if app_module.sensor_process:
@@ -243,8 +248,9 @@ def stop_sensor() -> Response:
# Release device from registry # Release device from registry
if sensor_active_device is not None: if sensor_active_device is not None:
app_module.release_sdr_device(sensor_active_device) app_module.release_sdr_device(sensor_active_device, sensor_active_sdr_type or 'rtlsdr')
sensor_active_device = None sensor_active_device = None
sensor_active_sdr_type = None
return jsonify({'status': 'stopped'}) return jsonify({'status': 'stopped'})
+13 -6
View File
@@ -87,6 +87,7 @@ def init_waterfall_websocket(app: Flask):
reader_thread = None reader_thread = None
stop_event = threading.Event() stop_event = threading.Event()
claimed_device = None claimed_device = None
claimed_sdr_type = None
# Queue for outgoing messages — only the main loop touches ws.send() # Queue for outgoing messages — only the main loop touches ws.send()
send_queue = queue.Queue(maxsize=120) send_queue = queue.Queue(maxsize=120)
@@ -141,8 +142,9 @@ def init_waterfall_websocket(app: Flask):
unregister_process(iq_process) unregister_process(iq_process)
iq_process = None iq_process = None
if claimed_device is not None: if claimed_device is not None:
app_module.release_sdr_device(claimed_device) app_module.release_sdr_device(claimed_device, claimed_sdr_type or 'rtlsdr')
claimed_device = None claimed_device = None
claimed_sdr_type = None
stop_event.clear() stop_event.clear()
# Flush stale frames from previous capture # Flush stale frames from previous capture
while not send_queue.empty(): while not send_queue.empty():
@@ -185,7 +187,7 @@ def init_waterfall_websocket(app: Flask):
end_freq = center_freq + effective_span_mhz / 2 end_freq = center_freq + effective_span_mhz / 2
# Claim the device # Claim the device
claim_err = app_module.claim_sdr_device(device_index, 'waterfall') claim_err = app_module.claim_sdr_device(device_index, 'waterfall', sdr_type.value)
if claim_err: if claim_err:
ws.send(json.dumps({ ws.send(json.dumps({
'status': 'error', 'status': 'error',
@@ -194,6 +196,7 @@ def init_waterfall_websocket(app: Flask):
})) }))
continue continue
claimed_device = device_index claimed_device = device_index
claimed_sdr_type = sdr_type.value
# Build I/Q capture command # Build I/Q capture command
try: try:
@@ -208,8 +211,9 @@ def init_waterfall_websocket(app: Flask):
bias_t=bias_t, bias_t=bias_t,
) )
except NotImplementedError as e: except NotImplementedError as e:
app_module.release_sdr_device(device_index) app_module.release_sdr_device(device_index, sdr_type.value)
claimed_device = None claimed_device = None
claimed_sdr_type = None
ws.send(json.dumps({ ws.send(json.dumps({
'status': 'error', 'status': 'error',
'message': str(e), 'message': str(e),
@@ -255,8 +259,9 @@ def init_waterfall_websocket(app: Flask):
safe_terminate(iq_process) safe_terminate(iq_process)
unregister_process(iq_process) unregister_process(iq_process)
iq_process = None iq_process = None
app_module.release_sdr_device(device_index) app_module.release_sdr_device(device_index, sdr_type.value)
claimed_device = None claimed_device = None
claimed_sdr_type = None
ws.send(json.dumps({ ws.send(json.dumps({
'status': 'error', 'status': 'error',
'message': f'Failed to start I/Q capture: {e}', 'message': f'Failed to start I/Q capture: {e}',
@@ -350,8 +355,9 @@ def init_waterfall_websocket(app: Flask):
unregister_process(iq_process) unregister_process(iq_process)
iq_process = None iq_process = None
if claimed_device is not None: if claimed_device is not None:
app_module.release_sdr_device(claimed_device) app_module.release_sdr_device(claimed_device, claimed_sdr_type or 'rtlsdr')
claimed_device = None claimed_device = None
claimed_sdr_type = None
stop_event.clear() stop_event.clear()
ws.send(json.dumps({'status': 'stopped'})) ws.send(json.dumps({'status': 'stopped'}))
@@ -366,7 +372,8 @@ def init_waterfall_websocket(app: Flask):
safe_terminate(iq_process) safe_terminate(iq_process)
unregister_process(iq_process) unregister_process(iq_process)
if claimed_device is not None: if claimed_device is not None:
app_module.release_sdr_device(claimed_device) app_module.release_sdr_device(claimed_device, claimed_sdr_type or 'rtlsdr')
claimed_sdr_type = None
# Complete WebSocket close handshake, then shut down the # Complete WebSocket close handshake, then shut down the
# raw socket so Werkzeug cannot write its HTTP 200 response # raw socket so Werkzeug cannot write its HTTP 200 response
# on top of the WebSocket stream (which browsers see as # on top of the WebSocket stream (which browsers see as
+51 -26
View File
@@ -449,7 +449,10 @@
devices.forEach((d, i) => { devices.forEach((d, i) => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = d.index; opt.value = d.index;
opt.textContent = `SDR ${d.index}: ${d.name}`; const sdrType = (d.sdr_type || d.type || 'rtlsdr').toLowerCase();
const sdrLabel = sdrType.toUpperCase();
opt.dataset.sdrType = sdrType;
opt.textContent = `SDR ${d.index} (${sdrLabel}): ${d.name}`;
aisSelect.appendChild(opt); aisSelect.appendChild(opt);
}); });
} }
@@ -457,18 +460,23 @@
// Populate DSC device selector // Populate DSC device selector
const dscSelect = document.getElementById('dscDeviceSelect'); const dscSelect = document.getElementById('dscDeviceSelect');
dscSelect.innerHTML = ''; dscSelect.innerHTML = '';
if (devices.length === 0) { const dscDevices = devices.filter(d => {
dscSelect.innerHTML = '<option value="0">No devices</option>'; const sdrType = (d.sdr_type || d.type || 'rtlsdr').toLowerCase();
return sdrType === 'rtlsdr';
});
if (dscDevices.length === 0) {
dscSelect.innerHTML = '<option value="0">No RTL-SDR found</option>';
} else { } else {
devices.forEach((d, i) => { dscDevices.forEach((d, i) => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = d.index; opt.value = d.index;
opt.textContent = `SDR ${d.index}: ${d.name}`; opt.dataset.sdrType = 'rtlsdr';
opt.textContent = `SDR ${d.index} (RTLSDR): ${d.name}`;
dscSelect.appendChild(opt); dscSelect.appendChild(opt);
}); });
// Default to second device if available // Default to second device if available
if (devices.length > 1) { if (dscDevices.length > 1) {
dscSelect.value = devices[1].index; dscSelect.value = dscDevices[1].index;
} }
} }
}) })
@@ -546,7 +554,9 @@
} }
function startTracking() { function startTracking() {
const device = document.getElementById('aisDeviceSelect').value; const aisSelect = document.getElementById('aisDeviceSelect');
const device = aisSelect.value;
const sdrType = (aisSelect.selectedOptions[0]?.dataset?.sdrType || 'rtlsdr').toLowerCase();
const gain = document.getElementById('aisGain').value; const gain = document.getElementById('aisGain').value;
// Check if using agent mode // Check if using agent mode
@@ -561,7 +571,7 @@
fetch(`/controller/agents/${aisCurrentAgent}/ais/start`, { fetch(`/controller/agents/${aisCurrentAgent}/ais/start`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device, gain, bias_t: getBiasTEnabled() }) body: JSON.stringify({ device, gain, bias_t: getBiasTEnabled(), sdr_type: sdrType })
}) })
.then(r => r.json()) .then(r => r.json())
.then(result => { .then(result => {
@@ -586,7 +596,7 @@
fetch('/ais/start', { fetch('/ais/start', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device, gain, bias_t: getBiasTEnabled() }) body: JSON.stringify({ device, gain, bias_t: getBiasTEnabled(), sdr_type: sdrType })
}) })
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
@@ -1170,7 +1180,9 @@
} }
function startDscTracking() { function startDscTracking() {
const device = document.getElementById('dscDeviceSelect').value; const dscSelect = document.getElementById('dscDeviceSelect');
const device = dscSelect.value;
const sdrType = (dscSelect.selectedOptions[0]?.dataset?.sdrType || 'rtlsdr').toLowerCase();
const gain = document.getElementById('dscGain').value; const gain = document.getElementById('dscGain').value;
// Check if using agent mode // Check if using agent mode
@@ -1185,7 +1197,7 @@
fetch(endpoint, { fetch(endpoint, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device, gain }) body: JSON.stringify({ device, gain, sdr_type: sdrType })
}) })
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
@@ -1617,21 +1629,32 @@
const aisSelect = document.getElementById('aisDeviceSelect'); const aisSelect = document.getElementById('aisDeviceSelect');
const dscSelect = document.getElementById('dscDeviceSelect'); const dscSelect = document.getElementById('dscDeviceSelect');
[aisSelect, dscSelect].forEach(select => { const aisDevices = devices || [];
const dscDevices = aisDevices.filter(device => {
const sdrType = (device.sdr_type || device.type || 'rtlsdr').toLowerCase();
return sdrType === 'rtlsdr';
});
const fillSelect = (select, list, emptyLabel) => {
if (!select) return; if (!select) return;
select.innerHTML = ''; select.innerHTML = '';
if (list.length === 0) {
if (devices.length === 0) { select.innerHTML = `<option value=\"0\">${emptyLabel}</option>`;
select.innerHTML = '<option value="0">No SDR found</option>'; return;
} else {
devices.forEach(device => {
const opt = document.createElement('option');
opt.value = device.index;
opt.textContent = `Device ${device.index}: ${device.name || device.type || 'SDR'}`;
select.appendChild(opt);
});
} }
}); list.forEach(device => {
const opt = document.createElement('option');
const sdrType = (device.sdr_type || device.type || 'rtlsdr').toLowerCase();
const sdrLabel = sdrType.toUpperCase();
opt.value = device.index;
opt.dataset.sdrType = sdrType;
opt.textContent = `Device ${device.index} (${sdrLabel}): ${device.name || device.type || 'SDR'}`;
select.appendChild(opt);
});
};
fillSelect(aisSelect, aisDevices, 'No SDR found');
fillSelect(dscSelect, dscDevices, 'No RTL-SDR found');
} }
// Override startTracking for agent support // Override startTracking for agent support
@@ -1645,13 +1668,15 @@
return; return;
} }
const device = document.getElementById('aisDeviceSelect').value; const aisSelect = document.getElementById('aisDeviceSelect');
const device = aisSelect.value;
const sdrType = (aisSelect.selectedOptions[0]?.dataset?.sdrType || 'rtlsdr').toLowerCase();
const gain = document.getElementById('aisGain').value; const gain = document.getElementById('aisGain').value;
fetch(`/controller/agents/${aisCurrentAgent}/ais/start`, { fetch(`/controller/agents/${aisCurrentAgent}/ais/start`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device, gain, bias_t: getBiasTEnabled() }) body: JSON.stringify({ device, gain, bias_t: getBiasTEnabled(), sdr_type: sdrType })
}) })
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {