Add ESP32 bootloader upgrade functionality with JSON API support

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-10-05 14:17:51 +00:00
parent 30fbf55b9a
commit 93908e758f
4 changed files with 170 additions and 1 deletions

View File

@@ -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));
}
</script>
<style>
@import url("style.css");
</style>
</head>
<body onload="GetV()">
<body onload="GetV(); checkESP32();">
<h2>WLED Software Update</h2>
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
Installed version: <span class="sip">WLED ##VERSION##</span><br>
@@ -37,6 +49,16 @@
<button id="rev" type="button" onclick="cR()">Revert update</button><br>
<button type="button" onclick="B()">Back</button>
</form>
<div id="bootloader-section" style="display:none;">
<hr class="sml">
<h2>ESP32 Bootloader Update</h2>
<div id="bootloader-hash" class="sip" style="margin-bottom:8px;"></div>
<form method='POST' action='./updatebootloader' id='bootupd' enctype='multipart/form-data' onsubmit="toggle('bootupd')">
<b>Warning:</b> Only upload verified ESP32 bootloader files!<br>
<input type='file' name='update' required><br>
<button type="submit">Update Bootloader</button>
</form>
</div>
<div id="Noupd" class="hide"><b>Updating...</b><br>Please do not close or refresh the page :)</div>
</body>
</html>

View File

@@ -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();

View File

@@ -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();

View File

@@ -18,6 +18,13 @@
#endif
#include "html_cpal.h"
#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)
#include <esp_partition.h>
#include <esp_ota_ops.h>
#include <bootloader_common.h>
#include <mbedtls/sha256.h>
#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);