console.log('🔄 SERVER STARTING - Version 1.0.3-relay-fix - ' + new Date().toISOString()); const express = require('express'); const cors = require('cors'); const morgan = require('morgan'); const mqtt = require('mqtt'); const aedes = require('aedes')(); const { createServer } = require('net'); const { WebSocketServer } = require('ws'); const http = require('http'); require('dotenv').config(); // Import database functions const db = require('./database'); // ===================================================== // CONFIGURATION // ===================================================== const PORT = process.env.PORT || 3000; const MQTT_PORT = process.env.MQTT_PORT || 1883; const WS_PORT = process.env.WS_PORT || 8883; // MQTT Client Configuration (untuk connect ke HiveMQ) const MQTT_BROKER_URL = process.env.MQTT_BROKER_URL || 'mqtt://broker.hivemq.com:1883'; const MQTT_TOPICS = { data: 'novil/pengering/data', status: 'novil/pengering/status', button: 'novil/pengering/button', control: 'novil/pengering/control' }; // ===================================================== // EXPRESS APP // ===================================================== const app = express(); app.use(cors()); app.use(express.json()); app.use(morgan('dev')); // ===================================================== // DATA STORAGE (MySQL Database) // ===================================================== let latestData = { suhu: 0, berat: 0, target: 0, relay1: false, relay2: false, relay3: false, relay4: false, status: 'DISCONNECTED', timestamp: new Date().toISOString() }; // Last data received timestamp let lastDataTimestamp = Date.now(); // Connection timeout (5 minutes = 300 seconds) const CONNECTION_TIMEOUT = 300000; // Check connection status periodically setInterval(() => { const now = Date.now(); const timeSinceLastData = now - lastDataTimestamp; // If no data received for more than CONNECTION_TIMEOUT, mark as disconnected if (timeSinceLastData > CONNECTION_TIMEOUT && latestData.status !== 'DISCONNECTED') { console.log('âš ī¸ No data received for', Math.floor(timeSinceLastData / 1000), 'seconds'); console.log(' Marking as DISCONNECTED'); latestData.status = 'DISCONNECTED'; // Optionally reset relay states latestData.relay1 = false; latestData.relay2 = false; latestData.relay3 = false; latestData.relay4 = false; } }, 5000); // Check every 5 seconds // Initialize database on startup (async () => { try { console.log('🔄 Initializing database connection...'); const connected = await db.testConnection(); if (connected) { console.log('✅ Database connected, initializing tables...'); await db.initDatabase(); // Load latest data from database try { const latest = await db.getLatestSensorData(); if (latest) { latestData = { suhu: latest.suhu, berat: latest.berat, target: latest.target, relay1: latest.relay1, relay2: latest.relay2, relay3: latest.relay3, relay4: latest.relay4, status: latest.status, timestamp: latest.timestamp }; console.log('✅ Latest data loaded from database'); } } catch (loadError) { console.warn('âš ī¸ Could not load latest data:', loadError.message); } } else { console.warn('âš ī¸ Running without database connection'); } } catch (error) { console.error('❌ Database initialization error:', error.message); console.warn('âš ī¸ Continuing without database...'); } })(); // ===================================================== // MQTT CLIENT (Connect to HiveMQ) // ===================================================== console.log('🔄 Connecting to MQTT Broker:', MQTT_BROKER_URL); const mqttClient = mqtt.connect(MQTT_BROKER_URL, { clientId: `backend_${Math.random().toString(16).slice(2, 10)}`, clean: true, connectTimeout: 4000, reconnectPeriod: 1000, keepalive: 60 }); mqttClient.on('connect', () => { console.log('✅ Connected to MQTT Broker (HiveMQ)'); // Subscribe to all topics mqttClient.subscribe([ MQTT_TOPICS.data, MQTT_TOPICS.status, MQTT_TOPICS.button, MQTT_TOPICS.control ], (err) => { if (err) { console.error('❌ Failed to subscribe:', err); } else { console.log('✅ Subscribed to topics:'); console.log(' -', MQTT_TOPICS.data); console.log(' -', MQTT_TOPICS.status); console.log(' -', MQTT_TOPICS.button); console.log(' -', MQTT_TOPICS.control); } }); }); mqttClient.on('error', (error) => { console.error('❌ MQTT Client Error:', error.message); }); mqttClient.on('reconnect', () => { console.log('🔄 Reconnecting to MQTT Broker...'); }); mqttClient.on('offline', () => { console.warn('âš ī¸ MQTT Client offline'); }); mqttClient.on('message', async (topic, message) => { const msg = message.toString(); console.log(`📨 MQTT Message received:`); console.log(` Topic: ${topic}`); console.log(` Message: ${msg}`); // ===================================================== // TOPIC: novil/pengering/data // Format: {"suhu":28.5,"berat":450,"target":315,"relay1":true,"relay2":true,"relay3":true,"relay4":false} // ===================================================== if (topic === MQTT_TOPICS.data) { try { const data = JSON.parse(msg); // Debug: log parsed data console.log('đŸ“Ļ Parsed MQTT data:', JSON.stringify(data, null, 2)); console.log(' Data type check:', { relay1_type: typeof data.relay1, relay1_value: data.relay1, relay2_type: typeof data.relay2, relay2_value: data.relay2, relay3_type: typeof data.relay3, relay3_value: data.relay3, relay4_type: typeof data.relay4, relay4_value: data.relay4 }); // Get current time in WIB (UTC+7) const now = new Date(); const wibTime = new Date(now.getTime() + (7 * 60 * 60 * 1000)); // Update last data timestamp lastDataTimestamp = Date.now(); // Helper function to map ESP32 status to backend status const mapStatus = (esp32Status) => { if (!esp32Status) return latestData.status; const statusMap = { 'SELESAI': 'COMPLETED', 'BERJALAN': 'RUNNING', 'SCANNING': 'SCANNING', 'READY': 'READY', 'CONNECTED': 'CONNECTED', 'ERROR': 'ERROR' }; return statusMap[esp32Status.toUpperCase()] || esp32Status; }; // Update latestData dengan data dari ESP32 (termasuk status relay) // Use explicit check for undefined/null to handle false values correctly latestData = { suhu: data.suhu || 0, berat: data.berat || 0, target: data.target || 0, relay1: (data.relay1 !== undefined && data.relay1 !== null) ? Boolean(data.relay1) : latestData.relay1, relay2: (data.relay2 !== undefined && data.relay2 !== null) ? Boolean(data.relay2) : latestData.relay2, relay3: (data.relay3 !== undefined && data.relay3 !== null) ? Boolean(data.relay3) : latestData.relay3, relay4: (data.relay4 !== undefined && data.relay4 !== null) ? Boolean(data.relay4) : latestData.relay4, status: mapStatus(data.status), // Map ESP32 status to backend status timestamp: wibTime.toISOString() }; console.log(' Final latestData relay values:', { relay1: latestData.relay1, relay2: latestData.relay2, relay3: latestData.relay3, relay4: latestData.relay4, status: latestData.status }); // Save to database try { await db.insertSensorData(latestData); console.log('✅ Data saved to MySQL database'); } catch (dbError) { console.error('❌ Failed to save to database:', dbError.message); } console.log('✅ Data updated:', latestData); } catch (e) { console.error('❌ Error parsing data:', e); } } // ===================================================== // TOPIC: novil/pengering/status // Format: "ESP32 CONNECTED", "PENGERINGAN DIMULAI", dll // ===================================================== if (topic === MQTT_TOPICS.status) { // Update status di latestData if (msg.includes('ESP32 CONNECTED')) { latestData.status = 'CONNECTED'; } else if (msg.includes('PENGERINGAN SIAP')) { latestData.status = 'READY'; } else if (msg.includes('PENGERINGAN DIMULAI')) { latestData.status = 'RUNNING'; } else if (msg.includes('PENGERINGAN BERJALAN')) { latestData.status = 'RUNNING'; } else if (msg.includes('PENGERINGAN SELESAI')) { latestData.status = 'COMPLETED'; } else if (msg.includes('SCAN BERAT')) { latestData.status = 'SCANNING'; } else if (msg.includes('DHT ERROR')) { latestData.status = 'ERROR'; } // Save to database try { await db.insertStatusHistory(msg); console.log('✅ Status saved to MySQL database'); } catch (dbError) { console.error('❌ Failed to save status to database:', dbError.message); } console.log('📊 Status:', msg); } // ===================================================== // TOPIC: novil/pengering/button // Format: "BUTTON PRESSED", "START_BUTTON", "RESET_BUTTON" // ===================================================== if (topic === MQTT_TOPICS.button) { console.log('🔘 Button Event:', msg); // Save button event to status history try { await db.insertStatusHistory(`BUTTON: ${msg}`); console.log('✅ Button event saved to MySQL database'); } catch (dbError) { console.error('❌ Failed to save button event:', dbError.message); } } // ===================================================== // TOPIC: novil/pengering/control // Format: "STATUS:HEATER_ON", "HEATER_ON", "FAN_OFF", dll // ===================================================== if (topic === MQTT_TOPICS.control) { console.log('đŸŽ›ī¸ Control Message:', msg); // Jika pesan dimulai dengan "STATUS:", ini adalah feedback dari ESP32 if (msg.startsWith('STATUS:')) { const statusMsg = msg.replace('STATUS:', ''); console.log('📊 Relay Status Update:', statusMsg); // Update relay state di latestData if (statusMsg === 'HEATER_ON') latestData.relay1 = true; else if (statusMsg === 'HEATER_OFF') latestData.relay1 = false; else if (statusMsg === 'FAN_ON') latestData.relay2 = true; else if (statusMsg === 'FAN_OFF') latestData.relay2 = false; else if (statusMsg === 'EXHAUST_ON') latestData.relay3 = true; else if (statusMsg === 'EXHAUST_OFF') latestData.relay3 = false; // Save status update to status_history try { await db.insertStatusHistory(`RELAY: ${statusMsg}`); console.log('✅ Relay status saved to status_history'); } catch (dbError) { console.error('❌ Failed to save relay status:', dbError.message); } // Save juga ke control_commands dengan source ESP32 try { await db.insertControlCommand(statusMsg, 'ESP32'); console.log('✅ Relay status saved to control_commands'); } catch (dbError) { console.error('❌ Failed to save to control_commands:', dbError.message); } } else { // Ini adalah command dari API/Flutter, simpan ke control_commands try { await db.insertControlCommand(msg, 'MQTT'); console.log('✅ Control command saved to control_commands'); } catch (dbError) { console.error('❌ Failed to save control command:', dbError.message); } } } }); // ===================================================== // MQTT BROKER (AEDES) // ===================================================== const mqttServer = createServer(aedes.handle); // MQTT Event Handlers aedes.on('client', (client) => { console.log(`📱 Client Connected: ${client.id}`); }); aedes.on('clientDisconnect', (client) => { console.log(`📴 Client Disconnected: ${client.id}`); }); aedes.on('publish', async (packet, client) => { if (client) { const topic = packet.topic; const message = packet.payload.toString(); console.log(`📨 Message from ${client.id}:`); console.log(` Topic: ${topic}`); console.log(` Message: ${message}`); // ===================================================== // TOPIC: novil/pengering/data // Format: {"suhu":28.5,"berat":450,"target":315,"relay1":true,"relay2":true,"relay3":true,"relay4":false} // ===================================================== if (topic === 'novil/pengering/data') { try { const data = JSON.parse(message); // Update last data timestamp lastDataTimestamp = Date.now(); // Helper function to map ESP32 status to backend status const mapStatus = (esp32Status) => { if (!esp32Status) return latestData.status; const statusMap = { 'SELESAI': 'COMPLETED', 'BERJALAN': 'RUNNING', 'SCANNING': 'SCANNING', 'READY': 'READY', 'CONNECTED': 'CONNECTED', 'ERROR': 'ERROR' }; return statusMap[esp32Status.toUpperCase()] || esp32Status; }; // Update latestData dengan data dari ESP32 (termasuk status relay) // Use explicit check for undefined/null to handle false values correctly latestData = { suhu: data.suhu || 0, berat: data.berat || 0, target: data.target || 0, relay1: (data.relay1 !== undefined && data.relay1 !== null) ? Boolean(data.relay1) : latestData.relay1, relay2: (data.relay2 !== undefined && data.relay2 !== null) ? Boolean(data.relay2) : latestData.relay2, relay3: (data.relay3 !== undefined && data.relay3 !== null) ? Boolean(data.relay3) : latestData.relay3, relay4: (data.relay4 !== undefined && data.relay4 !== null) ? Boolean(data.relay4) : latestData.relay4, status: mapStatus(data.status), // Map ESP32 status to backend status timestamp: new Date().toISOString() }; // Save to database try { await db.insertSensorData(latestData); console.log('✅ Data saved to database'); } catch (dbError) { console.error('❌ Failed to save to database:', dbError.message); } console.log('✅ Data updated:', latestData); } catch (e) { console.error('❌ Error parsing data:', e); } } // ===================================================== // TOPIC: novil/pengering/status // Format: "ESP32 CONNECTED", "PENGERINGAN DIMULAI", dll // ===================================================== if (topic === 'novil/pengering/status') { // Update status di latestData if (message.includes('ESP32 CONNECTED')) { latestData.status = 'CONNECTED'; } else if (message.includes('PENGERINGAN SIAP')) { latestData.status = 'READY'; } else if (message.includes('PENGERINGAN DIMULAI')) { latestData.status = 'RUNNING'; } else if (message.includes('PENGERINGAN BERJALAN')) { latestData.status = 'RUNNING'; } else if (message.includes('PENGERINGAN SELESAI')) { latestData.status = 'COMPLETED'; } else if (message.includes('SCAN BERAT')) { latestData.status = 'SCANNING'; } else if (message.includes('DHT ERROR')) { latestData.status = 'ERROR'; } // Save to database try { await db.insertStatusHistory(message); console.log('✅ Status saved to database'); } catch (dbError) { console.error('❌ Failed to save status to database:', dbError.message); } console.log('📊 Status:', message); } // ===================================================== // TOPIC: novil/pengering/button // Format: "BUTTON PRESSED", "START_BUTTON", "RESET_BUTTON" // ===================================================== if (topic === 'novil/pengering/button') { console.log('🔘 Button Event:', message); // Save button event to status history try { await db.insertStatusHistory(`BUTTON: ${message}`); } catch (dbError) { console.error('❌ Failed to save button event:', dbError.message); } } // ===================================================== // TOPIC: novil/pengering/control // Format: "STATUS:HEATER_ON", "HEATER_ON", "FAN_OFF", dll // ===================================================== if (topic === 'novil/pengering/control') { console.log('đŸŽ›ī¸ Control Message:', message); // Jika pesan dimulai dengan "STATUS:", ini adalah feedback dari ESP32 if (message.startsWith('STATUS:')) { const statusMsg = message.replace('STATUS:', ''); console.log('📊 Relay Status Update:', statusMsg); // Update relay state di latestData if (statusMsg === 'HEATER_ON') latestData.relay1 = true; else if (statusMsg === 'HEATER_OFF') latestData.relay1 = false; else if (statusMsg === 'FAN_ON') latestData.relay2 = true; else if (statusMsg === 'FAN_OFF') latestData.relay2 = false; else if (statusMsg === 'EXHAUST_ON') latestData.relay3 = true; else if (statusMsg === 'EXHAUST_OFF') latestData.relay3 = false; // Save status update to status_history try { await db.insertStatusHistory(`RELAY: ${statusMsg}`); console.log('✅ Relay status saved to database'); } catch (dbError) { console.error('❌ Failed to save relay status:', dbError.message); } } else { // Ini adalah command dari API/Flutter, simpan ke control_commands try { await db.insertControlCommand(message, 'MQTT'); console.log('✅ Control command saved to database'); } catch (dbError) { console.error('❌ Failed to save control command:', dbError.message); } } } } }); aedes.on('subscribe', (subscriptions, client) => { console.log(`đŸ“Ŧ Client ${client.id} subscribed to:`, subscriptions.map(s => s.topic).join(', ')); }); // Start MQTT Server (only if not in production or if explicitly enabled) if (process.env.ENABLE_MQTT_BROKER === 'true') { mqttServer.listen(MQTT_PORT, () => { console.log(`🚀 MQTT Broker running on port ${MQTT_PORT}`); }).on('error', (err) => { console.warn(`âš ī¸ MQTT Broker failed to start on port ${MQTT_PORT}:`, err.message); console.log('â„šī¸ MQTT Broker disabled. Use external MQTT broker (e.g., HiveMQ)'); }); } else { console.log('â„šī¸ MQTT Broker disabled (use external MQTT broker like HiveMQ)'); } // ===================================================== // WEBSOCKET MQTT (for web clients) // ===================================================== const httpServer = http.createServer(); const ws = new WebSocketServer({ server: httpServer }); ws.on('connection', (socket) => { const stream = require('websocket-stream')(socket); aedes.handle(stream); console.log('🌐 WebSocket MQTT client connected'); }); if (process.env.ENABLE_MQTT_BROKER === 'true') { httpServer.listen(WS_PORT, () => { console.log(`🌐 WebSocket MQTT running on port ${WS_PORT}`); }).on('error', (err) => { console.warn(`âš ī¸ WebSocket MQTT failed to start on port ${WS_PORT}:`, err.message); }); } else { console.log('â„šī¸ WebSocket MQTT disabled'); } // ===================================================== // REST API ENDPOINTS // ===================================================== // Health check app.get('/', (req, res) => { res.json({ status: 'OK', message: 'Pengering Ikan Backend Server - MQTT Client Enabled', version: '1.0.3-relay-fix', // Updated version with relay boolean fix uptime: process.uptime(), timestamp: new Date().toISOString() }); }); // Initialize database tables (manual trigger) app.post('/api/database/init', async (req, res) => { try { const connected = await db.testConnection(); if (!connected) { return res.status(500).json({ success: false, message: 'Database connection failed' }); } const initialized = await db.initDatabase(); if (initialized) { res.json({ success: true, message: 'Database tables initialized successfully' }); } else { res.status(500).json({ success: false, message: 'Failed to initialize database tables' }); } } catch (error) { res.status(500).json({ success: false, message: 'Error initializing database', error: error.message }); } }); // Test database connection app.get('/api/database/test', async (req, res) => { try { const connected = await db.testConnection(); if (connected) { res.json({ success: true, message: 'Database connection successful' }); } else { res.status(500).json({ success: false, message: 'Database connection failed' }); } } catch (error) { res.status(500).json({ success: false, message: 'Error testing database connection', error: error.message }); } }); // Post sensor data from ESP32 app.post('/api/data/sensor', async (req, res) => { try { const { suhu, berat, target, relay1, relay2, relay3, relay4, status } = req.body; // Validate data if (suhu === undefined || berat === undefined) { return res.status(400).json({ success: false, message: 'Suhu and berat are required' }); } // Update latest data latestData = { suhu: suhu || 0, berat: berat || 0, target: target || 0, relay1: relay1 || false, relay2: relay2 || false, relay3: relay3 || false, relay4: relay4 || false, status: status || 'UNKNOWN', timestamp: new Date().toISOString() }; // Save to database try { await db.insertSensorData(latestData); console.log('✅ Data from ESP32 saved to database'); } catch (dbError) { console.error('❌ Failed to save to database:', dbError.message); } res.json({ success: true, message: 'Data received successfully', data: latestData }); } catch (error) { res.status(500).json({ success: false, message: 'Error processing sensor data', error: error.message }); } }); // Post status from ESP32 app.post('/api/status', async (req, res) => { try { const { message } = req.body; if (!message) { return res.status(400).json({ success: false, message: 'Status message is required' }); } // Save to database try { await db.insertStatusHistory(message); console.log('✅ Status from ESP32 saved:', message); } catch (dbError) { console.error('❌ Failed to save status to database:', dbError.message); } res.json({ success: true, message: 'Status received successfully' }); } catch (error) { res.status(500).json({ success: false, message: 'Error processing status', error: error.message }); } }); // Get latest data app.get('/api/data/latest', (req, res) => { res.json({ success: true, data: latestData }); }); // Get data history app.get('/api/data/history', async (req, res) => { try { const limit = parseInt(req.query.limit) || 50; const history = await db.getSensorDataHistory(limit); res.json({ success: true, count: history.length, data: history }); } catch (error) { res.status(500).json({ success: false, message: 'Failed to get data history', error: error.message }); } }); // Get status history app.get('/api/status/history', async (req, res) => { try { const limit = parseInt(req.query.limit) || 50; const history = await db.getStatusHistory(limit); res.json({ success: true, count: history.length, data: history }); } catch (error) { res.status(500).json({ success: false, message: 'Failed to get status history', error: error.message }); } }); // Publish control command app.post('/api/control', async (req, res) => { const { command } = req.body; if (!command) { return res.status(400).json({ success: false, message: 'Command is required' }); } // Valid commands (sesuai dengan ESP32) const validCommands = [ 'HEATER_ON', 'HEATER_OFF', 'FAN_ON', 'FAN_OFF', 'LAMP_ON', 'LAMP_OFF', 'EXHAUST_ON', 'EXHAUST_OFF', 'START', // Mulai pengeringan 'RESET' // Reset ke mode ready ]; if (!validCommands.includes(command)) { return res.status(400).json({ success: false, message: 'Invalid command', validCommands: validCommands }); } // Save command to database try { await db.insertControlCommand(command, 'API'); } catch (dbError) { console.error('❌ Failed to save command to database:', dbError.message); } // Publish to MQTT (HiveMQ) mqttClient.publish(MQTT_TOPICS.control, command, { qos: 1 }, (err) => { if (err) { return res.status(500).json({ success: false, message: 'Failed to publish command', error: err.message }); } console.log(`✅ Command published to MQTT: ${command}`); res.json({ success: true, message: 'Command sent successfully', command: command }); }); }); // Get statistics app.get('/api/stats', async (req, res) => { try { const dbStats = await db.getStatistics(); res.json({ success: true, stats: { mqttConnected: mqttClient.connected, ...dbStats, latestData: latestData, uptime: process.uptime(), timestamp: new Date().toISOString() } }); } catch (error) { res.json({ success: true, stats: { mqttConnected: mqttClient.connected, latestData: latestData, uptime: process.uptime(), timestamp: new Date().toISOString(), databaseError: error.message } }); } }); // Clear history app.delete('/api/history/clear', async (req, res) => { try { await db.clearOldData(0); // Clear all data res.json({ success: true, message: 'History cleared successfully' }); } catch (error) { res.status(500).json({ success: false, message: 'Failed to clear history', error: error.message }); } }); // 404 handler app.use((req, res) => { res.status(404).json({ success: false, message: 'Endpoint not found' }); }); // Error handler app.use((err, req, res, next) => { console.error('❌ Error:', err); res.status(500).json({ success: false, message: 'Internal server error', error: err.message }); }); // ===================================================== // START EXPRESS SERVER // ===================================================== const server = app.listen(PORT, '0.0.0.0', () => { console.log(''); console.log('='.repeat(60)); console.log('🚀 PENGERING IKAN BACKEND SERVER v1.0.3-relay-fix'); console.log('='.repeat(60)); console.log(`📡 REST API: http://0.0.0.0:${PORT}`); console.log(`🔗 MQTT Broker: ${MQTT_BROKER_URL}`); console.log(`📊 MQTT Status: ${mqttClient.connected ? '✅ Connected' : 'âŗ Connecting...'}`); console.log(''); console.log('📋 Available Endpoints:'); console.log(' GET / - Health check'); console.log(' GET /api/data/latest - Get latest sensor data'); console.log(' GET /api/data/history - Get data history'); console.log(' GET /api/status/history - Get status history'); console.log(' GET /api/stats - Get server statistics'); console.log(' POST /api/control - Send control command'); console.log(' POST /api/data/sensor - Post sensor data (ESP32)'); console.log(' POST /api/status - Post status (ESP32)'); console.log(' DELETE /api/history/clear - Clear history'); console.log(''); console.log('📡 MQTT Topics:'); console.log(' Subscribe:', MQTT_TOPICS.data); console.log(' Subscribe:', MQTT_TOPICS.status); console.log(' Subscribe:', MQTT_TOPICS.button); console.log(' Publish: ', MQTT_TOPICS.control); console.log(''); console.log('✅ Server is ready!'); console.log('='.repeat(60)); }); server.on('error', (error) => { console.error('❌ Server error:', error); if (error.code === 'EADDRINUSE') { console.error(`Port ${PORT} is already in use`); } process.exit(1); }); // ===================================================== // GRACEFUL SHUTDOWN // ===================================================== process.on('SIGTERM', () => { console.log('🛑 SIGTERM received, shutting down gracefully...'); // Close MQTT client if (mqttClient) { mqttClient.end(() => { console.log('✅ MQTT client closed'); }); } // Close HTTP server server.close(() => { console.log('✅ HTTP server closed'); }); // Close MQTT broker if enabled if (process.env.ENABLE_MQTT_BROKER === 'true') { mqttServer.close(() => { console.log('✅ MQTT server closed'); }); httpServer.close(() => { console.log('✅ WebSocket server closed'); }); } process.exit(0); }); process.on('SIGINT', () => { console.log('🛑 SIGINT received, shutting down gracefully...'); // Close MQTT client if (mqttClient) { mqttClient.end(() => { console.log('✅ MQTT client closed'); }); } // Close HTTP server server.close(() => { console.log('✅ HTTP server closed'); }); process.exit(0); }); process.on('uncaughtException', (error) => { console.error('❌ Uncaught Exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); });