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
+
+
+
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 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);