From 93908e758fbd4c63b836c9fa9ea802fb1d44c50e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:17:51 +0000 Subject: [PATCH] Add ESP32 bootloader upgrade functionality with JSON API support Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/data/update.htm | 24 ++++++- wled00/fcn_declare.h | 3 + wled00/json.cpp | 3 + wled00/wled_server.cpp | 141 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 1 deletion(-) diff --git a/wled00/data/update.htm b/wled00/data/update.htm index 783a609e..60c4f416 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -18,13 +18,25 @@ window.open(getURL("/update?revert"),"_self"); } function GetV() {/*injected values here*/} + var isESP32 = false; + function checkESP32() { + fetch(getURL('/json/info')).then(r=>r.json()).then(d=>{ + isESP32 = d.arch && d.arch.startsWith('esp32'); + if (isESP32) { + gId('bootloader-section').style.display = 'block'; + if (d.bootloaderSHA256) { + gId('bootloader-hash').innerText = 'Current bootloader SHA256: ' + d.bootloaderSHA256; + } + } + }).catch(e=>console.error(e)); + } - +

WLED Software Update

Installed version: WLED ##VERSION##
@@ -37,6 +49,16 @@
+
Updating...
Please do not close or refresh the page :)
\ No newline at end of file diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 1d81655d..ecd65b70 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -545,6 +545,9 @@ void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& h void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error); void serveSettings(AsyncWebServerRequest* request, bool post = false); void serveSettingsJS(AsyncWebServerRequest* request); +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +String getBootloaderSHA256Hex(); +#endif //ws.cpp void handleWs(); diff --git a/wled00/json.cpp b/wled00/json.cpp index d2b771c5..b2f10729 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -817,6 +817,9 @@ void serializeInfo(JsonObject root) root[F("resetReason1")] = (int)rtc_get_reset_reason(1); #endif root[F("lwip")] = 0; //deprecated + #ifndef WLED_DISABLE_OTA + root[F("bootloaderSHA256")] = getBootloaderSHA256Hex(); + #endif #else root[F("arch")] = "esp8266"; root[F("core")] = ESP.getCoreVersion(); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 75b4ae3f..8233dfd7 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -18,6 +18,13 @@ #endif #include "html_cpal.h" +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) + #include + #include + #include + #include +#endif + // define flash strings once (saves flash memory) static const char s_redirecting[] PROGMEM = "Redirecting..."; static const char s_content_enc[] PROGMEM = "Content-Encoding"; @@ -28,6 +35,12 @@ static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_accessdenied[] PROGMEM = "Access Denied"; static const char _common_js[] PROGMEM = "/common.js"; +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +// Cache for bootloader SHA256 digest +static uint8_t bootloaderSHA256[32]; +static bool bootloaderSHA256Cached = false; +#endif + //Is this an IP? static bool isIp(const String &str) { for (size_t i = 0; i < str.length(); i++) { @@ -176,6 +189,61 @@ static String msgProcessor(const String& var) return String(); } +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +// Calculate and cache the bootloader SHA256 digest +static void calculateBootloaderSHA256() { + if (bootloaderSHA256Cached) return; + + // Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB + const uint32_t bootloaderOffset = 0x1000; + const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size + + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224) + + const size_t chunkSize = 256; + uint8_t buffer[chunkSize]; + + for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) { + size_t readSize = min(chunkSize, bootloaderSize - offset); + if (esp_flash_read(NULL, buffer, bootloaderOffset + offset, readSize) == ESP_OK) { + mbedtls_sha256_update(&ctx, buffer, readSize); + } + } + + mbedtls_sha256_finish(&ctx, bootloaderSHA256); + mbedtls_sha256_free(&ctx); + bootloaderSHA256Cached = true; +} + +// Get bootloader SHA256 as hex string +static String getBootloaderSHA256Hex() { + calculateBootloaderSHA256(); + + char hex[65]; + for (int i = 0; i < 32; i++) { + sprintf(hex + (i * 2), "%02x", bootloaderSHA256[i]); + } + hex[64] = '\0'; + return String(hex); +} + +// Verify if uploaded data is a valid ESP32 bootloader +static bool isValidBootloader(const uint8_t* data, size_t len) { + if (len < 32) return false; + + // Check for ESP32 bootloader magic byte (0xE9) + if (data[0] != 0xE9) return false; + + // Additional validation: check segment count is reasonable + uint8_t segmentCount = data[1]; + if (segmentCount > 16) return false; + + return true; +} +#endif + static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) { if (!correctPIN) { if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); @@ -466,6 +534,79 @@ void initServer() server.on(_update, HTTP_POST, notSupported, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){}); #endif +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) + // ESP32 bootloader update endpoint + server.on(F("/updatebootloader"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveSettings(request, true); // handle PIN page POST request + return; + } + if (otaLock) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254); + return; + } + if (Update.hasError()) { + serveMessage(request, 500, F("Bootloader update failed!"), F("Please check your file and retry!"), 254); + } else { + serveMessage(request, 200, F("Bootloader updated successfully!"), FPSTR(s_rebooting), 131); + doReboot = true; + } + },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){ + IPAddress client = request->client()->remoteIP(); + if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) { + DEBUG_PRINTLN(F("Attempted bootloader update from different/non-local subnet!")); + request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied)); + return; + } + if (!correctPIN || otaLock) return; + + if (!index) { + DEBUG_PRINTLN(F("Bootloader Update Start")); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().disableWatchdog(); + #endif + lastEditTime = millis(); // make sure PIN does not lock during update + strip.suspend(); + strip.resetSegments(); + + // Begin bootloader update - use U_FLASH and specify bootloader partition offset + if (!Update.begin(0x8000, U_FLASH, -1, 0x1000)) { + DEBUG_PRINTLN(F("Bootloader Update Begin Failed")); + Update.printError(Serial); + } + } + + // Verify bootloader magic on first chunk + if (index == 0 && !isValidBootloader(data, len)) { + DEBUG_PRINTLN(F("Invalid bootloader file!")); + Update.abort(); + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + return; + } + + if (!Update.hasError()) { + Update.write(data, len); + } + + if (isFinal) { + if (Update.end(true)) { + DEBUG_PRINTLN(F("Bootloader Update Success")); + bootloaderSHA256Cached = false; // Invalidate cached bootloader hash + } else { + DEBUG_PRINTLN(F("Bootloader Update Failed")); + Update.printError(Serial); + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + } + } + }); +#endif + #ifdef WLED_ENABLE_DMX server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap, dmxProcessor);