Merge upstream/main and resolve acars, vdl2, dashboard conflicts

Resolved conflicts:
- routes/acars.py: keep /messages and /clear endpoints for history reload
- routes/vdl2.py: keep /messages and /clear endpoints for history reload
- templates/adsb_dashboard.html: keep removal of hardcoded device-1
  defaults for ACARS/VDL2 selectors (users pick their own device)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mitchross
2026-02-27 14:47:57 -05:00
41 changed files with 5065 additions and 1951 deletions
+14
View File
@@ -300,6 +300,20 @@ SUBGHZ_PRESETS = {
}
# =============================================================================
# RADIOSONDE (Weather Balloon Tracking)
# =============================================================================
# UDP port for radiosonde_auto_rx telemetry broadcast
RADIOSONDE_UDP_PORT = 55673
# Radiosonde process termination timeout
RADIOSONDE_TERMINATE_TIMEOUT = 5
# Maximum age for balloon data before cleanup (30 min — balloons move slowly)
MAX_RADIOSONDE_AGE_SECONDS = 1800
# =============================================================================
# DEAUTH ATTACK DETECTION
# =============================================================================
+59 -32
View File
@@ -1,49 +1,57 @@
from __future__ import annotations
import logging
import os
import platform
import shutil
import subprocess
from typing import Any
import logging
import os
import platform
import shutil
import subprocess
from typing import Any
logger = logging.getLogger('intercept.dependencies')
# Additional paths to search for tools (e.g., /usr/sbin on Debian)
EXTRA_TOOL_PATHS = ['/usr/sbin', '/sbin']
# Tools installed to non-standard locations (not on PATH)
KNOWN_TOOL_PATHS: dict[str, list[str]] = {
'auto_rx.py': [
'/opt/radiosonde_auto_rx/auto_rx/auto_rx.py',
'/opt/auto_rx/auto_rx.py',
],
}
def check_tool(name: str) -> bool:
"""Check if a tool is installed."""
return get_tool_path(name) is not None
def get_tool_path(name: str) -> str | None:
"""Get the full path to a tool, checking standard PATH and extra locations."""
# Optional explicit override, e.g. INTERCEPT_RTL_FM_PATH=/opt/homebrew/bin/rtl_fm
env_key = f"INTERCEPT_{name.upper().replace('-', '_')}_PATH"
env_path = os.environ.get(env_key)
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
return env_path
# Prefer native Homebrew binaries on Apple Silicon to avoid mixing Rosetta
# /usr/local tools with arm64 Python/runtime.
if platform.system() == 'Darwin':
machine = platform.machine().lower()
preferred_paths: list[str] = []
if machine in {'arm64', 'aarch64'}:
preferred_paths.append('/opt/homebrew/bin')
preferred_paths.append('/usr/local/bin')
for base in preferred_paths:
full_path = os.path.join(base, name)
if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
return full_path
# First check standard PATH
path = shutil.which(name)
if path:
return path
def get_tool_path(name: str) -> str | None:
"""Get the full path to a tool, checking standard PATH and extra locations."""
# Optional explicit override, e.g. INTERCEPT_RTL_FM_PATH=/opt/homebrew/bin/rtl_fm
env_key = f"INTERCEPT_{name.upper().replace('-', '_')}_PATH"
env_path = os.environ.get(env_key)
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
return env_path
# Prefer native Homebrew binaries on Apple Silicon to avoid mixing Rosetta
# /usr/local tools with arm64 Python/runtime.
if platform.system() == 'Darwin':
machine = platform.machine().lower()
preferred_paths: list[str] = []
if machine in {'arm64', 'aarch64'}:
preferred_paths.append('/opt/homebrew/bin')
preferred_paths.append('/usr/local/bin')
for base in preferred_paths:
full_path = os.path.join(base, name)
if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
return full_path
# First check standard PATH
path = shutil.which(name)
if path:
return path
# Check additional paths (e.g., /usr/sbin for aircrack-ng on Debian)
for extra_path in EXTRA_TOOL_PATHS:
@@ -51,6 +59,11 @@ def get_tool_path(name: str) -> str | None:
if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
return full_path
# Check known non-standard install locations
for known_path in KNOWN_TOOL_PATHS.get(name, []):
if os.path.isfile(known_path):
return known_path
return None
@@ -447,6 +460,20 @@ TOOL_DEPENDENCIES = {
}
}
},
'radiosonde': {
'name': 'Radiosonde Tracking',
'tools': {
'auto_rx.py': {
'required': True,
'description': 'Radiosonde weather balloon decoder',
'install': {
'apt': 'Run ./setup.sh (clones from GitHub)',
'brew': 'Run ./setup.sh (clones from GitHub)',
'manual': 'https://github.com/projecthorus/radiosonde_auto_rx'
}
}
}
},
'tscm': {
'name': 'TSCM Counter-Surveillance',
'tools': {
+129 -87
View File
@@ -6,31 +6,31 @@ Detects RTL-SDR devices via rtl_test and other SDR hardware via SoapySDR.
from __future__ import annotations
import logging
import re
import shutil
import subprocess
import time
from typing import Optional
import logging
import re
import shutil
import subprocess
import time
from typing import Optional
from .base import SDRCapabilities, SDRDevice, SDRType
logger = logging.getLogger(__name__)
# Cache HackRF detection results so polling endpoints don't repeatedly run
# hackrf_info while the device is actively streaming in SubGHz mode.
_hackrf_cache: list[SDRDevice] = []
_hackrf_cache_ts: float = 0.0
_HACKRF_CACHE_TTL_SECONDS = 3.0
def _hackrf_probe_blocked() -> bool:
"""Return True when probing HackRF would interfere with an active stream."""
try:
from utils.subghz import get_subghz_manager
return get_subghz_manager().active_mode in {'rx', 'decode', 'tx', 'sweep'}
except Exception:
return False
logger = logging.getLogger(__name__)
# Cache HackRF detection results so polling endpoints don't repeatedly run
# hackrf_info while the device is actively streaming in SubGHz mode.
_hackrf_cache: list[SDRDevice] = []
_hackrf_cache_ts: float = 0.0
_HACKRF_CACHE_TTL_SECONDS = 3.0
def _hackrf_probe_blocked() -> bool:
"""Return True when probing HackRF would interfere with an active stream."""
try:
from utils.subghz import get_subghz_manager
return get_subghz_manager().active_mode in {'rx', 'decode', 'tx', 'sweep'}
except Exception:
return False
def _check_tool(name: str) -> bool:
@@ -112,21 +112,21 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
lib_paths = ['/usr/local/lib', '/opt/homebrew/lib']
current_ld = env.get('DYLD_LIBRARY_PATH', '')
env['DYLD_LIBRARY_PATH'] = ':'.join(lib_paths + [current_ld] if current_ld else lib_paths)
result = subprocess.run(
['rtl_test', '-t'],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
timeout=5,
env=env
)
output = result.stderr + result.stdout
# Parse device info from rtl_test output
# Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001"
# Require a non-empty serial to avoid matching malformed lines like "SN:".
device_pattern = r'(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$'
result = subprocess.run(
['rtl_test', '-t'],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
timeout=5,
env=env
)
output = result.stderr + result.stdout
# Parse device info from rtl_test output
# Format: "0: Realtek, RTL2838UHIDIR, SN: 00000001"
# Require a non-empty serial to avoid matching malformed lines like "SN:".
device_pattern = r'(\d+):\s+(.+?),\s*SN:\s*(\S+)\s*$'
from .rtlsdr import RTLSDRCommandBuilder
@@ -134,14 +134,14 @@ def detect_rtlsdr_devices() -> list[SDRDevice]:
line = line.strip()
match = re.match(device_pattern, line)
if match:
devices.append(SDRDevice(
sdr_type=SDRType.RTL_SDR,
index=int(match.group(1)),
name=match.group(2).strip().rstrip(','),
serial=match.group(3),
driver='rtlsdr',
capabilities=RTLSDRCommandBuilder.CAPABILITIES
))
devices.append(SDRDevice(
sdr_type=SDRType.RTL_SDR,
index=int(match.group(1)),
name=match.group(2).strip().rstrip(','),
serial=match.group(3),
driver='rtlsdr',
capabilities=RTLSDRCommandBuilder.CAPABILITIES
))
# Fallback: if we found devices but couldn't parse details
if not devices:
@@ -314,29 +314,29 @@ def _add_soapy_device(
))
def detect_hackrf_devices() -> list[SDRDevice]:
"""
Detect HackRF devices using native hackrf_info tool.
Fallback for when SoapySDR is not available.
"""
global _hackrf_cache, _hackrf_cache_ts
now = time.time()
# While HackRF is actively streaming in SubGHz mode, skip probe calls.
# Re-running hackrf_info during active RX/TX can disrupt the USB stream.
if _hackrf_probe_blocked():
return list(_hackrf_cache)
if _hackrf_cache and (now - _hackrf_cache_ts) < _HACKRF_CACHE_TTL_SECONDS:
return list(_hackrf_cache)
devices: list[SDRDevice] = []
if not _check_tool('hackrf_info'):
_hackrf_cache = devices
_hackrf_cache_ts = now
return devices
def detect_hackrf_devices() -> list[SDRDevice]:
"""
Detect HackRF devices using native hackrf_info tool.
Fallback for when SoapySDR is not available.
"""
global _hackrf_cache, _hackrf_cache_ts
now = time.time()
# While HackRF is actively streaming in SubGHz mode, skip probe calls.
# Re-running hackrf_info during active RX/TX can disrupt the USB stream.
if _hackrf_probe_blocked():
return list(_hackrf_cache)
if _hackrf_cache and (now - _hackrf_cache_ts) < _HACKRF_CACHE_TTL_SECONDS:
return list(_hackrf_cache)
devices: list[SDRDevice] = []
if not _check_tool('hackrf_info'):
_hackrf_cache = devices
_hackrf_cache_ts = now
return devices
try:
result = subprocess.run(
@@ -374,12 +374,12 @@ def detect_hackrf_devices() -> list[SDRDevice]:
capabilities=HackRFCommandBuilder.CAPABILITIES
))
except Exception as e:
logger.debug(f"HackRF detection error: {e}")
_hackrf_cache = list(devices)
_hackrf_cache_ts = now
return devices
except Exception as e:
logger.debug(f"HackRF detection error: {e}")
_hackrf_cache = list(devices)
_hackrf_cache_ts = now
return devices
def probe_rtlsdr_device(device_index: int) -> str | None:
@@ -413,31 +413,73 @@ def probe_rtlsdr_device(device_index: int) -> str | None:
lib_paths + [current_ld] if current_ld else lib_paths
)
result = subprocess.run(
# Use Popen with early termination instead of run() with full timeout.
# rtl_test prints device info to stderr quickly, then keeps running
# its test loop. We kill it as soon as we see success or failure.
proc = subprocess.Popen(
['rtl_test', '-d', str(device_index), '-t'],
capture_output=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=3,
env=env,
)
output = result.stderr + result.stdout
if 'usb_claim_interface' in output or 'Failed to open' in output:
import select
error_found = False
device_found = False
deadline = time.monotonic() + 3.0
try:
while time.monotonic() < deadline:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
# Wait for stderr output with timeout
ready, _, _ = select.select(
[proc.stderr], [], [], min(remaining, 0.1)
)
if ready:
line = proc.stderr.readline()
if not line:
break # EOF — process closed stderr
# Check for no-device messages first (before success check,
# since "No supported devices found" also contains "Found" + "device")
if 'no supported devices' in line.lower() or 'no matching devices' in line.lower():
error_found = True
break
if 'usb_claim_interface' in line or 'Failed to open' in line:
error_found = True
break
if 'Found' in line and 'device' in line.lower():
# Device opened successfully — no need to wait longer
device_found = True
break
if proc.poll() is not None:
break # Process exited
if not device_found and not error_found and proc.poll() is not None and proc.returncode != 0:
# rtl_test exited with error and we never saw a success message
error_found = True
finally:
try:
proc.kill()
except OSError:
pass
proc.wait()
if device_found:
# Allow the kernel to fully release the USB interface
# before the caller opens the device with dump1090/rtl_fm/etc.
time.sleep(0.5)
if error_found:
logger.warning(
f"RTL-SDR device {device_index} USB probe failed: "
f"device busy or unavailable"
)
return (
f'SDR device {device_index} is busy at the USB level'
f'another process outside INTERCEPT may be using it. '
f'Check for stale rtl_fm/rtl_433/dump1090 processes, '
f'or try a different device.'
f'SDR device {device_index} is not available'
f'check that the RTL-SDR is connected and not in use by another process.'
)
except subprocess.TimeoutExpired:
# rtl_test opened the device successfully and is running the
# test — that means the device *is* available.
pass
except Exception as e:
logger.debug(f"RTL-SDR probe error for device {device_index}: {e}")