diff --git a/data/index.css b/data/index.css index 4cad1fc..21ab08e 100644 --- a/data/index.css +++ b/data/index.css @@ -150,6 +150,35 @@ html { color: #fff; } + .swimmer-name { + font-size: clamp(1.5rem, 3.5vw, 2.2rem); + font-weight: bold; + margin-bottom: clamp(15px, 2vh, 25px); + padding: clamp(8px, 1.5vh, 12px) clamp(12px, 2vw, 18px); + background: rgba(255, 255, 255, 0.2); + border-radius: 15px; + border: 2px solid rgba(255, 255, 255, 0.3); + color: #fff; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(5px); + animation: fadeIn 0.5s ease-in; + text-align: center; + word-wrap: break-word; + line-height: 1.2; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + .time-display { font-size: clamp(3rem, 9vw, 10rem); font-weight: bold; @@ -280,6 +309,11 @@ html { body { padding: 10px; } + + .swimmer-name { + font-size: clamp(1.2rem, 4vw, 1.8rem); + margin-bottom: clamp(10px, 1.5vh, 20px); + } } @media (max-width: 480px) { @@ -299,4 +333,9 @@ html { .timer-container { padding: 0 2vw; } - } \ No newline at end of file + + .swimmer-name { + font-size: clamp(1rem, 4vw, 1.5rem); + padding: clamp(6px, 1vh, 10px) clamp(8px, 1.5vw, 12px); + } + } \ No newline at end of file diff --git a/data/index.html b/data/index.html index a8c117c..e0d17db 100644 --- a/data/index.html +++ b/data/index.html @@ -1,14 +1,14 @@ - + NinjaCross Timer - + @@ -29,12 +29,14 @@
+

🏊‍♀️ Bahn 1

00.00
Bereit
+

🏊‍♂️ Bahn 2

00.00
Bereit
@@ -64,6 +66,37 @@ let lastSync = Date.now(); let learningMode = false; let learningButton = ""; + let name1 = ""; + let name2 = ""; + const ws = new WebSocket(`ws://${window.location.host}/ws`); + +// Handle WebSocket events +ws.onopen = () => { + console.log("WebSocket connected"); +}; +ws.onclose = () => { + console.log("WebSocket disconnected"); +}; + +ws.onmessage = (event) => { + console.log("WebSocket message received:", event.data); + + try { + const data = JSON.parse(event.data); + + if (data.firstname && data.lastname && data.lane) { + if (data.lane === "start1") { + name1 = `${data.firstname} ${data.lastname}`; + } else if (data.lane === "start2") { + name2 = `${data.firstname} ${data.lastname}`; + } + + updateDisplay(); + } + } catch (error) { + console.error("Error processing WebSocket message:", error); + } +}; function formatTime(seconds) { if (seconds === 0) return "00.00"; @@ -83,7 +116,6 @@ display2 += (now - lastSync) / 1000; } - document.getElementById("time1").textContent = formatTime(display1); const s1 = document.getElementById("status1"); s1.className = `status ${status1}`; @@ -109,6 +141,24 @@ document.getElementById("best2").textContent = best2 > 0 ? formatTime(best2) + "s" : "--.-"; + // Namen anzeigen/verstecken - verbesserte Logik + const name1Element = document.getElementById("name1"); + const name2Element = document.getElementById("name2"); + + if (name1 && name1.trim() !== "") { + name1Element.textContent = name1; + name1Element.style.display = "block"; + } else { + name1Element.style.display = "none"; + } + + if (name2 && name2.trim() !== "") { + name2Element.textContent = name2; + name2Element.style.display = "block"; + } else { + name2Element.style.display = "none"; + } + // Lernmodus const learningDisplay = document.getElementById("learning-display"); if (learningMode) { @@ -139,7 +189,7 @@ ); } - // Sync with backend every 2 seconds + // Sync with backend every 1 second setInterval(syncFromBackend, 1000); // Smooth update every 50ms diff --git a/data/pictures/favicon.ico b/data/pictures/favicon.ico new file mode 100644 index 0000000..5f2904b Binary files /dev/null and b/data/pictures/favicon.ico differ diff --git a/data/rfid.html b/data/rfid.html index 19330bb..9709e92 100644 --- a/data/rfid.html +++ b/data/rfid.html @@ -4,6 +4,7 @@ + RFID Daten Eingabe diff --git a/data/settings.html b/data/settings.html index 8da9d6f..0895255 100644 --- a/data/settings.html +++ b/data/settings.html @@ -4,6 +4,7 @@ + diff --git a/src/communication.h b/src/communication.h index 207824d..6a70efa 100644 --- a/src/communication.h +++ b/src/communication.h @@ -11,6 +11,8 @@ #include "helper.h" #include #include +#include +#include struct TimestampData { uint64_t lastMessageTimestamp; // Timestamp from the device @@ -35,52 +37,6 @@ typedef struct { PicoMQTT::Server mqtt; -void processHeartbeat(const char* topic, const char* payload) { - String macAddress = String(topic).substring(15); - - StaticJsonDocument<200> doc; - DeserializationError error = deserializeJson(doc, payload); - - if (error) { - Serial.printf("JSON parsing failed for MAC %s: %s\n", macAddress.c_str(), error.c_str()); - return; - } - - uint64_t messageTimestamp = doc["timestamp"] | 0; - uint64_t currentLocalTime = getCurrentTimestampMs(); - - // Update timestamps for current device - if (deviceTimestamps.count(macAddress) > 0) { - TimestampData& data = deviceTimestamps[macAddress]; - uint64_t messageDiff = messageTimestamp - data.lastMessageTimestamp; - uint64_t localDiff = currentLocalTime - data.lastLocalTimestamp; - data.drift = localDiff - messageDiff; - } - - // Calculate drift relative to all other devices - Serial.printf("\nDrift analysis for device %s:\n", macAddress.c_str()); - Serial.println("----------------------------------------"); - - for (const auto& device : deviceTimestamps) { - if (device.first != macAddress) { // Skip comparing to self - int64_t timeDiff = messageTimestamp - device.second.lastMessageTimestamp; - int64_t relativeDrift = timeDiff - (currentLocalTime - device.second.lastLocalTimestamp); - - Serial.printf("Relative to %s:\n", device.first.c_str()); - Serial.printf(" Time difference: %lld ms\n", timeDiff); - Serial.printf(" Relative drift: %lld ms\n", relativeDrift); - Serial.println("----------------------------------------"); - } - } - - // Update stored timestamps for current device - deviceTimestamps[macAddress] = { - messageTimestamp, - currentLocalTime, - deviceTimestamps[macAddress].drift - }; -} - void readButtonJSON(const char * topic, const char * payload) { if(strcmp(topic, "aquacross/button/press") == 0){ @@ -133,22 +89,99 @@ void readButtonJSON(const char * topic, const char * payload) { } } +void readRFIDfromButton(const char * topic, const char * payload) { + // Create a JSON document to hold the button press data + StaticJsonDocument<256> doc; + DeserializationError error = deserializeJson(doc, payload); + if (!error) { + const char* mac = doc["buttonmac"] | "unknown"; + const char* uid = doc["uid"] | "unknown"; + + Serial.printf("RFID Read from Button:\n"); + Serial.printf(" Button MAC: %s\n", mac); + Serial.printf(" UID: %s\n", uid); + + // Convert buttonmac to byte array for comparison + auto macBytes = macStringToBytes(mac); + + // Check if the buttonmac matches buttonConfigs.start1.mac + if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0) { + // Fetch user data + UserData userData = checkUser(uid); + if (userData.exists) { + // Log user data + Serial.printf("User found for start1: %s %s, Alter: %d\n", + userData.firstname.c_str(), + userData.lastname.c_str(), + userData.alter); + + // Create JSON message to send to the frontend + StaticJsonDocument<128> messageDoc; + messageDoc["firstname"] = userData.firstname; + messageDoc["lastname"] = userData.lastname; + messageDoc["lane"] = "start1"; // Add lane information + + String message; + serializeJson(messageDoc, message); + + // Push the message to the frontend + pushUpdateToFrontend(message); + Serial.printf("Pushed user data for start1 to frontend: %s\n", message.c_str()); + } else { + Serial.println("User not found for UID: " + String(uid)); + } + } + // Check if the buttonmac matches buttonConfigs.start2.mac + else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0) { + // Fetch user data + UserData userData = checkUser(uid); + if (userData.exists) { + // Log user data + Serial.printf("User found for start2: %s %s, Alter: %d\n", + userData.firstname.c_str(), + userData.lastname.c_str(), + userData.alter); + + // Create JSON message to send to the frontend + StaticJsonDocument<128> messageDoc; + messageDoc["firstname"] = userData.firstname; + messageDoc["lastname"] = userData.lastname; + messageDoc["lane"] = "start2"; // Add lane information + + String message; + serializeJson(messageDoc, message); + + // Push the message to the frontend + pushUpdateToFrontend(message); + Serial.printf("Pushed user data for start2 to frontend: %s\n", message.c_str()); + } else { + Serial.println("User not found for UID: " + String(uid)); + } + } else { + Serial.println("Button MAC does not match start1.mac or start2.mac"); + } + } else { + Serial.println("Failed to parse RFID JSON"); + } +} void setupMqttServer() { // Set up the MQTT server with the desired port // Subscribe to a topic pattern and attach a callback mqtt.subscribe("#", [](const char * topic, const char * payload) { - if (strncmp(topic, "heartbeat/alive/", 15) == 0) { - processHeartbeat(topic, payload); - } else if (strcmp(topic, "aquacross/button/press") == 0) { + //Message received callback + //Serial.printf("Received message on topic '%s': %s\n", topic, payload); + if (strcmp(topic, "aquacross/button/press") == 0) { readButtonJSON(topic, payload); + } else if (strncmp(topic, "aquacross/button/rfid/", 22) == 0) { + readRFIDfromButton(topic, payload); + // Handle RFID read messages + } updateStatusLED(3); }); - - // Add the button subscription - + // Start the MQTT server mqtt.begin(); @@ -168,6 +201,7 @@ void loopMqttServer() { mqtt.publish("sync/time", timeStr); lastPublish = millis(); } + } void sendMQTTMessage(const char * topic, const char * message) { @@ -187,3 +221,5 @@ void sendMQTTJSONMessage(const char * topic, const JsonDocument & doc) { Serial.printf("Published JSON message to topic '%s': %s\n", topic, jsonString.c_str()); } + + diff --git a/src/databasebackend.h b/src/databasebackend.h index b5f9b28..22f1b87 100644 --- a/src/databasebackend.h +++ b/src/databasebackend.h @@ -1,3 +1,4 @@ +#pragma once #include #include #include "master.h" @@ -17,34 +18,76 @@ bool backendOnline() { http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN); int httpCode = http.GET(); + bool isOnline = (httpCode == HTTP_CODE_OK); - if (httpCode == HTTP_CODE_OK) { - return true; + if (isOnline) { Serial.println("Database server connection successful"); } else { - return false; Serial.printf("Database server connection failed, error: %d\n", httpCode); } http.end(); + return isOnline; } -bool userExists(const String& userId) { +struct UserData { + String uid; + String firstname; + String lastname; + int alter; + bool exists; +}; + +// UserData checkUser(const String& uid) is defined only once to avoid redefinition errors. +UserData checkUser(const String& uid) { + + UserData userData = {"", "", "", 0, false}; + if (!backendOnline()) { - Serial.println("No internet connection, cannot check user existence."); - return false; + Serial.println("No internet connection, cannot check user."); + return userData; } HTTPClient http; - http.begin(String(BACKEND_SERVER) + "/api/users/" + userId); + http.begin(String(BACKEND_SERVER) + "/api/users/find"); + http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization", String("Bearer ") + BACKEND_TOKEN); - //Post request to check if user exists - int httpCode = http.POST(""); + // Create JSON payload + StaticJsonDocument<200> requestDoc; + requestDoc["uid"] = uid; + String requestBody; + serializeJson(requestDoc, requestBody); + + int httpCode = http.POST(requestBody); + + if (httpCode == HTTP_CODE_OK) { + String payload = http.getString(); + StaticJsonDocument<512> responseDoc; + DeserializationError error = deserializeJson(responseDoc, payload); + + if (!error) { + userData.uid = responseDoc["uid"].as(); + userData.firstname = responseDoc["firstname"].as(); + userData.lastname = responseDoc["lastname"].as(); + userData.alter = responseDoc["alter"] | 0; + userData.exists = true; + } + } else { + Serial.printf("User check failed, HTTP code: %d\n", httpCode); + } + + http.end(); + return userData; +} +// Keep this for backward compatibility +bool userExists(const String& uid) { + return checkUser(uid).exists; } void setupBackendRoutes(AsyncWebServer& server) { + server.on("/api/health", HTTP_GET, [](AsyncWebServerRequest *request) { DynamicJsonDocument doc(64); diff --git a/src/master.cpp b/src/master.cpp index 2b60a5b..cf96831 100644 --- a/src/master.cpp +++ b/src/master.cpp @@ -162,10 +162,26 @@ void saveWifiSettings() { } void loadWifiSettings() { - preferences.begin("wifi", true); - ssidSTA = preferences.getString("ssid", "").c_str(); - passwordSTA = preferences.getString("password", "").c_str(); - preferences.end(); + preferences.begin("wifi", true); + + // Speicher freigeben, falls bereits zugewiesen + if (ssidSTA) { + free(ssidSTA); + } + if (passwordSTA) { + free(passwordSTA); + } + + // Neue Werte laden und dynamisch zuweisen + String ssid = preferences.getString("ssid", ""); + String password = preferences.getString("password", ""); + ssidSTA = strdup(ssid.c_str()); + passwordSTA = strdup(password.c_str()); + + preferences.end(); + + // Debug-Ausgabe + Serial.printf("WLAN-Einstellungen geladen: SSID=%s, Passwort=%s\n", ssidSTA, passwordSTA); } int checkLicence() { @@ -179,7 +195,6 @@ String getTimerDataJSON() { DynamicJsonDocument doc(1024); unsigned long currentTime = millis(); - // Bahn 1 if (timerData.isRunning1) { doc["time1"] = (currentTime - timerData.startTime1) / 1000.0; @@ -234,7 +249,7 @@ void setup() { setupTimeAPI(server); setupLicenceAPI(server); setupDebugAPI(server); - setupBackendRoutes(server); + setupBackendRoutes(server);// Speichere WLAN-Einstellungen, falls noch nicht vorhanden // Gespeicherte Daten laden @@ -246,6 +261,7 @@ void setup() { setupOTA(&server); setupRoutes(); + setupWebSocket(); setupLED(); setupMqttServer(); // MQTT Server initialisieren @@ -255,4 +271,5 @@ void setup() { void loop() { checkAutoReset(); loopMqttServer(); // MQTT Server in der Loop aufrufen + loopWebSocket(); } diff --git a/src/rfid.h b/src/rfid.h index ae355b2..a840930 100644 --- a/src/rfid.h +++ b/src/rfid.h @@ -1,3 +1,4 @@ +#pragma once #include #include #include @@ -21,10 +22,6 @@ struct User { unsigned long timestamp; }; -// Array für Benutzer (max 100 Benutzer) -User users[100]; -int userCount = 0; - void setupRFID() { // SPI und RFID initialisieren diff --git a/src/webserverrouter.h b/src/webserverrouter.h index f1bf43b..d3d8cc0 100644 --- a/src/webserverrouter.h +++ b/src/webserverrouter.h @@ -1,17 +1,24 @@ +#pragma once #include #include "master.h" #include +#include #include #include #include #include +#include AsyncWebServer server(80); +AsyncWebSocket ws("/ws"); void setupRoutes(){ - // Web Server Routes + // Web Server Routes + + // Attach WebSocket to the server + server.addHandler(&ws); server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/index.html", "text/html"); @@ -166,34 +173,35 @@ void setupRoutes(){ }); // Setze WLAN-Name und Passwort (POST) - server.on("/api/set-wifi", HTTP_POST, [](AsyncWebServerRequest *request){ + server.on("/api/set-wifi", HTTP_POST, [](AsyncWebServerRequest *request) { Serial.println("/api/set-wifi called"); String ssid, password; if (request->hasParam("ssid", true)) { - ssid = request->getParam("ssid", true)->value(); + ssid = request->getParam("ssid", true)->value(); } if (request->hasParam("password", true)) { - password = request->getParam("password", true)->value(); + password = request->getParam("password", true)->value(); } if (ssid.length() > 0) { - // Hier speichern wir die neuen Werte (z.B. in Preferences oder global) - // Beispiel: strcpy(ssidSTA, ssid.c_str()); - // Beispiel: strcpy(passwordSTA, password.c_str()); - // In deinem Projekt ggf. persistent speichern! - // Hier als global (unsicher, nach Neustart verloren!): - ssidSTA = strdup(ssid.c_str()); - passwordSTA = strdup(password.c_str()); - // Rückmeldung - DynamicJsonDocument doc(64); - doc["success"] = true; - String result; - serializeJson(doc, result); - request->send(200, "application/json", result); - Serial.println("WiFi-Settings updated (nur bis zum Neustart aktiv!)"); + // Speicher freigeben, bevor neue Werte zugewiesen werden + free(ssidSTA); + free(passwordSTA); + + // Neue Werte zuweisen + ssidSTA = strdup(ssid.c_str()); + passwordSTA = strdup(password.c_str()); + + // Rückmeldung + DynamicJsonDocument doc(64); + doc["success"] = true; + String result; + serializeJson(doc, result); + request->send(200, "application/json", result); + Serial.println("WiFi-Settings updated (nur bis zum Neustart aktiv!)"); } else { - request->send(400, "application/json", "{\"success\":false,\"error\":\"SSID fehlt\"}"); + request->send(400, "application/json", "{\"success\":false,\"error\":\"SSID fehlt\"}"); } - }); +}); // Liefert aktuelle WLAN-Einstellungen (GET) server.on("/api/get-wifi", HTTP_GET, [](AsyncWebServerRequest *request){ @@ -210,4 +218,25 @@ void setupRoutes(){ server.begin(); Serial.println("Web Server gestartet"); +} + +void setupWebSocket() { + ws.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { + if (type == WS_EVT_CONNECT) { + Serial.printf("WebSocket client connected: %u\n", client->id()); + } else if (type == WS_EVT_DISCONNECT) { + Serial.printf("WebSocket client disconnected: %u\n", client->id()); + } else if (type == WS_EVT_DATA) { + // Handle incoming WebSocket messages if needed + Serial.printf("WebSocket message received: %s\n", (char *)data); + } + }); +} + +void pushUpdateToFrontend(const String &message) { + ws.textAll(message); // Send the message to all connected clients +} + +void loopWebSocket() { + ws.cleanupClients(); // Clean up disconnected clients } \ No newline at end of file diff --git a/src/wificlass.h b/src/wificlass.h index a06eb80..83ba500 100644 --- a/src/wificlass.h +++ b/src/wificlass.h @@ -13,8 +13,8 @@ String uniqueSSID; const char* ssidAP; const char* passwordAP = nullptr; -const char* ssidSTA = "Obiwlankenobi"; -const char* passwordSTA = "Delfine1!"; +char* ssidSTA = strdup("Obiwlankenobi"); +char* passwordSTA = strdup("Delfine1!"); PrettyOTA OTAUpdates;