diff --git a/routes/drone.py b/routes/drone.py
index 2be77cd..04a162b 100644
--- a/routes/drone.py
+++ b/routes/drone.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
+import os
import platform
import queue
import subprocess
@@ -80,7 +81,12 @@ def devices():
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"}
+ {
+ "name": dev,
+ "display_name": f"{port} ({dev})",
+ "type": "internal",
+ "monitor_capable": False,
+ }
)
break
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
@@ -100,6 +106,7 @@ def devices():
"name": current,
"display_name": f"{current} ({iface_type})",
"type": iface_type,
+ "monitor_capable": True,
}
)
current = None
@@ -110,7 +117,12 @@ def devices():
if "IEEE 802.11" in line:
iface = line.split()[0]
result["wifi_interfaces"].append(
- {"name": iface, "display_name": f"{iface} (managed)", "type": "managed"}
+ {
+ "name": iface,
+ "display_name": f"{iface} (managed)",
+ "type": "managed",
+ "monitor_capable": True,
+ }
)
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
pass
@@ -130,7 +142,24 @@ def devices():
except Exception:
pass
- return jsonify({"status": "ok", "devices": result})
+ running_as_root = os.geteuid() == 0
+ warnings = []
+ if not running_as_root:
+ warnings.append(
+ {
+ "type": "privileges",
+ "message": "Not running as root — WiFi monitor mode may be unavailable.",
+ }
+ )
+
+ return jsonify(
+ {
+ "status": "ok",
+ "devices": result,
+ "running_as_root": running_as_root,
+ "warnings": warnings,
+ }
+ )
@drone_bp.route("/status")
diff --git a/static/css/modes/drone.css b/static/css/modes/drone.css
index 7941125..d417b16 100644
--- a/static/css/modes/drone.css
+++ b/static/css/modes/drone.css
@@ -8,6 +8,8 @@
height: 100%;
overflow: hidden;
background: var(--bg-primary);
+ padding: 0;
+ box-sizing: border-box;
}
.drone-visuals-header {
diff --git a/static/js/modes/drone.js b/static/js/modes/drone.js
index 234bdcb..9b10f06 100644
--- a/static/js/modes/drone.js
+++ b/static/js/modes/drone.js
@@ -1,83 +1,25 @@
-(function DroneMode() {
+var DroneMode = (function () {
'use strict';
- let _sse = null;
- let _map = null;
- let _markers = {};
- let _trails = {};
- let _running = false;
+ var _sse = null;
+ var _map = null;
+ var _markers = {};
+ var _trails = {};
+ var _initialized = false;
function init() {
+ if (_initialized) {
+ _refreshStatus();
+ return;
+ }
+ _initialized = true;
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');
- if (!mapEl || typeof L === 'undefined') return;
- _map = L.map('droneMainMap', { zoomControl: true }).setView([20, 0], 2);
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- attribution: '© OpenStreetMap',
- maxZoom: 18,
- }).addTo(_map);
- }
-
function destroy() {
_disconnectSSE();
if (_map) {
@@ -86,6 +28,22 @@
}
_markers = {};
_trails = {};
+ _initialized = false;
+ }
+
+ function invalidateMap() {
+ if (_map) _map.invalidateSize();
+ }
+
+ function _initMap() {
+ if (_map) return;
+ var mapEl = document.getElementById('droneMainMap');
+ if (!mapEl || typeof L === 'undefined') return;
+ _map = L.map('droneMainMap', { zoomControl: true }).setView([20, 0], 2);
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap',
+ maxZoom: 18,
+ }).addTo(_map);
}
function _connectSSE() {
@@ -93,7 +51,7 @@
_sse = new EventSource('/drone/stream');
_sse.addEventListener('message', function (e) {
try {
- const msg = JSON.parse(e.data);
+ var msg = JSON.parse(e.data);
if (msg.type === 'contact') _handleContact(msg.data);
} catch (_) {}
});
@@ -115,11 +73,11 @@
}
function _upsertCard(contact) {
- const listEl = document.getElementById('droneContactList');
- const emptyEl = document.getElementById('droneContactEmpty');
+ var listEl = document.getElementById('droneContactList');
+ var emptyEl = document.getElementById('droneContactEmpty');
if (!listEl) return;
if (emptyEl) emptyEl.style.display = 'none';
- let card = document.getElementById('drone-card-' + contact.id);
+ var card = document.getElementById('drone-card-' + contact.id);
if (!card) {
card = document.createElement('div');
card.id = 'drone-card-' + contact.id;
@@ -128,14 +86,14 @@
listEl.prepend(card);
}
card.className = 'drone-contact-card ' + contact.risk_level + '-risk';
- const complianceLabel = contact.compliant
+ var complianceLabel = contact.compliant
? 'Remote ID'
: 'No Remote ID';
- const vectors = (contact.detection_vectors || []).map(function (v) {
+ var vectors = (contact.detection_vectors || []).map(function (v) {
return '' + v + '';
}).join('');
- const alt = contact.altitude_m != null ? contact.altitude_m.toFixed(0) + ' m' : '—';
- const spd = contact.speed_ms != null ? contact.speed_ms.toFixed(1) + ' m/s' : '—';
+ var alt = contact.altitude_m != null ? contact.altitude_m.toFixed(0) + ' m' : '—';
+ var spd = contact.speed_ms != null ? contact.speed_ms.toFixed(1) + ' m/s' : '—';
card.innerHTML = [
'
',
'
' + (contact.serial_number || contact.id) + '',
@@ -148,15 +106,15 @@
function _upsertMapMarker(contact) {
if (!_map) return;
- const lat = contact.position[0];
- const lon = contact.position[1];
+ var lat = contact.position[0];
+ var lon = contact.position[1];
if (_markers[contact.id]) {
_markers[contact.id].setLatLng([lat, lon]);
} else {
- const color = contact.risk_level === 'high' ? 'var(--accent-red)' :
- contact.risk_level === 'medium' ? 'var(--accent-yellow)' :
- 'var(--accent-cyan)';
- const icon = L.divIcon({
+ var color = contact.risk_level === 'high' ? 'var(--accent-red)' :
+ contact.risk_level === 'medium' ? 'var(--accent-yellow)' :
+ 'var(--accent-cyan)';
+ var icon = L.divIcon({
className: 'drone-map-icon' + (contact.risk_level === 'high' ? ' drone-marker-high-risk' : ''),
html: '
',
iconSize: [10, 10],
@@ -166,7 +124,7 @@
.addTo(_map)
.bindPopup('
' + (contact.serial_number || contact.id) + 'Risk: ' + contact.risk_level);
}
- const trailPoints = (contact.position_history || []).map(function (p) {
+ var trailPoints = (contact.position_history || []).map(function (p) {
return [p.lat, p.lon];
});
if (_trails[contact.id]) {
@@ -191,9 +149,9 @@
fetch('/drone/contacts')
.then(function (r) { return r.json(); })
.then(function (contacts) {
- const nonCompliant = contacts.filter(function (c) { return !c.compliant; }).length;
- const highRisk = contacts.filter(function (c) { return c.risk_level === 'high'; }).length;
- const set = function (id, val) { const el = document.getElementById(id); if (el) el.textContent = val; };
+ var nonCompliant = contacts.filter(function (c) { return !c.compliant; }).length;
+ var highRisk = contacts.filter(function (c) { return c.risk_level === 'high'; }).length;
+ var set = function (id, val) { var el = document.getElementById(id); if (el) el.textContent = val; };
set('droneContactCount', contacts.length);
set('droneNonCompliantCount', nonCompliant);
set('droneVsContacts', contacts.length);
@@ -207,7 +165,6 @@
fetch('/drone/status')
.then(function (r) { return r.json(); })
.then(function (data) {
- _running = data.running;
_setRunningUI(data.running);
_updateVectorPills(data.vectors || []);
})
@@ -215,11 +172,11 @@
}
function _start() {
- 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;
+ var ifaceVal = document.getElementById('droneWifiIface')?.value || '';
+ var iface = ifaceVal || null;
+ var rtlVal = document.getElementById('droneRtlIndex')?.value;
+ var rtlIndex = rtlVal !== '' && rtlVal != null ? parseInt(rtlVal, 10) : 0;
+ var useHackrf = document.getElementById('droneUseHackrf')?.checked ?? true;
fetch('/drone/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -237,32 +194,30 @@
}
function _setRunningUI(running) {
- const startBtn = document.getElementById('droneStartBtn');
- const stopBtn = document.getElementById('droneStopBtn');
- const statusEl = document.getElementById('droneStatusText');
+ var startBtn = document.getElementById('droneStartBtn');
+ var stopBtn = document.getElementById('droneStopBtn');
+ var statusEl = document.getElementById('droneStatusText');
if (startBtn) startBtn.disabled = running;
if (stopBtn) stopBtn.disabled = !running;
if (statusEl) {
statusEl.textContent = running ? 'Active' : 'Standby';
statusEl.style.color = running ? 'var(--accent-green)' : 'var(--accent-yellow)';
}
+ // Sync global state for switchMode stop phase
+ if (typeof isDroneRunning !== 'undefined') isDroneRunning = running;
}
function _updateVectorPills(activeVectors) {
- const pillMap = {
+ var pillMap = {
'REMOTE_ID': 'dronePillRemoteId',
'RTL433': 'dronePill433',
'HACKRF': 'dronePillHackrf',
};
- Object.entries(pillMap).forEach(function ([key, id]) {
- const el = document.getElementById(id);
+ Object.keys(pillMap).forEach(function (key) {
+ var el = document.getElementById(pillMap[key]);
if (el) el.classList.toggle('active', activeVectors.some(function (v) { return v.includes(key); }));
});
}
- function invalidateMap() {
- if (_map) _map.invalidateSize();
- }
-
- window.DroneMode = { init: init, destroy: destroy, invalidateMap: invalidateMap };
+ return { init: init, destroy: destroy, invalidateMap: invalidateMap };
})();
diff --git a/templates/index.html b/templates/index.html
index a283dcb..bb39da7 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -4694,6 +4694,9 @@
if (isTscmRunning) {
stopTasks.push(awaitStopAction('tscm', () => stopTscmSweep(), LOCAL_STOP_TIMEOUT_MS));
}
+ if (isDroneRunning) {
+ stopTasks.push(awaitStopAction('drone', () => fetch('/drone/stop', { method: 'POST' }), LOCAL_STOP_TIMEOUT_MS));
+ }
if (stopTasks.length) {
await Promise.allSettled(stopTasks);
@@ -4919,6 +4922,11 @@
refreshTscmDevices();
}
+ // Initialize Drone mode when selected
+ if (mode === 'drone') {
+ refreshDroneDevices();
+ }
+
// Module destroy is now handled by moduleDestroyMap above.
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
@@ -12056,6 +12064,64 @@
// Scanner and receiver logic are handled by Waterfall mode.
+ // ============================================
+ // Drone Intelligence Functions
+ // ============================================
+ var isDroneRunning = false;
+
+ async function refreshDroneDevices() {
+ try {
+ const resp = await fetch('/drone/devices');
+ const data = await resp.json();
+ const devs = data.devices || {};
+
+ const wifiSel = document.getElementById('droneWifiIface');
+ if (wifiSel) {
+ wifiSel.innerHTML = '';
+ if (devs.wifi_interfaces && devs.wifi_interfaces.length > 0) {
+ devs.wifi_interfaces.forEach(iface => {
+ const opt = document.createElement('option');
+ opt.value = iface.name;
+ opt.textContent = iface.display_name || iface.name;
+ wifiSel.appendChild(opt);
+ });
+ } else {
+ wifiSel.innerHTML = '
';
+ }
+ }
+
+ const sdrSel = document.getElementById('droneRtlIndex');
+ if (sdrSel) {
+ sdrSel.innerHTML = '';
+ if (devs.sdr_devices && devs.sdr_devices.length > 0) {
+ devs.sdr_devices.forEach(dev => {
+ const opt = document.createElement('option');
+ opt.value = dev.index !== undefined ? dev.index : 0;
+ opt.textContent = dev.display_name || dev.name || 'SDR Device';
+ sdrSel.appendChild(opt);
+ });
+ } else {
+ sdrSel.innerHTML = '
';
+ }
+ }
+
+ const warnEl = document.getElementById('droneDeviceWarnings');
+ if (warnEl) {
+ if (!data.running_as_root) {
+ warnEl.textContent = 'Not running as root — WiFi monitor mode may be unavailable.';
+ warnEl.style.display = 'block';
+ } else {
+ warnEl.style.display = 'none';
+ }
+ }
+ } catch (e) {
+ const wifiSel = document.getElementById('droneWifiIface');
+ if (wifiSel) wifiSel.innerHTML = '
';
+ const sdrSel = document.getElementById('droneRtlIndex');
+ if (sdrSel) sdrSel.innerHTML = '
';
+ }
+ }
+
// ============================================
// TSCM (Counter-Surveillance) Functions
// ============================================
diff --git a/templates/partials/modes/drone.html b/templates/partials/modes/drone.html
index 9cea10f..c98e82c 100644
--- a/templates/partials/modes/drone.html
+++ b/templates/partials/modes/drone.html
@@ -19,7 +19,7 @@