diff --git a/client/src/components/MovementAlertsPanel.jsx b/client/src/components/MovementAlertsPanel.jsx new file mode 100644 index 0000000..19d40fe --- /dev/null +++ b/client/src/components/MovementAlertsPanel.jsx @@ -0,0 +1,263 @@ +import React, { useState } from 'react'; +import { format } from 'date-fns'; +import { useSocket } from '../contexts/SocketContext'; +import { + ExclamationTriangleIcon, + InformationCircleIcon, + ChevronDownIcon, + ChevronUpIcon, + SignalIcon, + ArrowTrendingUpIcon, + ArrowTrendingDownIcon, + EyeIcon +} from '@heroicons/react/24/outline'; + +const MovementAlertsPanel = () => { + const { movementAlerts, clearMovementAlerts } = useSocket(); + const [expandedAlert, setExpandedAlert] = useState(null); + const [filter, setFilter] = useState('all'); // all, critical, high, medium + + const getAlertIcon = (alertLevel) => { + if (alertLevel >= 3) return ; + if (alertLevel >= 2) return ; + return ; + }; + + const getAlertColor = (alertLevel) => { + if (alertLevel >= 3) return 'border-red-200 bg-red-50'; + if (alertLevel >= 2) return 'border-orange-200 bg-orange-50'; + return 'border-blue-200 bg-blue-50'; + }; + + const getProximityColor = (level) => { + switch (level) { + case 'VERY_CLOSE': return 'text-red-700 bg-red-100'; + case 'CLOSE': return 'text-orange-700 bg-orange-100'; + case 'MEDIUM': return 'text-yellow-700 bg-yellow-100'; + case 'FAR': return 'text-green-700 bg-green-100'; + case 'VERY_FAR': return 'text-gray-700 bg-gray-100'; + default: return 'text-gray-700 bg-gray-100'; + } + }; + + const getMovementIcon = (trend) => { + if (trend === 'STRENGTHENING') return ; + if (trend === 'WEAKENING') return ; + return ; + }; + + const filteredAlerts = movementAlerts.filter(alert => { + if (filter === 'all') return true; + if (filter === 'critical') return alert.analysis.alertLevel >= 3; + if (filter === 'high') return alert.analysis.alertLevel === 2; + if (filter === 'medium') return alert.analysis.alertLevel === 1; + return true; + }); + + const droneTypes = { + 1: "DJI Mavic", + 2: "Racing Drone", + 3: "DJI Phantom", + 4: "Fixed Wing", + 5: "Surveillance", + 0: "Unknown" + }; + + return ( +
+
+
+
+

Movement Alerts

+ {movementAlerts.length > 0 && ( + + {movementAlerts.length} + + )} +
+ +
+ + + {movementAlerts.length > 0 && ( + + )} +
+
+
+ +
+ {filteredAlerts.length === 0 ? ( +
+ +

No movement alerts

+

Drone movement patterns will appear here

+
+ ) : ( + filteredAlerts.map((alert, index) => ( +
+
+
+
+ {getAlertIcon(alert.analysis.alertLevel)} + +
+
+

+ Drone {alert.droneId} • Device {alert.deviceId} +

+ + {format(new Date(alert.timestamp), 'HH:mm:ss')} + +
+ +

+ {alert.analysis.description} +

+ + {alert.analysis.rssiTrend && ( +
+
+ {getMovementIcon(alert.analysis.rssiTrend.trend)} + + {alert.analysis.rssiTrend.trend.toLowerCase()} + +
+ +
+ RSSI: + + {alert.detection.rssi}dBm + +
+ + {alert.analysis.proximityLevel && ( + + {alert.analysis.proximityLevel.replace('_', ' ')} + + )} +
+ )} +
+
+ + +
+ + {expandedAlert === index && ( +
+
+
+ Drone Type: +
+ {droneTypes[alert.detection.drone_type] || 'Unknown'} +
+
+ +
+ Frequency: +
{alert.detection.freq}MHz
+
+ +
+ Confidence: +
+ {(alert.detection.confidence_level * 100).toFixed(0)}% +
+
+ +
+ Signal Duration: +
+ {(alert.detection.signal_duration / 1000).toFixed(1)}s +
+
+
+ + {alert.analysis.movement && ( +
+ Movement Pattern: +
+
Pattern: {alert.analysis.movement.pattern}
+ {alert.analysis.movement.speed > 0 && ( +
Speed: {alert.analysis.movement.speed.toFixed(1)} m/s
+ )} + {alert.analysis.movement.totalDistance > 0 && ( +
Distance: {(alert.analysis.movement.totalDistance * 1000).toFixed(0)}m
+ )} +
+
+ )} + + {alert.analysis.detectionCount && ( +
+ Tracking Stats: +
+
{alert.analysis.detectionCount} detections over {(alert.analysis.timeTracked / 60).toFixed(1)} minutes
+
+
+ )} + + {alert.history && alert.history.length > 0 && ( +
+ Recent RSSI History: +
+ {alert.history.slice(-5).map((point, i) => ( +
+
-50 ? 'bg-red-500' : + point.rssi > -60 ? 'bg-orange-500' : + point.rssi > -70 ? 'bg-yellow-500' : + 'bg-green-500' + }`}> + {point.rssi} +
+
+ {format(new Date(point.timestamp), 'HH:mm')} +
+
+ ))} +
+
+ )} +
+ )} +
+
+ )) + )} +
+
+ ); +}; + +export default MovementAlertsPanel; diff --git a/client/src/contexts/SocketContext.jsx b/client/src/contexts/SocketContext.jsx index 81af457..e4b3a7f 100644 --- a/client/src/contexts/SocketContext.jsx +++ b/client/src/contexts/SocketContext.jsx @@ -10,6 +10,8 @@ export const SocketProvider = ({ children }) => { const [connected, setConnected] = useState(false); const [recentDetections, setRecentDetections] = useState([]); const [deviceStatus, setDeviceStatus] = useState({}); + const [movementAlerts, setMovementAlerts] = useState([]); + const [droneTracking, setDroneTracking] = useState(new Map()); const { isAuthenticated } = useAuth(); useEffect(() => { @@ -49,9 +51,25 @@ export const SocketProvider = ({ children }) => { setRecentDetections(prev => [detection, ...prev.slice(0, 49)]); // Keep last 50 + // Update drone tracking + if (detection.movement_analysis) { + const trackingKey = `${detection.drone_id}_${detection.device_id}`; + setDroneTracking(prev => { + const newTracking = new Map(prev); + newTracking.set(trackingKey, { + droneId: detection.drone_id, + deviceId: detection.device_id, + lastDetection: detection, + analysis: detection.movement_analysis, + timestamp: Date.now() + }); + return newTracking; + }); + } + // Show toast notification toast.error( - `Drone detected by ${detection.device.name || `Device ${detection.device_id}`}`, + `Drone ${detection.drone_id} detected by ${detection.device?.name || `Device ${detection.device_id}`}`, { duration: 5000, icon: '🚨', @@ -59,6 +77,32 @@ export const SocketProvider = ({ children }) => { ); }); + // Listen for drone movement alerts + newSocket.on('drone_movement_alert', (alertData) => { + console.log('Drone movement alert:', alertData); + + setMovementAlerts(prev => [alertData, ...prev.slice(0, 19)]); // Keep last 20 alerts + + // Show priority-based notifications + const alertIcon = alertData.analysis.alertLevel >= 3 ? '🚨' : + alertData.analysis.alertLevel >= 2 ? '⚠️' : '📍'; + + const alertColor = alertData.analysis.alertLevel >= 3 ? 'error' : + alertData.analysis.alertLevel >= 2 ? 'warning' : 'info'; + + toast[alertColor === 'error' ? 'error' : alertColor === 'warning' ? 'error' : 'info']( + alertData.analysis.description, + { + duration: alertData.analysis.alertLevel >= 2 ? 10000 : 6000, + icon: alertIcon, + style: { + background: alertData.analysis.alertLevel >= 3 ? '#fee2e2' : + alertData.analysis.alertLevel >= 2 ? '#fef3c7' : '#e0f2fe' + } + } + ); + }); + // Listen for device heartbeats newSocket.on('device_heartbeat', (heartbeat) => { console.log('Device heartbeat:', heartbeat); @@ -109,14 +153,27 @@ export const SocketProvider = ({ children }) => { setRecentDetections([]); }; + const clearMovementAlerts = () => { + setMovementAlerts([]); + }; + + const getDroneTracking = (droneId, deviceId) => { + const trackingKey = `${droneId}_${deviceId}`; + return droneTracking.get(trackingKey); + }; + const value = { socket, connected, recentDetections, deviceStatus, + movementAlerts, + droneTracking, joinDeviceRoom, leaveDeviceRoom, - clearRecentDetections + clearRecentDetections, + clearMovementAlerts, + getDroneTracking }; return ( diff --git a/client/src/pages/Dashboard.jsx b/client/src/pages/Dashboard.jsx index f9152a3..6c158d1 100644 --- a/client/src/pages/Dashboard.jsx +++ b/client/src/pages/Dashboard.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useSocket } from '../contexts/SocketContext'; +import MovementAlertsPanel from '../components/MovementAlertsPanel'; import api from '../services/api'; import { ServerIcon, @@ -30,7 +31,8 @@ const Dashboard = () => { const [deviceActivity, setDeviceActivity] = useState([]); const [recentActivity, setRecentActivity] = useState([]); const [loading, setLoading] = useState(true); - const { recentDetections, deviceStatus, connected } = useSocket(); + const [showMovementAlerts, setShowMovementAlerts] = useState(true); + const { recentDetections, deviceStatus, connected, movementAlerts } = useSocket(); useEffect(() => { fetchDashboardData(); @@ -318,6 +320,70 @@ const Dashboard = () => { + + {/* Movement Alerts Panel */} +
+
+ +
+ + {/* Movement Summary Stats */} +
+

+ Movement Tracking +

+ +
+
+
+
Critical Alerts
+
Very close approaches
+
+
+ {movementAlerts.filter(a => a.analysis.alertLevel >= 3).length} +
+
+ +
+
+
High Priority
+
Approaching drones
+
+
+ {movementAlerts.filter(a => a.analysis.alertLevel === 2).length} +
+
+ +
+
+
Medium Priority
+
Movement changes
+
+
+ {movementAlerts.filter(a => a.analysis.alertLevel === 1).length} +
+
+ +
+
+
+ Total Tracked: + {movementAlerts.length} events +
+
+ Last Alert: + + {movementAlerts.length > 0 + ? format(new Date(movementAlerts[0].timestamp), 'HH:mm:ss') + : 'None' + } + +
+
+
+
+
+
); }; diff --git a/client/src/pages/MapView.jsx b/client/src/pages/MapView.jsx index bd78eb6..411e9d0 100644 --- a/client/src/pages/MapView.jsx +++ b/client/src/pages/MapView.jsx @@ -468,6 +468,55 @@ const DroneDetectionPopup = ({ detection, age, droneTypes }) => ( + {/* Movement Analysis */} + {detection.movement_analysis && ( +
+ Movement Analysis: +
+
= 3 ? 'bg-red-100 text-red-800' : + detection.movement_analysis.alertLevel >= 2 ? 'bg-orange-100 text-orange-800' : + detection.movement_analysis.alertLevel >= 1 ? 'bg-blue-100 text-blue-800' : + 'bg-gray-100 text-gray-800' + }`}> + {detection.movement_analysis.description} +
+ + {detection.movement_analysis.rssiTrend && ( +
+ Trend: + + {detection.movement_analysis.rssiTrend.trend} + {detection.movement_analysis.rssiTrend.change !== 0 && ( + + ({detection.movement_analysis.rssiTrend.change > 0 ? '+' : ''}{detection.movement_analysis.rssiTrend.change.toFixed(1)}dB) + + )} + +
+ )} + + {detection.movement_analysis.proximityLevel && ( +
+ Proximity: + + {detection.movement_analysis.proximityLevel.replace('_', ' ')} + +
+ )} +
+
+ )} +
Detected by: Device {detection.device_id}
diff --git a/server/routes/droneDetection.js b/server/routes/droneDetection.js index 3e7de9b..78051c3 100644 --- a/server/routes/droneDetection.js +++ b/server/routes/droneDetection.js @@ -4,10 +4,32 @@ const Joi = require('joi'); const { DroneDetection, Device } = require('../models'); const { Op } = require('sequelize'); const AlertService = require('../services/alertService'); +const DroneTrackingService = require('../services/droneTrackingService'); const { validateRequest } = require('../middleware/validation'); -// Initialize AlertService instance +// Initialize services const alertService = new AlertService(); +const droneTracker = new DroneTrackingService(); + +// Handle movement alerts from the tracking service +droneTracker.on('movement_alert', (alertData) => { + const { io } = require('../index'); + if (io) { + // Emit to dashboard with detailed movement information + io.emitToDashboard('drone_movement_alert', { + ...alertData, + timestamp: new Date().toISOString() + }); + + // Emit to specific device room + io.emitToDevice(alertData.deviceId, 'drone_movement_alert', { + ...alertData, + timestamp: new Date().toISOString() + }); + + console.log(`🚨 Movement Alert: ${alertData.analysis.description} (Drone ${alertData.droneId})`); + } +}); // Validation schema for drone detection const droneDetectionSchema = Joi.object({ @@ -45,7 +67,13 @@ router.post('/', validateRequest(droneDetectionSchema), async (req, res) => { server_timestamp: new Date() }); - // Emit real-time update via Socket.IO + // Process detection through tracking service for movement analysis + const movementAnalysis = droneTracker.processDetection({ + ...detectionData, + server_timestamp: detection.server_timestamp + }); + + // Emit real-time update via Socket.IO with movement analysis req.io.emit('drone_detection', { id: detection.id, device_id: detection.device_id, @@ -56,6 +84,9 @@ router.post('/', validateRequest(droneDetectionSchema), async (req, res) => { geo_lat: detection.geo_lat, geo_lon: detection.geo_lon, server_timestamp: detection.server_timestamp, + confidence_level: detection.confidence_level, + signal_duration: detection.signal_duration, + movement_analysis: movementAnalysis, device: { id: device.id, name: device.name, @@ -229,4 +260,55 @@ router.get('/:id', async (req, res) => { } }); +// GET /api/detections/tracking/active - Get active drone tracking information +router.get('/tracking/active', async (req, res) => { + try { + const activeTracking = droneTracker.getAllActiveTracking(); + + res.json({ + success: true, + data: { + active_drones: activeTracking.length, + tracking_data: activeTracking + } + }); + + } catch (error) { + console.error('Error fetching active tracking:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch active tracking data', + error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' + }); + } +}); + +// GET /api/detections/tracking/:droneId/:deviceId - Get specific drone tracking +router.get('/tracking/:droneId/:deviceId', async (req, res) => { + try { + const { droneId, deviceId } = req.params; + const trackingData = droneTracker.getDroneStatus(droneId, deviceId); + + if (!trackingData) { + return res.status(404).json({ + success: false, + message: 'No tracking data found for this drone-device combination' + }); + } + + res.json({ + success: true, + data: trackingData + }); + + } catch (error) { + console.error('Error fetching drone tracking:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch drone tracking data', + error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' + }); + } +}); + module.exports = router; diff --git a/server/services/droneTrackingService.js b/server/services/droneTrackingService.js new file mode 100644 index 0000000..b8eaaf8 --- /dev/null +++ b/server/services/droneTrackingService.js @@ -0,0 +1,285 @@ +const EventEmitter = require('events'); + +class DroneTrackingService extends EventEmitter { + constructor() { + super(); + this.droneHistory = new Map(); // Map of drone_id -> detection history + this.droneProximityAlerts = new Map(); // Map of drone_id -> current status + this.proximityThresholds = { + VERY_CLOSE: -40, // < -40dBm + CLOSE: -50, // -40 to -50dBm + MEDIUM: -60, // -50 to -60dBm + FAR: -70, // -60 to -70dBm + VERY_FAR: -80 // -70 to -80dBm + }; + + // Clean up old history every 30 minutes + setInterval(() => this.cleanupOldHistory(), 30 * 60 * 1000); + } + + processDetection(detection) { + const droneId = detection.drone_id; + const deviceId = detection.device_id; + const droneKey = `${droneId}_${deviceId}`; + + // Get or create history for this drone-device pair + if (!this.droneHistory.has(droneKey)) { + this.droneHistory.set(droneKey, []); + } + + const history = this.droneHistory.get(droneKey); + const now = Date.now(); + + // Add current detection to history + const detectionRecord = { + timestamp: now, + rssi: detection.rssi, + geo_lat: detection.geo_lat, + geo_lon: detection.geo_lon, + confidence: detection.confidence_level, + freq: detection.freq, + signal_duration: detection.signal_duration + }; + + history.push(detectionRecord); + + // Keep only last 50 detections per drone-device pair + if (history.length > 50) { + history.splice(0, history.length - 50); + } + + // Analyze movement and proximity trends + const analysis = this.analyzeMovementTrend(history, detection); + + // Emit movement alerts if significant changes detected + if (analysis.alertLevel > 0) { + this.emit('movement_alert', { + droneId, + deviceId, + detection, + analysis, + history: history.slice(-5) // Last 5 detections for context + }); + } + + return analysis; + } + + analyzeMovementTrend(history, currentDetection) { + if (history.length < 2) { + return this.createAnalysis('INITIAL', 0, 'First detection'); + } + + const recent = history.slice(-5); // Last 5 detections + const current = recent[recent.length - 1]; + const previous = recent[recent.length - 2]; + + // Calculate RSSI trend + const rssiTrend = this.calculateRSSITrend(recent); + const proximityLevel = this.getProximityLevel(current.rssi); + const previousProximityLevel = this.getProximityLevel(previous.rssi); + + // Calculate distance trend (if we have coordinates) + let distanceTrend = null; + if (recent.length >= 2 && this.hasValidCoordinates(recent)) { + distanceTrend = this.calculateDistanceTrend(recent); + } + + // Calculate movement speed and direction + const movement = this.calculateMovementMetrics(recent); + + // Determine alert level and type + let alertLevel = 0; + let alertType = 'MONITORING'; + let description = 'Drone being tracked'; + + // High priority alerts + if (proximityLevel === 'VERY_CLOSE' && rssiTrend.trend === 'STRENGTHENING') { + alertLevel = 3; + alertType = 'CRITICAL_APPROACH'; + description = `🚨 CRITICAL: Drone very close and approaching (${current.rssi}dBm)`; + } else if (proximityLevel === 'CLOSE' && rssiTrend.trend === 'STRENGTHENING') { + alertLevel = 2; + alertType = 'HIGH_APPROACH'; + description = `⚠️ HIGH: Drone approaching detector (${current.rssi}dBm, trend: +${rssiTrend.change.toFixed(1)}dB)`; + } else if (rssiTrend.change > 10 && rssiTrend.trend === 'STRENGTHENING') { + alertLevel = 2; + alertType = 'RAPID_APPROACH'; + description = `📈 RAPID: Drone rapidly approaching (+${rssiTrend.change.toFixed(1)}dB in ${rssiTrend.timeSpan}s)`; + } + + // Medium priority alerts + else if (proximityLevel !== previousProximityLevel) { + alertLevel = 1; + alertType = 'PROXIMITY_CHANGE'; + description = `📍 Drone moved from ${previousProximityLevel} to ${proximityLevel} range`; + } else if (Math.abs(rssiTrend.change) > 5) { + alertLevel = 1; + alertType = rssiTrend.trend === 'STRENGTHENING' ? 'APPROACHING' : 'DEPARTING'; + description = `${rssiTrend.trend === 'STRENGTHENING' ? '🔴' : '🟢'} Drone ${rssiTrend.trend.toLowerCase()} (${rssiTrend.change > 0 ? '+' : ''}${rssiTrend.change.toFixed(1)}dB)`; + } + + return this.createAnalysis(alertType, alertLevel, description, { + rssiTrend, + proximityLevel, + previousProximityLevel, + distanceTrend, + movement, + detectionCount: history.length, + timeTracked: (current.timestamp - history[0].timestamp) / 1000 + }); + } + + calculateRSSITrend(detections) { + if (detections.length < 2) return { trend: 'STABLE', change: 0, timeSpan: 0 }; + + const latest = detections[detections.length - 1]; + const oldest = detections[0]; + const change = latest.rssi - oldest.rssi; + const timeSpan = (latest.timestamp - oldest.timestamp) / 1000; + + let trend = 'STABLE'; + if (change > 2) trend = 'STRENGTHENING'; + else if (change < -2) trend = 'WEAKENING'; + + return { trend, change, timeSpan, rate: change / Math.max(timeSpan, 1) }; + } + + calculateDistanceTrend(detections) { + if (detections.length < 2) return null; + + const distances = detections.map((d, i) => { + if (i === 0) return 0; + return this.calculateDistance( + detections[i-1].geo_lat, detections[i-1].geo_lon, + d.geo_lat, d.geo_lon + ); + }).filter(d => d > 0); + + if (distances.length === 0) return null; + + const totalDistance = distances.reduce((sum, d) => sum + d, 0); + const avgSpeed = totalDistance / ((detections[detections.length - 1].timestamp - detections[0].timestamp) / 1000); + + return { totalDistance, avgSpeed, movementPoints: distances.length }; + } + + calculateMovementMetrics(detections) { + if (detections.length < 3) return { speed: 0, direction: null, pattern: 'INSUFFICIENT_DATA' }; + + const movements = []; + for (let i = 1; i < detections.length; i++) { + const prev = detections[i - 1]; + const curr = detections[i]; + const timeDiff = (curr.timestamp - prev.timestamp) / 1000; + + if (timeDiff > 0 && this.hasValidCoordinates([prev, curr])) { + const distance = this.calculateDistance(prev.geo_lat, prev.geo_lon, curr.geo_lat, curr.geo_lon); + const speed = (distance * 1000) / timeDiff; // m/s + movements.push({ distance, speed, timeDiff }); + } + } + + if (movements.length === 0) return { speed: 0, direction: null, pattern: 'STATIONARY' }; + + const avgSpeed = movements.reduce((sum, m) => sum + m.speed, 0) / movements.length; + const totalDistance = movements.reduce((sum, m) => sum + m.distance, 0); + + // Determine movement pattern + let pattern = 'MOVING'; + if (avgSpeed < 1) pattern = 'HOVERING'; + else if (avgSpeed > 10) pattern = 'FAST_MOVING'; + else if (totalDistance < 0.1) pattern = 'CIRCLING'; + + return { speed: avgSpeed, totalDistance, pattern, movements: movements.length }; + } + + getProximityLevel(rssi) { + if (rssi >= this.proximityThresholds.VERY_CLOSE) return 'VERY_CLOSE'; + if (rssi >= this.proximityThresholds.CLOSE) return 'CLOSE'; + if (rssi >= this.proximityThresholds.MEDIUM) return 'MEDIUM'; + if (rssi >= this.proximityThresholds.FAR) return 'FAR'; + return 'VERY_FAR'; + } + + calculateDistance(lat1, lon1, lat2, lon2) { + const R = 6371; // Earth radius in km + const dLat = this.toRad(lat2 - lat1); + const dLon = this.toRad(lon2 - lon1); + const a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) * + Math.sin(dLon/2) * Math.sin(dLon/2); + return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * R; + } + + toRad(deg) { + return deg * (Math.PI/180); + } + + hasValidCoordinates(detections) { + return detections.every(d => d.geo_lat && d.geo_lon && + Math.abs(d.geo_lat) <= 90 && + Math.abs(d.geo_lon) <= 180); + } + + createAnalysis(alertType, alertLevel, description, details = {}) { + return { + alertType, + alertLevel, + description, + timestamp: Date.now(), + ...details + }; + } + + cleanupOldHistory() { + const cutoffTime = Date.now() - (2 * 60 * 60 * 1000); // 2 hours ago + + for (const [key, history] of this.droneHistory.entries()) { + const filtered = history.filter(record => record.timestamp > cutoffTime); + if (filtered.length === 0) { + this.droneHistory.delete(key); + } else { + this.droneHistory.set(key, filtered); + } + } + } + + getDroneStatus(droneId, deviceId) { + const droneKey = `${droneId}_${deviceId}`; + const history = this.droneHistory.get(droneKey) || []; + + if (history.length === 0) return null; + + const latest = history[history.length - 1]; + const analysis = this.analyzeMovementTrend(history, { rssi: latest.rssi }); + + return { + droneId, + deviceId, + lastSeen: latest.timestamp, + currentRSSI: latest.rssi, + proximityLevel: this.getProximityLevel(latest.rssi), + detectionCount: history.length, + analysis, + recentHistory: history.slice(-10) + }; + } + + getAllActiveTracking() { + const activeTracking = []; + const fiveMinutesAgo = Date.now() - (5 * 60 * 1000); + + for (const [key, history] of this.droneHistory.entries()) { + const latest = history[history.length - 1]; + if (latest.timestamp > fiveMinutesAgo) { + const [droneId, deviceId] = key.split('_'); + activeTracking.push(this.getDroneStatus(droneId, deviceId)); + } + } + + return activeTracking; + } +} + +module.exports = DroneTrackingService;