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;