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; } /** * Track a new detection (alias for processDetection) * @param {Object} detection - The drone detection to track * @returns {Object} - Tracking result */ trackDetection(detection) { return this.processDetection(detection); } /** * Clear all tracking data */ clear() { this.droneHistory.clear(); this.droneProximityAlerts.clear(); } } module.exports = DroneTrackingService;