const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const mqtt = require('mqtt'); const cors = require('cors'); const bodyParser = require('body-parser'); const path = require('path'); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "*", methods: ["GET", "POST"] } }); const PORT = 80; const MQTT_BROKER = 'mqtt://localhost:1883'; // Middleware app.use(cors()); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(express.static(path.join(__dirname, 'debug_server'))); // State - simuliert ESP32 Datenstrukturen const state = { timerData1: { startTime: 0, localStartTime: 0, finishedSince: 0, endTime: 0, bestTime: 0, isRunning: false, isReady: true, isArmed: false, RFIDUID: "" }, timerData2: { startTime: 0, localStartTime: 0, finishedSince: 0, endTime: 0, bestTime: 0, isRunning: false, isReady: true, isArmed: false, RFIDUID: "" }, buttonConfigs: { start1: { mac: [0, 0, 0, 0, 0, 0], isAssigned: false, voltage: 0, lastHeartbeat: 0, heartbeatActive: false }, stop1: { mac: [0, 0, 0, 0, 0, 0], isAssigned: false, voltage: 0, lastHeartbeat: 0, heartbeatActive: false }, start2: { mac: [0, 0, 0, 0, 0, 0], isAssigned: false, voltage: 0, lastHeartbeat: 0, heartbeatActive: false }, stop2: { mac: [0, 0, 0, 0, 0, 0], isAssigned: false, voltage: 0, lastHeartbeat: 0, heartbeatActive: false } }, learningMode: false, learningStep: 0, maxTimeBeforeReset: 300000, maxTimeDisplay: 20000, minTimeForLeaderboard: 5000, masterlocation: "", gamemode: 0, // 0=Individual, 1=Wettkampf startCompetition: false, laneConfigType: 0, lane1DifficultyType: 0, lane2DifficultyType: 0, localTimes: [], wifi: { ssid: "", password: "" }, start1FoundLocally: false, start2FoundLocally: false, start1UID: "", start2UID: "" }; // Helper: millis() - simuliert Arduino millis() function millis() { return Date.now(); } // Helper: getTimerDataJSON() - simuliert getTimerDataJSON() function getTimerDataJSON() { const currentTime = millis(); const data = {}; // Lane 1 if (state.timerData1.isRunning) { data.time1 = (currentTime - state.timerData1.localStartTime) / 1000.0; data.status1 = "running"; } else if (state.timerData1.endTime > 0) { data.time1 = (state.timerData1.endTime - state.timerData1.startTime) / 1000.0; data.status1 = "finished"; } else if (state.timerData1.isArmed) { data.time1 = 0; data.status1 = "armed"; } else { data.time1 = 0; data.status1 = "ready"; } // Lane 2 if (state.timerData2.isRunning) { data.time2 = (currentTime - state.timerData2.localStartTime) / 1000.0; data.status2 = "running"; } else if (state.timerData2.endTime > 0) { data.time2 = (state.timerData2.endTime - state.timerData2.startTime) / 1000.0; data.status2 = "finished"; } else if (state.timerData2.isArmed) { data.time2 = 0; data.status2 = "armed"; } else { data.time2 = 0; data.status2 = "ready"; } // Best times data.best1 = state.timerData1.bestTime / 1000.0; data.best2 = state.timerData2.bestTime / 1000.0; // Learning mode data.learningMode = state.learningMode; if (state.learningMode) { const buttons = ["Start Bahn 1", "Stop Bahn 1", "Start Bahn 2", "Stop Bahn 2"]; data.learningButton = buttons[state.learningStep]; } return JSON.stringify(data); } // Timer-Logik: IndividualMode function individualMode(action, press, lane, timestamp = 0) { const ts = timestamp > 0 ? timestamp : millis(); if (action === "start" && press === 2 && lane === 1) { if (!state.timerData1.isRunning && state.timerData1.isReady) { state.timerData1.isReady = false; state.timerData1.startTime = ts; state.timerData1.localStartTime = millis(); state.timerData1.isRunning = true; state.timerData1.endTime = 0; state.timerData1.isArmed = false; publishLaneStatus(1, "running"); console.log("Bahn 1 gestartet"); } } if (action === "stop" && press === 1 && lane === 1) { if (state.timerData1.isRunning) { state.timerData1.endTime = ts; state.timerData1.finishedSince = millis(); state.timerData1.isRunning = false; const currentTime = state.timerData1.endTime - state.timerData1.startTime; if (state.timerData1.bestTime === 0 || currentTime < state.timerData1.bestTime) { state.timerData1.bestTime = currentTime; } publishLaneStatus(1, "stopped"); console.log(`Bahn 1 gestoppt - Zeit: ${(currentTime / 1000.0).toFixed(2)}s`); } } if (action === "start" && press === 2 && lane === 2) { if (!state.timerData2.isRunning && state.timerData2.isReady) { state.timerData2.isReady = false; state.timerData2.startTime = ts; state.timerData2.localStartTime = millis(); state.timerData2.isRunning = true; state.timerData2.endTime = 0; state.timerData2.isArmed = false; publishLaneStatus(2, "running"); console.log("Bahn 2 gestartet"); } } if (action === "stop" && press === 1 && lane === 2) { if (state.timerData2.isRunning) { state.timerData2.endTime = ts; state.timerData2.finishedSince = millis(); state.timerData2.isRunning = false; const currentTime = state.timerData2.endTime - state.timerData2.startTime; if (state.timerData2.bestTime === 0 || currentTime < state.timerData2.bestTime) { state.timerData2.bestTime = currentTime; } publishLaneStatus(2, "stopped"); console.log(`Bahn 2 gestoppt - Zeit: ${(currentTime / 1000.0).toFixed(2)}s`); } } } // Helper: publishLaneStatus function publishLaneStatus(lane, status) { if (mqttClient && mqttClient.connected) { const topic = `aquacross/lanes/lane${lane}`; const message = JSON.stringify({ lane, status }); mqttClient.publish(topic, message); } } // Helper: pushUpdateToFrontend function pushUpdateToFrontend(message) { io.emit('update', message); } // MQTT Client Setup let mqttClient = null; let mqttReconnectInterval = null; function connectMQTT() { // Don't reconnect if already connected or connecting if (mqttClient && (mqttClient.connected || mqttClient.connecting)) { return; } // Clear any existing reconnect interval if (mqttReconnectInterval) { clearInterval(mqttReconnectInterval); mqttReconnectInterval = null; } // Close existing client if any if (mqttClient) { mqttClient.end(true); } console.log('[MQTT] Attempting to connect to broker at', MQTT_BROKER); mqttClient = mqtt.connect(MQTT_BROKER, { reconnectPeriod: 5000, connectTimeout: 10000, clientId: 'mock-esp32-' + Math.random().toString(16).substr(2, 8) }); mqttClient.on('connect', () => { console.log('[MQTT] Connected to broker'); // Subscribe to all relevant topics mqttClient.subscribe('aquacross/button/#', (err) => { if (!err) console.log('[MQTT] Subscribed to aquacross/button/#'); }); mqttClient.subscribe('aquacross/button/rfid/#', (err) => { if (!err) console.log('[MQTT] Subscribed to aquacross/button/rfid/#'); }); mqttClient.subscribe('aquacross/battery/#', (err) => { if (!err) console.log('[MQTT] Subscribed to aquacross/battery/#'); }); mqttClient.subscribe('heartbeat/alive/#', (err) => { if (!err) console.log('[MQTT] Subscribed to heartbeat/alive/#'); }); mqttClient.subscribe('aquacross/competition/toMaster', (err) => { if (!err) console.log('[MQTT] Subscribed to aquacross/competition/toMaster'); }); mqttClient.subscribe('aquacross/button/status/#', (err) => { if (!err) console.log('[MQTT] Subscribed to aquacross/button/status/#'); }); }); mqttClient.on('message', (topic, message) => { const payload = message.toString(); console.log(`[MQTT] Received on ${topic}: ${payload}`); // Handle different topic types if (topic.startsWith('aquacross/button/rfid/')) { handleRFIDTopic(topic, payload); } else if (topic.startsWith('aquacross/button/status/')) { handleButtonStatusTopic(topic, payload); } else if (topic.startsWith('aquacross/button/')) { handleButtonTopic(topic, payload); } else if (topic.startsWith('aquacross/battery/')) { handleBatteryTopic(topic, payload); } else if (topic.startsWith('heartbeat/alive/')) { handleHeartbeatTopic(topic, payload); } else if (topic === 'aquacross/competition/toMaster') { if (payload === 'start') { state.startCompetition = true; runCompetition(); } } }); mqttClient.on('error', (err) => { console.error('[MQTT] Error:', err.message || err); if (err.code === 'ECONNREFUSED') { console.log('[MQTT] Broker not available at', MQTT_BROKER, '- will retry automatically'); } }); mqttClient.on('close', () => { console.log('[MQTT] Connection closed'); }); mqttClient.on('offline', () => { console.log('[MQTT] Client offline, will reconnect automatically...'); }); mqttClient.on('reconnect', () => { console.log('[MQTT] Reconnecting to broker...'); }); } // MQTT Topic Handlers function handleButtonTopic(topic, payload) { try { const buttonId = topic.replace('aquacross/button/', ''); const data = JSON.parse(payload); const pressType = data.type || 0; const timestamp = data.timestamp || millis(); console.log(`Button Press: ${buttonId}, Type: ${pressType}, Timestamp: ${timestamp}`); // Simulate button assignment check (simplified) // In real implementation, would check MAC addresses if (state.learningMode) { // Handle learning mode return; } // Trigger action based on button (simplified - would check MAC in real implementation) if (pressType === 2) { // Start button if (buttonId.includes('start1') || buttonId.includes('00:00:00:00:00:01')) { individualMode("start", 2, 1, timestamp); } else if (buttonId.includes('start2') || buttonId.includes('00:00:00:00:00:02')) { individualMode("start", 2, 2, timestamp); } } else if (pressType === 1) { // Stop button if (buttonId.includes('stop1') || buttonId.includes('00:00:00:00:00:03')) { individualMode("stop", 1, 1, timestamp); } else if (buttonId.includes('stop2') || buttonId.includes('00:00:00:00:00:04')) { individualMode("stop", 1, 2, timestamp); } } } catch (err) { console.error('Error handling button topic:', err); } } function handleRFIDTopic(topic, payload) { try { const buttonId = topic.replace('aquacross/button/rfid/', ''); const data = JSON.parse(payload); const uid = data.uid || ''; console.log(`RFID Read: ${buttonId}, UID: ${uid}`); // Send to frontend const message = JSON.stringify({ name: uid, lane: buttonId.includes('start1') ? 'start1' : 'start2' }); pushUpdateToFrontend(message); } catch (err) { console.error('Error handling RFID topic:', err); } } function handleBatteryTopic(topic, payload) { try { const buttonId = topic.replace('aquacross/battery/', ''); const data = JSON.parse(payload); const voltage = data.voltage || 0; console.log(`Battery: ${buttonId}, Voltage: ${voltage}`); // Update button config if known // Send to frontend const message = JSON.stringify({ button: buttonId, mac: buttonId, batteryLevel: Math.round((voltage - 3200) / 50) // Simple calculation }); pushUpdateToFrontend(message); } catch (err) { console.error('Error handling battery topic:', err); } } function handleHeartbeatTopic(topic, payload) { try { const buttonId = topic.replace('heartbeat/alive/', ''); console.log(`Heartbeat: ${buttonId}`); // Update button heartbeat // Send to frontend const message = JSON.stringify({ button: buttonId, mac: buttonId, active: true }); pushUpdateToFrontend(message); } catch (err) { console.error('Error handling heartbeat topic:', err); } } function handleButtonStatusTopic(topic, payload) { try { const buttonId = topic.replace('aquacross/button/status/', ''); const data = JSON.parse(payload); const available = data.available !== false; const sleep = data.sleep === true; console.log(`Button Status: ${buttonId}, Available: ${available}, Sleep: ${sleep}`); // Send to frontend const message = JSON.stringify({ button: buttonId, mac: buttonId, available: available, sleep: sleep, timestamp: data.timestamp || Date.now() }); pushUpdateToFrontend(message); } catch (err) { console.error('Error handling button status topic:', err); } } function runCompetition() { if (state.timerData1.isArmed && state.timerData2.isArmed && state.startCompetition) { const startNow = millis(); state.timerData1.isReady = false; state.timerData1.startTime = startNow; state.timerData1.localStartTime = millis(); state.timerData1.isRunning = true; state.timerData1.endTime = 0; state.timerData1.isArmed = false; publishLaneStatus(1, "running"); state.timerData2.isReady = false; state.timerData2.startTime = startNow; state.timerData2.localStartTime = millis(); state.timerData2.isRunning = true; state.timerData2.endTime = 0; state.timerData2.isArmed = false; publishLaneStatus(2, "running"); console.log("Competition started"); } } // API Routes app.get('/api/data', (req, res) => { res.json(JSON.parse(getTimerDataJSON())); }); app.post('/api/reset-best', (req, res) => { state.timerData1.bestTime = 0; state.timerData2.bestTime = 0; state.localTimes = []; res.json({ success: true }); }); app.post('/api/unlearn-button', (req, res) => { state.buttonConfigs.start1.isAssigned = false; state.buttonConfigs.stop1.isAssigned = false; state.buttonConfigs.start2.isAssigned = false; state.buttonConfigs.stop2.isAssigned = false; res.json({ success: true }); }); app.post('/api/set-max-time', (req, res) => { if (req.body.maxTime) { state.maxTimeBeforeReset = parseInt(req.body.maxTime) * 1000; } if (req.body.maxTimeDisplay) { state.maxTimeDisplay = parseInt(req.body.maxTimeDisplay) * 1000; } if (req.body.minTimeForLeaderboard) { state.minTimeForLeaderboard = parseInt(req.body.minTimeForLeaderboard) * 1000; } res.json({ success: true }); }); app.get('/api/get-settings', (req, res) => { res.json({ maxTime: state.maxTimeBeforeReset / 1000, maxTimeDisplay: state.maxTimeDisplay / 1000, minTimeForLeaderboard: state.minTimeForLeaderboard / 1000 }); }); app.post('/api/start-learning', (req, res) => { state.learningMode = true; state.learningStep = 0; res.json({ success: true }); }); app.post('/api/stop-learning', (req, res) => { state.learningMode = false; state.learningStep = 0; res.json({ success: true }); }); app.get('/api/learn/status', (req, res) => { res.json({ active: state.learningMode, step: state.learningStep }); }); app.get('/api/buttons/status', (req, res) => { res.json({ lane1Start: state.buttonConfigs.start1.isAssigned, lane1StartVoltage: state.buttonConfigs.start1.voltage, lane1Stop: state.buttonConfigs.stop1.isAssigned, lane1StopVoltage: state.buttonConfigs.stop1.voltage, lane2Start: state.buttonConfigs.start2.isAssigned, lane2StartVoltage: state.buttonConfigs.start2.voltage, lane2Stop: state.buttonConfigs.stop2.isAssigned, lane2StopVoltage: state.buttonConfigs.stop2.voltage }); }); app.get('/api/info', (req, res) => { const connected = [ state.buttonConfigs.start1.isAssigned, state.buttonConfigs.stop1.isAssigned, state.buttonConfigs.start2.isAssigned, state.buttonConfigs.stop2.isAssigned ].filter(Boolean).length; res.json({ ip: "127.0.0.1", ipSTA: "127.0.0.1", channel: 1, mac: "AA:BB:CC:DD:EE:FF", freeMemory: 1024 * 1024, connectedButtons: connected, isOnline: true, valid: "Ja", tier: 1 }); }); app.post('/api/set-wifi', (req, res) => { if (req.body.ssid) { state.wifi.ssid = req.body.ssid; state.wifi.password = req.body.password || ""; res.json({ success: true }); } else { res.status(400).json({ success: false, error: "SSID fehlt" }); } }); app.get('/api/get-wifi', (req, res) => { res.json({ ssid: state.wifi.ssid, password: state.wifi.password }); }); app.post('/api/set-location', (req, res) => { if (req.body.name) { state.masterlocation = req.body.name; } res.json({ success: true }); }); app.get('/api/get-location', (req, res) => { res.json({ locationid: state.masterlocation }); }); app.get('/api/updateButtons', (req, res) => { if (mqttClient && mqttClient.connected) { mqttClient.publish('aquacross/update/flag', '1'); } res.json({ success: true }); }); app.post('/api/set-mode', (req, res) => { if (req.body.mode) { state.gamemode = req.body.mode === "individual" ? 0 : 1; res.json({ success: true }); } else { res.status(400).json({ success: false, error: "Modus fehlt" }); } }); app.get('/api/get-mode', (req, res) => { res.json({ mode: state.gamemode === 0 ? "individual" : "wettkampf" }); }); app.post('/api/set-lane-config', (req, res) => { if (req.body.type) { state.laneConfigType = req.body.type === "identical" ? 0 : 1; if (state.laneConfigType === 1) { if (req.body.lane1Difficulty) { state.lane1DifficultyType = req.body.lane1Difficulty === "light" ? 0 : 1; } if (req.body.lane2Difficulty) { state.lane2DifficultyType = req.body.lane2Difficulty === "light" ? 0 : 1; } } res.json({ success: true }); } else { res.status(400).json({ success: false, error: "Lane type missing" }); } }); app.get('/api/get-lane-config', (req, res) => { const config = { type: state.laneConfigType === 0 ? "identical" : "different" }; if (state.laneConfigType === 1) { config.lane1Difficulty = state.lane1DifficultyType === 0 ? "light" : "heavy"; config.lane2Difficulty = state.lane2DifficultyType === 0 ? "light" : "heavy"; } res.json(config); }); // Debug Endpoints app.get('/api/debug/start1', (req, res) => { individualMode("start", 2, 1, millis()); res.send("handleStart1() called"); }); app.get('/api/debug/stop1', (req, res) => { individualMode("stop", 1, 1, millis()); res.send("handleStop1() called"); }); app.get('/api/debug/start2', (req, res) => { individualMode("start", 2, 2, millis()); res.send("handleStart2() called"); }); app.get('/api/debug/stop2', (req, res) => { individualMode("stop", 1, 2, millis()); res.send("handleStop2() called"); }); // WebSocket Setup io.on('connection', (socket) => { console.log(`[WebSocket] Client connected: ${socket.id}`); socket.on('disconnect', () => { console.log(`[WebSocket] Client disconnected: ${socket.id}`); }); }); // Time sync - publish every 5 seconds setInterval(() => { if (mqttClient && mqttClient.connected) { mqttClient.publish('sync/time', millis().toString()); } }, 5000); // Auto-reset check setInterval(() => { const currentTime = millis(); if (state.gamemode === 0) { // Individual mode if (!state.timerData1.isRunning && state.timerData1.endTime > 0 && state.timerData1.finishedSince > 0) { if (currentTime - state.timerData1.finishedSince > state.maxTimeDisplay) { state.timerData1.startTime = 0; state.timerData1.endTime = 0; state.timerData1.finishedSince = 0; state.timerData1.isReady = true; publishLaneStatus(1, "ready"); } } if (!state.timerData2.isRunning && state.timerData2.endTime > 0 && state.timerData2.finishedSince > 0) { if (currentTime - state.timerData2.finishedSince > state.maxTimeDisplay) { state.timerData2.startTime = 0; state.timerData2.endTime = 0; state.timerData2.finishedSince = 0; state.timerData2.isReady = true; publishLaneStatus(2, "ready"); } } } }, 1000); // Start server server.listen(PORT, () => { console.log(`[Server] Mock ESP32 Server running on port ${PORT}`); console.log(`[Server] Web UI available at http://localhost:${PORT}`); // Wait a moment before trying to connect to MQTT broker // This gives the broker time to start if both are started together setTimeout(() => { console.log('[MQTT] Attempting initial connection to broker...'); connectMQTT(); }, 2000); // Also set up a periodic check (backup retry mechanism) // Note: mqtt.js already has auto-reconnect, this is just a backup mqttReconnectInterval = setInterval(() => { if (!mqttClient || (!mqttClient.connected && !mqttClient.connecting)) { console.log('[MQTT] Connection check: Not connected, attempting reconnect...'); connectMQTT(); } }, 15000); // Check every 15 seconds if not connected }); // Graceful shutdown process.on('SIGINT', () => { console.log('\n[Server] Shutting down...'); if (mqttClient) { mqttClient.end(); } server.close(() => { console.log('[Server] Server closed'); process.exit(0); }); });