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;