719 lines
21 KiB
JavaScript
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);
|
|
});
|
|
});
|