diff --git a/data/firmware.bin b/data/firmware.bin index 1d9b269..26b150b 100644 Binary files a/data/firmware.bin and b/data/firmware.bin differ diff --git a/data/settings.html b/data/settings.html index fd2552d..7e7e6aa 100644 --- a/data/settings.html +++ b/data/settings.html @@ -1,4 +1,4 @@ - + @@ -255,6 +255,7 @@
IP-Adresse: Laden...
Kanal: Laden...
MAC-Adresse: Laden...
+
Internet: Laden...
Freier Speicher: Laden...
Verbundene Buttons: Laden... @@ -325,8 +326,9 @@ minute: "2-digit", second: "2-digit", }); - document.getElementById("currentTime").textContent = - `System Zeit: ${timeString}`; + document.getElementById( + "currentTime" + ).textContent = `System Zeit: ${timeString}`; } else { document.getElementById("currentTime").textContent = "Aktuelle Zeit: Fehler beim Laden"; @@ -356,7 +358,7 @@ }) .catch((error) => { console.log( - "Zeit konnte nicht geladen werden, verwende Browser-Zeit", + "Zeit konnte nicht geladen werden, verwende Browser-Zeit" ); syncWithBrowserTime(); }); @@ -401,7 +403,7 @@ } }) .catch((error) => - showMessage("Verbindungsfehler beim Setzen der Zeit", "error"), + showMessage("Verbindungsfehler beim Setzen der Zeit", "error") ); } @@ -438,7 +440,7 @@ } }) .catch((error) => - showMessage("Verbindungsfehler beim Setzen der Zeit", "error"), + showMessage("Verbindungsfehler beim Setzen der Zeit", "error") ); }); @@ -459,7 +461,7 @@ data.maxTimeDisplay || 20; }) .catch((error) => - showMessage("Fehler beim Laden der Einstellungen", "error"), + showMessage("Fehler beim Laden der Einstellungen", "error") ); } @@ -474,6 +476,9 @@ data.channel || "Unbekannt"; document.getElementById("macAddress").textContent = data.mac || "Unbekannt"; + document.getElementById("isOnline").textContent = data.isOnline + ? "Ja" + : "Nein"; document.getElementById("freeMemory").textContent = (data.freeMemory || 0) + " Bytes"; document.getElementById("connectedButtons").textContent = @@ -499,7 +504,7 @@ loadLocations(); }) .catch((error) => - showMessage("Fehler beim Laden der Lizenz", "error"), + showMessage("Fehler beim Laden der Lizenz", "error") ); } @@ -537,7 +542,7 @@ data.password || ""; }) .catch((error) => - showMessage("Fehler beim Laden der WLAN-Einstellungen", "error"), + showMessage("Fehler beim Laden der WLAN-Einstellungen", "error") ); } @@ -559,7 +564,7 @@ } if ( !confirm( - "Der Server wird nach dem setzten neu gestartet. Fortsetzten?", + "Der Server wird nach dem setzten neu gestartet. Fortsetzten?" ) ) { return; @@ -570,19 +575,21 @@ headers: { "Content-Type": "application/x-www-form-urlencoded", }, - body: `ssid=${encodeURIComponent(ssid)}&password=${encodeURIComponent(password)}`, + body: `ssid=${encodeURIComponent( + ssid + )}&password=${encodeURIComponent(password)}`, }) .then((response) => response.json()) .then((data) => { if (data.success) { showMessage( "WLAN-Einstellungen erfolgreich gespeichert!", - "success", + "success" ); } else { showMessage( data.error || "Fehler beim Speichern der WLAN-Einstellungen", - "error", + "error" ); } }) @@ -593,10 +600,10 @@ function updateOTAButtonAccess(licenseLevel) { const otaButton = document.getElementById("otaUpdateBtn"); const otarestrictionNotice = document.getElementById( - "otaRestrictionNotice", + "otaRestrictionNotice" ); const otacurrentLevelSpan = document.getElementById( - "currentLicenseLevel", + "currentLicenseLevel" ); const level = parseInt(licenseLevel) || 0; @@ -625,14 +632,14 @@ if (response.status === 200) { showMessage( "Buttons führen das Update erfolgreich aus!", - "success", + "success" ); } else { showMessage("Fehler beim Senden der MQTT Message", "error"); } }) .catch((error) => - showMessage("Verbindungsfehler beim MQTT Publish", "error"), + showMessage("Verbindungsfehler beim MQTT Publish", "error") ); } } @@ -641,10 +648,10 @@ const wifiSubmitBtn = document.getElementById("wifiSubmitBtn"); const wifiForm = document.getElementById("wifiForm"); const wifiRestrictionNotice = document.getElementById( - "wifiRestrictionNotice", + "wifiRestrictionNotice" ); const wifiCurrentLevelSpan = document.getElementById( - "currentLicenseLevel", + "currentLicenseLevel" ); const level = parseInt(licenseLevel) || 0; @@ -679,14 +686,14 @@ ) { showMessage( "OTA Update erfordert Lizenz Level 2 oder höher", - "error", + "error" ); return; } if ( confirm( - "Möchten Sie wirklich ein OTA Update durchführen? Das Gerät wird während des Updates neu gestartet.", + "Möchten Sie wirklich ein OTA Update durchführen? Das Gerät wird während des Updates neu gestartet." ) ) { window.location.href = "/update"; @@ -701,7 +708,7 @@ const maxTime = parseInt(document.getElementById("maxTime").value); const maxTimeDisplay = parseInt( - document.getElementById("maxTimeDisplay").value, + document.getElementById("maxTimeDisplay").value ); fetch("/api/set-max-time", { @@ -720,7 +727,7 @@ if (data.success) { showMessage( "Einstellungen erfolgreich gespeichert!", - "success", + "success" ); } else { showMessage("Fehler beim Speichern der Einstellungen", "error"); @@ -738,7 +745,7 @@ if (data.success) { showMessage( "Beste Zeiten erfolgreich zurückgesetzt!", - "success", + "success" ); } else { showMessage("Fehler beim Zurücksetzen", "error"); @@ -786,8 +793,9 @@ // Anlern-Anweisung aktualisieren function updateLearningInstruction() { if (learningStep < learningSteps.length) { - document.getElementById("learningInstruction").innerHTML = - `Drücken Sie jetzt den Button für: ${learningSteps[learningStep]}`; + document.getElementById( + "learningInstruction" + ).innerHTML = `Drücken Sie jetzt den Button für: ${learningSteps[learningStep]}`; } } @@ -814,7 +822,7 @@ if (learningStep > 0) { showMessage( `${learningSteps[learningStep - 1]} erfolgreich angelernt!`, - "success", + "success" ); } } @@ -836,13 +844,13 @@ if (data.success) { showMessage( "Button-Zuweisungen erfolgreich zurückgesetzt!", - "success", + "success" ); loadSystemInfo(); } else { showMessage( "Fehler beim Zurücksetzen der Button-Zuweisungen", - "error", + "error" ); } }) @@ -857,22 +865,46 @@ .then((data) => { let statusText = "Button-Status:\n\n"; statusText += `Bahn 1 Start: ${ - data.lane1Start ? "Konfiguriert" : "Nicht konfiguriert" + data.lane1Start + ? `Konfiguriert${ + data.lane1StartVoltage !== undefined + ? " (Batterystand: " + data.lane1StartVoltage + " %)" + : "" + }` + : "Nicht konfiguriert" }\n`; statusText += `Bahn 1 Stop: ${ - data.lane1Stop ? "Konfiguriert" : "Nicht konfiguriert" + data.lane1Stop + ? `Konfiguriert${ + data.lane1StopVoltage !== undefined + ? " (Batterystand: " + data.lane1StopVoltage + " %)" + : "" + }` + : "Nicht konfiguriert" }\n`; statusText += `Bahn 2 Start: ${ - data.lane2Start ? "Konfiguriert" : "Nicht konfiguriert" + data.lane2Start + ? `Konfiguriert${ + data.lane2StartVoltage !== undefined + ? " (Batterystand: " + data.lane2StartVoltage + " %)" + : "" + }` + : "Nicht konfiguriert" }\n`; statusText += `Bahn 2 Stop: ${ - data.lane2Stop ? "Konfiguriert" : "Nicht konfiguriert" + data.lane2Stop + ? `Konfiguriert${ + data.lane2StopVoltage !== undefined + ? " (Batterystand: " + data.lane2StopVoltage + " %)" + : "" + }` + : "Nicht konfiguriert" }\n`; alert(statusText); }) .catch((error) => - showMessage("Fehler beim Laden des Button-Status", "error"), + showMessage("Fehler beim Laden des Button-Status", "error") ); } @@ -925,7 +957,7 @@ .catch((error) => { console.log( "Aktueller Standort konnte nicht geladen werden:", - error, + error ); }); } @@ -934,10 +966,10 @@ function updateLocationAccess(licenseLevel) { const locationSubmitBtn = document.getElementById("locationSubmitBtn"); const locationRestrictionNotice = document.getElementById( - "locationRestrictionNotice", + "locationRestrictionNotice" ); const locationCurrentLevelSpan = document.getElementById( - "currentLocationLicenseLevel", + "currentLocationLicenseLevel" ); const locationSelect = document.getElementById("locationSelect"); @@ -975,7 +1007,7 @@ ) { showMessage( "Standort-Konfiguration erfordert Lizenz Level 3 oder höher", - "error", + "error" ); return; } diff --git a/src/communication.h b/src/communication.h index 2632b93..9da611a 100644 --- a/src/communication.h +++ b/src/communication.h @@ -157,59 +157,64 @@ void handleHeartbeatTopic(const char *topic, const char *payload) { * berechnet den Ladezustand und sendet ein JSON an das Frontend. */ void handleBatteryTopic(const char *topic, const char *payload) { - int batteryLevel = 0; + float batteryLevelV = 0; + int batteryLevelP = 0; String topicStr(topic); int lastSlash = topicStr.lastIndexOf('/'); if (lastSlash < 0) return; String macStr = topicStr.substring(lastSlash + 1); - auto macBytes = macStringToBytes(macStr.c_str()); - - String buttonType = "unknown"; - if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0) { - buttonType = "start1"; - } else if (memcmp(macBytes.data(), buttonConfigs.stop1.mac, 6) == 0) { - buttonType = "stop1"; - } else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0) { - buttonType = "start2"; - } else if (memcmp(macBytes.data(), buttonConfigs.stop2.mac, 6) == 0) { - buttonType = "stop2"; - } - // Parse payload for timestamp (optional, falls im Payload enthalten) StaticJsonDocument<128> payloadDoc; if (payload && strlen(payload) > 0 && deserializeJson(payloadDoc, payload) == DeserializationError::Ok) { if (payloadDoc.containsKey("voltage")) { - batteryLevel = payloadDoc["voltage"]; + batteryLevelV = payloadDoc["voltage"]; } } // Berechne die Prozentzahl des Batteriestands für eine 1S LIPO batteryLevel // sind Volts // Hier wird angenommen, dass 3.7V 100% entspricht und 3.0V 0% - batteryLevel = - batteryLevel * 1000; // Umwandlung von V in mV für genauere Berechnung - if (batteryLevel < 3200) { - batteryLevel = 0; // 0% bei 3.0V - } else if (batteryLevel > 3700) { - batteryLevel = 100; // 100% bei 3.7V + + if (batteryLevelV < 3200) { + batteryLevelP = 0; // 0% bei 3.2V + } else if (batteryLevelV > 3700) { + batteryLevelP = 100; // 100% bei 3.7V } else { - batteryLevel = map(batteryLevel, 3000, 3700, 0, 100); // Linear Mapping + batteryLevelP = map(batteryLevelV, 3200, 3700, 0, 100); // Linear Mapping + } + + auto macBytes = macStringToBytes(macStr.c_str()); + String buttonType = "unknown"; + if (memcmp(macBytes.data(), buttonConfigs.start1.mac, 6) == 0) { + buttonType = "start1"; + buttonConfigs.start1.voltage = batteryLevelP; + } else if (memcmp(macBytes.data(), buttonConfigs.stop1.mac, 6) == 0) { + buttonType = "stop1"; + buttonConfigs.stop1.voltage = batteryLevelP; + } else if (memcmp(macBytes.data(), buttonConfigs.start2.mac, 6) == 0) { + buttonType = "start2"; + buttonConfigs.start2.voltage = batteryLevelP; + } else if (memcmp(macBytes.data(), buttonConfigs.stop2.mac, 6) == 0) { + buttonType = "stop2"; + buttonConfigs.stop2.voltage = batteryLevelP; } // JSON bauen StaticJsonDocument<128> doc; doc["button"] = buttonType; doc["mac"] = macStr; - doc["batteryLevel"] = batteryLevel; + doc["batteryLevel"] = batteryLevelP; 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()); + + // Serial.printf("Battery level for %s (%s): %d%%\n", buttonType.c_str(), + macStr.c_str(), batteryLevelP); } /** diff --git a/src/databasebackend.h b/src/databasebackend.h index 1025ed6..7c1aa09 100644 --- a/src/databasebackend.h +++ b/src/databasebackend.h @@ -4,8 +4,8 @@ #include const char *BACKEND_SERVER = "http://db.reptilfpv.de:3000"; -String BACKEND_TOKEN = - licence; // Use the licence as the token for authentication +extern String licence; // Declare licence as an external variable defined elsewhere +String BACKEND_TOKEN = licence; // Use the licence as the token for authentication bool backendOnline() { diff --git a/src/helper.h b/src/helper.h index c8529d0..80ffa4b 100644 --- a/src/helper.h +++ b/src/helper.h @@ -3,6 +3,7 @@ #include + std::array macStringToBytes(const char *macStr) { std::array bytes; sscanf(macStr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &bytes[0], &bytes[1], diff --git a/src/master.h b/src/master.h index 39c0c6c..5837ea2 100644 --- a/src/master.h +++ b/src/master.h @@ -33,6 +33,7 @@ struct TimerData { struct ButtonConfig { uint8_t mac[6]; bool isAssigned = false; + float voltage = 0.0; }; struct ButtonConfigs { diff --git a/src/timesync.h b/src/timesync.h index ca37863..a6fc2b8 100644 --- a/src/timesync.h +++ b/src/timesync.h @@ -49,6 +49,29 @@ String getCurrentTimeJSON() { return response; } +void syncTimeWithNTP(const char *ntpServer = "pool.ntp.org", + long gmtOffset_sec = 3600, int daylightOffset_sec = 0) { + configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); + Serial.println("Warte auf NTP-Zeit (max 5s)..."); + struct tm timeinfo; + unsigned long start = millis(); + bool synced = false; + while (millis() - start < 5000) { + if (getLocalTime(&timeinfo, 10)) { // 10ms Timeout pro Versuch + synced = true; + break; + } + delay(10); // Kurze Pause, damit der Loop nicht blockiert + } + if (synced) { + Serial.println("\nNTP-Zeit synchronisiert!"); + Serial.printf("Aktuelle Zeit: %02d:%02d:%02d\n", timeinfo.tm_hour, + timeinfo.tm_min, timeinfo.tm_sec); + } else { + Serial.println("\nNTP-Sync fehlgeschlagen (Timeout nach 5s)"); + } +} + // Hilfsfunktion: Setzt die Systemzeit auf den angegebenen Zeitstempel. bool setSystemTime(long timestamp) { struct timeval tv; diff --git a/src/webserverrouter.h b/src/webserverrouter.h index 9737c5a..224a219 100644 --- a/src/webserverrouter.h +++ b/src/webserverrouter.h @@ -143,9 +143,13 @@ void setupRoutes() { [](AsyncWebServerRequest *request) { DynamicJsonDocument doc(128); doc["lane1Start"] = buttonConfigs.start1.isAssigned; + doc["lane1StartVoltage"] = buttonConfigs.start1.voltage; doc["lane1Stop"] = buttonConfigs.stop1.isAssigned; + doc["lane1StopVoltage"] = buttonConfigs.stop1.voltage; doc["lane2Start"] = buttonConfigs.start2.isAssigned; + doc["lane2StartVoltage"] = buttonConfigs.start2.voltage; doc["lane2Stop"] = buttonConfigs.stop2.isAssigned; + doc["lane2StopVoltage"] = buttonConfigs.stop2.voltage; String result; serializeJson(doc, result); request->send(200, "application/json", result); @@ -181,7 +185,7 @@ void setupRoutes() { if (buttonConfigs.stop2.isAssigned) connected++; doc["connectedButtons"] = connected; - + doc["isOnline"] = isInternetAvailable() ? true : false; doc["valid"] = checkLicence() > 0 ? "Ja" : "Nein"; doc["tier"] = checkLicence(); diff --git a/src/wificlass.h b/src/wificlass.h index 7be0e60..7b6e477 100644 --- a/src/wificlass.h +++ b/src/wificlass.h @@ -8,12 +8,14 @@ #include "licenceing.h" #include "master.h" +#include "timesync.h" String uniqueSSID; PrettyOTA OTAUpdates; String getUniqueSSID(); +bool isInternetAvailable(); // Initialisiert das WLAN als Access Point oder Station und startet mDNS/OTA. void setupWifi() { @@ -48,6 +50,9 @@ void setupWifi() { Serial.println("Erfolgreich mit WLAN verbunden!"); Serial.print("IP Adresse: "); Serial.println(WiFi.localIP()); + if (isInternetAvailable) { + syncTimeWithNTP(); + } // Synchronisiere Zeit mit NTP } // Only wait for connection if ssidSTA and passwordSTA are set @@ -95,4 +100,9 @@ String getUniqueSSID() { return String("AquaCross-") + String(uniqueId); } -// WiFi als Access Point +// Prüft, ob das Internet erreichbar ist (z.B. durch Ping auf 8.8.8.8) +bool isInternetAvailable() { + WiFiClient client; + // Versuche, eine Verbindung zu Googles DNS auf Port 53 herzustellen + return client.connect("8.8.8.8", 53); +}