diff --git a/docker-start.bat b/docker-start.bat new file mode 100644 index 0000000..e69de29 diff --git a/docker-start.sh b/docker-start.sh new file mode 100644 index 0000000..e69de29 diff --git a/server/routes/detectors.js b/server/routes/detectors.js new file mode 100644 index 0000000..85bb6c3 --- /dev/null +++ b/server/routes/detectors.js @@ -0,0 +1,248 @@ +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; diff --git a/server/routes/droneDetection.js b/server/routes/droneDetection.js deleted file mode 100644 index 78051c3..0000000 --- a/server/routes/droneDetection.js +++ /dev/null @@ -1,314 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const Joi = require('joi'); -const { DroneDetection, Device } = require('../models'); -const { Op } = require('sequelize'); -const AlertService = require('../services/alertService'); -const DroneTrackingService = require('../services/droneTrackingService'); -const { validateRequest } = require('../middleware/validation'); - -// 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})`); - } -}); - -// Validation schema for drone detection -const droneDetectionSchema = Joi.object({ - device_id: Joi.number().integer().required(), - geo_lat: Joi.number().min(-90).max(90).default(0), - geo_lon: Joi.number().min(-180).max(180).default(0), - device_timestamp: Joi.number().integer().min(0).default(0), - drone_type: Joi.number().integer().min(0).default(0), - rssi: Joi.number().integer().default(0), - freq: Joi.number().integer().required(), - drone_id: Joi.number().integer().required(), - confidence_level: Joi.number().min(0).max(1).optional(), - signal_duration: Joi.number().integer().min(0).optional() -}); - -// POST /api/detections - Receive drone detection data -router.post('/', validateRequest(droneDetectionSchema), async (req, res) => { - try { - const detectionData = req.body; - - // Ensure device exists or create it - 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 the detection record - const detection = await DroneDetection.create({ - ...detectionData, - server_timestamp: new Date() - }); - - // Process detection through tracking service for movement analysis - const movementAnalysis = droneTracker.processDetection({ - ...detectionData, - server_timestamp: detection.server_timestamp - }); - - // Emit real-time update via Socket.IO with movement analysis - 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 - alertService.processAlert(detection).catch(error => { - console.error('Alert processing error:', error); - }); - - res.status(201).json({ - success: true, - data: detection, - message: 'Drone detection recorded successfully' - }); - - } catch (error) { - console.error('Error creating drone detection:', error); - res.status(500).json({ - success: false, - message: 'Failed to record drone detection', - error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' - }); - } -}); - -// GET /api/detections - Get drone detections with filtering -router.get('/', async (req, res) => { - try { - const { - device_id, - drone_id, - start_date, - end_date, - limit = 100, - offset = 0, - order = 'DESC' - } = req.query; - - const whereClause = {}; - - if (device_id) whereClause.device_id = device_id; - if (drone_id) whereClause.drone_id = drone_id; - - if (start_date || end_date) { - whereClause.server_timestamp = {}; - if (start_date) whereClause.server_timestamp[Op.gte] = new Date(start_date); - if (end_date) whereClause.server_timestamp[Op.lte] = new Date(end_date); - } - - const detections = await DroneDetection.findAndCountAll({ - where: whereClause, - include: [{ - model: Device, - as: 'device', - attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description'] - }], - limit: Math.min(parseInt(limit), 1000), // Max 1000 records - offset: parseInt(offset), - order: [['server_timestamp', order]] - }); - - res.json({ - success: true, - data: detections.rows, - pagination: { - total: detections.count, - limit: parseInt(limit), - offset: parseInt(offset), - pages: Math.ceil(detections.count / parseInt(limit)) - } - }); - - } catch (error) { - console.error('Error fetching drone detections:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch drone detections', - error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' - }); - } -}); - -// GET /api/detections/stats - Get detection statistics -router.get('/stats', async (req, res) => { - try { - const { device_id, hours = 24 } = req.query; - - const whereClause = { - server_timestamp: { - [Op.gte]: new Date(Date.now() - hours * 60 * 60 * 1000) - } - }; - - if (device_id) whereClause.device_id = device_id; - - const [totalDetections, uniqueDrones, uniqueDevices, avgRssi] = await Promise.all([ - DroneDetection.count({ where: whereClause }), - DroneDetection.count({ - where: whereClause, - distinct: true, - col: 'drone_id' - }), - DroneDetection.count({ - where: whereClause, - distinct: true, - col: 'device_id' - }), - DroneDetection.findAll({ - where: whereClause, - attributes: [ - [sequelize.fn('AVG', sequelize.col('rssi')), 'avg_rssi'] - ] - }) - ]); - - res.json({ - success: true, - data: { - total_detections: totalDetections, - unique_drones: uniqueDrones, - active_devices: uniqueDevices, - average_rssi: Math.round(avgRssi[0]?.dataValues?.avg_rssi || 0), - time_period_hours: hours - } - }); - - } catch (error) { - console.error('Error fetching detection stats:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch detection statistics', - error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' - }); - } -}); - -// GET /api/detections/:id - Get specific detection -router.get('/:id', async (req, res) => { - try { - const detection = await DroneDetection.findByPk(req.params.id, { - include: [{ - model: Device, - as: 'device', - attributes: ['id', 'name', 'geo_lat', 'geo_lon', 'location_description'] - }] - }); - - if (!detection) { - return res.status(404).json({ - success: false, - message: 'Detection not found' - }); - } - - res.json({ - success: true, - data: detection - }); - - } catch (error) { - console.error('Error fetching detection:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch detection', - error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' - }); - } -}); - -// GET /api/detections/tracking/active - Get active drone tracking information -router.get('/tracking/active', async (req, res) => { - try { - const activeTracking = droneTracker.getAllActiveTracking(); - - res.json({ - success: true, - data: { - active_drones: activeTracking.length, - tracking_data: activeTracking - } - }); - - } catch (error) { - console.error('Error fetching active tracking:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch active tracking data', - error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' - }); - } -}); - -// GET /api/detections/tracking/:droneId/:deviceId - Get specific drone tracking -router.get('/tracking/:droneId/:deviceId', async (req, res) => { - try { - const { droneId, deviceId } = req.params; - const trackingData = droneTracker.getDroneStatus(droneId, deviceId); - - if (!trackingData) { - return res.status(404).json({ - success: false, - message: 'No tracking data found for this drone-device combination' - }); - } - - res.json({ - success: true, - data: trackingData - }); - - } catch (error) { - console.error('Error fetching drone tracking:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch drone tracking data', - error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' - }); - } -}); - -module.exports = router; diff --git a/server/routes/heartbeat.js b/server/routes/heartbeat.js deleted file mode 100644 index d62541d..0000000 --- a/server/routes/heartbeat.js +++ /dev/null @@ -1,199 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const Joi = require('joi'); -const { Heartbeat, Device } = require('../models'); -const { validateRequest } = require('../middleware/validation'); - -// Validation schema for heartbeat -const heartbeatSchema = Joi.object({ - type: Joi.string().valid('heartbeat').required(), - key: Joi.string().required(), - 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() -}); - -// POST /api/heartbeat - Receive heartbeat from devices -router.post('/', validateRequest(heartbeatSchema), async (req, res) => { - try { - const { type, key, device_id, ...heartbeatData } = req.body; - - // 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 - // This is a fallback for devices that only send key - const keyMatch = key.match(/device[_-]?(\d+)/i); - deviceId = keyMatch ? parseInt(keyMatch[1]) : key.hashCode(); // Simple hash if no pattern - } - - // 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 - const heartbeat = await Heartbeat.create({ - device_id: deviceId, - device_key: key, - ...heartbeatData, - received_at: new Date() - }); - - // Emit real-time update via Socket.IO - req.io.emit('device_heartbeat', { - device_id: deviceId, - device_key: key, - timestamp: heartbeat.received_at, - status: 'online', - ...heartbeatData - }); - - res.status(201).json({ - success: true, - data: heartbeat, - message: 'Heartbeat recorded successfully' - }); - - } catch (error) { - console.error('Error processing heartbeat:', error); - res.status(500).json({ - success: false, - message: 'Failed to process heartbeat', - error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' - }); - } -}); - -// GET /api/heartbeat/status - Get device status overview -router.get('/status', async (req, res) => { - try { - const devices = await Device.findAll({ - attributes: [ - 'id', - 'name', - 'geo_lat', - 'geo_lon', - 'last_heartbeat', - 'heartbeat_interval', - 'is_active' - ], - include: [{ - model: Heartbeat, - as: 'heartbeats', - limit: 1, - order: [['received_at', 'DESC']], - attributes: ['battery_level', 'signal_strength', 'temperature', 'firmware_version'] - }] - }); - - const now = new Date(); - const deviceStatus = devices.map(device => { - const timeSinceLastHeartbeat = device.last_heartbeat - ? (now - new Date(device.last_heartbeat)) / 1000 - : null; - - const expectedInterval = device.heartbeat_interval || 300; // 5 minutes default - const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2); - - return { - device_id: device.id, - name: device.name, - geo_lat: device.geo_lat, - geo_lon: device.geo_lon, - status: device.is_active ? (isOnline ? 'online' : 'offline') : 'inactive', - last_heartbeat: device.last_heartbeat, - time_since_last_heartbeat: timeSinceLastHeartbeat, - latest_data: device.heartbeats[0] || null - }; - }); - - const summary = { - total_devices: devices.length, - online: deviceStatus.filter(d => d.status === 'online').length, - offline: deviceStatus.filter(d => d.status === 'offline').length, - inactive: deviceStatus.filter(d => d.status === 'inactive').length - }; - - res.json({ - success: true, - data: { - summary, - devices: deviceStatus - } - }); - - } catch (error) { - console.error('Error fetching device status:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch device status', - error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' - }); - } -}); - -// GET /api/heartbeat/device/:deviceId - Get heartbeat history for specific device -router.get('/device/:deviceId', async (req, res) => { - try { - const { deviceId } = req.params; - const { limit = 50, offset = 0 } = req.query; - - const heartbeats = await Heartbeat.findAndCountAll({ - where: { device_id: deviceId }, - limit: Math.min(parseInt(limit), 1000), - offset: parseInt(offset), - order: [['received_at', 'DESC']], - include: [{ - model: Device, - as: 'device', - attributes: ['id', 'name', 'geo_lat', 'geo_lon'] - }] - }); - - res.json({ - success: true, - data: heartbeats.rows, - pagination: { - total: heartbeats.count, - limit: parseInt(limit), - offset: parseInt(offset), - pages: Math.ceil(heartbeats.count / parseInt(limit)) - } - }); - - } catch (error) { - console.error('Error fetching device heartbeats:', error); - res.status(500).json({ - success: false, - message: 'Failed to fetch device heartbeats', - error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' - }); - } -}); - -// Helper function to generate simple hash from string -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; diff --git a/server/routes/index.js b/server/routes/index.js index 0cd5302..9492667 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -2,33 +2,30 @@ const express = require('express'); const router = express.Router(); // Import route modules -const droneDetectionRoutes = require('./droneDetection'); -const heartbeatRoutes = require('./heartbeat'); const deviceRoutes = require('./device'); const userRoutes = require('./user'); const alertRoutes = require('./alert'); const dashboardRoutes = require('./dashboard'); const healthRoutes = require('./health'); const debugRoutes = require('./debug'); +const detectorsRoutes = require('./detectors'); // API versioning -router.use('/v1/detections', droneDetectionRoutes); -router.use('/v1/heartbeat', heartbeatRoutes); router.use('/v1/devices', deviceRoutes); router.use('/v1/users', userRoutes); router.use('/v1/alerts', alertRoutes); router.use('/v1/dashboard', dashboardRoutes); router.use('/v1/health', healthRoutes); +router.use('/v1/detectors', detectorsRoutes); // Default routes (no version prefix for backward compatibility) -router.use('/detections', droneDetectionRoutes); -router.use('/heartbeat', heartbeatRoutes); router.use('/devices', deviceRoutes); router.use('/users', userRoutes); router.use('/alerts', alertRoutes); router.use('/dashboard', dashboardRoutes); router.use('/health', healthRoutes); router.use('/debug', debugRoutes); +router.use('/detectors', detectorsRoutes); // API documentation endpoint router.get('/', (req, res) => {