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 @@

WiFi Interface

- + @@ -29,7 +29,7 @@

SDR Settings

- + @@ -42,6 +42,8 @@
+ +