mirror of
https://github.com/smittix/intercept.git
synced 2026-06-22 03:58:52 -07:00
4ba8a40af9
Add /drone/devices endpoint that enumerates available WiFi interfaces (via iw/iwconfig) and RTL-SDR devices (via SDRFactory.detect_devices), matching the pattern used by TSCM. Sidebar WiFi interface and RTL-SDR inputs are now <select> elements populated on init() from /drone/devices, consistent with how other modes expose hardware selection. HackRF checkbox remains as a toggle since it's a binary capability rather than an enumerated device list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
210 lines
7.2 KiB
Python
210 lines
7.2 KiB
Python
"""Drone intelligence routes — multi-vector UAV detection."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import platform
|
|
import queue
|
|
import subprocess
|
|
import threading
|
|
|
|
from flask import Blueprint, Response, jsonify, request
|
|
|
|
import app as app_module
|
|
from utils.constants import SSE_KEEPALIVE_INTERVAL, SSE_QUEUE_TIMEOUT
|
|
from utils.drone.correlator import DroneCorrelator
|
|
from utils.drone.remote_id import RemoteIDScanner
|
|
from utils.drone.rf_detector import RFDetector
|
|
from utils.sse import sse_stream_fanout
|
|
from utils.validation import validate_device_index
|
|
|
|
logger = logging.getLogger("intercept.drone")
|
|
|
|
drone_bp = Blueprint("drone", __name__, url_prefix="/drone")
|
|
|
|
_correlator: DroneCorrelator | None = None
|
|
_remote_id_scanner: RemoteIDScanner | None = None
|
|
_rf_detector: RFDetector | None = None
|
|
_obs_queue: queue.Queue | None = None # raw observations from scanners/detectors
|
|
_relay_thread: threading.Thread | None = None
|
|
_drone_running = False
|
|
_drone_lock = threading.Lock()
|
|
|
|
_SENTINEL = object()
|
|
|
|
|
|
def _relay_observations() -> None:
|
|
"""Read raw observations from _obs_queue and feed them into the correlator."""
|
|
while True:
|
|
obs = _obs_queue.get()
|
|
if obs is _SENTINEL:
|
|
break
|
|
if _correlator is not None:
|
|
_correlator.process(obs)
|
|
|
|
|
|
def _ensure_workers() -> None:
|
|
global _correlator, _remote_id_scanner, _rf_detector, _obs_queue, _relay_thread
|
|
if _obs_queue is None:
|
|
_obs_queue = queue.Queue(maxsize=512)
|
|
if _correlator is None:
|
|
_correlator = DroneCorrelator(output_queue=app_module.drone_queue)
|
|
if _remote_id_scanner is None:
|
|
_remote_id_scanner = RemoteIDScanner(output_queue=_obs_queue)
|
|
if _rf_detector is None:
|
|
_rf_detector = RFDetector(output_queue=_obs_queue)
|
|
if _relay_thread is None or not _relay_thread.is_alive():
|
|
_relay_thread = threading.Thread(target=_relay_observations, daemon=True)
|
|
_relay_thread.start()
|
|
|
|
|
|
@drone_bp.route("/devices")
|
|
def devices():
|
|
"""Return available WiFi interfaces and SDR devices for drone detection."""
|
|
result: dict = {"wifi_interfaces": [], "sdr_devices": []}
|
|
|
|
# WiFi interfaces via iw/iwconfig
|
|
if platform.system() == "Darwin":
|
|
try:
|
|
out = subprocess.run(
|
|
["networksetup", "-listallhardwareports"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
).stdout
|
|
lines = out.split("\n")
|
|
for i, line in enumerate(lines):
|
|
if "Wi-Fi" in line or "AirPort" in line:
|
|
port = line.replace("Hardware Port:", "").strip()
|
|
for j in range(i + 1, min(i + 3, len(lines))):
|
|
if "Device:" in lines[j]:
|
|
dev = lines[j].split("Device:")[1].strip()
|
|
result["wifi_interfaces"].append(
|
|
{"name": dev, "display_name": f"{port} ({dev})", "type": "internal"}
|
|
)
|
|
break
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
pass
|
|
else:
|
|
try:
|
|
out = subprocess.run(["iw", "dev"], capture_output=True, text=True, timeout=5).stdout
|
|
current: str | None = None
|
|
for line in out.split("\n"):
|
|
line = line.strip()
|
|
if line.startswith("Interface"):
|
|
current = line.split()[1]
|
|
elif current and "type" in line:
|
|
iface_type = line.split()[-1]
|
|
result["wifi_interfaces"].append(
|
|
{
|
|
"name": current,
|
|
"display_name": f"{current} ({iface_type})",
|
|
"type": iface_type,
|
|
}
|
|
)
|
|
current = None
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
try:
|
|
out = subprocess.run(["iwconfig"], capture_output=True, text=True, timeout=5).stdout
|
|
for line in out.split("\n"):
|
|
if "IEEE 802.11" in line:
|
|
iface = line.split()[0]
|
|
result["wifi_interfaces"].append(
|
|
{"name": iface, "display_name": f"{iface} (managed)", "type": "managed"}
|
|
)
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
pass
|
|
|
|
# SDR devices
|
|
try:
|
|
from utils.sdr import SDRFactory
|
|
|
|
for sdr in SDRFactory.detect_devices():
|
|
sdr_type = sdr.sdr_type.value if hasattr(sdr.sdr_type, "value") else str(sdr.sdr_type)
|
|
display = sdr.name
|
|
if sdr.serial and sdr.serial not in ("N/A", "Unknown"):
|
|
display = f"{sdr.name} (SN: {sdr.serial[-8:]})"
|
|
result["sdr_devices"].append(
|
|
{"index": sdr.index, "name": sdr.name, "display_name": display, "type": sdr_type}
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
return jsonify({"status": "ok", "devices": result})
|
|
|
|
|
|
@drone_bp.route("/status")
|
|
def status():
|
|
vectors = []
|
|
if _remote_id_scanner and _remote_id_scanner.running:
|
|
vectors.append("REMOTE_ID")
|
|
if _rf_detector and _rf_detector.running:
|
|
vectors.append("RF")
|
|
return jsonify(
|
|
{
|
|
"running": _drone_running,
|
|
"vectors": vectors,
|
|
"contact_count": len(_correlator.get_all()) if _correlator else 0,
|
|
}
|
|
)
|
|
|
|
|
|
@drone_bp.route("/contacts")
|
|
def contacts():
|
|
if not _correlator:
|
|
return jsonify([])
|
|
return jsonify(_correlator.get_all())
|
|
|
|
|
|
@drone_bp.route("/start", methods=["POST"])
|
|
def start():
|
|
global _drone_running
|
|
body = request.json or {}
|
|
wifi_iface = body.get("wifi_iface") or None
|
|
try:
|
|
rtl_index = validate_device_index(body.get("rtl_sdr_index", 0))
|
|
except ValueError as exc:
|
|
return jsonify({"error": str(exc)}), 400
|
|
use_hackrf = bool(body.get("use_hackrf", True))
|
|
|
|
with _drone_lock:
|
|
_ensure_workers()
|
|
if not _drone_running:
|
|
if _remote_id_scanner:
|
|
_remote_id_scanner.start(wifi_iface=wifi_iface)
|
|
if _rf_detector:
|
|
_rf_detector.start(rtl_sdr_index=rtl_index, use_hackrf=use_hackrf)
|
|
_drone_running = True
|
|
logger.info("Drone detection started")
|
|
|
|
return jsonify({"status": "ok", "running": True})
|
|
|
|
|
|
@drone_bp.route("/stop", methods=["POST"])
|
|
def stop():
|
|
global _drone_running
|
|
with _drone_lock:
|
|
if _remote_id_scanner:
|
|
_remote_id_scanner.stop()
|
|
if _rf_detector:
|
|
_rf_detector.stop()
|
|
if _obs_queue is not None:
|
|
_obs_queue.put_nowait(_SENTINEL)
|
|
_drone_running = False
|
|
logger.info("Drone detection stopped")
|
|
return jsonify({"status": "ok", "running": False})
|
|
|
|
|
|
@drone_bp.route("/stream")
|
|
def stream():
|
|
return Response(
|
|
sse_stream_fanout(
|
|
source_queue=app_module.drone_queue,
|
|
channel_key="drone",
|
|
timeout=SSE_QUEUE_TIMEOUT,
|
|
keepalive_interval=SSE_KEEPALIVE_INTERVAL,
|
|
),
|
|
mimetype="text/event-stream",
|
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
|
)
|