Initial commit
This commit is contained in:
318
server/routes/device.js
Normal file
318
server/routes/device.js
Normal file
@@ -0,0 +1,318 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
const { Device, DroneDetection, Heartbeat } = require('../models');
|
||||
const { validateRequest } = require('../middleware/validation');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// Validation schema for device
|
||||
const deviceSchema = Joi.object({
|
||||
id: Joi.number().integer().required(),
|
||||
name: Joi.string().max(255).optional(),
|
||||
geo_lat: Joi.number().min(-90).max(90).optional(),
|
||||
geo_lon: Joi.number().min(-180).max(180).optional(),
|
||||
location_description: Joi.string().optional(),
|
||||
heartbeat_interval: Joi.number().integer().min(60).max(3600).optional(),
|
||||
firmware_version: Joi.string().optional(),
|
||||
installation_date: Joi.date().optional(),
|
||||
notes: Joi.string().optional()
|
||||
});
|
||||
|
||||
const updateDeviceSchema = Joi.object({
|
||||
name: Joi.string().max(255).optional(),
|
||||
geo_lat: Joi.number().min(-90).max(90).optional(),
|
||||
geo_lon: Joi.number().min(-180).max(180).optional(),
|
||||
location_description: Joi.string().optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
heartbeat_interval: Joi.number().integer().min(60).max(3600).optional(),
|
||||
firmware_version: Joi.string().optional(),
|
||||
installation_date: Joi.date().optional(),
|
||||
notes: Joi.string().optional()
|
||||
});
|
||||
|
||||
// GET /api/devices - Get all devices
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
include_stats = false,
|
||||
active_only = false,
|
||||
limit = 100,
|
||||
offset = 0
|
||||
} = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
if (active_only === 'true') {
|
||||
whereClause.is_active = true;
|
||||
}
|
||||
|
||||
const includeOptions = [];
|
||||
|
||||
if (include_stats === 'true') {
|
||||
// Include latest heartbeat and detection count
|
||||
includeOptions.push({
|
||||
model: Heartbeat,
|
||||
as: 'heartbeats',
|
||||
limit: 1,
|
||||
order: [['received_at', 'DESC']],
|
||||
required: false,
|
||||
attributes: ['received_at', 'battery_level', 'signal_strength', 'temperature']
|
||||
});
|
||||
}
|
||||
|
||||
const devices = await Device.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: includeOptions,
|
||||
limit: Math.min(parseInt(limit), 1000),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
// If stats requested, get detection counts
|
||||
let devicesWithStats = devices.rows;
|
||||
if (include_stats === 'true') {
|
||||
devicesWithStats = await Promise.all(devices.rows.map(async (device) => {
|
||||
const detectionCount = await DroneDetection.count({
|
||||
where: {
|
||||
device_id: device.id,
|
||||
server_timestamp: {
|
||||
[Op.gte]: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const timeSinceLastHeartbeat = device.last_heartbeat
|
||||
? (now - new Date(device.last_heartbeat)) / 1000
|
||||
: null;
|
||||
|
||||
const expectedInterval = device.heartbeat_interval || 300;
|
||||
const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2);
|
||||
|
||||
return {
|
||||
...device.toJSON(),
|
||||
stats: {
|
||||
detections_24h: detectionCount,
|
||||
status: device.is_active ? (isOnline ? 'online' : 'offline') : 'inactive',
|
||||
time_since_last_heartbeat: timeSinceLastHeartbeat
|
||||
}
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: devicesWithStats,
|
||||
pagination: {
|
||||
total: devices.count,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
pages: Math.ceil(devices.count / parseInt(limit))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching devices:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch devices',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/devices/map - Get devices with location data for map display
|
||||
router.get('/map', async (req, res) => {
|
||||
try {
|
||||
const devices = await Device.findAll({
|
||||
where: {
|
||||
is_active: true,
|
||||
geo_lat: { [Op.ne]: null },
|
||||
geo_lon: { [Op.ne]: null }
|
||||
},
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
'geo_lat',
|
||||
'geo_lon',
|
||||
'location_description',
|
||||
'last_heartbeat'
|
||||
]
|
||||
});
|
||||
|
||||
// Get recent detections for each device
|
||||
const devicesWithDetections = await Promise.all(devices.map(async (device) => {
|
||||
const recentDetections = await DroneDetection.count({
|
||||
where: {
|
||||
device_id: device.id,
|
||||
server_timestamp: {
|
||||
[Op.gte]: new Date(Date.now() - 10 * 60 * 1000) // Last 10 minutes
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const timeSinceLastHeartbeat = device.last_heartbeat
|
||||
? (now - new Date(device.last_heartbeat)) / 1000
|
||||
: null;
|
||||
|
||||
const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < 600; // 10 minutes
|
||||
|
||||
return {
|
||||
...device.toJSON(),
|
||||
has_recent_detections: recentDetections > 0,
|
||||
detection_count_10m: recentDetections,
|
||||
status: isOnline ? 'online' : 'offline'
|
||||
};
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: devicesWithDetections
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching devices for map:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch devices for map',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/devices/:id - Get specific device
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const device = await Device.findByPk(req.params.id, {
|
||||
include: [
|
||||
{
|
||||
model: Heartbeat,
|
||||
as: 'heartbeats',
|
||||
limit: 5,
|
||||
order: [['received_at', 'DESC']]
|
||||
},
|
||||
{
|
||||
model: DroneDetection,
|
||||
as: 'detections',
|
||||
limit: 10,
|
||||
order: [['server_timestamp', 'DESC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Device not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: device
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching device:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch device',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/devices - Create new device (admin only)
|
||||
router.post('/', authenticateToken, validateRequest(deviceSchema), async (req, res) => {
|
||||
try {
|
||||
const device = await Device.create(req.body);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: device,
|
||||
message: 'Device created successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating device:', error);
|
||||
|
||||
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: 'Device with this ID already exists'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to create device',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/devices/:id - Update device (admin only)
|
||||
router.put('/:id', authenticateToken, validateRequest(updateDeviceSchema), async (req, res) => {
|
||||
try {
|
||||
const device = await Device.findByPk(req.params.id);
|
||||
|
||||
if (!device) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Device not found'
|
||||
});
|
||||
}
|
||||
|
||||
await device.update(req.body);
|
||||
|
||||
// Emit real-time update
|
||||
req.io.emit('device_updated', device);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: device,
|
||||
message: 'Device updated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating device:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to update device',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/devices/:id - Delete device (admin only)
|
||||
router.delete('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const device = await Device.findByPk(req.params.id);
|
||||
|
||||
if (!device) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Device not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Soft delete by setting is_active to false
|
||||
await device.update({ is_active: false });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Device deactivated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting device:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to delete device',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user