From 8182b5f684492898308bad8d9caae571ffff5782 Mon Sep 17 00:00:00 2001 From: Manuel Weiser Date: Fri, 21 Feb 2025 09:24:54 +0100 Subject: [PATCH] feat: enhance OTA upload handling with chunk validation and timeout checks --- html/upgrade.html | 150 +++++++++++++++++++++++++++++----------------- platformio.ini | 41 ++++++++----- src/main.cpp | 5 +- src/ota.cpp | 95 +++++++++++++++++++++++++---- src/ota.h | 1 + src/wlan.cpp | 1 + 6 files changed, 210 insertions(+), 83 deletions(-) diff --git a/html/upgrade.html b/html/upgrade.html index 8d3a8e1..bcca8d1 100644 --- a/html/upgrade.html +++ b/html/upgrade.html @@ -64,6 +64,22 @@ statusContainer.style.display = 'none'; } + // Größenbeschränkung für Upload + const MAX_FILE_SIZE = 4000000; // 4MB + + async function checkMagicByte(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const arr = new Uint8Array(reader.result); + // Prüfe auf Magic Byte 0xE9 für ESP32 Firmware + resolve(arr[0] === 0xE9); + }; + reader.onerror = () => reject(reader.error); + reader.readAsArrayBuffer(file.slice(0, 1)); + }); + } + document.getElementById('updateForm').addEventListener('submit', async (e) => { e.preventDefault(); const form = e.target; @@ -73,8 +89,25 @@ return; } - const formData = new FormData(); - formData.append('update', file); + if (file.size > MAX_FILE_SIZE) { + alert('File too large. Maximum size is 4MB.'); + return; + } + + // Prüfe Magic Byte für normale Firmware-Dateien + if (!file.name.endsWith('_spiffs.bin')) { + try { + const isValidFirmware = await checkMagicByte(file); + if (!isValidFirmware) { + alert('Invalid firmware file. Missing ESP32 magic byte.'); + return; + } + } catch (error) { + console.error('Error checking magic byte:', error); + alert('Could not verify firmware file.'); + return; + } + } const progress = document.querySelector('.progress-bar'); const progressContainer = document.querySelector('.progress-container'); @@ -85,69 +118,74 @@ status.className = 'status'; form.querySelector('input[type=submit]').disabled = true; - const xhr = new XMLHttpRequest(); - xhr.open('POST', '/update', true); + // Chunk-basierter Upload mit Retry-Logik + const chunkSize = 8192; // Optimale Chunk-Größe + const maxRetries = 3; + let offset = 0; - xhr.upload.onprogress = (e) => { - if (e.lengthComputable) { - const percentComplete = (e.loaded / e.total) * 100; + async function uploadChunk(chunk, retryCount = 0) { + try { + const response = await fetch('/update', { + method: 'POST', + body: chunk, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-File-Name': file.name, + 'X-Chunk-Offset': offset.toString(), + 'X-Chunk-Size': chunk.size.toString(), + 'X-Total-Size': file.size.toString() + }, + timeout: 30000 // 30 Sekunden Timeout + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || response.statusText); + } + + return true; + } catch (error) { + console.error(`Chunk upload failed (attempt ${retryCount + 1}):`, error); + if (retryCount < maxRetries) { + await new Promise(resolve => setTimeout(resolve, 1000)); // Warte 1 Sekunde vor Retry + return uploadChunk(chunk, retryCount + 1); + } + throw error; + } + } + + try { + while (offset < file.size) { + const end = Math.min(offset + chunkSize, file.size); + const chunk = file.slice(offset, end); + + await uploadChunk(chunk); + + offset = end; + const percentComplete = (offset / file.size) * 100; progress.style.width = percentComplete + '%'; progress.textContent = Math.round(percentComplete) + '%'; - } - }; - - xhr.onload = function() { - try { - let response = this.responseText; - try { - const jsonResponse = JSON.parse(response); - response = jsonResponse.message; - - if (jsonResponse.restart) { - status.textContent = response + " Redirecting in 20 seconds..."; - let countdown = 20; - const timer = setInterval(() => { - countdown--; - if (countdown <= 0) { - clearInterval(timer); - window.location.href = '/'; - } else { - status.textContent = response + ` Redirecting in ${countdown} seconds...`; - } - }, 1000); - } - } catch (e) { - if (!isNaN(response)) { - const percent = parseInt(response); - progress.style.width = percent + '%'; - progress.textContent = percent + '%'; - return; - } - } - status.textContent = response; - status.classList.add(xhr.status === 200 ? 'success' : 'error'); - status.style.display = 'block'; - - if (xhr.status !== 200) { - form.querySelector('input[type=submit]').disabled = false; - } - } catch (error) { - status.textContent = 'Error: ' + error.message; - status.classList.add('error'); - status.style.display = 'block'; - form.querySelector('input[type=submit]').disabled = false; + // Kleine Pause zwischen den Chunks für bessere Stabilität + await new Promise(resolve => setTimeout(resolve, 100)); } - }; - xhr.onerror = function() { - status.textContent = 'Update failed: Network error'; + // Final success handler + status.textContent = 'Update successful! Device will restart...'; + status.classList.add('success'); + status.style.display = 'block'; + + // Warte auf Neustart und Redirect + setTimeout(() => { + window.location.href = '/'; + }, 20000); + + } catch (error) { + status.textContent = 'Update failed: ' + error.message; status.classList.add('error'); status.style.display = 'block'; form.querySelector('input[type=submit]').disabled = false; - }; - - xhr.send(formData); + } }); diff --git a/platformio.ini b/platformio.ini index 28af4b3..12525be 100644 --- a/platformio.ini +++ b/platformio.ini @@ -50,23 +50,36 @@ build_flags = -DOTA_DEBUG=1 -DARDUINO_RUNNING_CORE=1 -DARDUINO_EVENT_RUNNING_CORE=1 - -DCONFIG_OPTIMIZATION_LEVEL_DEBUG=1 -DCONFIG_ESP32_PANIC_PRINT_REBOOT - -DCONFIG_ARDUINO_OTA_READSIZE=1024 + -DCONFIG_ARDUINO_OTA_READSIZE=8192 -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 -DCONFIG_ASYNC_TCP_USE_WDT=0 - -DCONFIG_LWIP_TCP_MSS=1460 - -DOTA_PARTITION_SUBTYPE=0x10 - -DPARTITION_TABLE_OFFSET=0x8000 - -DPARTITION_TABLE_SIZE=0x1000 - -DCONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=1 - -DCONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=1 - -DCONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON=1 - -DCONFIG_BOOTLOADER_RESERVE_RTC_SIZE=0x1000 - -DCONFIG_PARTITION_TABLE_OFFSET=0x8000 - -DCONFIG_PARTITION_TABLE_MD5=y - -DBOOT_APP_PARTITION_OTA_0=1 - -DCONFIG_LOG_DEFAULT_LEVEL=3 + -DASYNC_TCP_USE_WDT=0 + -DCONFIG_LWIP_TCP_TMR_INTERVAL=25 + -DCONFIG_LWIP_TCP_MSS=1436 + -DCONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744 + -DCONFIG_LWIP_TCP_WND_DEFAULT=5744 + -DCONFIG_LWIP_TCP_RCV_BUF_DEFAULT=11488 + -DCONFIG_LWIP_TCP_RECVMBOX_SIZE=64 + -DCONFIG_LWIP_MAX_SOCKETS=10 + -DCONFIG_LWIP_MAX_ACTIVE_TCP=16 + -DCONFIG_LWIP_MAX_LISTENING_TCP=16 + -DCONFIG_LWIP_TCP_MAXRTX=6 + -DCONFIG_LWIP_TCP_SYNMAXRTX=3 + -DCONFIG_LWIP_TCP_QUEUE_OOSEQ=0 + -DCONFIG_LWIP_TCP_OVERSIZE_MSS=0 + -DCONFIG_LWIP_TCP_RTO_TIME=1500 + -DCONFIG_LWIP_IPV6=0 + -DCONFIG_LWIP_STATS=0 + -DCONFIG_LWIP_USE_ONLY_LWIP_SELECT=1 + -DCONFIG_LWIP_NETIF_LOOPBACK=0 + -DCONFIG_LWIP_IRAM_OPTIMIZATION=1 + -DCONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 + -DCONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 + -DCONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 + -DCONFIG_ESP32_WIFI_TX_BA_WIN=6 + -DCONFIG_ESP32_WIFI_RX_BA_WIN=6 + -DCONFIG_ESP32_WIFI_CSI_ENABLED=0 extra_scripts = scripts/extra_script.py diff --git a/src/main.cpp b/src/main.cpp index e91448d..d57a839 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -78,6 +78,8 @@ uint8_t wifiErrorCounter = 0; // ##### PROGRAM START ##### void loop() { + + /* // Überprüfe den WLAN-Status if (WiFi.status() != WL_CONNECTED) { wifiErrorCounter++; @@ -87,7 +89,8 @@ void loop() { wifiOn = true; } if (wifiErrorCounter > 20) ESP.restart(); - + */ + unsigned long currentMillis = millis(); // Send AMS Data min every Minute diff --git a/src/ota.cpp b/src/ota.cpp index 6a2fa2e..be8f3f0 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -7,7 +7,16 @@ #include "scale.h" #include "nfc.h" +#define UPLOAD_TIMEOUT_MS 60000 // 60 Sekunden Timeout für den gesamten Upload +#define CHUNK_RESPONSE_TIMEOUT_MS 10000 // 10 Sekunden Timeout pro Chunk +#define MAX_FAILED_CHUNKS 3 // Maximale Anzahl fehlgeschlagener Chunks bevor Abbruch +#define MAX_FILE_SIZE 4000000 // 4MB Limit + static bool tasksAreStopped = false; +static uint32_t lastChunkTime = 0; +static size_t failedChunks = 0; +static size_t expectedOffset = 0; +static size_t totalSize = 0; void stopAllTasks() { Serial.println("Stopping RFID Reader"); @@ -80,24 +89,44 @@ void checkForStagedUpdate() { void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { static File stagingFile; + static uint32_t uploadStartTime = 0; if (!index) { + // Überprüfe Gesamtgröße im Header + if (request->hasHeader("X-Total-Size")) { + totalSize = request->header("X-Total-Size").toInt(); + if (totalSize > MAX_FILE_SIZE) { + request->send(413, "application/json", + "{\"status\":\"error\",\"message\":\"File too large\"}"); + return; + } + } + + uploadStartTime = millis(); + lastChunkTime = millis(); + expectedOffset = 0; + failedChunks = 0; + bool isSpiffsUpdate = filename.endsWith("_spiffs.bin"); Serial.printf("Update Start: %s (type: %s)\n", filename.c_str(), isSpiffsUpdate ? "SPIFFS" : "OTA"); + Serial.printf("Total size: %u bytes\n", totalSize); + // Überprüfe Header für Chunk-Informationen + if (request->hasHeader("X-Chunk-Offset")) { + String offsetStr = request->header("X-Chunk-Offset"); + expectedOffset = offsetStr.toInt(); + } + if (request->contentLength() == 0) { request->send(400, "application/json", "{\"status\":\"error\",\"message\":\"Invalid file size\"}"); return; } - // Stop tasks before update - if (!tasksAreStopped && (RfidReaderTask || BambuMqttTask || ScaleTask)) { + if (!tasksAreStopped) { stopAllTasks(); tasksAreStopped = true; } - size_t updateSize = request->contentLength(); - if (isSpiffsUpdate) { if (!SPIFFS.begin(true)) { request->send(400, "application/json", @@ -105,15 +134,13 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind return; } - // Start SPIFFS update - if (!Update.begin(updateSize, U_SPIFFS)) { + if (!Update.begin(totalSize > 0 ? totalSize : request->contentLength(), U_SPIFFS)) { Update.printError(Serial); request->send(400, "application/json", "{\"status\":\"error\",\"message\":\"SPIFFS update initialization failed\"}"); return; } } else { - // Regular OTA update stagingFile = SPIFFS.open("/firmware.bin", "w"); if (!stagingFile) { request->send(400, "application/json", @@ -123,9 +150,53 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind } } + // Chunk Validierung + if (request->hasHeader("X-Chunk-Offset")) { + size_t chunkOffset = request->header("X-Chunk-Offset").toInt(); + if (chunkOffset != expectedOffset) { + failedChunks++; + if (failedChunks >= MAX_FAILED_CHUNKS) { + if (stagingFile) { + stagingFile.close(); + SPIFFS.remove("/firmware.bin"); + } + Update.abort(); + request->send(400, "application/json", + "{\"status\":\"error\",\"message\":\"Too many failed chunks\"}"); + return; + } + request->send(400, "application/json", + "{\"status\":\"error\",\"message\":\"Invalid chunk offset\"}"); + return; + } + } + + // Timeout Überprüfungen + uint32_t currentTime = millis(); + if (currentTime - uploadStartTime > UPLOAD_TIMEOUT_MS) { + if (stagingFile) { + stagingFile.close(); + SPIFFS.remove("/firmware.bin"); + } + Update.abort(); + request->send(408, "application/json", "{\"status\":\"error\",\"message\":\"Upload timeout\"}"); + return; + } + + if (currentTime - lastChunkTime > CHUNK_RESPONSE_TIMEOUT_MS) { + if (stagingFile) { + stagingFile.close(); + SPIFFS.remove("/firmware.bin"); + } + Update.abort(); + request->send(408, "application/json", "{\"status\":\"error\",\"message\":\"Chunk timeout\"}"); + return; + } + lastChunkTime = currentTime; + if (stagingFile) { - // Stage 1: Write to SPIFFS - if (stagingFile.write(data, len) != len) { + size_t written = stagingFile.write(data, len); + if (written != len) { stagingFile.close(); SPIFFS.remove("/firmware.bin"); request->send(400, "application/json", @@ -133,7 +204,6 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind return; } } else { - // Direct SPIFFS update if (Update.write(data, len) != len) { Update.printError(Serial); request->send(400, "application/json", @@ -142,16 +212,17 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind } } + expectedOffset += len; + if (final) { if (stagingFile) { - // Finish Stage 1 stagingFile.close(); Serial.println("Stage 1 complete - firmware staged in SPIFFS"); request->send(200, "application/json", "{\"status\":\"success\",\"message\":\"Update staged successfully! Starting stage 2...\"}"); + delay(100); performStageTwo(); } else { - // Finish direct SPIFFS update if (!Update.end(true)) { Update.printError(Serial); request->send(400, "application/json", diff --git a/src/ota.h b/src/ota.h index 14beaf7..c126b76 100644 --- a/src/ota.h +++ b/src/ota.h @@ -10,5 +10,6 @@ void stopAllTasks(); void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); +void checkForStagedUpdate(); #endif \ No newline at end of file diff --git a/src/wlan.cpp b/src/wlan.cpp index 780dd8a..bb94583 100644 --- a/src/wlan.cpp +++ b/src/wlan.cpp @@ -11,6 +11,7 @@ bool wm_nonblocking = false; void initWiFi() { WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP + WiFi.setSleep(false); // disable sleep mode //esp_wifi_set_max_tx_power(72); // Setze maximale Sendeleistung auf 20dBm