diff --git a/wled00/data/update.htm b/wled00/data/update.htm
index d8b8876ef2..e93a113fae 100644
--- a/wled00/data/update.htm
+++ b/wled00/data/update.htm
@@ -29,6 +29,13 @@
if (data.arch == "esp8266") {
toggle('rev');
}
+ const isESP32 = data.arch && (data.arch.toLowerCase() === 'esp32' || data.arch.toLowerCase() === 'esp32-s2');
+ if (isESP32) {
+ gId('bootloader-section').style.display = 'block';
+ if (data.bootloaderSHA256) {
+ gId('bootloader-hash').innerText = 'Current bootloader SHA256: ' + data.bootloaderSHA256;
+ }
+ }
})
.catch(error => {
console.log('Could not fetch device info:', error);
@@ -42,8 +49,7 @@
@import url("style.css");
-
-
+
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/json.cpp b/wled00/json.cpp
index 8204319425..a5ef74757d 100644
--- a/wled00/json.cpp
+++ b/wled00/json.cpp
@@ -820,6 +820,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/ota_update.cpp b/wled00/ota_update.cpp
index afda59a942..6a5cf29cd3 100644
--- a/wled00/ota_update.cpp
+++ b/wled00/ota_update.cpp
@@ -4,12 +4,15 @@
#ifdef ESP32
#include
#include
+#include
+#include
#endif
// Platform-specific metadata locations
#ifdef ESP32
constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata
#define UPDATE_ERROR errorString
+const size_t BOOTLOADER_OFFSET = 0x1000;
#elif defined(ESP8266)
constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset
#define UPDATE_ERROR getErrorString
@@ -254,4 +257,472 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data,
// Upload complete
context->uploadComplete = true;
}
-}
\ No newline at end of file
+}
+
+#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
+// Cache for bootloader SHA256 digest as hex string
+static String bootloaderSHA256HexCache = "";
+
+// Calculate and cache the bootloader SHA256 digest as hex string
+void calculateBootloaderSHA256() {
+ if (!bootloaderSHA256HexCache.isEmpty()) return;
+
+ // Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB
+ const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size
+
+ // Calculate SHA256
+ uint8_t sha256[32];
+ 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, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) {
+ mbedtls_sha256_update(&ctx, buffer, readSize);
+ }
+ }
+
+ mbedtls_sha256_finish(&ctx, sha256);
+ mbedtls_sha256_free(&ctx);
+
+ // Convert to hex string and cache it
+ char hex[65];
+ for (int i = 0; i < 32; i++) {
+ sprintf(hex + (i * 2), "%02x", sha256[i]);
+ }
+ hex[64] = '\0';
+ bootloaderSHA256HexCache = String(hex);
+}
+
+// Get bootloader SHA256 as hex string
+String getBootloaderSHA256Hex() {
+ calculateBootloaderSHA256();
+ return bootloaderSHA256HexCache;
+}
+
+// Invalidate cached bootloader SHA256 (call after bootloader update)
+void invalidateBootloaderSHA256Cache() {
+ bootloaderSHA256HexCache = "";
+}
+
+// Verify complete buffered bootloader using ESP-IDF validation approach
+// This matches the key validation steps from esp_image_verify() in ESP-IDF
+// Returns the actual bootloader data pointer and length via the buffer and len parameters
+bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootloaderErrorMsg) {
+ size_t availableLen = len;
+ if (!bootloaderErrorMsg) {
+ DEBUG_PRINTLN(F("bootloaderErrorMsg is null"));
+ return false;
+ }
+ // ESP32 image header structure (based on esp_image_format.h)
+ // Offset 0: magic (0xE9)
+ // Offset 1: segment_count
+ // Offset 2: spi_mode
+ // Offset 3: spi_speed (4 bits) + spi_size (4 bits)
+ // Offset 4-7: entry_addr (uint32_t)
+ // Offset 8: wp_pin
+ // Offset 9-11: spi_pin_drv[3]
+ // Offset 12-13: chip_id (uint16_t, little-endian)
+ // Offset 14: min_chip_rev
+ // Offset 15-22: reserved[8]
+ // Offset 23: hash_appended
+
+ const size_t MIN_IMAGE_HEADER_SIZE = 24;
+
+ // 1. Validate minimum size for header
+ if (len < MIN_IMAGE_HEADER_SIZE) {
+ *bootloaderErrorMsg = "Bootloader too small - invalid header";
+ return false;
+ }
+
+ // Check if the bootloader starts at offset 0x1000 (common in partition table dumps)
+ // This happens when someone uploads a complete flash dump instead of just the bootloader
+ if (len > BOOTLOADER_OFFSET + MIN_IMAGE_HEADER_SIZE &&
+ buffer[BOOTLOADER_OFFSET] == 0xE9 &&
+ buffer[0] != 0xE9) {
+ DEBUG_PRINTF_P(PSTR("Bootloader magic byte detected at offset 0x%04X - adjusting buffer\n"), BOOTLOADER_OFFSET);
+ // Adjust buffer pointer to start at the actual bootloader
+ buffer = buffer + BOOTLOADER_OFFSET;
+ len = len - BOOTLOADER_OFFSET;
+
+ // Re-validate size after adjustment
+ if (len < MIN_IMAGE_HEADER_SIZE) {
+ *bootloaderErrorMsg = "Bootloader at offset 0x1000 too small - invalid header";
+ return false;
+ }
+ }
+
+ // 2. Magic byte check (matches esp_image_verify step 1)
+ if (buffer[0] != 0xE9) {
+ *bootloaderErrorMsg = "Invalid bootloader magic byte (expected 0xE9, got 0x" + String(buffer[0], HEX) + ")";
+ return false;
+ }
+
+ // 3. Segment count validation (matches esp_image_verify step 2)
+ uint8_t segmentCount = buffer[1];
+ if (segmentCount == 0 || segmentCount > 16) {
+ *bootloaderErrorMsg = "Invalid segment count: " + String(segmentCount);
+ return false;
+ }
+
+ // 4. SPI mode validation (basic sanity check)
+ uint8_t spiMode = buffer[2];
+ if (spiMode > 3) { // Valid modes are 0-3 (QIO, QOUT, DIO, DOUT)
+ *bootloaderErrorMsg = "Invalid SPI mode: " + String(spiMode);
+ return false;
+ }
+
+ // 5. Chip ID validation (matches esp_image_verify step 3)
+ uint16_t chipId = buffer[12] | (buffer[13] << 8); // Little-endian
+
+ // Known ESP32 chip IDs from ESP-IDF:
+ // 0x0000 = ESP32
+ // 0x0002 = ESP32-S2
+ // 0x0005 = ESP32-C3
+ // 0x0009 = ESP32-S3
+ // 0x000C = ESP32-C2
+ // 0x000D = ESP32-C6
+ // 0x0010 = ESP32-H2
+
+ #if defined(CONFIG_IDF_TARGET_ESP32)
+ if (chipId != 0x0000) {
+ *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32 (0x0000), got 0x" + String(chipId, HEX);
+ return false;
+ }
+ #elif defined(CONFIG_IDF_TARGET_ESP32S2)
+ if (chipId != 0x0002) {
+ *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S2 (0x0002), got 0x" + String(chipId, HEX);
+ return false;
+ }
+ #elif defined(CONFIG_IDF_TARGET_ESP32C3)
+ if (chipId != 0x0005) {
+ *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C3 (0x0005), got 0x" + String(chipId, HEX);
+ return false;
+ }
+ *bootloaderErrorMsg = "ESP32-C3 update not supported yet";
+ return false;
+ #elif defined(CONFIG_IDF_TARGET_ESP32S3)
+ if (chipId != 0x0009) {
+ *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-S3 (0x0009), got 0x" + String(chipId, HEX);
+ return false;
+ }
+ *bootloaderErrorMsg = "ESP32-S3 update not supported yet";
+ return false;
+ #elif defined(CONFIG_IDF_TARGET_ESP32C6)
+ if (chipId != 0x000D) {
+ *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C6 (0x000D), got 0x" + String(chipId, HEX);
+ return false;
+ }
+ *bootloaderErrorMsg = "ESP32-C6 update not supported yet";
+ return false;
+ #else
+ // Generic validation - chip ID should be valid
+ if (chipId > 0x00FF) {
+ *bootloaderErrorMsg = "Invalid chip ID: 0x" + String(chipId, HEX);
+ return false;
+ }
+ *bootloaderErrorMsg = "Unknown ESP32 target - bootloader update not supported";
+ return false;
+ #endif
+
+ // 6. Entry point validation (should be in valid memory range)
+ uint32_t entryAddr = buffer[4] | (buffer[5] << 8) | (buffer[6] << 16) | (buffer[7] << 24);
+ // ESP32 bootloader entry points are typically in IRAM range (0x40000000 - 0x40400000)
+ // or ROM range (0x40000000 and above)
+ if (entryAddr < 0x40000000 || entryAddr > 0x50000000) {
+ *bootloaderErrorMsg = "Invalid entry address: 0x" + String(entryAddr, HEX);
+ return false;
+ }
+
+ // 7. Basic segment structure validation
+ // Each segment has a header: load_addr (4 bytes) + data_len (4 bytes)
+ size_t offset = MIN_IMAGE_HEADER_SIZE;
+ size_t actualBootloaderSize = MIN_IMAGE_HEADER_SIZE;
+
+ for (uint8_t i = 0; i < segmentCount && offset + 8 <= len; i++) {
+ uint32_t segmentSize = buffer[offset + 4] | (buffer[offset + 5] << 8) |
+ (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24);
+
+ // Segment size sanity check
+ // ESP32 classic bootloader segments can be larger, C3 are smaller
+ if (segmentSize > 0x20000) { // 128KB max per segment (very generous)
+ *bootloaderErrorMsg = "Segment " + String(i) + " too large: " + String(segmentSize) + " bytes";
+ return false;
+ }
+
+ offset += 8 + segmentSize; // Skip segment header and data
+ }
+
+ actualBootloaderSize = offset;
+
+ // 8. Check for appended SHA256 hash (byte 23 in header)
+ // If hash_appended != 0, there's a 32-byte SHA256 hash after the segments
+ uint8_t hashAppended = buffer[23];
+ if (hashAppended != 0) {
+ actualBootloaderSize += 32;
+ if (actualBootloaderSize > availableLen) {
+ *bootloaderErrorMsg = "Bootloader missing SHA256 trailer";
+ return false;
+ }
+ DEBUG_PRINTF_P(PSTR("Bootloader has appended SHA256 hash\n"));
+ }
+
+ // 9. The image may also have a 1-byte checksum after segments/hash
+ // Check if there's at least one more byte available
+ if (actualBootloaderSize + 1 <= availableLen) {
+ // There's likely a checksum byte
+ actualBootloaderSize += 1;
+ } else if (actualBootloaderSize > availableLen) {
+ *bootloaderErrorMsg = "Bootloader truncated before checksum";
+ return false;
+ }
+
+ // 10. Align to 16 bytes (ESP32 requirement for flash writes)
+ // The bootloader image must be 16-byte aligned
+ if (actualBootloaderSize % 16 != 0) {
+ size_t alignedSize = ((actualBootloaderSize + 15) / 16) * 16;
+ // Make sure we don't exceed available data
+ if (alignedSize <= len) {
+ actualBootloaderSize = alignedSize;
+ }
+ }
+
+ DEBUG_PRINTF_P(PSTR("Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\n"),
+ segmentCount, actualBootloaderSize, len, hashAppended);
+
+ // 11. Verify we have enough data for all segments + hash + checksum
+ if (actualBootloaderSize > availableLen) {
+ *bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(actualBootloaderSize) + " bytes, have " + String(availableLen) + " bytes";
+ return false;
+ }
+
+ if (offset > availableLen) {
+ *bootloaderErrorMsg = "Bootloader truncated - expected at least " + String(offset) + " bytes, have " + String(len) + " bytes";
+ return false;
+ }
+
+ // Update len to reflect actual bootloader size (including hash and checksum, with alignment)
+ // This is critical - we must write the complete image including checksums
+ len = actualBootloaderSize;
+
+ return true;
+}
+
+// Bootloader OTA context structure
+struct BootloaderUpdateContext {
+ // State flags
+ bool replySent = false;
+ bool uploadComplete = false;
+ String errorMessage;
+
+ // Buffer to hold bootloader data
+ uint8_t* buffer = nullptr;
+ size_t bytesBuffered = 0;
+ const uint32_t bootloaderOffset = 0x1000;
+ const uint32_t maxBootloaderSize = 0x10000; // 64KB buffer size
+};
+
+// Cleanup bootloader OTA context
+static void endBootloaderOTA(AsyncWebServerRequest *request) {
+ BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject);
+ request->_tempObject = nullptr;
+
+ DEBUG_PRINTF_P(PSTR("EndBootloaderOTA %x --> %x\n"), (uintptr_t)request, (uintptr_t)context);
+ if (context) {
+ if (context->buffer) {
+ free(context->buffer);
+ context->buffer = nullptr;
+ }
+
+ // If update failed, restore system state
+ if (!context->uploadComplete || !context->errorMessage.isEmpty()) {
+ strip.resume();
+ #if WLED_WATCHDOG_TIMEOUT > 0
+ WLED::instance().enableWatchdog();
+ #endif
+ }
+
+ delete context;
+ }
+}
+
+// Initialize bootloader OTA context
+bool initBootloaderOTA(AsyncWebServerRequest *request) {
+ if (request->_tempObject) {
+ return true; // Already initialized
+ }
+
+ BootloaderUpdateContext* context = new BootloaderUpdateContext();
+ if (!context) {
+ DEBUG_PRINTLN(F("Failed to allocate bootloader OTA context"));
+ return false;
+ }
+
+ request->_tempObject = context;
+ request->onDisconnect([=]() { endBootloaderOTA(request); }); // ensures cleanup on disconnect
+
+ DEBUG_PRINTLN(F("Bootloader Update Start - initializing buffer"));
+ #if WLED_WATCHDOG_TIMEOUT > 0
+ WLED::instance().disableWatchdog();
+ #endif
+ lastEditTime = millis(); // make sure PIN does not lock during update
+ strip.suspend();
+ strip.resetSegments();
+
+ // Check available heap before attempting allocation
+ size_t freeHeap = getFreeHeapSize();
+ DEBUG_PRINTF_P(PSTR("Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\n"), freeHeap, context->maxBootloaderSize);
+
+ context->buffer = (uint8_t*)malloc(context->maxBootloaderSize);
+ if (!context->buffer) {
+ size_t freeHeapNow = getFreeHeapSize();
+ DEBUG_PRINTF_P(PSTR("Failed to allocate %d byte bootloader buffer! Free heap: %d bytes\n"), context->maxBootloaderSize, freeHeapNow);
+ context->errorMessage = "Out of memory! Free heap: " + String(freeHeapNow) + " bytes, need: " + String(context->maxBootloaderSize) + " bytes";
+ strip.resume();
+ #if WLED_WATCHDOG_TIMEOUT > 0
+ WLED::instance().enableWatchdog();
+ #endif
+ return false;
+ }
+
+ context->bytesBuffered = 0;
+ return true;
+}
+
+// Set bootloader OTA replied flag
+void setBootloaderOTAReplied(AsyncWebServerRequest *request) {
+ BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject);
+ if (context) {
+ context->replySent = true;
+ }
+}
+
+// Get bootloader OTA result
+std::pair getBootloaderOTAResult(AsyncWebServerRequest *request) {
+ BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject);
+
+ if (!context) {
+ return std::make_pair(true, String(F("Internal error: No bootloader OTA context")));
+ }
+
+ bool needsReply = !context->replySent;
+ String errorMsg = context->errorMessage;
+
+ // If upload was successful, return empty string and trigger reboot
+ if (context->uploadComplete && errorMsg.isEmpty()) {
+ doReboot = true;
+ endBootloaderOTA(request);
+ return std::make_pair(needsReply, String());
+ }
+
+ // If there was an error, return it
+ if (!errorMsg.isEmpty()) {
+ endBootloaderOTA(request);
+ return std::make_pair(needsReply, errorMsg);
+ }
+
+ // Should never happen
+ return std::make_pair(true, String(F("Internal software failure")));
+}
+
+// Handle bootloader OTA data
+void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) {
+ BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject);
+
+ if (!context) {
+ DEBUG_PRINTLN(F("No bootloader OTA context - ignoring data"));
+ return;
+ }
+
+ if (!context->errorMessage.isEmpty()) {
+ return;
+ }
+
+ // Buffer the incoming data
+ if (context->buffer && context->bytesBuffered + len <= context->maxBootloaderSize) {
+ memcpy(context->buffer + context->bytesBuffered, data, len);
+ context->bytesBuffered += len;
+ DEBUG_PRINTF_P(PSTR("Bootloader buffer progress: %d / %d bytes\n"), context->bytesBuffered, context->maxBootloaderSize);
+ } else if (!context->buffer) {
+ DEBUG_PRINTLN(F("Bootloader buffer not allocated!"));
+ context->errorMessage = "Internal error: Bootloader buffer not allocated";
+ return;
+ } else {
+ size_t totalSize = context->bytesBuffered + len;
+ DEBUG_PRINTLN(F("Bootloader size exceeds maximum!"));
+ context->errorMessage = "Bootloader file too large: " + String(totalSize) + " bytes (max: " + String(context->maxBootloaderSize) + " bytes)";
+ return;
+ }
+
+ // Only write to flash when upload is complete
+ if (isFinal) {
+ DEBUG_PRINTLN(F("Bootloader Upload Complete - validating and flashing"));
+
+ if (context->buffer && context->bytesBuffered > 0) {
+ // Prepare pointers for verification (may be adjusted if bootloader at offset)
+ const uint8_t* bootloaderData = context->buffer;
+ size_t bootloaderSize = context->bytesBuffered;
+
+ // Verify the complete bootloader image before flashing
+ // Note: verifyBootloaderImage may adjust bootloaderData pointer and bootloaderSize
+ // for validation purposes only
+ if (!verifyBootloaderImage(bootloaderData, bootloaderSize, &context->errorMessage)) {
+ DEBUG_PRINTLN(F("Bootloader validation failed!"));
+ // Error message already set by verifyBootloaderImage
+ } else {
+ // Calculate offset to write to flash
+ // If bootloaderData was adjusted (partition table detected), we need to skip it in flash too
+ size_t flashOffset = context->bootloaderOffset;
+ const uint8_t* dataToWrite = context->buffer;
+ size_t bytesToWrite = context->bytesBuffered;
+
+ // If validation adjusted the pointer, it means we have a partition table at the start
+ // In this case, we should skip writing the partition table and write bootloader at 0x1000
+ if (bootloaderData != context->buffer) {
+ // bootloaderData was adjusted - skip partition table in our data
+ size_t partitionTableSize = bootloaderData - context->buffer;
+ dataToWrite = bootloaderData;
+ bytesToWrite = bootloaderSize;
+ DEBUG_PRINTF_P(PSTR("Skipping %d bytes of partition table data\n"), partitionTableSize);
+ }
+
+ DEBUG_PRINTF_P(PSTR("Bootloader validation passed - writing %d bytes to flash at 0x%04X\n"),
+ bytesToWrite, flashOffset);
+
+ // Calculate erase size (must be multiple of 4KB)
+ size_t eraseSize = ((bytesToWrite + 0xFFF) / 0x1000) * 0x1000;
+ if (eraseSize > context->maxBootloaderSize) {
+ eraseSize = context->maxBootloaderSize;
+ }
+
+ // Erase bootloader region
+ DEBUG_PRINTF_P(PSTR("Erasing %d bytes at 0x%04X...\n"), eraseSize, flashOffset);
+ esp_err_t err = esp_flash_erase_region(NULL, flashOffset, eraseSize);
+ if (err != ESP_OK) {
+ DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err);
+ context->errorMessage = "Flash erase failed (error code: " + String(err) + ")";
+ } else {
+ // Write the validated bootloader data to flash
+ err = esp_flash_write(NULL, dataToWrite, flashOffset, bytesToWrite);
+ if (err != ESP_OK) {
+ DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err);
+ context->errorMessage = "Flash write failed (error code: " + String(err) + ")";
+ } else {
+ DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written to 0x%04X\n"),
+ bytesToWrite, flashOffset);
+ // Invalidate cached bootloader hash
+ invalidateBootloaderSHA256Cache();
+ context->uploadComplete = true;
+ }
+ }
+ }
+ } else if (context->bytesBuffered == 0) {
+ context->errorMessage = "No bootloader data received";
+ }
+ }
+}
+#endif
diff --git a/wled00/ota_update.h b/wled00/ota_update.h
index c8fd702643..82d97d6ce4 100644
--- a/wled00/ota_update.h
+++ b/wled00/ota_update.h
@@ -50,3 +50,65 @@ std::pair getOTAResult(AsyncWebServerRequest *request);
* @return bool indicating if a reply is necessary; string with error message if the update failed.
*/
void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);
+
+#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
+/**
+ * Calculate and cache the bootloader SHA256 digest
+ * Reads the bootloader from flash at offset 0x1000 and computes SHA256 hash
+ */
+void calculateBootloaderSHA256();
+
+/**
+ * Get bootloader SHA256 as hex string
+ * @return String containing 64-character hex representation of SHA256 hash
+ */
+String getBootloaderSHA256Hex();
+
+/**
+ * Invalidate cached bootloader SHA256 (call after bootloader update)
+ * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex
+ */
+void invalidateBootloaderSHA256Cache();
+
+/**
+ * Verify complete buffered bootloader using ESP-IDF validation approach
+ * This matches the key validation steps from esp_image_verify() in ESP-IDF
+ * @param buffer Reference to pointer to bootloader binary data (will be adjusted if offset detected)
+ * @param len Reference to length of bootloader data (will be adjusted to actual size)
+ * @param bootloaderErrorMsg Pointer to String to store error message (must not be null)
+ * @return true if validation passed, false otherwise
+ */
+bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootloaderErrorMsg);
+
+/**
+ * Create a bootloader OTA context object on an AsyncWebServerRequest
+ * @param request Pointer to web request object
+ * @return true if allocation was successful, false if not
+ */
+bool initBootloaderOTA(AsyncWebServerRequest *request);
+
+/**
+ * Indicate to the bootloader OTA subsystem that a reply has already been generated
+ * @param request Pointer to web request object
+ */
+void setBootloaderOTAReplied(AsyncWebServerRequest *request);
+
+/**
+ * Retrieve the bootloader OTA result.
+ * @param request Pointer to web request object
+ * @return bool indicating if a reply is necessary; string with error message if the update failed.
+ */
+std::pair getBootloaderOTAResult(AsyncWebServerRequest *request);
+
+/**
+ * Process a block of bootloader OTA data. This is a passthrough of an ArUploadHandlerFunction.
+ * Requires that initBootloaderOTA be called on the handler object before any work will be done.
+ * @param request Pointer to web request object
+ * @param index Offset in to uploaded file
+ * @param data New data bytes
+ * @param len Length of new data bytes
+ * @param isFinal Indicates that this is the last block
+ */
+void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);
+#endif
+
diff --git a/wled00/wled.h b/wled00/wled.h
index 7f3188bef9..d1cddd8fba 100644
--- a/wled00/wled.h
+++ b/wled00/wled.h
@@ -189,6 +189,9 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument;
#include "FastLED.h"
#include "const.h"
#include "fcn_declare.h"
+#ifndef WLED_DISABLE_OTA
+ #include "ota_update.h"
+#endif
#include "NodeStruct.h"
#include "pin_manager.h"
#include "colors.h"
diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp
index 1039747746..4a833e1636 100644
--- a/wled00/wled_server.cpp
+++ b/wled00/wled_server.cpp
@@ -15,6 +15,7 @@
#include "html_cpal.h"
#include "html_edit.h"
+
// define flash strings once (saves flash memory)
static const char s_redirecting[] PROGMEM = "Redirecting...";
static const char s_content_enc[] PROGMEM = "Content-Encoding";
@@ -32,6 +33,7 @@ static const char s_no_store[] PROGMEM = "no-store";
static const char s_expires[] PROGMEM = "Expires";
static const char _common_js[] PROGMEM = "/common.js";
+
//Is this an IP?
static bool isIp(const String &str) {
for (size_t i = 0; i < str.length(); i++) {
@@ -180,6 +182,7 @@ static String msgProcessor(const String& var)
return String();
}
+
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));
@@ -527,6 +530,53 @@ 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 (request->_tempObject) {
+ auto bootloader_result = getBootloaderOTAResult(request);
+ if (bootloader_result.first) {
+ if (bootloader_result.second.length() > 0) {
+ serveMessage(request, 500, F("Bootloader update failed!"), bootloader_result.second, 254);
+ } else {
+ serveMessage(request, 200, F("Bootloader updated successfully!"), FPSTR(s_rebooting), 131);
+ }
+ }
+ } else {
+ // No context structure - something's gone horribly wrong
+ serveMessage(request, 500, F("Bootloader update failed!"), F("Internal server fault"), 254);
+ }
+ },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
+ if (index == 0) {
+ // Privilege checks
+ 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!"));
+ serveMessage(request, 401, FPSTR(s_accessdenied), F("Client is not on local subnet."), 254);
+ setBootloaderOTAReplied(request);
+ return;
+ }
+ if (!correctPIN) {
+ serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
+ setBootloaderOTAReplied(request);
+ return;
+ }
+ if (otaLock) {
+ serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
+ setBootloaderOTAReplied(request);
+ return;
+ }
+
+ // Allocate the context structure
+ if (!initBootloaderOTA(request)) {
+ return; // Error will be dealt with after upload in response handler, above
+ }
+ }
+
+ handleBootloaderOTAData(request, index, data, len, isFinal);
+ });
+#endif
+
#ifdef WLED_ENABLE_DMX
server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap, dmxProcessor);