249 lines
7.6 KiB
JavaScript
249 lines
7.6 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');
|
|
|
|
// 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})`);
|
|
}
|
|
});
|
|
|
|
// Unified schema that accepts both heartbeat and detection payloads
|
|
const detectorSchema = Joi.object({
|
|
// Heartbeat fields
|
|
type: Joi.string().valid('heartbeat').when('device_id', {
|
|
not: Joi.exist(),
|
|
then: Joi.required()
|
|
}),
|
|
key: Joi.string().when('type', {
|
|
is: 'heartbeat',
|
|
then: Joi.required()
|
|
}),
|
|
|
|
// Optional heartbeat fields (from original heartbeat route)
|
|
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 fields
|
|
device_id: Joi.number().integer().when('type', {
|
|
not: 'heartbeat',
|
|
then: Joi.required()
|
|
}),
|
|
geo_lat: Joi.number().min(-90).max(90).when('device_id', {
|
|
is: Joi.exist(),
|
|
then: Joi.required()
|
|
}),
|
|
geo_lon: Joi.number().min(-180).max(180).when('device_id', {
|
|
is: Joi.exist(),
|
|
then: Joi.required()
|
|
}),
|
|
device_timestamp: Joi.number().integer().min(0).when('device_id', {
|
|
is: Joi.exist(),
|
|
then: Joi.required()
|
|
}),
|
|
drone_type: Joi.number().integer().min(0).when('device_id', {
|
|
is: Joi.exist(),
|
|
then: Joi.required()
|
|
}),
|
|
rssi: Joi.number().when('device_id', {
|
|
is: Joi.exist(),
|
|
then: Joi.required()
|
|
}),
|
|
freq: Joi.number().when('device_id', {
|
|
is: Joi.exist(),
|
|
then: Joi.required()
|
|
}),
|
|
drone_id: Joi.number().integer().when('device_id', {
|
|
is: Joi.exist(),
|
|
then: Joi.required()
|
|
}),
|
|
|
|
// Optional detection fields
|
|
confidence_level: Joi.number().min(0).max(1).optional(),
|
|
signal_duration: Joi.number().integer().min(0).optional()
|
|
}).or('type', 'device_id'); // Must have either type (heartbeat) or device_id (detection)
|
|
|
|
// POST /api/detectors - Unified endpoint for heartbeats and detections
|
|
router.post('/', validateRequest(detectorSchema), async (req, res) => {
|
|
try {
|
|
// 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}`);
|
|
|
|
// If device_id is not provided, try to find device by key
|
|
let deviceId = device_id;
|
|
if (!deviceId) {
|
|
// Try to extract device ID from key or use key as identifier
|
|
const keyMatch = key.match(/device[_-]?(\d+)/i);
|
|
deviceId = keyMatch ? parseInt(keyMatch[1]) : key.hashCode();
|
|
}
|
|
|
|
// Ensure device exists or create it
|
|
const [device] = await Device.findOrCreate({
|
|
where: { id: deviceId },
|
|
defaults: {
|
|
id: deviceId,
|
|
name: `Device ${deviceId}`,
|
|
last_heartbeat: new Date()
|
|
}
|
|
});
|
|
|
|
// 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;
|
|
|
|
console.log(`🚁 Drone detection received from device ${detectionData.device_id}: drone_id=${detectionData.drone_id}, type=${detectionData.drone_type}, rssi=${detectionData.rssi}`);
|
|
|
|
// Ensure device exists or create it (from original detection route)
|
|
const [device] = await Device.findOrCreate({
|
|
where: { id: detectionData.device_id },
|
|
defaults: {
|
|
id: detectionData.device_id,
|
|
geo_lat: detectionData.geo_lat || 0,
|
|
geo_lon: detectionData.geo_lon || 0,
|
|
last_heartbeat: new Date()
|
|
}
|
|
});
|
|
|
|
// 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;
|