Add ESP32 bootloader upgrade functionality with JSON API support
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user