move hardbeat handling into backend, add publish lanestate for lighttower

This commit is contained in:
Carsten Graf
2025-07-31 01:15:44 +02:00
parent 4a1e0b8bca
commit 0166e1a695
7 changed files with 123 additions and 20 deletions

Binary file not shown.

View File

@@ -144,7 +144,26 @@
}; };
ws.onmessage = (event) => { ws.onmessage = (event) => {
console.log("WebSocket message received:", event.data); const data = JSON.parse(event.data);
if (data.button && data.mac && data.active !== undefined) {
const indicatorId =
data.button === "start1"
? "heartbeat1"
: data.button === "stop1"
? "heartbeat2"
: data.button === "start2"
? "heartbeat3"
: data.button === "stop2"
? "heartbeat4"
: null;
if (indicatorId) {
if (data.active) {
document.getElementById(indicatorId).classList.add("active");
} else {
document.getElementById(indicatorId).classList.remove("active");
}
}
}
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
@@ -328,20 +347,23 @@
let display1 = timer1; let display1 = timer1;
let display2 = timer2; let display2 = timer2;
if (status1 === "running") { // Status für Bahn 1
const s1 = document.getElementById("status1");
const lane1Connected = areBothButtonsConnected(1);
// Status für Bahn 2
const s2 = document.getElementById("status2");
const lane2Connected = areBothButtonsConnected(2);
if (status1 === "running" && lane1Connected) {
display1 += (now - lastSync) / 1000; display1 += (now - lastSync) / 1000;
} }
if (status2 === "running") { if (status2 === "running" && lane2Connected) {
display2 += (now - lastSync) / 1000; display2 += (now - lastSync) / 1000;
} }
document.getElementById("time1").textContent = formatTime(display1); document.getElementById("time1").textContent = formatTime(display1);
// Status für Bahn 1 if (!lane1Connected) {
const s1 = document.getElementById("status1");
const lane1Connected = areBothButtonsConnected(1);
if (status1 === "ready" && !lane1Connected) {
s1.className = "status standby"; s1.className = "status standby";
s1.textContent = "Standby: Bitte beide Buttons 1x betätigen"; s1.textContent = "Standby: Bitte beide Buttons 1x betätigen";
} else { } else {
@@ -356,13 +378,9 @@
document.getElementById("time2").textContent = formatTime(display2); document.getElementById("time2").textContent = formatTime(display2);
// Status für Bahn 2 if (!lane2Connected) {
const s2 = document.getElementById("status2");
const lane2Connected = areBothButtonsConnected(2);
if (status2 === "ready" && !lane2Connected) {
s2.className = "status standby"; s2.className = "status standby";
s2.textContent = "Standby: Bitte beide Buttons 1x betätigen"; s2.textContent = "Standby: Bitte beide 1x betätigen";
} else { } else {
s2.className = `status ${status2}`; s2.className = `status ${status2}`;
s2.textContent = s2.textContent =

View File

@@ -89,3 +89,19 @@ lib_deps =
mlesniew/PicoMQTT@^1.3.0 mlesniew/PicoMQTT@^1.3.0
miguelbalboa/MFRC522@^1.4.12 miguelbalboa/MFRC522@^1.4.12
adafruit/RTClib@^2.1.4 adafruit/RTClib@^2.1.4
[env:esp32-s3-devkitc-1]
board = esp32-s3-devkitc-1
monitor_speed = 115200
board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
build_flags =
-DARDUINO_USB_CDC_ON_BOOT=1
-DBATTERY_PIN=35
lib_deps =
bblanchon/ArduinoJson@^7.4.1
esp32async/ESPAsyncWebServer@^3.7.7
esp32async/AsyncTCP@^3.4.2
mlesniew/PicoMQTT@^1.3.0
miguelbalboa/MFRC522@^1.4.12
adafruit/RTClib@^2.1.4

View File

@@ -109,7 +109,6 @@ void readButtonJSON(const char *topic, const char *payload) {
* extrahiert MAC und Timestamp und sendet ein JSON an das Frontend. * extrahiert MAC und Timestamp und sendet ein JSON an das Frontend.
*/ */
void handleHeartbeatTopic(const char *topic, const char *payload) { void handleHeartbeatTopic(const char *topic, const char *payload) {
// Topic-Format: heartbeat/alive/CC:DB:A7:2F:95:08
String topicStr(topic); String topicStr(topic);
int lastSlash = topicStr.lastIndexOf('/'); int lastSlash = topicStr.lastIndexOf('/');
if (lastSlash < 0) if (lastSlash < 0)
@@ -119,17 +118,22 @@ void handleHeartbeatTopic(const char *topic, const char *payload) {
auto macBytes = macStringToBytes(macStr.c_str()); auto macBytes = macStringToBytes(macStr.c_str());
String buttonType = "unknown"; String buttonType = "unknown";
ButtonConfig *btn = nullptr;
if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0) { if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0) {
buttonType = "start1"; buttonType = "start1";
btn = &buttonConfigs.start1;
} else if (memcmp(macBytes.data(), buttonConfigs.stop1.mac, 6) == 0) { } else if (memcmp(macBytes.data(), buttonConfigs.stop1.mac, 6) == 0) {
buttonType = "stop1"; buttonType = "stop1";
btn = &buttonConfigs.stop1;
} else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0) { } else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0) {
buttonType = "start2"; buttonType = "start2";
btn = &buttonConfigs.start2;
} else if (memcmp(macBytes.data(), buttonConfigs.stop2.mac, 6) == 0) { } else if (memcmp(macBytes.data(), buttonConfigs.stop2.mac, 6) == 0) {
buttonType = "stop2"; buttonType = "stop2";
btn = &buttonConfigs.stop2;
} }
// Parse payload for timestamp (optional, falls im Payload enthalten) // Parse payload for timestamp (optional)
uint64_t timestamp = millis(); uint64_t timestamp = millis();
StaticJsonDocument<128> payloadDoc; StaticJsonDocument<128> payloadDoc;
if (payload && strlen(payload) > 0 && if (payload && strlen(payload) > 0 &&
@@ -139,17 +143,22 @@ void handleHeartbeatTopic(const char *topic, const char *payload) {
} }
} }
// Update heartbeat info
if (btn) {
btn->lastHeartbeat = millis();
btn->heartbeatActive = true;
}
// JSON bauen // JSON bauen
StaticJsonDocument<128> doc; StaticJsonDocument<128> doc;
doc["button"] = buttonType; doc["button"] = buttonType;
doc["mac"] = macStr; doc["mac"] = macStr;
doc["timestamp"] = timestamp; doc["timestamp"] = timestamp;
doc["active"] = true; // always true on heartbeat
String json; String json;
serializeJson(doc, json); serializeJson(doc, json);
pushUpdateToFrontend( pushUpdateToFrontend(json);
json); // Diese Funktion schickt das JSON an alle WebSocket-Clients
// Serial.printf("Published heartbeat JSON: %s\n", json.c_str());
} }
/** /**
@@ -213,7 +222,21 @@ void handleBatteryTopic(const char *topic, const char *payload) {
pushUpdateToFrontend( pushUpdateToFrontend(
json); // Diese Funktion schickt das JSON an alle WebSocket-Clients json); // Diese Funktion schickt das JSON an alle WebSocket-Clients
// Serial.printf("Battery level for %s (%s): %d%%\n", buttonType.c_str(),macStr.c_str(), batteryLevelP); // Serial.printf("Battery level for %s (%s): %d%%\n",
// buttonType.c_str(),macStr.c_str(), batteryLevelP);
}
void publishLaneStatus(int lane, String status) {
JsonDocument messageDoc;
messageDoc["lane"] = lane;
messageDoc["status"] = status;
String message;
serializeJson(messageDoc, message);
// Topic dynamisch nach Bahn wählen
String topic = "aquacross/lanes/lane" + String(lane);
mqtt.publish(topic.c_str(), message);
} }
/** /**
@@ -366,3 +389,32 @@ void sendMQTTJSONMessage(const char *topic, const JsonDocument &doc) {
Serial.printf("Published JSON message to topic '%s': %s\n", topic, Serial.printf("Published JSON message to topic '%s': %s\n", topic,
jsonString.c_str()); jsonString.c_str());
} }
void checkHeartbeatTimeouts(unsigned long timeoutMs = 10000) {
struct ButtonRef {
ButtonConfig &btn;
const char *type;
};
ButtonRef buttons[] = {{buttonConfigs.start1, "start1"},
{buttonConfigs.stop1, "stop1"},
{buttonConfigs.start2, "start2"},
{buttonConfigs.stop2, "stop2"}};
unsigned long now = millis();
for (auto &b : buttons) {
if (b.btn.isAssigned && b.btn.heartbeatActive &&
now - b.btn.lastHeartbeat > timeoutMs) {
b.btn.heartbeatActive = false;
// Send inactive status to frontend
StaticJsonDocument<128> doc;
doc["button"] = b.type;
doc["mac"] = b.btn.mac;
doc["active"] = false;
String json;
serializeJson(doc, json);
pushUpdateToFrontend(json);
}
}
}

View File

@@ -8,3 +8,10 @@ std::array<uint8_t, 6> macStringToBytes(const char *macStr) {
&bytes[2], &bytes[3], &bytes[4], &bytes[5]); &bytes[2], &bytes[3], &bytes[4], &bytes[5]);
return bytes; return bytes;
} }
std::string macToString(const std::array<uint8_t, 6> &macBytes) {
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", macBytes[0],
macBytes[1], macBytes[2], macBytes[3], macBytes[4], macBytes[5]);
return std::string(macStr);
}

View File

@@ -31,6 +31,7 @@ void handleStart1(uint64_t timestamp = 0) {
timerData.localStartTime1 = millis(); // Set local start time timerData.localStartTime1 = millis(); // Set local start time
timerData.isRunning1 = true; timerData.isRunning1 = true;
timerData.endTime1 = 0; timerData.endTime1 = 0;
publishLaneStatus(1, "running");
Serial.println("Bahn 1 gestartet"); Serial.println("Bahn 1 gestartet");
} }
} }
@@ -46,6 +47,7 @@ void handleStop1(uint64_t timestamp = 0) {
timerData.bestTime1 = currentTime; timerData.bestTime1 = currentTime;
saveBestTimes(); saveBestTimes();
} }
publishLaneStatus(1, "stopped");
Serial.println("Bahn 1 gestoppt - Zeit: " + String(currentTime / 1000.0) + Serial.println("Bahn 1 gestoppt - Zeit: " + String(currentTime / 1000.0) +
"s"); "s");
} }
@@ -58,6 +60,7 @@ void handleStart2(uint64_t timestamp = 0) {
timerData.localStartTime2 = millis(); // Set local start time timerData.localStartTime2 = millis(); // Set local start time
timerData.isRunning2 = true; timerData.isRunning2 = true;
timerData.endTime2 = 0; timerData.endTime2 = 0;
publishLaneStatus(2, "running");
Serial.println("Bahn 2 gestartet"); Serial.println("Bahn 2 gestartet");
} }
} }
@@ -73,6 +76,7 @@ void handleStop2(uint64_t timestamp = 0) {
timerData.bestTime2 = currentTime; timerData.bestTime2 = currentTime;
saveBestTimes(); saveBestTimes();
} }
publishLaneStatus(2, "stopped");
Serial.println("Bahn 2 gestoppt - Zeit: " + String(currentTime / 1000.0) + Serial.println("Bahn 2 gestoppt - Zeit: " + String(currentTime / 1000.0) +
"s"); "s");
} }
@@ -85,6 +89,7 @@ void checkAutoReset() {
(currentTime - timerData.localStartTime1 > maxTimeBeforeReset)) { (currentTime - timerData.localStartTime1 > maxTimeBeforeReset)) {
timerData.isRunning1 = false; timerData.isRunning1 = false;
timerData.startTime1 = 0; timerData.startTime1 = 0;
publishLaneStatus(1, "ready");
Serial.println("Bahn 1 automatisch zurückgesetzt"); Serial.println("Bahn 1 automatisch zurückgesetzt");
} }
@@ -92,6 +97,7 @@ void checkAutoReset() {
(currentTime - timerData.localStartTime2 > maxTimeBeforeReset)) { (currentTime - timerData.localStartTime2 > maxTimeBeforeReset)) {
timerData.isRunning2 = false; timerData.isRunning2 = false;
timerData.startTime2 = 0; timerData.startTime2 = 0;
publishLaneStatus(2, "ready");
Serial.println("Bahn 2 automatisch zurückgesetzt"); Serial.println("Bahn 2 automatisch zurückgesetzt");
} }
@@ -114,6 +120,7 @@ void checkAutoReset() {
// Push the message to the frontend // Push the message to the frontend
pushUpdateToFrontend(message); pushUpdateToFrontend(message);
publishLaneStatus(1, "ready");
Serial.println("Bahn 1 automatisch auf 'Bereit' zurückgesetzt"); Serial.println("Bahn 1 automatisch auf 'Bereit' zurückgesetzt");
} }
@@ -137,6 +144,7 @@ void checkAutoReset() {
// Push the message to the frontend // Push the message to the frontend
pushUpdateToFrontend(message); pushUpdateToFrontend(message);
publishLaneStatus(2, "ready");
Serial.println("Bahn 2 automatisch auf 'Bereit' zurückgesetzt"); Serial.println("Bahn 2 automatisch auf 'Bereit' zurückgesetzt");
} }

View File

@@ -34,6 +34,8 @@ struct ButtonConfig {
uint8_t mac[6]; uint8_t mac[6];
bool isAssigned = false; bool isAssigned = false;
float voltage = 0.0; float voltage = 0.0;
unsigned long lastHeartbeat = 0; // Zeit des letzten Heartbeats (millis)
bool heartbeatActive = false; // Status: aktiv/inaktiv
}; };
struct ButtonConfigs { struct ButtonConfigs {