mirror of
https://github.com/smittix/intercept.git
synced 2026-07-16 13:28:11 -07:00
feat(drone): add Flask blueprint, register routes, wire drone_queue
Implements Task 5: creates routes/drone.py with /status, /contacts, /start, /stop, and /stream (SSE fanout) endpoints; registers the drone_bp blueprint in routes/__init__.py; adds drone_queue to app.py; adds opendroneid>=1.0 to requirements.txt. All 39 drone tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -317,6 +317,9 @@ deauth_detector = None
|
|||||||
deauth_detector_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
deauth_detector_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
deauth_detector_lock = threading.Lock()
|
deauth_detector_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Drone Intelligence
|
||||||
|
drone_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# GLOBAL STATE DICTIONARIES
|
# GLOBAL STATE DICTIONARIES
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ meshtastic>=2.0.0
|
|||||||
|
|
||||||
# Deauthentication attack detection (optional - for WiFi TSCM)
|
# Deauthentication attack detection (optional - for WiFi TSCM)
|
||||||
scapy>=2.4.5
|
scapy>=2.4.5
|
||||||
|
opendroneid>=1.0
|
||||||
|
|
||||||
# QR code generation for Meshtastic channels (optional)
|
# QR code generation for Meshtastic channels (optional)
|
||||||
qrcode[pil]>=7.4
|
qrcode[pil]>=7.4
|
||||||
|
|||||||
+4
-1
@@ -18,6 +18,7 @@ def register_blueprints(app):
|
|||||||
from .bt_locate import bt_locate_bp
|
from .bt_locate import bt_locate_bp
|
||||||
from .controller import controller_bp
|
from .controller import controller_bp
|
||||||
from .correlation import correlation_bp
|
from .correlation import correlation_bp
|
||||||
|
from .drone import drone_bp
|
||||||
from .dsc import dsc_bp
|
from .dsc import dsc_bp
|
||||||
from .gps import gps_bp
|
from .gps import gps_bp
|
||||||
from .ground_station import ground_station_bp
|
from .ground_station import ground_station_bp
|
||||||
@@ -91,6 +92,7 @@ def register_blueprints(app):
|
|||||||
app.register_blueprint(system_bp) # System health monitoring
|
app.register_blueprint(system_bp) # System health monitoring
|
||||||
app.register_blueprint(ook_bp) # Generic OOK signal decoder
|
app.register_blueprint(ook_bp) # Generic OOK signal decoder
|
||||||
app.register_blueprint(ground_station_bp) # Ground station automation
|
app.register_blueprint(ground_station_bp) # Ground station automation
|
||||||
|
app.register_blueprint(drone_bp) # Drone intelligence / UAV detection
|
||||||
|
|
||||||
# Exempt all API blueprints from CSRF (they use JSON, not form tokens)
|
# Exempt all API blueprints from CSRF (they use JSON, not form tokens)
|
||||||
if _csrf:
|
if _csrf:
|
||||||
@@ -99,5 +101,6 @@ def register_blueprints(app):
|
|||||||
|
|
||||||
# Initialize TSCM state with queue and lock from app
|
# Initialize TSCM state with queue and lock from app
|
||||||
import app as app_module
|
import app as app_module
|
||||||
if hasattr(app_module, 'tscm_queue') and hasattr(app_module, 'tscm_lock'):
|
|
||||||
|
if hasattr(app_module, "tscm_queue") and hasattr(app_module, "tscm_lock"):
|
||||||
init_tscm_state(app_module.tscm_queue, app_module.tscm_lock)
|
init_tscm_state(app_module.tscm_queue, app_module.tscm_lock)
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""Drone intelligence routes — multi-vector UAV detection."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
if not _drone_running:
|
||||||
|
_remote_id_scanner.start(wifi_iface=wifi_iface)
|
||||||
|
_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
|
||||||
|
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"},
|
||||||
|
)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import json
|
||||||
|
import queue
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
import app as app_module
|
||||||
|
from routes.drone import drone_bp
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_app_state(mocker):
|
||||||
|
mocker.patch.object(app_module, "drone_queue", queue.Queue())
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def drone_app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.register_blueprint(drone_bp)
|
||||||
|
app.config["TESTING"] = True
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(drone_app):
|
||||||
|
return drone_app.test_client()
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_returns_json(client):
|
||||||
|
resp = client.get("/drone/status")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = json.loads(resp.data)
|
||||||
|
assert "running" in data
|
||||||
|
assert "vectors" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_contacts_returns_empty_list_when_idle(client):
|
||||||
|
resp = client.get("/drone/contacts")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = json.loads(resp.data)
|
||||||
|
assert data == [] or isinstance(data, list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_returns_ok(client):
|
||||||
|
with (
|
||||||
|
patch("routes.drone._correlator"),
|
||||||
|
patch("routes.drone._remote_id_scanner"),
|
||||||
|
patch("routes.drone._rf_detector"),
|
||||||
|
):
|
||||||
|
resp = client.post("/drone/start", json={})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_stop_returns_ok(client):
|
||||||
|
resp = client.post("/drone/stop")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_stream_returns_event_stream(client):
|
||||||
|
resp = client.get("/drone/stream")
|
||||||
|
assert resp.content_type.startswith("text/event-stream")
|
||||||
Reference in New Issue
Block a user