const express = require('express'); const router = express.Router(); const Joi = require('joi'); const { validateRequest } = require('../middleware/validation'); const { Heartbeat, Device, DroneDetection } = require('../models'); const AlertService = require('../services/alertService'); const DroneTrackingService = require('../services/droneTrackingService'); const { getDroneTypeInfo, getDroneTypeName } = require('../utils/droneTypes'); // Configuration for debugging and data storage const DEBUG_CONFIG = { storeHeartbeats: process.env.STORE_HEARTBEATS === 'true', // Store heartbeat data for debugging storeNoneDetections: process.env.STORE_DRONE_TYPE0 === 'true', // Store drone_type 0 for debugging logAllDetections: process.env.LOG_ALL_DETECTIONS === 'true' // Log all detection data }; // 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})`); } }); // Simplified unified schema - validate based on payload type const detectorSchema = Joi.alternatives().try( // Heartbeat schema Joi.object({ type: Joi.string().valid('heartbeat').required(), key: Joi.string().required(), // Optional heartbeat fields device_id: Joi.number().integer().optional(), geo_lat: Joi.number().min(-90).max(90).optional(), geo_lon: Joi.number().min(-180).max(180).optional(), location_description: Joi.string().optional(), signal_strength: Joi.number().integer().optional(), battery_level: Joi.number().integer().min(0).max(100).optional(), temperature: Joi.number().optional(), uptime: Joi.number().integer().min(0).optional(), memory_usage: Joi.number().integer().min(0).max(100).optional(), firmware_version: Joi.string().optional() }), // Detection schema Joi.object({ device_id: Joi.number().integer().required(), geo_lat: Joi.number().min(-90).max(90).required(), geo_lon: Joi.number().min(-180).max(180).required(), device_timestamp: Joi.number().integer().min(0).required(), drone_type: Joi.number().integer().min(0).required(), rssi: Joi.number().required(), freq: Joi.number().required(), drone_id: Joi.number().integer().required(), // Optional detection fields confidence_level: Joi.number().min(0).max(1).optional(), signal_duration: Joi.number().integer().min(0).optional() }) ); // POST /api/detectors - Unified endpoint for heartbeats and detections router.post('/', validateRequest(detectorSchema), async (req, res) => { try { // Log the full incoming payload for debugging console.log('📦 Full payload received:', JSON.stringify(req.body, null, 2)); console.log('📡 Request headers:', JSON.stringify({ 'user-agent': req.headers['user-agent'], 'content-type': req.headers['content-type'], 'content-length': req.headers['content-length'] }, null, 2)); // Determine if this is a heartbeat or detection based on payload if (req.body.type === 'heartbeat') { return await handleHeartbeat(req, res); } else if (req.body.device_id) { return await handleDetection(req, res); } else { return res.status(400).json({ success: false, error: 'Invalid payload: must be either heartbeat or detection format' }); } } catch (error) { console.error('Error in detectors endpoint:', error); res.status(500).json({ success: false, error: 'Internal server error' }); } }); // Handle heartbeat payload async function handleHeartbeat(req, res) { const { type, key, device_id, geo_lat, geo_lon, location_description, ...heartbeatData } = req.body; console.log(`💓 Heartbeat received from device key: ${key}`); console.log('💗 Complete heartbeat data:', JSON.stringify(req.body, null, 2)); // Use device_id if provided, otherwise use key as device identifier let deviceId = device_id || key; // Convert to integer if it's a numeric string if (typeof deviceId === 'string' && /^\d+$/.test(deviceId)) { deviceId = parseInt(deviceId); } console.log(`📌 Using device ID: ${deviceId}`); // Check if device exists and is approved let device = await Device.findOne({ where: { id: deviceId } }); if (!device) { // Create new device as unapproved with coordinates if provided const deviceData = { id: deviceId, name: `Device ${deviceId}`, last_heartbeat: new Date(), is_approved: false }; // Add coordinates if provided in heartbeat if (geo_lat && geo_lon) { deviceData.geo_lat = geo_lat; deviceData.geo_lon = geo_lon; console.log(`📍 Setting device coordinates: ${geo_lat}, ${geo_lon}`); } // Add location description if provided if (location_description) { deviceData.location_description = location_description; console.log(`📍 Setting device location: ${location_description}`); } device = await Device.create(deviceData); // Emit notification for new device requiring approval req.io.emit('new_device_pending', { device_id: deviceId, device_key: key, timestamp: new Date().toISOString(), message: `New device ${deviceId} (${key}) requires approval` }); console.log(`⚠️ New unapproved device ${deviceId} created, awaiting approval`); return res.status(202).json({ success: false, error: 'Device not approved', message: 'Device has been registered but requires approval before it can send data', device_id: deviceId, approval_required: true }); } if (!device.is_approved) { console.log(`🚫 Heartbeat rejected from unapproved device ${deviceId}`); // Emit reminder notification req.io.emit('device_approval_reminder', { device_id: deviceId, device_key: key, timestamp: new Date().toISOString(), message: `Device ${deviceId} (${key}) still awaiting approval` }); return res.status(403).json({ success: false, error: 'Device not approved', message: 'Device requires approval before it can send data', device_id: deviceId, approval_required: true }); } // Device exists and is approved - continue with heartbeat processing // Update device's last heartbeat await device.update({ last_heartbeat: new Date() }); // Create heartbeat record with all optional fields const heartbeat = await Heartbeat.create({ device_id: deviceId, device_key: key, ...heartbeatData, received_at: new Date() }); // Emit real-time update via Socket.IO (from original heartbeat route) req.io.emit('device_heartbeat', { device_id: deviceId, device_key: key, timestamp: heartbeat.received_at, status: 'online', ...heartbeatData }); console.log(`✅ Heartbeat recorded for device ${deviceId}`); return res.status(201).json({ success: true, data: heartbeat, message: 'Heartbeat recorded successfully' }); } // Handle detection payload async function handleDetection(req, res) { const detectionData = req.body; // Get drone type information const droneTypeInfo = getDroneTypeInfo(detectionData.drone_type); console.log(`🚁 Drone detection received from device ${detectionData.device_id}: drone_id=${detectionData.drone_id}, type=${detectionData.drone_type} (${droneTypeInfo.name}), rssi=${detectionData.rssi}`); console.log(`🎯 Drone Type Details: ${droneTypeInfo.category} | Threat: ${droneTypeInfo.threat_level} | ${droneTypeInfo.description}`); console.log('🔍 Complete detection data:', JSON.stringify(detectionData, null, 2)); // Check if device exists and is approved let device = await Device.findOne({ where: { id: detectionData.device_id } }); if (!device) { // Create new device as unapproved device = await Device.create({ id: detectionData.device_id, name: `Device ${detectionData.device_id}`, geo_lat: detectionData.geo_lat || 0, geo_lon: detectionData.geo_lon || 0, last_heartbeat: new Date(), is_approved: false }); // Emit notification for new device requiring approval req.io.emit('new_device_pending', { device_id: detectionData.device_id, timestamp: new Date().toISOString(), message: `New device ${detectionData.device_id} requires approval` }); console.log(`⚠️ New unapproved device ${detectionData.device_id} created, awaiting approval`); return res.status(202).json({ success: false, error: 'Device not approved', message: 'Device has been registered but requires approval before it can send data', device_id: detectionData.device_id, approval_required: true }); } if (!device.is_approved) { console.log(`🚫 Detection rejected from unapproved device ${detectionData.device_id}`); // Emit reminder notification req.io.emit('device_approval_reminder', { device_id: detectionData.device_id, timestamp: new Date().toISOString(), message: `Device ${detectionData.device_id} still awaiting approval` }); return res.status(403).json({ success: false, error: 'Device not approved', message: 'Device requires approval before it can send data', device_id: detectionData.device_id, approval_required: true }); } // Handle drone type 0 (None) - should not trigger alarms or be stored as detection if (detectionData.drone_type === 0) { if (DEBUG_CONFIG.logAllDetections) { console.log(`🔍 Debug: Drone type 0 (None) received from device ${detectionData.device_id}`); } if (!DEBUG_CONFIG.storeNoneDetections) { // Don't store in database, just acknowledge receipt return res.status(200).json({ success: true, message: 'Heartbeat received (no detection)', stored: false, debug: DEBUG_CONFIG.logAllDetections }); } // If debugging enabled, store but mark as debug data console.log(`🐛 Debug mode: Storing drone type 0 detection for debugging`); } // Create detection record const detection = await DroneDetection.create({ ...detectionData, server_timestamp: new Date() }); // Process detection through tracking service for movement analysis (from original) const movementAnalysis = droneTracker.processDetection({ ...detectionData, server_timestamp: detection.server_timestamp }); // Emit real-time update via Socket.IO with movement analysis (from original) req.io.emit('drone_detection', { id: detection.id, device_id: detection.device_id, drone_id: detection.drone_id, drone_type: detection.drone_type, rssi: detection.rssi, freq: detection.freq, 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, geo_lat: device.geo_lat, geo_lon: device.geo_lon } }); // Process alerts asynchronously (from original) alertService.processAlert(detection, req.io).catch(error => { console.error('Alert processing error:', error); }); console.log(`✅ Detection recorded and alert processing initiated for detection ${detection.id}`); return res.status(201).json({ success: true, data: { ...detection.toJSON(), movement_analysis: movementAnalysis }, message: 'Drone detection recorded successfully' }); } // Helper function for string hashing (if key is not numeric) String.prototype.hashCode = function() { let hash = 0; if (this.length === 0) return hash; for (let i = 0; i < this.length; i++) { const char = this.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash); }; module.exports = router;