200 lines
6.0 KiB
JavaScript
200 lines
6.0 KiB
JavaScript
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;
|