Files
AquaMasterMQTT/mock-server/mock_esp32_server.js
Carsten Graf a67e29b9e4
Some checks failed
/ build (push) Has been cancelled
Add DevServer (brokern)
2026-01-24 15:08:14 +01:00

719 lines
21 KiB
JavaScript

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);
});
});