Files
drone-detector/server/routes/detectors.js
2025-09-16 07:33:10 +02:00

370 lines
13 KiB
JavaScript
Raw Blame History

const express = require('express');
const router = express.Router();
const Joi = require('joi');
const { validateRequest } = require('../middleware/validation');
// Use global test models if available, otherwise use regular models
function getModels() {
if (global.__TEST_MODELS__) {
console.log('🔧 DEBUG: Using global test models in detectors route');
return global.__TEST_MODELS__;
}
return 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
storeRawPayload: process.env.STORE_RAW_PAYLOAD === 'true' // Store complete raw payload for debugging
};
// 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(),
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;
// Get models dynamically (use test models if available)
const { Device, Heartbeat } = getModels();
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) {
// Device not found - reject heartbeat and require manual registration
console.log(`<EFBFBD> Heartbeat rejected from unknown device ${deviceId} - device must be manually registered first`);
return res.status(404).json({
success: false,
error: 'Device not registered',
message: 'Device not found. Please register the device manually through the UI before sending data.',
device_id: deviceId,
registration_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 heartbeatRecord = {
device_id: deviceId,
device_key: key,
...heartbeatData,
received_at: new Date()
};
// Add raw payload if debugging is enabled
if (DEBUG_CONFIG.storeRawPayload) {
heartbeatRecord.raw_payload = req.body;
if (DEBUG_CONFIG.logAllDetections) {
console.log(`🔍 Storing heartbeat raw payload for debugging: ${JSON.stringify(req.body)}`);
}
}
const heartbeat = await Heartbeat.create(heartbeatRecord);
// 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 models dynamically (use test models if available)
const { Device, DroneDetection } = getModels();
// 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
console.log(`🔍 DEBUG: Looking for device with ID: ${detectionData.device_id} (type: ${typeof detectionData.device_id})`);
console.log(`🔍 DEBUG: Device model being used:`, Device);
console.log(`🔍 DEBUG: Sequelize instance:`, Device.sequelize.constructor.name);
// Get all devices to see what's actually in the database
const allDevices = await Device.findAll();
console.log(`🔍 DEBUG: Total devices in database: ${allDevices.length}`);
allDevices.forEach(d => {
console.log(` - Device ID: ${d.id} (type: ${typeof d.id}), name: ${d.name}, approved: ${d.is_approved}, active: ${d.is_active}`);
});
let device = await Device.findOne({ where: { id: detectionData.device_id } });
console.log(`🔍 DEBUG: Device lookup result:`, device ? `Found device ${device.id}` : 'Device not found');
if (!device) {
// Device not found - reject detection and require manual registration
console.log(`🚫 Detection rejected from unknown device ${detectionData.device_id} - device must be manually registered first`);
return res.status(404).json({
success: false,
error: 'Device not registered',
message: 'Device not found. Please register the device manually through the UI before sending data.',
device_id: detectionData.device_id,
registration_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) - store for debugging but don't trigger alarms
let isDebugDetection = false;
if (detectionData.drone_type === 0) {
if (!DEBUG_CONFIG.storeNoneDetections) {
// When debug mode is disabled, return early for drone_type 0
console.log(`🔍 Drone type 0 detection skipped - debug mode disabled`);
return res.status(200).json({
success: true,
message: 'Detection skipped - drone type 0 detections not stored when debug mode disabled',
device_id: detectionData.device_id,
debug_mode: false
});
}
if (DEBUG_CONFIG.logAllDetections) {
console.log(`🔍 Debug: Drone type 0 (None) received from device ${detectionData.device_id} - storing for debug purposes`);
}
isDebugDetection = true;
}
// Create detection record
const detectionRecord = {
...detectionData,
server_timestamp: new Date()
};
// Add raw payload if debugging is enabled
if (DEBUG_CONFIG.storeRawPayload) {
detectionRecord.raw_payload = req.body;
if (DEBUG_CONFIG.logAllDetections) {
console.log(`🔍 Storing raw payload for debugging: ${JSON.stringify(req.body)}`);
}
}
const detection = await DroneDetection.create(detectionRecord);
// 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)
// Skip real-time updates for debug detections (drone_type 0)
if (!isDebugDetection) {
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}`);
} else {
console.log(`🐛 Debug detection stored for device ${detection.device_id} (drone_type 0) - no alerts or real-time updates`);
}
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;