diff --git a/data/firmware.bin b/data/firmware.bin index 89a1a10..1c22703 100644 Binary files a/data/firmware.bin and b/data/firmware.bin differ diff --git a/data/index.html b/data/index.html index 3032c51..a0cf1f6 100644 --- a/data/index.html +++ b/data/index.html @@ -144,7 +144,26 @@ }; 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 { const data = JSON.parse(event.data); @@ -328,20 +347,23 @@ let display1 = timer1; 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; } - if (status2 === "running") { + if (status2 === "running" && lane2Connected) { display2 += (now - lastSync) / 1000; } document.getElementById("time1").textContent = formatTime(display1); - // Status für Bahn 1 - const s1 = document.getElementById("status1"); - const lane1Connected = areBothButtonsConnected(1); - - if (status1 === "ready" && !lane1Connected) { + if (!lane1Connected) { s1.className = "status standby"; s1.textContent = "Standby: Bitte beide Buttons 1x betätigen"; } else { @@ -356,13 +378,9 @@ document.getElementById("time2").textContent = formatTime(display2); - // Status für Bahn 2 - const s2 = document.getElementById("status2"); - const lane2Connected = areBothButtonsConnected(2); - - if (status2 === "ready" && !lane2Connected) { + if (!lane2Connected) { s2.className = "status standby"; - s2.textContent = "Standby: Bitte beide Buttons 1x betätigen"; + s2.textContent = "Standby: Bitte beide 1x betätigen"; } else { s2.className = `status ${status2}`; s2.textContent = diff --git a/platformio.ini b/platformio.ini index 262a2b3..b4909fc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -89,3 +89,19 @@ lib_deps = mlesniew/PicoMQTT@^1.3.0 miguelbalboa/MFRC522@^1.4.12 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 \ No newline at end of file diff --git a/src/communication.h b/src/communication.h index a192e48..614ebb1 100644 --- a/src/communication.h +++ b/src/communication.h @@ -109,7 +109,6 @@ void readButtonJSON(const char *topic, const char *payload) { * extrahiert MAC und Timestamp und sendet ein JSON an das Frontend. */ void handleHeartbeatTopic(const char *topic, const char *payload) { - // Topic-Format: heartbeat/alive/CC:DB:A7:2F:95:08 String topicStr(topic); int lastSlash = topicStr.lastIndexOf('/'); if (lastSlash < 0) @@ -119,17 +118,22 @@ void handleHeartbeatTopic(const char *topic, const char *payload) { auto macBytes = macStringToBytes(macStr.c_str()); String buttonType = "unknown"; + ButtonConfig *btn = nullptr; if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0) { buttonType = "start1"; + btn = &buttonConfigs.start1; } else if (memcmp(macBytes.data(), buttonConfigs.stop1.mac, 6) == 0) { buttonType = "stop1"; + btn = &buttonConfigs.stop1; } else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0) { buttonType = "start2"; + btn = &buttonConfigs.start2; } else if (memcmp(macBytes.data(), buttonConfigs.stop2.mac, 6) == 0) { buttonType = "stop2"; + btn = &buttonConfigs.stop2; } - // Parse payload for timestamp (optional, falls im Payload enthalten) + // Parse payload for timestamp (optional) uint64_t timestamp = millis(); StaticJsonDocument<128> payloadDoc; 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 StaticJsonDocument<128> doc; doc["button"] = buttonType; doc["mac"] = macStr; doc["timestamp"] = timestamp; + doc["active"] = true; // always true on heartbeat String json; serializeJson(doc, json); - pushUpdateToFrontend( - json); // Diese Funktion schickt das JSON an alle WebSocket-Clients - // Serial.printf("Published heartbeat JSON: %s\n", json.c_str()); + pushUpdateToFrontend(json); } /** @@ -213,7 +222,21 @@ void handleBatteryTopic(const char *topic, const char *payload) { pushUpdateToFrontend( 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, 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); + } + } +} diff --git a/src/helper.h b/src/helper.h index e94dd37..c2fe1ac 100644 --- a/src/helper.h +++ b/src/helper.h @@ -7,4 +7,11 @@ std::array macStringToBytes(const char *macStr) { sscanf(macStr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &bytes[0], &bytes[1], &bytes[2], &bytes[3], &bytes[4], &bytes[5]); return bytes; +} + +std::string macToString(const std::array &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); } \ No newline at end of file diff --git a/src/master.cpp b/src/master.cpp index d48965f..4fc66eb 100644 --- a/src/master.cpp +++ b/src/master.cpp @@ -31,6 +31,7 @@ void handleStart1(uint64_t timestamp = 0) { timerData.localStartTime1 = millis(); // Set local start time timerData.isRunning1 = true; timerData.endTime1 = 0; + publishLaneStatus(1, "running"); Serial.println("Bahn 1 gestartet"); } } @@ -46,6 +47,7 @@ void handleStop1(uint64_t timestamp = 0) { timerData.bestTime1 = currentTime; saveBestTimes(); } + publishLaneStatus(1, "stopped"); Serial.println("Bahn 1 gestoppt - Zeit: " + String(currentTime / 1000.0) + "s"); } @@ -58,6 +60,7 @@ void handleStart2(uint64_t timestamp = 0) { timerData.localStartTime2 = millis(); // Set local start time timerData.isRunning2 = true; timerData.endTime2 = 0; + publishLaneStatus(2, "running"); Serial.println("Bahn 2 gestartet"); } } @@ -73,6 +76,7 @@ void handleStop2(uint64_t timestamp = 0) { timerData.bestTime2 = currentTime; saveBestTimes(); } + publishLaneStatus(2, "stopped"); Serial.println("Bahn 2 gestoppt - Zeit: " + String(currentTime / 1000.0) + "s"); } @@ -85,6 +89,7 @@ void checkAutoReset() { (currentTime - timerData.localStartTime1 > maxTimeBeforeReset)) { timerData.isRunning1 = false; timerData.startTime1 = 0; + publishLaneStatus(1, "ready"); Serial.println("Bahn 1 automatisch zurückgesetzt"); } @@ -92,6 +97,7 @@ void checkAutoReset() { (currentTime - timerData.localStartTime2 > maxTimeBeforeReset)) { timerData.isRunning2 = false; timerData.startTime2 = 0; + publishLaneStatus(2, "ready"); Serial.println("Bahn 2 automatisch zurückgesetzt"); } @@ -114,6 +120,7 @@ void checkAutoReset() { // Push the message to the frontend pushUpdateToFrontend(message); + publishLaneStatus(1, "ready"); Serial.println("Bahn 1 automatisch auf 'Bereit' zurückgesetzt"); } @@ -137,6 +144,7 @@ void checkAutoReset() { // Push the message to the frontend pushUpdateToFrontend(message); + publishLaneStatus(2, "ready"); Serial.println("Bahn 2 automatisch auf 'Bereit' zurückgesetzt"); } diff --git a/src/master.h b/src/master.h index 5837ea2..1dfac6f 100644 --- a/src/master.h +++ b/src/master.h @@ -34,6 +34,8 @@ struct ButtonConfig { uint8_t mac[6]; bool isAssigned = false; float voltage = 0.0; + unsigned long lastHeartbeat = 0; // Zeit des letzten Heartbeats (millis) + bool heartbeatActive = false; // Status: aktiv/inaktiv }; struct ButtonConfigs {