Files
intercept/routes/drone.py
T
James Smith 1a99a7213f fix(drone): add lock on _drone_running and null guards before start() calls
Concurrent POST /drone/start under gevent would race on _drone_running;
lock mirrors the ais_lock / dsc_lock pattern used throughout the codebase.
Null guards prevent AttributeError if worker constructors fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 17:39:23 +01:00

106 lines
3.1 KiB
Python

"""Drone intelligence routes — multi-vector UAV detection."""
from __future__ import annotations
import logging
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
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
_drone_running = False
_drone_lock = threading.Lock()
def _ensure_workers() -> None:
global _correlator, _remote_id_scanner, _rf_detector
if _correlator is None:
_correlator = DroneCorrelator(output_queue=app_module.drone_queue)
if _remote_id_scanner is None:
_remote_id_scanner = RemoteIDScanner(output_queue=app_module.drone_queue)
if _rf_detector is None:
_rf_detector = RFDetector(output_queue=app_module.drone_queue)
@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
_ensure_workers()
wifi_iface = request.json.get("wifi_iface") if request.json else None
rtl_index = int((request.json or {}).get("rtl_sdr_index", 0))
use_hackrf = bool((request.json or {}).get("use_hackrf", True))
with _drone_lock:
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()
_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"},
)