From 4b25b72b2e30a4c701515916008a7e8622c33c0b Mon Sep 17 00:00:00 2001 From: Manuel Weiser Date: Sat, 22 Feb 2025 19:50:12 +0100 Subject: [PATCH] feat: implement enhanced update progress handling and WebSocket notifications --- html/upgrade.html | 201 ++++++++++++++++-------------------- src/website.cpp | 252 +++++++++++++++++++++++++--------------------- 2 files changed, 224 insertions(+), 229 deletions(-) diff --git a/html/upgrade.html b/html/upgrade.html index cef0b12..f57d301 100644 --- a/html/upgrade.html +++ b/html/upgrade.html @@ -154,81 +154,103 @@ const progress = document.querySelector('.progress-bar'); const progressContainer = document.querySelector('.progress-container'); const status = document.querySelector('.status'); - - // WebSocket für Update-Progress - const ws = new WebSocket('ws://' + window.location.host + '/ws'); let updateInProgress = false; + let lastReceivedProgress = 0; - ws.onmessage = function(event) { - try { - const data = JSON.parse(event.data); - if (data.type === "updateProgress" && updateInProgress) { - progressContainer.style.display = 'block'; - - // Setze den Fortschritt nur wenn er größer ist als der aktuelle - const currentProgress = parseInt(progress.textContent); - const newProgress = parseInt(data.progress); - if (isNaN(currentProgress) || newProgress > currentProgress) { - progress.style.width = data.progress + '%'; - progress.textContent = data.progress + '%'; - } - - // Zeige verschiedene Status-Nachrichten - if (data.status === "finalizing") { - status.textContent = "Finalizing update..."; - status.classList.add('success'); - status.style.display = 'block'; - } else if (data.status === "complete" || data.status === "success") { - status.textContent = "Update successful! Device is restarting... Page will reload in 30 seconds."; - status.classList.add('success'); - status.style.display = 'block'; + // WebSocket Handling + let ws = null; + let wsReconnectTimer = null; + + function connectWebSocket() { + ws = new WebSocket('ws://' + window.location.host + '/ws'); + + ws.onmessage = function(event) { + try { + const data = JSON.parse(event.data); + if (data.type === "updateProgress" && updateInProgress) { + // Zeige Fortschrittsbalken + progressContainer.style.display = 'block'; - // Versuche die WebSocket-Verbindung sauber zu schließen - try { - ws.close(); - } catch (e) { - console.log('WebSocket already closed'); + // Aktualisiere den Fortschritt nur wenn er größer ist + const newProgress = parseInt(data.progress); + if (!isNaN(newProgress) && newProgress >= lastReceivedProgress) { + progress.style.width = newProgress + '%'; + progress.textContent = newProgress + '%'; + lastReceivedProgress = newProgress; } + // Zeige Status-Nachricht + if (data.message || data.status) { + status.textContent = data.message || getStatusMessage(data.status); + status.className = 'status success'; + status.style.display = 'block'; + + // Starte Reload wenn Update erfolgreich + if (data.status === 'success' || lastReceivedProgress >= 98) { + clearTimeout(wsReconnectTimer); + setTimeout(() => { + window.location.href = '/'; + }, 30000); + } + } + } + } catch (e) { + console.error('WebSocket message error:', e); + } + }; + + ws.onclose = function() { + if (updateInProgress) { + // Wenn der Fortschritt hoch genug ist, gehen wir von einem erfolgreichen Update aus + if (lastReceivedProgress >= 85) { + status.textContent = "Update appears successful! Device is restarting... Page will reload in 30 seconds."; + status.className = 'status success'; + status.style.display = 'block'; + clearTimeout(wsReconnectTimer); setTimeout(() => { window.location.href = '/'; }, 30000); + } else { + // Versuche Reconnect bei niedrigem Fortschritt + wsReconnectTimer = setTimeout(connectWebSocket, 1000); } } - } catch (e) { - console.error('WebSocket message error:', e); - } - }; + }; - ws.onclose = function() { - // Wenn das Update läuft und der Fortschritt hoch ist, zeige Success - if (updateInProgress) { - const currentProgress = parseInt(progress.textContent); - if (!isNaN(currentProgress) && currentProgress >= 90) { + ws.onerror = function(err) { + console.error('WebSocket error:', err); + if (updateInProgress && lastReceivedProgress >= 85) { status.textContent = "Update appears successful! Device is restarting... Page will reload in 30 seconds."; - status.classList.add('success'); + status.className = 'status success'; status.style.display = 'block'; - - setTimeout(() => { - window.location.href = '/'; - }, 30000); - } else { - status.textContent = "Connection lost. Please wait 30 seconds and check if the update was successful..."; - status.classList.add('warning'); - status.style.display = 'block'; - setTimeout(() => { window.location.href = '/'; }, 30000); } + }; + } + + // Initial WebSocket connection + connectWebSocket(); + + function getStatusMessage(status) { + switch(status) { + case 'starting': return 'Starting update...'; + case 'uploading': return 'Uploading...'; + case 'finalizing': return 'Finalizing update...'; + case 'restoring': return 'Restoring configurations...'; + case 'preparing': return 'Preparing for restart...'; + case 'success': return 'Update successful! Device is restarting... Page will reload in 30 seconds.'; + default: return 'Updating...'; } - }; + } function handleUpdate(e) { e.preventDefault(); const form = e.target; const file = form.update.files[0]; const updateType = form.dataset.type; + if (!file) { alert('Please select a file.'); return; @@ -244,6 +266,7 @@ return; } + // Reset UI updateInProgress = true; progressContainer.style.display = 'block'; status.style.display = 'none'; @@ -251,83 +274,33 @@ progress.style.width = '0%'; progress.textContent = '0%'; + // Disable submit buttons document.querySelectorAll('form input[type=submit]').forEach(btn => btn.disabled = true); + // Send update const xhr = new XMLHttpRequest(); xhr.open('POST', '/update', true); xhr.onload = function() { - if (xhr.status === 200) { - try { - const response = JSON.parse(xhr.responseText); - if (response.success) { - if (progress.textContent !== '100%') { - progress.style.width = '100%'; - progress.textContent = '100%'; - } - status.textContent = "Update successful! Device is restarting... Page will reload in 30 seconds."; - status.classList.add('success'); - status.style.display = 'block'; - setTimeout(() => { - window.location.href = '/'; - }, 30000); - } else { - updateInProgress = false; - status.textContent = response.message || "Update failed"; - status.classList.add('error'); - status.style.display = 'block'; - document.querySelectorAll('form input[type=submit]').forEach(btn => btn.disabled = false); - } - } catch (e) { - if (progress.textContent === '100%') { - // Wenn 100% erreicht wurden, nehmen wir an, dass das Update erfolgreich war - status.textContent = "Update appears successful! Device is restarting... Page will reload in 30 seconds."; - status.classList.add('success'); - status.style.display = 'block'; - setTimeout(() => { - window.location.href = '/'; - }, 30000); - } else { - handleUpdateError("Invalid server response"); - } - } - } else { - if (progress.textContent === '100%') { - // Bei 100% Fortschritt gehen wir von einem erfolgreichen Update aus - status.textContent = "Update appears successful! Device is restarting... Page will reload in 30 seconds."; - status.classList.add('success'); - status.style.display = 'block'; - setTimeout(() => { - window.location.href = '/'; - }, 30000); - } else { - handleUpdateError("Server error: " + xhr.status); - } + if (xhr.status !== 200 && !progress.textContent.startsWith('100')) { + status.textContent = "Update failed: " + (xhr.responseText || "Unknown error"); + status.className = 'status error'; + status.style.display = 'block'; + updateInProgress = false; + document.querySelectorAll('form input[type=submit]').forEach(btn => btn.disabled = false); } }; xhr.onerror = function() { - if (progress.textContent === '100%') { - // Bei 100% Fortschritt gehen wir von einem erfolgreichen Update aus - status.textContent = "Update appears successful! Device is restarting... Page will reload in 30 seconds."; - status.classList.add('success'); + if (!progress.textContent.startsWith('100')) { + status.textContent = "Network error during update"; + status.className = 'status error'; status.style.display = 'block'; - setTimeout(() => { - window.location.href = '/'; - }, 30000); - } else { - handleUpdateError("Network error during update"); + updateInProgress = false; + document.querySelectorAll('form input[type=submit]').forEach(btn => btn.disabled = false); } }; - function handleUpdateError(message) { - updateInProgress = false; - status.textContent = message; - status.classList.add('error'); - status.style.display = 'block'; - document.querySelectorAll('form input[type=submit]').forEach(btn => btn.disabled = false); - } - const formData = new FormData(); formData.append('update', file); xhr.send(formData); diff --git a/src/website.cpp b/src/website.cpp index 9d66a80..42f891f 100644 --- a/src/website.cpp +++ b/src/website.cpp @@ -30,6 +30,42 @@ String spoolmanUrlBackup; // Globale Variable für den Update-Typ static int currentUpdateCommand = 0; +// Globale Update-Variablen +static size_t updateTotalSize = 0; +static size_t updateWritten = 0; +static bool isSpiffsUpdate = false; + +void sendUpdateProgress(int progress, const char* status = nullptr, const char* message = nullptr) { + static int lastSentProgress = -1; + + // Verhindere zu häufige Updates + if (progress == lastSentProgress && !status && !message) { + return; + } + + String progressMsg = "{\"type\":\"updateProgress\",\"progress\":" + String(progress); + if (status) { + progressMsg += ",\"status\":\"" + String(status) + "\""; + } + if (message) { + progressMsg += ",\"message\":\"" + String(message) + "\""; + } + progressMsg += "}"; + + // Sende die Nachricht mehrmals mit Verzögerung für wichtige Updates + if (status || abs(progress - lastSentProgress) >= 10 || progress == 100) { + for (int i = 0; i < 2; i++) { + ws.textAll(progressMsg); + delay(100); // Längerer Delay zwischen Nachrichten + } + } else { + ws.textAll(progressMsg); + delay(50); + } + + lastSentProgress = progress; +} + void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { if (type == WS_EVT_CONNECT) { Serial.println("Neuer Client verbunden!"); @@ -171,6 +207,105 @@ void sendAmsData(AsyncWebSocketClient *client) { } } +void handleUpdate(AsyncWebServer &server) { + AsyncCallbackWebHandler* updateHandler = new AsyncCallbackWebHandler(); + updateHandler->setUri("/update"); + updateHandler->setMethod(HTTP_POST); + + updateHandler->onUpload([](AsyncWebServerRequest *request, String filename, + size_t index, uint8_t *data, size_t len, bool final) { + if (!index) { + updateTotalSize = request->contentLength(); + updateWritten = 0; + isSpiffsUpdate = (filename.indexOf("website") > -1); + + if (isSpiffsUpdate) { + // Backup vor dem Update + sendUpdateProgress(0, "backup", "Backing up configurations..."); + delay(200); + backupJsonConfigs(); + delay(200); + + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL); + if (!partition || !Update.begin(partition->size, U_SPIFFS)) { + request->send(400, "application/json", "{\"success\":false,\"message\":\"Update initialization failed\"}"); + return; + } + sendUpdateProgress(5, "starting", "Starting SPIFFS update..."); + delay(200); + } else { + if (!Update.begin(updateTotalSize)) { + request->send(400, "application/json", "{\"success\":false,\"message\":\"Update initialization failed\"}"); + return; + } + sendUpdateProgress(0, "starting", "Starting firmware update..."); + delay(200); + } + } + + if (len) { + if (Update.write(data, len) != len) { + request->send(400, "application/json", "{\"success\":false,\"message\":\"Write failed\"}"); + return; + } + + updateWritten += len; + int currentProgress; + + // Berechne den Fortschritt basierend auf dem Update-Typ + if (isSpiffsUpdate) { + // SPIFFS: 5-75% für Upload + currentProgress = 5 + (updateWritten * 100) / updateTotalSize; + } else { + // Firmware: 0-100% für Upload + currentProgress = 1 + (updateWritten * 100) / updateTotalSize; + } + + static int lastProgress = -1; + if (currentProgress != lastProgress && (currentProgress % 10 == 0 || final)) { + sendUpdateProgress(currentProgress, "uploading"); + oledShowMessage("Update: " + String(currentProgress) + "%"); + delay(50); + lastProgress = currentProgress; + } + } + + if (final) { + if (Update.end(true)) { + if (isSpiffsUpdate) { + restoreJsonConfigs(); + } + } else { + request->send(400, "application/json", "{\"success\":false,\"message\":\"Update finalization failed\"}"); + } + } + }); + + updateHandler->onRequest([](AsyncWebServerRequest *request) { + if (Update.hasError()) { + request->send(400, "application/json", "{\"success\":false,\"message\":\"Update failed\"}"); + return; + } + + // Erste 100% Nachricht + ws.textAll("{\"type\":\"updateProgress\",\"progress\":100,\"status\":\"success\",\"message\":\"Update successful! Restarting device...\"}"); + delay(2000); // Längerer Delay für die erste Nachricht + + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", + "{\"success\":true,\"message\":\"Update successful! Restarting device...\"}"); + response->addHeader("Connection", "close"); + request->send(response); + + // Zweite 100% Nachricht zur Sicherheit + ws.textAll("{\"type\":\"updateProgress\",\"progress\":100,\"status\":\"success\",\"message\":\"Update successful! Restarting device...\"}"); + delay(3000); // Noch längerer Delay vor dem Neustart + + ESP.restart(); + }); + + server.addHandler(updateHandler); +} + void setupWebserver(AsyncWebServer &server) { // Deaktiviere alle Debug-Ausgaben Serial.setDebugOutput(false); @@ -373,121 +508,8 @@ void setupWebserver(AsyncWebServer &server) { request->send(response); }); - // Update-Handler mit verbesserter Fehlerbehandlung - server.on("/update", HTTP_POST, - [](AsyncWebServerRequest *request) { - bool success = !Update.hasError(); - - if (success && currentUpdateCommand == U_SPIFFS) { - restoreJsonConfigs(); - delay(200); // Warte auf Restore-Abschluss - } - - String message = success ? "Update successful" : String("Update failed: ") + Update.errorString(); - - // Sende finale Bestätigung über WebSocket mit eindeutigem Status - ws.textAll("{\"type\":\"updateProgress\",\"progress\":100,\"status\":\"complete\",\"success\":true}"); - delay(1000); // Längerer Delay für WebSocket - - AsyncWebServerResponse *response = request->beginResponse( - success ? 200 : 400, - "application/json", - "{\"success\":" + String(success ? "true" : "false") + ",\"message\":\"" + message + "\"}" - ); - response->addHeader("Connection", "close"); - request->send(response); - - if (success) { - oledShowMessage("Update successful"); - delay(2000); // Noch längerer Delay vor Neustart - ESP.restart(); - } else { - oledShowMessage("Update failed"); - } - }, - [](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { - static size_t updateSize = 0; - static size_t totalWritten = 0; - - if (!index) { - updateSize = request->contentLength(); - totalWritten = 0; - currentUpdateCommand = (filename.indexOf("website") > -1) ? U_SPIFFS : U_FLASH; - - if (currentUpdateCommand == U_SPIFFS) { - oledShowMessage("SPIFFS Update..."); - backupJsonConfigs(); - - const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL); - if (!partition) { - String errorMsg = "SPIFFS partition not found"; - request->send(400, "application/json", "{\"success\":false,\"message\":\"" + errorMsg + "\"}"); - return; - } - - if (!Update.begin(partition->size, currentUpdateCommand)) { - String errorMsg = String("Update begin failed: ") + Update.errorString(); - request->send(400, "application/json", "{\"success\":false,\"message\":\"" + errorMsg + "\"}"); - return; - } - } else { - oledShowMessage("Firmware Update..."); - if (!Update.begin(updateSize, currentUpdateCommand)) { - String errorMsg = String("Update begin failed: ") + Update.errorString(); - request->send(400, "application/json", "{\"success\":false,\"message\":\"" + errorMsg + "\"}"); - return; - } - } - } - - if (len) { - if (Update.write(data, len) != len) { - String errorMsg = String("Write failed: ") + Update.errorString(); - request->send(400, "application/json", "{\"success\":false,\"message\":\"" + errorMsg + "\"}"); - return; - } - - totalWritten += len; - int currentProgress; - - // Unterschiedliche Fortschrittsberechnung für SPIFFS und Firmware - if (currentUpdateCommand == U_SPIFFS) { - // SPIFFS Update: Fortschritt basierend auf Upload-Größe - currentProgress = (totalWritten * 100) / updateSize; - // Skaliere den Fortschritt auf 0-90%, da das Schreiben ins SPIFFS länger dauert - currentProgress = (currentProgress * 90) / 100; - } else { - // Firmware Update: Normaler Fortschritt - currentProgress = (totalWritten * 100) / updateSize; - } - - static int lastProgress = -1; - if (currentProgress != lastProgress) { - if (currentProgress % 5 == 0) { - oledShowMessage(String(currentProgress) + "% complete"); - } - lastProgress = currentProgress; - ws.textAll("{\"type\":\"updateProgress\",\"progress\":" + String(currentProgress) + "}"); - } - } - - if (final) { - if (!Update.end(true)) { - String errorMsg = String("Update end failed: ") + Update.errorString(); - request->send(400, "application/json", "{\"success\":false,\"message\":\"" + errorMsg + "\"}"); - return; - } - - // Bei SPIFFS Update zeige 95% an, da noch das Restore kommt - if (currentUpdateCommand == U_SPIFFS) { - ws.textAll("{\"type\":\"updateProgress\",\"progress\":95,\"status\":\"finalizing\"}"); - } else { - ws.textAll("{\"type\":\"updateProgress\",\"progress\":100,\"status\":\"finalizing\"}"); - } - delay(200); - } - } - ); + // Update-Handler registrieren + handleUpdate(server); server.on("/api/version", HTTP_GET, [](AsyncWebServerRequest *request){ String fm_version = VERSION;