diff --git a/routes/drone.py b/routes/drone.py index 8303738..2be77cd 100644 --- a/routes/drone.py +++ b/routes/drone.py @@ -3,7 +3,9 @@ from __future__ import annotations import logging +import platform import queue +import subprocess import threading from flask import Blueprint, Response, jsonify, request @@ -56,6 +58,81 @@ def _ensure_workers() -> None: _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 = [] diff --git a/static/js/modes/drone.js b/static/js/modes/drone.js index 3c39e92..234bdcb 100644 --- a/static/js/modes/drone.js +++ b/static/js/modes/drone.js @@ -10,11 +10,63 @@ function init() { document.getElementById('droneStartBtn')?.addEventListener('click', _start); document.getElementById('droneStopBtn')?.addEventListener('click', _stop); + _refreshDevices(); _initMap(); _connectSSE(); _refreshStatus(); } + function _refreshDevices() { + fetch('/drone/devices') + .then(function (r) { return r.json(); }) + .then(function (data) { + const devs = data.devices || {}; + _populateSelect( + 'droneWifiIface', + devs.wifi_interfaces || [], + function (i) { return i.name; }, + function (i) { return i.display_name || i.name; }, + 'No WiFi interfaces found' + ); + _populateSelect( + 'droneRtlIndex', + devs.sdr_devices || [], + function (d) { return d.index; }, + function (d) { return d.display_name || d.name; }, + 'No SDR devices found' + ); + }) + .catch(function () { + _setSelectError('droneWifiIface', 'Failed to load interfaces'); + _setSelectError('droneRtlIndex', 'Failed to load devices'); + }); + } + + function _populateSelect(id, items, valFn, labelFn, emptyMsg) { + const sel = document.getElementById(id); + if (!sel) return; + sel.innerHTML = ''; + if (!items.length) { + const opt = document.createElement('option'); + opt.value = ''; + opt.textContent = emptyMsg; + sel.appendChild(opt); + return; + } + items.forEach(function (item) { + const opt = document.createElement('option'); + opt.value = valFn(item); + opt.textContent = labelFn(item); + sel.appendChild(opt); + }); + } + + function _setSelectError(id, msg) { + const sel = document.getElementById(id); + if (!sel) return; + sel.innerHTML = ''; + } + function _initMap() { if (_map) return; const mapEl = document.getElementById('droneMainMap'); @@ -163,8 +215,10 @@ } function _start() { - const iface = document.getElementById('droneWifiIface')?.value.trim() || null; - const rtlIndex = parseInt(document.getElementById('droneRtlIndex')?.value, 10) || 0; + const ifaceVal = document.getElementById('droneWifiIface')?.value || ''; + const iface = ifaceVal || null; + const rtlVal = document.getElementById('droneRtlIndex')?.value; + const rtlIndex = rtlVal !== '' && rtlVal != null ? parseInt(rtlVal, 10) : 0; const useHackrf = document.getElementById('droneUseHackrf')?.checked ?? true; fetch('/drone/start', { method: 'POST', diff --git a/templates/partials/modes/drone.html b/templates/partials/modes/drone.html index 188c2c4..9cea10f 100644 --- a/templates/partials/modes/drone.html +++ b/templates/partials/modes/drone.html @@ -20,15 +20,19 @@

WiFi Interface

- +

SDR Settings

- - + +