mirror of
https://github.com/wled/WLED.git
synced 2026-07-01 01:01:33 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 202144a32e | |||
| 08ccfe5697 | |||
| c8bda1abee | |||
| 0d911e93bc | |||
| 5a9bdad2ec | |||
| 379f9c9ffa | |||
| 4750ac5010 | |||
| 0bc64b49b6 | |||
| 2d0771fcaf | |||
| 1706fdce3d | |||
| 7d0a338058 | |||
| 319edc7ad4 | |||
| 7e8bb20560 | |||
| d12bf77831 | |||
| d935975ec1 | |||
| ce5f6d7019 | |||
| 430a82af85 | |||
| 69263c198a | |||
| e82b519c92 | |||
| ed496fb426 | |||
| 66573be212 | |||
| 4cdaa57dce | |||
| 79762f45b2 | |||
| 501b6e7de5 | |||
| 693f3b0b04 | |||
| f6d1f3b433 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wled",
|
||||
"version": "0.15.1",
|
||||
"version": "0.15.2-beta2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wled",
|
||||
"version": "0.15.1",
|
||||
"version": "0.15.2-beta2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"clean-css": "^5.3.3",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wled",
|
||||
"version": "0.15.2-beta1",
|
||||
"version": "0.15.2-beta2",
|
||||
"description": "Tools for WLED project",
|
||||
"main": "tools/cdata.js",
|
||||
"directories": {
|
||||
|
||||
+4
-2
@@ -273,8 +273,8 @@ board_build.partitions = ${esp32.default_partitions} ;; default partioning for
|
||||
;;
|
||||
;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly.
|
||||
;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio.
|
||||
platform = espressif32@ ~6.3.2
|
||||
platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them)
|
||||
platform = https://github.com/tasmota/platform-espressif32/releases/download/2023.06.02/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.9 with IPv6 support, based on IDF 4.4.4
|
||||
platform_packages =
|
||||
build_unflags = ${common.build_unflags}
|
||||
build_flags = -g
|
||||
-Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one
|
||||
@@ -449,6 +449,8 @@ lib_deps = ${esp32_idf_V4.lib_deps}
|
||||
${esp32.AR_lib_deps}
|
||||
monitor_filters = esp32_exception_decoder
|
||||
board_build.partitions = ${esp32.default_partitions}
|
||||
board_build.flash_mode = dio
|
||||
upload_speed = 921600
|
||||
|
||||
[env:esp32dev_8M]
|
||||
board = esp32dev
|
||||
|
||||
+1
-1
@@ -580,7 +580,7 @@ uint16_t mode_twinkle(void) {
|
||||
SEGENV.step = it;
|
||||
}
|
||||
|
||||
unsigned PRNG16 = SEGENV.aux1;
|
||||
uint16_t PRNG16 = SEGENV.aux1;
|
||||
|
||||
for (unsigned i = 0; i < SEGENV.aux0; i++)
|
||||
{
|
||||
|
||||
+25
-1
@@ -636,9 +636,32 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
|
||||
return (doc["sv"] | true);
|
||||
}
|
||||
|
||||
|
||||
static const char s_cfg_json[] PROGMEM = "/cfg.json";
|
||||
|
||||
bool backupConfig() {
|
||||
return backupFile(s_cfg_json);
|
||||
}
|
||||
|
||||
bool restoreConfig() {
|
||||
return restoreFile(s_cfg_json);
|
||||
}
|
||||
|
||||
bool verifyConfig() {
|
||||
return validateJsonFile(s_cfg_json);
|
||||
}
|
||||
|
||||
// rename config file and reboot
|
||||
// if the cfg file doesn't exist, such as after a reset, do nothing
|
||||
void resetConfig() {
|
||||
if (WLED_FS.exists(s_cfg_json)) {
|
||||
DEBUG_PRINTLN(F("Reset config"));
|
||||
char backupname[32];
|
||||
snprintf_P(backupname, sizeof(backupname), PSTR("/rst.%s"), &s_cfg_json[1]);
|
||||
WLED_FS.rename(s_cfg_json, backupname);
|
||||
doReboot = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool deserializeConfigFromFS() {
|
||||
[[maybe_unused]] bool success = deserializeConfigSec();
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
@@ -676,6 +699,7 @@ bool deserializeConfigFromFS() {
|
||||
|
||||
void serializeConfig() {
|
||||
serializeConfigSec();
|
||||
backupConfig(); // backup before writing new config
|
||||
|
||||
DEBUG_PRINTLN(F("Writing settings to /cfg.json..."));
|
||||
|
||||
|
||||
+21
-5
@@ -5,7 +5,7 @@
|
||||
<title>WLED Update</title>
|
||||
<script>
|
||||
function B() { window.history.back(); }
|
||||
function U() { document.getElementById("uf").style.display="none";document.getElementById("msg").style.display="block"; }
|
||||
function U() { document.getElementById("uf").style.display="none";document.getElementById("bootloader-section").style.display="none";document.getElementById("msg").style.display="block"; }
|
||||
function GetV() {
|
||||
// Fetch device info via JSON API instead of compiling it in
|
||||
fetch('/json/info')
|
||||
@@ -18,6 +18,13 @@
|
||||
if (data.arch == "esp8266") {
|
||||
toggle('rev');
|
||||
}
|
||||
const isESP32 = data.arch && (data.arch.toLowerCase() === 'esp32' || data.arch.toLowerCase() === 'esp32-s2');
|
||||
if (isESP32) {
|
||||
document.getElementById('bootloader-section').style.display = 'block';
|
||||
if (data.bootloaderSHA256) {
|
||||
document.getElementById('bootloader-hash').innerText = 'Current bootloader SHA256: ' + data.bootloaderSHA256;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Could not fetch device info:', error);
|
||||
@@ -31,15 +38,14 @@
|
||||
@import url("style.css");
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload="GetV()">
|
||||
<body onload="GetV();">
|
||||
<h2>WLED Software Update</h2>
|
||||
<form method='POST' action='./update' id='uf' enctype='multipart/form-data' onsubmit="U()">
|
||||
Installed version: <span class="sip installed-version">Loading...</span><br>
|
||||
Release: <span class="sip release-name">Loading...</span><br>
|
||||
Download the latest binary: <a href="https://github.com/Aircoookie/WLED/releases" target="_blank"
|
||||
Download the latest binary: <a href="https://github.com/wled/WLED/releases" target="_blank"
|
||||
style="vertical-align: text-bottom; display: inline-flex;">
|
||||
<img src="https://img.shields.io/github/release/Aircoookie/WLED.svg?style=flat-square"></a><br>
|
||||
<img src="https://img.shields.io/github/release/wled/WLED.svg?style=flat-square"></a><br>
|
||||
<input type="hidden" name="skipValidation" value="" id="sV">
|
||||
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
|
||||
<input type='checkbox' onchange="sV.value=checked?1:''" id="skipValidation">
|
||||
@@ -47,6 +53,16 @@
|
||||
<button type="submit">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="U()">
|
||||
<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="msg"><b>Updating...</b><br>Please do not close or refresh the page :)</div>
|
||||
</body>
|
||||
</html>
|
||||
+22
-4
@@ -24,6 +24,10 @@ void handleIO();
|
||||
void IRAM_ATTR touchButtonISR();
|
||||
|
||||
//cfg.cpp
|
||||
bool backupConfig();
|
||||
bool restoreConfig();
|
||||
bool verifyConfig();
|
||||
void resetConfig();
|
||||
bool deserializeConfig(JsonObject doc, bool fromFS = false);
|
||||
bool deserializeConfigFromFS();
|
||||
bool deserializeConfigSec();
|
||||
@@ -114,10 +118,15 @@ bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest
|
||||
bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest);
|
||||
void updateFSInfo();
|
||||
void closeFile();
|
||||
inline bool writeObjectToFileUsingId(const String &file, uint16_t id, JsonDocument* content) { return writeObjectToFileUsingId(file.c_str(), id, content); };
|
||||
inline bool writeObjectToFile(const String &file, const char* key, JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); };
|
||||
inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest) { return readObjectFromFileUsingId(file.c_str(), id, dest); };
|
||||
inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest) { return readObjectFromFile(file.c_str(), key, dest); };
|
||||
inline bool writeObjectToFileUsingId(const String &file, uint16_t id, const JsonDocument* content) { return writeObjectToFileUsingId(file.c_str(), id, content); };
|
||||
inline bool writeObjectToFile(const String &file, const char* key, const JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); };
|
||||
inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); };
|
||||
inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); };
|
||||
bool copyFile(const char* src_path, const char* dst_path);
|
||||
bool backupFile(const char* filename);
|
||||
bool restoreFile(const char* filename);
|
||||
bool validateJsonFile(const char* filename);
|
||||
void dumpFilesToSerial();
|
||||
|
||||
//hue.cpp
|
||||
void handleHue();
|
||||
@@ -399,6 +408,15 @@ void enumerateLedmaps();
|
||||
uint8_t get_random_wheel_index(uint8_t pos);
|
||||
float mapf(float x, float in_min, float in_max, float out_min, float out_max);
|
||||
|
||||
void handleBootLoop(); // detect and handle bootloops
|
||||
#ifndef ESP8266
|
||||
void bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config
|
||||
#endif
|
||||
|
||||
void handleBootLoop(); // detect and handle bootloops
|
||||
#ifndef ESP8266
|
||||
void bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config
|
||||
#endif
|
||||
// RAII guard class for the JSON Buffer lock
|
||||
// Modeled after std::lock_guard
|
||||
class JSONBufferGuard {
|
||||
|
||||
+153
@@ -438,3 +438,156 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// copy a file, delete destination file if incomplete to prevent corrupted files
|
||||
bool copyFile(const char* src_path, const char* dst_path) {
|
||||
DEBUG_PRINTF("copyFile from %s to %s\n", src_path, dst_path);
|
||||
if(!WLED_FS.exists(src_path)) {
|
||||
DEBUG_PRINTLN(F("file not found"));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true; // is set to false on error
|
||||
File src = WLED_FS.open(src_path, "r");
|
||||
File dst = WLED_FS.open(dst_path, "w");
|
||||
|
||||
if (src && dst) {
|
||||
uint8_t buf[128]; // copy file in 128-byte blocks
|
||||
while (src.available() > 0) {
|
||||
size_t bytesRead = src.read(buf, sizeof(buf));
|
||||
if (bytesRead == 0) {
|
||||
success = false;
|
||||
break; // error, no data read
|
||||
}
|
||||
size_t bytesWritten = dst.write(buf, bytesRead);
|
||||
if (bytesWritten != bytesRead) {
|
||||
success = false;
|
||||
break; // error, not all data written
|
||||
}
|
||||
}
|
||||
} else {
|
||||
success = false; // error, could not open files
|
||||
}
|
||||
if(src) src.close();
|
||||
if(dst) dst.close();
|
||||
if (!success) {
|
||||
DEBUG_PRINTLN(F("copy failed"));
|
||||
WLED_FS.remove(dst_path); // delete incomplete file
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
// compare two files, return true if identical
|
||||
bool compareFiles(const char* path1, const char* path2) {
|
||||
DEBUG_PRINTF("compareFile %s and %s\n", path1, path2);
|
||||
if (!WLED_FS.exists(path1) || !WLED_FS.exists(path2)) {
|
||||
DEBUG_PRINTLN(F("file not found"));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool identical = true; // set to false on mismatch
|
||||
File f1 = WLED_FS.open(path1, "r");
|
||||
File f2 = WLED_FS.open(path2, "r");
|
||||
|
||||
if (f1 && f2) {
|
||||
uint8_t buf1[128], buf2[128];
|
||||
while (f1.available() > 0 || f2.available() > 0) {
|
||||
size_t len1 = f1.read(buf1, sizeof(buf1));
|
||||
size_t len2 = f2.read(buf2, sizeof(buf2));
|
||||
|
||||
if (len1 != len2) {
|
||||
identical = false;
|
||||
break; // files differ in size or read failed
|
||||
}
|
||||
|
||||
if (memcmp(buf1, buf2, len1) != 0) {
|
||||
identical = false;
|
||||
break; // files differ in content
|
||||
}
|
||||
}
|
||||
} else {
|
||||
identical = false; // error opening files
|
||||
}
|
||||
|
||||
if (f1) f1.close();
|
||||
if (f2) f2.close();
|
||||
return identical;
|
||||
}
|
||||
|
||||
static const char s_backup_fmt[] PROGMEM = "/bkp.%s";
|
||||
|
||||
bool backupFile(const char* filename) {
|
||||
DEBUG_PRINTF("backup %s \n", filename);
|
||||
if (!validateJsonFile(filename)) {
|
||||
DEBUG_PRINTLN(F("broken file"));
|
||||
return false;
|
||||
}
|
||||
char backupname[32];
|
||||
snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename
|
||||
|
||||
if (copyFile(filename, backupname)) {
|
||||
DEBUG_PRINTLN(F("backup ok"));
|
||||
return true;
|
||||
}
|
||||
DEBUG_PRINTLN(F("backup failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool restoreFile(const char* filename) {
|
||||
DEBUG_PRINTF("restore %s \n", filename);
|
||||
char backupname[32];
|
||||
snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename
|
||||
|
||||
if (!WLED_FS.exists(backupname)) {
|
||||
DEBUG_PRINTLN(F("no backup found"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateJsonFile(backupname)) {
|
||||
DEBUG_PRINTLN(F("broken backup"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (copyFile(backupname, filename)) {
|
||||
DEBUG_PRINTLN(F("restore ok"));
|
||||
return true;
|
||||
}
|
||||
DEBUG_PRINTLN(F("restore failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool validateJsonFile(const char* filename) {
|
||||
if (!WLED_FS.exists(filename)) return false;
|
||||
File file = WLED_FS.open(filename, "r");
|
||||
if (!file) return false;
|
||||
StaticJsonDocument<0> doc, filter; // https://arduinojson.org/v6/how-to/validate-json/
|
||||
bool result = deserializeJson(doc, file, DeserializationOption::Filter(filter)) == DeserializationError::Ok;
|
||||
file.close();
|
||||
if (!result) {
|
||||
DEBUG_PRINTF_P(PSTR("Invalid JSON file %s\n"), filename);
|
||||
} else {
|
||||
DEBUG_PRINTF_P(PSTR("Valid JSON file %s\n"), filename);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// print contents of all files in root dir to Serial except wsec files
|
||||
void dumpFilesToSerial() {
|
||||
File rootdir = WLED_FS.open("/", "r");
|
||||
File rootfile = rootdir.openNextFile();
|
||||
while (rootfile) {
|
||||
size_t len = strlen(rootfile.name());
|
||||
// skip files starting with "wsec" and dont end in .json
|
||||
if (strncmp(rootfile.name(), "wsec", 4) != 0 && len >= 6 && strcmp(rootfile.name() + len - 5, ".json") == 0) {
|
||||
Serial.println(rootfile.name());
|
||||
while (rootfile.available()) {
|
||||
Serial.write(rootfile.read());
|
||||
}
|
||||
Serial.println();
|
||||
Serial.println();
|
||||
}
|
||||
rootfile.close();
|
||||
rootfile = rootdir.openNextFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -753,6 +753,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();
|
||||
|
||||
+476
-1
@@ -3,12 +3,15 @@
|
||||
|
||||
#ifdef ESP32
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_flash.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#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
|
||||
@@ -73,6 +76,9 @@ static void endOTA(AsyncWebServerRequest *request) {
|
||||
// If the upload is incomplete, Update.end(false) should error out.
|
||||
if (Update.end(context->uploadComplete)) {
|
||||
// Update successful!
|
||||
#ifndef ESP8266
|
||||
bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update
|
||||
#endif
|
||||
doReboot = true;
|
||||
context->needsRestart = false;
|
||||
}
|
||||
@@ -109,6 +115,7 @@ static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
|
||||
strip.suspend();
|
||||
strip.resetSegments(); // free as much memory as you can
|
||||
context->needsRestart = true;
|
||||
backupConfig(); // backup current config in case the update ends badly
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("OTA Update Start, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context);
|
||||
|
||||
@@ -249,4 +256,472 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data,
|
||||
// Upload complete
|
||||
context->uploadComplete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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<BootloaderUpdateContext*>(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!\n"), context->maxBootloaderSize);
|
||||
context->errorMessage = "Out of memory! 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<BootloaderUpdateContext*>(request->_tempObject);
|
||||
if (context) {
|
||||
context->replySent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Get bootloader OTA result
|
||||
std::pair<bool, String> getBootloaderOTAResult(AsyncWebServerRequest *request) {
|
||||
BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(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<BootloaderUpdateContext*>(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
|
||||
|
||||
@@ -50,3 +50,65 @@ std::pair<bool, String> 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<bool, String> 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
|
||||
|
||||
|
||||
+151
@@ -1,6 +1,16 @@
|
||||
#include "wled.h"
|
||||
#include "fcn_declare.h"
|
||||
#include "const.h"
|
||||
#ifdef ESP8266
|
||||
#include "user_interface.h" // for bootloop detection
|
||||
#else
|
||||
#include <Update.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
|
||||
#include "esp32/rtc.h" // for bootloop detection
|
||||
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)
|
||||
#include "soc/rtc.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
//helper to get int value at a position in string
|
||||
@@ -594,3 +604,144 @@ uint8_t get_random_wheel_index(uint8_t pos) {
|
||||
float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
|
||||
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
||||
}
|
||||
|
||||
// bootloop detection and handling
|
||||
// checks if the ESP reboots multiple times due to a crash or watchdog timeout
|
||||
// if a bootloop is detected: restore settings from backup, then reset settings, then switch boot image (and repeat)
|
||||
|
||||
#define BOOTLOOP_INTERVAL_MILLIS 120000 // time limit between crashes: 120 seconds (2 minutes)
|
||||
#define BOOTLOOP_THRESHOLD 5 // number of consecutive crashes to trigger bootloop detection
|
||||
#define BOOTLOOP_ACTION_RESTORE 0 // default action: restore config from /bkp.cfg.json
|
||||
#define BOOTLOOP_ACTION_RESET 1 // if restore does not work, reset config (rename /cfg.json to /rst.cfg.json)
|
||||
#define BOOTLOOP_ACTION_OTA 2 // swap the boot partition
|
||||
#define BOOTLOOP_ACTION_DUMP 3 // nothing seems to help, dump files to serial and reboot (until hardware reset)
|
||||
|
||||
// Platform-agnostic abstraction
|
||||
enum class ResetReason {
|
||||
Power,
|
||||
Software,
|
||||
Crash,
|
||||
Brownout
|
||||
};
|
||||
|
||||
#ifdef ESP8266
|
||||
// Place variables in RTC memory via references, since RTC memory is not exposed via the linker in the Non-OS SDK
|
||||
// Use an offset of 32 as there's some hints that the first 128 bytes of "user" memory are used by the OTA system
|
||||
// Ref: https://github.com/esp8266/Arduino/blob/78d0d0aceacc1553f45ad8154592b0af22d1eede/cores/esp8266/Esp.cpp#L168
|
||||
static volatile uint32_t& bl_last_boottime = *(RTC_USER_MEM + 32);
|
||||
static volatile uint32_t& bl_crashcounter = *(RTC_USER_MEM + 33);
|
||||
static volatile uint32_t& bl_actiontracker = *(RTC_USER_MEM + 34);
|
||||
|
||||
static inline ResetReason rebootReason() {
|
||||
uint32_t resetReason = system_get_rst_info()->reason;
|
||||
if (resetReason == REASON_EXCEPTION_RST
|
||||
|| resetReason == REASON_WDT_RST
|
||||
|| resetReason == REASON_SOFT_WDT_RST)
|
||||
return ResetReason::Crash;
|
||||
if (resetReason == REASON_SOFT_RESTART)
|
||||
return ResetReason::Software;
|
||||
return ResetReason::Power;
|
||||
}
|
||||
|
||||
static inline uint32_t getRtcMillis() { return system_get_rtc_time() / 160; }; // rtc ticks ~160000Hz
|
||||
|
||||
#else
|
||||
// variables in RTC_NOINIT memory persist between reboots (but not on hardware reset)
|
||||
RTC_NOINIT_ATTR static uint32_t bl_last_boottime;
|
||||
RTC_NOINIT_ATTR static uint32_t bl_crashcounter;
|
||||
RTC_NOINIT_ATTR static uint32_t bl_actiontracker;
|
||||
|
||||
static inline ResetReason rebootReason() {
|
||||
esp_reset_reason_t reason = esp_reset_reason();
|
||||
if (reason == ESP_RST_BROWNOUT) return ResetReason::Brownout;
|
||||
if (reason == ESP_RST_SW) return ResetReason::Software;
|
||||
if (reason == ESP_RST_PANIC || reason == ESP_RST_WDT || reason == ESP_RST_INT_WDT || reason == ESP_RST_TASK_WDT) return ResetReason::Crash;
|
||||
return ResetReason::Power;
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
|
||||
static inline uint32_t getRtcMillis() { return esp_rtc_get_time_us() / 1000; }
|
||||
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)
|
||||
static inline uint32_t getRtcMillis() { return rtc_time_slowclk_to_us(rtc_time_get(), rtc_clk_slow_freq_get_hz()) / 1000; }
|
||||
#endif
|
||||
|
||||
void bootloopCheckOTA() { bl_actiontracker = BOOTLOOP_ACTION_OTA; } // swap boot image if bootloop is detected instead of restoring config
|
||||
|
||||
#endif
|
||||
|
||||
// detect bootloop by checking the reset reason and the time since last boot
|
||||
static bool detectBootLoop() {
|
||||
uint32_t rtctime = getRtcMillis();
|
||||
bool result = false;
|
||||
|
||||
switch(rebootReason()) {
|
||||
case ResetReason::Power:
|
||||
bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // init action tracker if not an intentional reboot (e.g. from OTA or bootloop handler)
|
||||
// fall through
|
||||
case ResetReason::Software:
|
||||
// no crash detected, reset counter
|
||||
bl_crashcounter = 0;
|
||||
break;
|
||||
|
||||
case ResetReason::Crash:
|
||||
{
|
||||
uint32_t rebootinterval = rtctime - bl_last_boottime;
|
||||
if (rebootinterval < BOOTLOOP_INTERVAL_MILLIS) {
|
||||
bl_crashcounter++;
|
||||
if (bl_crashcounter >= BOOTLOOP_THRESHOLD) {
|
||||
DEBUG_PRINTLN(F("!BOOTLOOP DETECTED!"));
|
||||
bl_crashcounter = 0;
|
||||
if(bl_actiontracker > BOOTLOOP_ACTION_DUMP) bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // reset action tracker if out of bounds
|
||||
result = true;
|
||||
}
|
||||
} else {
|
||||
// Reset counter on long intervals to track only consecutive short-interval crashes
|
||||
bl_crashcounter = 0;
|
||||
// TODO: crash reporting goes here
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ResetReason::Brownout:
|
||||
// crash due to brownout can't be detected unless using flash memory to store bootloop variables
|
||||
DEBUG_PRINTLN(F("brownout detected"));
|
||||
//restoreConfig(); // TODO: blindly restoring config if brownout detected is a bad idea, need a better way (if at all)
|
||||
break;
|
||||
}
|
||||
|
||||
bl_last_boottime = rtctime; // store current runtime for next reboot
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void handleBootLoop() {
|
||||
DEBUG_PRINTF_P(PSTR("checking for bootloop: time %d, counter %d, action %d\n"), bl_last_boottime, bl_crashcounter, bl_actiontracker);
|
||||
if (!detectBootLoop()) return; // no bootloop detected
|
||||
|
||||
switch(bl_actiontracker) {
|
||||
case BOOTLOOP_ACTION_RESTORE:
|
||||
restoreConfig();
|
||||
++bl_actiontracker;
|
||||
break;
|
||||
case BOOTLOOP_ACTION_RESET:
|
||||
resetConfig();
|
||||
++bl_actiontracker;
|
||||
break;
|
||||
case BOOTLOOP_ACTION_OTA:
|
||||
#ifndef ESP8266
|
||||
if(Update.canRollBack()) {
|
||||
DEBUG_PRINTLN(F("Swapping boot partition..."));
|
||||
Update.rollBack(); // swap boot partition
|
||||
}
|
||||
++bl_actiontracker;
|
||||
break;
|
||||
#else
|
||||
// fall through
|
||||
#endif
|
||||
case BOOTLOOP_ACTION_DUMP:
|
||||
dumpFilesToSerial();
|
||||
break;
|
||||
}
|
||||
|
||||
ESP.restart(); // restart cleanly and don't wait for another crash
|
||||
}
|
||||
|
||||
@@ -404,6 +404,9 @@ void WLED::setup()
|
||||
DEBUGFS_PRINTLN(F("FS failed!"));
|
||||
errorFlag = ERR_FS_BEGIN;
|
||||
}
|
||||
|
||||
handleBootLoop(); // check for bootloop and take action (requires WLED_FS)
|
||||
|
||||
#ifdef WLED_ADD_EEPROM_SUPPORT
|
||||
else deEEP();
|
||||
#else
|
||||
@@ -419,6 +422,11 @@ void WLED::setup()
|
||||
WLED_SET_AP_SSID(); // otherwise it is empty on first boot until config is saved
|
||||
multiWiFi.push_back(WiFiConfig(CLIENT_SSID,CLIENT_PASS)); // initialise vector with default WiFi
|
||||
|
||||
if(!verifyConfig()) {
|
||||
if(!restoreConfig()) {
|
||||
resetConfig();
|
||||
}
|
||||
}
|
||||
DEBUG_PRINTLN(F("Reading config"));
|
||||
bool needsCfgSave = deserializeConfigFromFS();
|
||||
DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap());
|
||||
|
||||
@@ -183,6 +183,9 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;
|
||||
#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 "bus_manager.h"
|
||||
|
||||
+73
-35
@@ -16,7 +16,7 @@
|
||||
#endif
|
||||
|
||||
constexpr uint32_t WLED_CUSTOM_DESC_MAGIC = 0x57535453; // "WSTS" (WLED System Tag Structure)
|
||||
constexpr uint32_t WLED_CUSTOM_DESC_VERSION = 1;
|
||||
constexpr uint32_t WLED_CUSTOM_DESC_VERSION = 2; // v1 - original PR; v2 - "safe to update from" version
|
||||
|
||||
// Compile-time validation that release name doesn't exceed maximum length
|
||||
static_assert(sizeof(WLED_RELEASE_NAME) <= WLED_RELEASE_NAME_MAX_LEN,
|
||||
@@ -59,6 +59,7 @@ const wled_metadata_t __attribute__((section(BUILD_METADATA_SECTION))) WLED_BUIL
|
||||
TOSTRING(WLED_VERSION),
|
||||
WLED_RELEASE_NAME, // release_name
|
||||
std::integral_constant<uint32_t, djb2_hash_constexpr(WLED_RELEASE_NAME)>::value, // hash - computed at compile time; integral_constant enforces this
|
||||
{ 0, 0, 0 }, // All other platforms can update safely
|
||||
};
|
||||
|
||||
static const char repoString_s[] PROGMEM = WLED_REPO;
|
||||
@@ -80,40 +81,47 @@ const __FlashStringHelper* brandString = FPSTR(brandString_s);
|
||||
* @return true if structure was found and extracted, false otherwise
|
||||
*/
|
||||
bool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc) {
|
||||
if (!binaryData || !extractedDesc || dataSize < sizeof(wled_metadata_t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {
|
||||
const wled_metadata_t* custom_desc = (const wled_metadata_t*)(binaryData + offset);
|
||||
|
||||
// Check for magic number
|
||||
if (custom_desc->magic == WLED_CUSTOM_DESC_MAGIC) {
|
||||
// Found potential match, validate version
|
||||
if (custom_desc->desc_version != WLED_CUSTOM_DESC_VERSION) {
|
||||
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
|
||||
offset, custom_desc->desc_version);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate hash using runtime function
|
||||
uint32_t expected_hash = djb2_hash_runtime(custom_desc->release_name);
|
||||
if (custom_desc->hash != expected_hash) {
|
||||
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Valid structure found - copy entire structure
|
||||
memcpy(extractedDesc, custom_desc, sizeof(wled_metadata_t));
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Extracted WLED structure at offset %u: '%s'\n"),
|
||||
offset, extractedDesc->release_name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
|
||||
if (!binaryData || !extractedDesc || dataSize < sizeof(wled_metadata_t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {
|
||||
if ((binaryData[offset]) == static_cast<char>(WLED_CUSTOM_DESC_MAGIC)) {
|
||||
// First byte matched; check next in an alignment-safe way
|
||||
uint32_t data_magic;
|
||||
memcpy(&data_magic, binaryData + offset, sizeof(data_magic));
|
||||
|
||||
// Check for magic number
|
||||
if (data_magic == WLED_CUSTOM_DESC_MAGIC) {
|
||||
wled_metadata_t candidate;
|
||||
memcpy(&candidate, binaryData + offset, sizeof(candidate));
|
||||
|
||||
// Found potential match, validate version
|
||||
if (candidate.desc_version > WLED_CUSTOM_DESC_VERSION) {
|
||||
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
|
||||
offset, candidate.desc_version);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate hash using runtime function
|
||||
uint32_t expected_hash = djb2_hash_runtime(candidate.release_name);
|
||||
if (candidate.hash != expected_hash) {
|
||||
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Valid structure found - copy entire structure
|
||||
*extractedDesc = candidate;
|
||||
|
||||
DEBUG_PRINTF_P(PSTR("Extracted WLED structure at offset %u: '%s'\n"),
|
||||
offset, extractedDesc->release_name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -144,13 +152,43 @@ bool shouldAllowOTA(const wled_metadata_t& firmwareDescription, char* errorMessa
|
||||
|
||||
if (strncmp_P(safeFirmwareRelease, releaseString, WLED_RELEASE_NAME_MAX_LEN) != 0) {
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
snprintf_P(errorMessage, errorMessageLen, PSTR("Firmware compatibility mismatch: current='%s', uploaded='%s'."),
|
||||
snprintf_P(errorMessage, errorMessageLen, PSTR("Firmware release name mismatch: current='%s', uploaded='%s'."),
|
||||
releaseString, safeFirmwareRelease);
|
||||
errorMessage[errorMessageLen - 1] = '\0'; // Ensure null termination
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firmwareDescription.desc_version > 1) {
|
||||
// Add safe version check
|
||||
// Parse our version (x.y.z) and compare it to the "safe version" array
|
||||
const char* our_version = versionString;
|
||||
for(unsigned v_index = 0; v_index < 3; ++v_index) {
|
||||
char* our_version_end = nullptr;
|
||||
long our_v_parsed = strtol(our_version, &our_version_end, 10);
|
||||
if (!our_version_end || (our_version_end == our_version)) {
|
||||
// We were built with a malformed version string
|
||||
// We blame the integrator and attempt the update anyways - nothing the user can do to fix this
|
||||
break;
|
||||
}
|
||||
|
||||
if (firmwareDescription.safe_update_version[v_index] > our_v_parsed) {
|
||||
if (errorMessage && errorMessageLen > 0) {
|
||||
snprintf_P(errorMessage, errorMessageLen, PSTR("Cannot update from this version: requires at least %d.%d.%d, current='%s'."),
|
||||
firmwareDescription.safe_update_version[0], firmwareDescription.safe_update_version[1], firmwareDescription.safe_update_version[2],
|
||||
versionString);
|
||||
errorMessage[errorMessageLen - 1] = '\0'; // Ensure null termination
|
||||
}
|
||||
return false;
|
||||
} else if (firmwareDescription.safe_update_version[v_index] < our_v_parsed) {
|
||||
break; // no need to check the other components
|
||||
}
|
||||
|
||||
if (*our_version_end == '.') ++our_version_end;
|
||||
our_version = our_version_end;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: additional checks go here
|
||||
|
||||
return true;
|
||||
|
||||
@@ -26,6 +26,7 @@ typedef struct {
|
||||
char wled_version[WLED_VERSION_MAX_LEN];
|
||||
char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)
|
||||
uint32_t hash; // Structure sanity check
|
||||
uint8_t safe_update_version[3]; // Indicates version it's known to be safe to install this update from: major, minor, patch
|
||||
} __attribute__((packed)) wled_metadata_t;
|
||||
|
||||
|
||||
|
||||
@@ -420,6 +420,45 @@ void initServer()
|
||||
});
|
||||
#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
|
||||
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){
|
||||
|
||||
Reference in New Issue
Block a user