Fix jwt-token
This commit is contained in:
0
docker-start.bat
Normal file
0
docker-start.bat
Normal file
0
docker-start.sh
Normal file
0
docker-start.sh
Normal file
248
server/routes/detectors.js
Normal file
248
server/routes/detectors.js
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user