feat: implement enhanced update progress handling and WebSocket notifications

This commit is contained in:
Manuel Weiser 2025-02-22 19:50:12 +01:00
parent 5c59016f94
commit 4b25b72b2e
2 changed files with 224 additions and 229 deletions

View File

@ -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;
// 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';
// Setze den Fortschritt nur wenn er größer ist als der aktuelle
const currentProgress = parseInt(progress.textContent);
// Aktualisiere den Fortschritt nur wenn er größer ist
const newProgress = parseInt(data.progress);
if (isNaN(currentProgress) || newProgress > currentProgress) {
progress.style.width = data.progress + '%';
progress.textContent = data.progress + '%';
if (!isNaN(newProgress) && newProgress >= lastReceivedProgress) {
progress.style.width = newProgress + '%';
progress.textContent = newProgress + '%';
lastReceivedProgress = newProgress;
}
// 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');
// Zeige Status-Nachricht
if (data.message || data.status) {
status.textContent = data.message || getStatusMessage(data.status);
status.className = 'status success';
status.style.display = 'block';
// Versuche die WebSocket-Verbindung sauber zu schließen
try {
ws.close();
} catch (e) {
console.log('WebSocket already closed');
}
// 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() {
// Wenn das Update läuft und der Fortschritt hoch ist, zeige Success
if (updateInProgress) {
const currentProgress = parseInt(progress.textContent);
if (!isNaN(currentProgress) && currentProgress >= 90) {
// 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.classList.add('success');
status.className = 'status success';
status.style.display = 'block';
clearTimeout(wsReconnectTimer);
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';
// Versuche Reconnect bei niedrigem Fortschritt
wsReconnectTimer = setTimeout(connectWebSocket, 1000);
}
}
};
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.className = 'status success';
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,82 +274,32 @@
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');
if (xhr.status !== 200 && !progress.textContent.startsWith('100')) {
status.textContent = "Update failed: " + (xhr.responseText || "Unknown error");
status.className = 'status error';
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);
}
}
};
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");
}
};
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);

View File

@ -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;