346 lines
12 KiB
JavaScript
346 lines
12 KiB
JavaScript
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(),
|
|
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, ...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
|
|
device = await Device.create({
|
|
id: deviceId,
|
|
name: `Device ${deviceId}`,
|
|
last_heartbeat: new Date(),
|
|
is_approved: false
|
|
});
|
|
|
|
// 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
|
|
});
|
|
}
|
|
|
|
// 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;
|