diff --git a/wled00/data/update.htm b/wled00/data/update.htm
index 783a609ece..8c360c7809 100644
--- a/wled00/data/update.htm
+++ b/wled00/data/update.htm
@@ -19,12 +19,26 @@
}
function GetV() {/*injected values here*/}
+
-
+
WLED Software Update
+
+
+
ESP32 Bootloader Update
+
+
+
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 1d81655d6d..ecd65b7018 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 d2b771c590..b2f1072975 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 75b4ae3f5a..5fa773b83d 100644
--- a/wled00/wled_server.cpp
+++ b/wled00/wled_server.cpp
@@ -18,6 +18,14 @@
#endif
#include "html_cpal.h"
+#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
+ #include
+ #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 +36,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 +190,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((size_t)(bootloaderSize - offset), chunkSize);
+ 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
+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 +535,106 @@ 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;
+
+ static size_t bootloaderBytesWritten = 0;
+ static bool bootloaderErased = false;
+ const uint32_t bootloaderOffset = 0x1000;
+ const uint32_t maxBootloaderSize = 0x8000; // 32KB max
+
+ 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();
+ bootloaderBytesWritten = 0;
+ bootloaderErased = false;
+
+ // Verify bootloader magic on first chunk
+ if (!isValidBootloader(data, len)) {
+ DEBUG_PRINTLN(F("Invalid bootloader file!"));
+ strip.resume();
+ #if WLED_WATCHDOG_TIMEOUT > 0
+ WLED::instance().enableWatchdog();
+ #endif
+ Update.abort();
+ return;
+ }
+
+ // Erase bootloader region (32KB)
+ DEBUG_PRINTLN(F("Erasing bootloader region..."));
+ esp_err_t err = esp_flash_erase_region(NULL, bootloaderOffset, maxBootloaderSize);
+ if (err != ESP_OK) {
+ DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err);
+ strip.resume();
+ #if WLED_WATCHDOG_TIMEOUT > 0
+ WLED::instance().enableWatchdog();
+ #endif
+ Update.abort();
+ return;
+ }
+ bootloaderErased = true;
+ }
+
+ // Write data to flash at bootloader offset
+ if (bootloaderErased && bootloaderBytesWritten + len <= maxBootloaderSize) {
+ esp_err_t err = esp_flash_write(NULL, data, bootloaderOffset + bootloaderBytesWritten, len);
+ if (err != ESP_OK) {
+ DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err);
+ Update.abort();
+ } else {
+ bootloaderBytesWritten += len;
+ }
+ } else if (!bootloaderErased) {
+ DEBUG_PRINTLN(F("Bootloader region not erased!"));
+ Update.abort();
+ } else {
+ DEBUG_PRINTLN(F("Bootloader size exceeds maximum!"));
+ Update.abort();
+ }
+
+ if (isFinal) {
+ if (!Update.hasError() && bootloaderBytesWritten > 0) {
+ DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written\n"), bootloaderBytesWritten);
+ bootloaderSHA256Cached = false; // Invalidate cached bootloader hash
+ } else {
+ DEBUG_PRINTLN(F("Bootloader Update Failed"));
+ 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);