TKK_E32231503/backend/server.js

879 lines
27 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
};
// 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);
// Get current time in WIB (UTC+7)
const now = new Date();
const wibTime = new Date(now.getTime() + (7 * 60 * 60 * 1000));
// Update latestData dengan data dari ESP32 (termasuk status relay)
latestData = {
suhu: data.suhu || 0,
berat: data.berat || 0,
target: data.target || 0,
relay1: data.relay1 !== undefined ? data.relay1 : latestData.relay1, // Update dari ESP32
relay2: data.relay2 !== undefined ? data.relay2 : latestData.relay2,
relay3: data.relay3 !== undefined ? data.relay3 : latestData.relay3,
relay4: data.relay4 !== undefined ? data.relay4 : latestData.relay4,
status: latestData.status, // Keep current status
timestamp: wibTime.toISOString()
};
// 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 latestData dengan data dari ESP32 (termasuk status relay)
latestData = {
suhu: data.suhu || 0,
berat: data.berat || 0,
target: data.target || 0,
relay1: data.relay1 !== undefined ? data.relay1 : latestData.relay1, // Update dari ESP32
relay2: data.relay2 !== undefined ? data.relay2 : latestData.relay2,
relay3: data.relay3 !== undefined ? data.relay3 : latestData.relay3,
relay4: data.relay4 !== undefined ? data.relay4 : latestData.relay4,
status: latestData.status, // Keep current 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.1',
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');
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);
});