From 30fbf55b9af2de826ac7cd9f444f8bf830a13241 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:09:10 +0000 Subject: [PATCH 01/66] Initial plan From 93908e758fbd4c63b836c9fa9ea802fb1d44c50e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:17:51 +0000 Subject: [PATCH 02/66] Add ESP32 bootloader upgrade functionality with JSON API support Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/data/update.htm | 24 ++++++- wled00/fcn_declare.h | 3 + wled00/json.cpp | 3 + wled00/wled_server.cpp | 141 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 1 deletion(-) diff --git a/wled00/data/update.htm b/wled00/data/update.htm index 783a609ece..60c4f4163d 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -18,13 +18,25 @@ window.open(getURL("/update?revert"),"_self"); } function GetV() {/*injected values here*/} + var isESP32 = false; + function checkESP32() { + fetch(getURL('/json/info')).then(r=>r.json()).then(d=>{ + isESP32 = d.arch && d.arch.startsWith('esp32'); + if (isESP32) { + gId('bootloader-section').style.display = 'block'; + if (d.bootloaderSHA256) { + gId('bootloader-hash').innerText = 'Current bootloader SHA256: ' + d.bootloaderSHA256; + } + } + }).catch(e=>console.error(e)); + } - +

WLED Software Update

Installed version: WLED ##VERSION##
@@ -37,6 +49,16 @@

WLED Software Update


+
Updating...
Please do not close or refresh the page :)
\ No newline at end of file diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 1d81655d6d..ecd65b7018 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -545,6 +545,9 @@ void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& h void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error); void serveSettings(AsyncWebServerRequest* request, bool post = false); void serveSettingsJS(AsyncWebServerRequest* request); +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +String getBootloaderSHA256Hex(); +#endif //ws.cpp void handleWs(); diff --git a/wled00/json.cpp b/wled00/json.cpp index d2b771c590..b2f1072975 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -817,6 +817,9 @@ void serializeInfo(JsonObject root) root[F("resetReason1")] = (int)rtc_get_reset_reason(1); #endif root[F("lwip")] = 0; //deprecated + #ifndef WLED_DISABLE_OTA + root[F("bootloaderSHA256")] = getBootloaderSHA256Hex(); + #endif #else root[F("arch")] = "esp8266"; root[F("core")] = ESP.getCoreVersion(); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 75b4ae3f5a..8233dfd74f 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -18,6 +18,13 @@ #endif #include "html_cpal.h" +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) + #include + #include + #include + #include +#endif + // define flash strings once (saves flash memory) static const char s_redirecting[] PROGMEM = "Redirecting..."; static const char s_content_enc[] PROGMEM = "Content-Encoding"; @@ -28,6 +35,12 @@ static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_accessdenied[] PROGMEM = "Access Denied"; static const char _common_js[] PROGMEM = "/common.js"; +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +// Cache for bootloader SHA256 digest +static uint8_t bootloaderSHA256[32]; +static bool bootloaderSHA256Cached = false; +#endif + //Is this an IP? static bool isIp(const String &str) { for (size_t i = 0; i < str.length(); i++) { @@ -176,6 +189,61 @@ static String msgProcessor(const String& var) return String(); } +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +// Calculate and cache the bootloader SHA256 digest +static void calculateBootloaderSHA256() { + if (bootloaderSHA256Cached) return; + + // Bootloader is at fixed offset 0x1000 (4KB) and is typically 32KB + const uint32_t bootloaderOffset = 0x1000; + const uint32_t bootloaderSize = 0x8000; // 32KB, typical bootloader size + + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224) + + const size_t chunkSize = 256; + uint8_t buffer[chunkSize]; + + for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) { + size_t readSize = min(chunkSize, bootloaderSize - offset); + if (esp_flash_read(NULL, buffer, bootloaderOffset + offset, readSize) == ESP_OK) { + mbedtls_sha256_update(&ctx, buffer, readSize); + } + } + + mbedtls_sha256_finish(&ctx, bootloaderSHA256); + mbedtls_sha256_free(&ctx); + bootloaderSHA256Cached = true; +} + +// Get bootloader SHA256 as hex string +static String getBootloaderSHA256Hex() { + calculateBootloaderSHA256(); + + char hex[65]; + for (int i = 0; i < 32; i++) { + sprintf(hex + (i * 2), "%02x", bootloaderSHA256[i]); + } + hex[64] = '\0'; + return String(hex); +} + +// Verify if uploaded data is a valid ESP32 bootloader +static bool isValidBootloader(const uint8_t* data, size_t len) { + if (len < 32) return false; + + // Check for ESP32 bootloader magic byte (0xE9) + if (data[0] != 0xE9) return false; + + // Additional validation: check segment count is reasonable + uint8_t segmentCount = data[1]; + if (segmentCount > 16) return false; + + return true; +} +#endif + static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) { if (!correctPIN) { if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); @@ -466,6 +534,79 @@ void initServer() server.on(_update, HTTP_POST, notSupported, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){}); #endif +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) + // ESP32 bootloader update endpoint + server.on(F("/updatebootloader"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveSettings(request, true); // handle PIN page POST request + return; + } + if (otaLock) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254); + return; + } + if (Update.hasError()) { + serveMessage(request, 500, F("Bootloader update failed!"), F("Please check your file and retry!"), 254); + } else { + serveMessage(request, 200, F("Bootloader updated successfully!"), FPSTR(s_rebooting), 131); + doReboot = true; + } + },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){ + IPAddress client = request->client()->remoteIP(); + if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) { + DEBUG_PRINTLN(F("Attempted bootloader update from different/non-local subnet!")); + request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied)); + return; + } + if (!correctPIN || otaLock) return; + + if (!index) { + DEBUG_PRINTLN(F("Bootloader Update Start")); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().disableWatchdog(); + #endif + lastEditTime = millis(); // make sure PIN does not lock during update + strip.suspend(); + strip.resetSegments(); + + // Begin bootloader update - use U_FLASH and specify bootloader partition offset + if (!Update.begin(0x8000, U_FLASH, -1, 0x1000)) { + DEBUG_PRINTLN(F("Bootloader Update Begin Failed")); + Update.printError(Serial); + } + } + + // Verify bootloader magic on first chunk + if (index == 0 && !isValidBootloader(data, len)) { + DEBUG_PRINTLN(F("Invalid bootloader file!")); + Update.abort(); + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + return; + } + + if (!Update.hasError()) { + Update.write(data, len); + } + + if (isFinal) { + if (Update.end(true)) { + DEBUG_PRINTLN(F("Bootloader Update Success")); + bootloaderSHA256Cached = false; // Invalidate cached bootloader hash + } else { + DEBUG_PRINTLN(F("Bootloader Update Failed")); + Update.printError(Serial); + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + } + } + }); +#endif + #ifdef WLED_ENABLE_DMX server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap, dmxProcessor); From a18a661c73036136d3e1c6515d51ed17ede7ce7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:20:14 +0000 Subject: [PATCH 03/66] Add esp_flash.h include for ESP32 bootloader flash operations Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/wled_server.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 8233dfd74f..601162def1 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -21,6 +21,7 @@ #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) #include #include + #include #include #include #endif From c3e18905c17e9c1f3ee5308b82ff38dc0312f410 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:24:29 +0000 Subject: [PATCH 04/66] Improve bootloader flash implementation with proper erase and write operations Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/wled_server.cpp | 65 ++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 601162def1..6eb6d1efe3 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -561,6 +561,11 @@ void initServer() } if (!correctPIN || otaLock) return; + static size_t bootloaderBytesWritten = 0; + static bool bootloaderErased = false; + const uint32_t bootloaderOffset = 0x1000; + const uint32_t maxBootloaderSize = 0x8000; // 32KB max + if (!index) { DEBUG_PRINTLN(F("Bootloader Update Start")); #if WLED_WATCHDOG_TIMEOUT > 0 @@ -569,36 +574,58 @@ void initServer() lastEditTime = millis(); // make sure PIN does not lock during update strip.suspend(); strip.resetSegments(); + bootloaderBytesWritten = 0; + bootloaderErased = false; - // 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 (!isValidBootloader(data, len)) { + DEBUG_PRINTLN(F("Invalid bootloader file!")); + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + Update.abort(); + return; + } + + // Erase bootloader region (32KB) + DEBUG_PRINTLN(F("Erasing bootloader region...")); + esp_err_t err = esp_flash_erase_region(NULL, bootloaderOffset, maxBootloaderSize); + if (err != ESP_OK) { + DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err); + strip.resume(); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + Update.abort(); + return; } + bootloaderErased = true; } - // Verify bootloader magic on first chunk - if (index == 0 && !isValidBootloader(data, len)) { - DEBUG_PRINTLN(F("Invalid bootloader file!")); + // Write data to flash at bootloader offset + if (bootloaderErased && bootloaderBytesWritten + len <= maxBootloaderSize) { + esp_err_t err = esp_flash_write(NULL, data, bootloaderOffset + bootloaderBytesWritten, len); + if (err != ESP_OK) { + DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err); + Update.abort(); + } else { + bootloaderBytesWritten += len; + } + } else if (!bootloaderErased) { + DEBUG_PRINTLN(F("Bootloader region not erased!")); + Update.abort(); + } else { + DEBUG_PRINTLN(F("Bootloader size exceeds maximum!")); 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")); + if (!Update.hasError() && bootloaderBytesWritten > 0) { + DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written\n"), bootloaderBytesWritten); 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(); From 042ed39464e2ad2e217f0bdcd25b6572eb2d0e00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:57:24 +0000 Subject: [PATCH 05/66] Fix: Remove static keyword from getBootloaderSHA256Hex() to match declaration Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/wled_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 6eb6d1efe3..7286982fbf 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -219,7 +219,7 @@ static void calculateBootloaderSHA256() { } // Get bootloader SHA256 as hex string -static String getBootloaderSHA256Hex() { +String getBootloaderSHA256Hex() { calculateBootloaderSHA256(); char hex[65]; From f5f3fc338fecf7357854a8608b853b0aefa4c8d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 15:12:46 +0000 Subject: [PATCH 06/66] Fix: Cast min() arguments to size_t for ESP32-C3 compatibility Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/wled_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 7286982fbf..5fa773b83d 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -207,7 +207,7 @@ static void calculateBootloaderSHA256() { uint8_t buffer[chunkSize]; for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) { - size_t readSize = min(chunkSize, bootloaderSize - offset); + size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize); if (esp_flash_read(NULL, buffer, bootloaderOffset + offset, readSize) == ESP_OK) { mbedtls_sha256_update(&ctx, buffer, readSize); } From d79b02379e42517832b17dbedf59e8a21b24854f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 15:39:43 +0000 Subject: [PATCH 07/66] Fix: Move bootloader JavaScript to separate script block to avoid GetV() injection removal Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/data/update.htm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wled00/data/update.htm b/wled00/data/update.htm index 60c4f4163d..8c360c7809 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -18,6 +18,8 @@ window.open(getURL("/update?revert"),"_self"); } function GetV() {/*injected values here*/} + + @@ -26,30 +27,13 @@ var ctx = c.getContext('2d'); if (ctx) { // Access the rendering context // use parent WS or open new - var ws; - try { - ws = top.window.ws; - } catch (e) {} - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send("{'lv':true}"); - } else { - let l = window.location; - let pathn = l.pathname; - let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/"); - let url = l.origin.replace("http","ws"); - if (paths.length > 1) { - url += "/" + paths[0]; - } - ws = new WebSocket(url+"/ws"); - ws.onopen = ()=>{ - ws.send("{'lv':true}"); - } - } - ws.binaryType = "arraybuffer"; + var ws = connectWs(()=>{ + ws.send('{"lv":true}'); + }); ws.addEventListener('message',(e)=>{ try { if (toString.call(e.data) === '[object ArrayBuffer]') { - let leds = new Uint8Array(event.data); + let leds = new Uint8Array(e.data); if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp let mW = leds[2]; // matrix width let mH = leds[3]; // matrix height diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 4d7c7b666c..4309bc9ffd 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -30,11 +30,19 @@ void handleDDPPacket(e131_packet_t* p) { uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed; start += DMXAddress / ddpChannelsPerLed; - unsigned stop = start + htons(p->dataLen) / ddpChannelsPerLed; + uint16_t dataLen = htons(p->dataLen); + unsigned stop = start + dataLen / ddpChannelsPerLed; uint8_t* data = p->data; unsigned c = 0; if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later + unsigned numLeds = stop - start; // stop >= start is guaranteed + unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array + if (maxDataIndex > dataLen) { + DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting.")); + return; + } + if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); diff --git a/wled00/ws.cpp b/wled00/ws.cpp index 3a97459fee..6a02247203 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -5,6 +5,12 @@ */ #ifdef WLED_ENABLE_WEBSOCKETS +// define some constants for binary protocols, dont use defines but C++ style constexpr +constexpr uint8_t BINARY_PROTOCOL_GENERIC = 0xFF; // generic / auto detect NOT IMPLEMENTED +constexpr uint8_t BINARY_PROTOCOL_E131 = P_E131; // = 0, untested! +constexpr uint8_t BINARY_PROTOCOL_ARTNET = P_ARTNET; // = 1, untested! +constexpr uint8_t BINARY_PROTOCOL_DDP = P_DDP; // = 2 + uint16_t wsLiveClientId = 0; unsigned long wsLastLiveTime = 0; //uint8_t* wsFrameBuffer = nullptr; @@ -25,7 +31,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp // data packet AwsFrameInfo * info = (AwsFrameInfo*)arg; if(info->final && info->index == 0 && info->len == len){ - // the whole message is in a single frame and we got all of its data (max. 1450 bytes) + // the whole message is in a single frame and we got all of its data (max. 1428 bytes / ESP8266: 528 bytes) if(info->opcode == WS_TEXT) { if (len > 0 && len < 10 && data[0] == 'p') { @@ -71,8 +77,29 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp // force broadcast in 500ms after updating client //lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this } + }else if (info->opcode == WS_BINARY) { + // first byte determines protocol. Note: since e131_packet_t is "packed", the compiler handles alignment issues + //DEBUG_PRINTF_P(PSTR("WS binary message: len %u, byte0: %u\n"), len, data[0]); + int offset = 1; // offset to skip protocol byte + switch (data[0]) { + case BINARY_PROTOCOL_E131: + handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131); + break; + case BINARY_PROTOCOL_ARTNET: + handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET); + break; + case BINARY_PROTOCOL_DDP: + if (len < 10 + offset) return; // DDP header is 10 bytes (+1 protocol byte) + size_t ddpDataLen = (data[8+offset] << 8) | data[9+offset]; // data length in bytes from DDP header + uint8_t flags = data[0+offset]; + if ((flags & DDP_TIMECODE_FLAG) ) ddpDataLen += 4; // timecode flag adds 4 bytes to data length + if (len < (10 + offset + ddpDataLen)) return; // not enough data, prevent out of bounds read + // could be a valid DDP packet, forward to handler + handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP); + } } } else { + DEBUG_PRINTF_P(PSTR("WS multipart message: final %u index %u len %u total %u\n"), info->final, info->index, len, (uint32_t)info->len); //message is comprised of multiple frames or the frame is split into multiple packets //if(info->index == 0){ //if (!wsFrameBuffer && len < 4096) wsFrameBuffer = new uint8_t[4096]; From eb80fdf733c006c4610a57f3386bf88907a86a26 Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Wed, 8 Oct 2025 22:48:53 -0400 Subject: [PATCH 19/66] Game of Life Rework RAM and speed optimizations. Better repeat detection. Mutation toggle. Blur option added. --- wled00/FX.cpp | 243 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 147 insertions(+), 96 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 5b29b48ddd..4bdef3539e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5196,112 +5196,163 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f /////////////////////////////////////////// // 2D Cellular Automata Game of life // /////////////////////////////////////////// -typedef struct ColorCount { - CRGB color; - int8_t count; -} colorCount; +typedef struct Cell { + uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2; +} Cell; -uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color +uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ + // and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up + const int cols = SEG_W, rows = SEG_H; + const unsigned maxIndex = cols * rows; - const int cols = SEG_W; - const int rows = SEG_H; - const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; - const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled - const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) - - if (!SEGENV.allocateData(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed - CRGB *prevLeds = reinterpret_cast(SEGENV.data); - uint16_t *crcBuffer = reinterpret_cast(SEGENV.data + dataSize); - - CRGB backgroundColor = SEGCOLOR(1); - - if (SEGENV.call == 0 || strip.now - SEGMENT.step > 3000) { - SEGENV.step = strip.now; - SEGENV.aux0 = 0; - - //give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen) - for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { - unsigned state = hw_random8()%2; - if (state == 0) - SEGMENT.setPixelColorXY(x,y, backgroundColor); - else - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255)); + if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed + + Cell *cells = reinterpret_cast (SEGENV.data); + + uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity + bool mutate = SEGMENT.check3; + uint8_t blur = map(SEGMENT.custom1, 0, 255, 255, 4); + + uint32_t bgColor = SEGCOLOR(1); + uint32_t birthColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 255); + + bool setup = SEGENV.call == 0; + if (setup) { + // Calculate glider length LCM(rows,cols)*4 once + unsigned a = rows, b = cols; + while (b) { unsigned t = b; b = a % b; a = t; } + gliderLength = (cols * rows / a) << 2; + } + + if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix + bool paused = SEGENV.step > strip.now; + + // Setup New Game of Life + if ((!paused && generation == 0) || setup) { + SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds + generation = 1; + paused = true; + //Setup Grid + memset(cells, 0, maxIndex * sizeof(Cell)); + + for (unsigned i = maxIndex; i--; ) { + bool isAlive = !hw_random8(3); // ~33% + cells[i].alive = isAlive; + cells[i].faded = !isAlive; + unsigned x = i % cols, y = i / cols; + cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1); + + SEGMENT.setPixelColorXY(x, y, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor); + } + } + + if (paused || (strip.now - SEGENV.step < 1000 / map(SEGMENT.speed,0,255,1,42))) { + // Redraw if paused or between updates to remove blur + for (unsigned i = maxIndex; i--; ) { + if (!cells[i].alive) { + uint32_t cellColor = SEGMENT.getPixelColorXY(i % cols, i / cols); + if (cellColor != bgColor) { + uint32_t newColor; + bool needsColor = false; + if (cells[i].faded) { newColor = bgColor; needsColor = true; } + else { + uint32_t blended = color_blend(cellColor, bgColor, 2); + if (blended == cellColor) { blended = bgColor; cells[i].faded = 1; } + newColor = blended; needsColor = true; + } + if (needsColor) SEGMENT.setPixelColorXY(i % cols, i / cols, newColor); + } + } } - - for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) prevLeds[XY(x,y)] = CRGB::Black; - memset(crcBuffer, 0, sizeof(uint16_t)*crcBufferLen); - } else if (strip.now - SEGENV.step < FRAMETIME_FIXED * (uint32_t)map(SEGMENT.speed,0,255,64,4)) { - // update only when appropriate time passes (in 42 FPS slots) return FRAMETIME; - } + + } + + // Repeat detection + bool updateOscillator = generation % 16 == 0; + bool updateSpaceship = gliderLength && generation % gliderLength == 0; + bool repeatingOscillator = true, repeatingSpaceship = true, emptyGrid = true; + + unsigned cIndex = maxIndex-1; + for (unsigned y = rows; y--; ) for (unsigned x = cols; x--; cIndex--) { + Cell& cell = cells[cIndex]; + + if (cell.alive) emptyGrid = false; + if (cell.oscillatorCheck != cell.alive) repeatingOscillator = false; + if (cell.spaceshipCheck != cell.alive) repeatingSpaceship = false; + if (updateOscillator) cell.oscillatorCheck = cell.alive; + if (updateSpaceship) cell.spaceshipCheck = cell.alive; + + unsigned neighbors = 0, aliveParents = 0, parentIdx[3]; + // Count alive neighbors + for (int i = 1; i >= -1; i--) for (int j = 1; j >= -1; j--) if (i || j) { + int nX = x + j, nY = y + i; + if (cell.edgeCell) { + nX = (nX + cols) % cols; + nY = (nY + rows) % rows; + } + unsigned nIndex = nX + nY * cols; + Cell& neighbor = cells[nIndex]; + if (neighbor.alive) { + if (++neighbors > 3) break; + if (!neighbor.toggleStatus) { // Alive and not dying + parentIdx[aliveParents++] = nIndex; + } + } + } - //copy previous leds (save previous generation) - //NOTE: using lossy getPixelColor() is a benefit as endlessly repeating patterns will eventually fade out causing a reset - for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) prevLeds[XY(x,y)] = SEGMENT.getPixelColorXY(x,y); - - //calculate new leds - for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { - - colorCount colorsCount[9]; // count the different colors in the 3*3 matrix - for (int i=0; i<9; i++) colorsCount[i] = {backgroundColor, 0}; // init colorsCount - - // iterate through neighbors and count them and their different colors - int neighbors = 0; - for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix - if (i==0 && j==0) continue; // ignore itself - // wrap around segment - int xx = x+i, yy = y+j; - if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0; - if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0; - - unsigned xy = XY(xx, yy); // previous cell xy to check - // count different neighbours and colors - if (prevLeds[xy] != backgroundColor) { - neighbors++; - bool colorFound = false; - int k; - for (k=0; k<9 && colorsCount[k].count != 0; k++) - if (colorsCount[k].color == prevLeds[xy]) { - colorsCount[k].count++; - colorFound = true; - } - if (!colorFound) colorsCount[k] = {prevLeds[xy], 1}; //add new color found in the array + uint32_t newColor; + bool needsColor = false; + + if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation + cell.toggleStatus = 1; + if (blur == 255) cell.faded = 1; + newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColorXY(x, y), bgColor, blur); + needsColor = true; + } + else if (!cell.alive) { + if (neighbors == 3 && (!mutate || hw_random8(128)) || // Normal birth with 1/128 failure chance if mutate + (mutate && neighbors == 2 && !hw_random8(128))) { // Mutation birth with 2 neighbors with 1/128 chance if mutate + cell.toggleStatus = 1; + cell.faded = 0; + + if (aliveParents) { + // Set color based on random neighbor + unsigned parentIndex = parentIdx[random8(aliveParents)]; + birthColor = SEGMENT.getPixelColorXY(parentIndex % cols, parentIndex / cols); + } + newColor = birthColor; + needsColor = true; + } + else if (!cell.faded) {// No change, fade dead cells + uint32_t cellColor = SEGMENT.getPixelColorXY(x, y); + uint32_t blended = color_blend(cellColor, bgColor, blur); + if (blended == cellColor) { blended = bgColor; cell.faded = 1; } + newColor = blended; + needsColor = true; } - } // i,j - - // Rules of Life - uint32_t col = uint32_t(prevLeds[XY(x,y)]) & 0x00FFFFFF; // uint32_t operator returns RGBA, we want RGBW -> cut off "alpha" byte - uint32_t bgc = RGBW32(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0); - if ((col != bgc) && (neighbors < 2)) SEGMENT.setPixelColorXY(x,y, bgc); // Loneliness - else if ((col != bgc) && (neighbors > 3)) SEGMENT.setPixelColorXY(x,y, bgc); // Overpopulation - else if ((col == bgc) && (neighbors == 3)) { // Reproduction - // find dominant color and assign it to a cell - colorCount dominantColorCount = {backgroundColor, 0}; - for (int i=0; i<9 && colorsCount[i].count != 0; i++) - if (colorsCount[i].count > dominantColorCount.count) dominantColorCount = colorsCount[i]; - // assign the dominant color w/ a bit of randomness to avoid "gliders" - if (dominantColorCount.count > 0 && hw_random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color); - } else if ((col == bgc) && (neighbors == 2) && !hw_random8(128)) { // Mutation - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255)); - } - // else do nothing! - } //x,y - - // calculate CRC16 of leds - uint16_t crc = crc16((const unsigned char*)prevLeds, dataSize); - // check if we had same CRC and reset if needed - bool repetition = false; - for (int i=0; i Date: Tue, 14 Oct 2025 22:58:22 -0400 Subject: [PATCH 20/66] Game of Life Optimizations Adjust mutation logic. Use 1D get/set. Reduce code size. --- wled00/FX.cpp | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 4bdef3539e..f0f4276f27 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5197,7 +5197,7 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f // 2D Cellular Automata Game of life // /////////////////////////////////////////// typedef struct Cell { - uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2; + uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2; } Cell; uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ @@ -5207,7 +5207,7 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: const unsigned maxIndex = cols * rows; if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed - + Cell *cells = reinterpret_cast (SEGENV.data); uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity @@ -5230,20 +5230,20 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: // Setup New Game of Life if ((!paused && generation == 0) || setup) { - SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds + SEGENV.step = strip.now + 1280; // show initial state for 1.28 seconds generation = 1; paused = true; //Setup Grid memset(cells, 0, maxIndex * sizeof(Cell)); - for (unsigned i = maxIndex; i--; ) { + for (unsigned i = 0; i < maxIndex; i++) { bool isAlive = !hw_random8(3); // ~33% cells[i].alive = isAlive; cells[i].faded = !isAlive; unsigned x = i % cols, y = i / cols; cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1); - SEGMENT.setPixelColorXY(x, y, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor); + SEGMENT.setPixelColor(i, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor); } } @@ -5251,22 +5251,21 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: // Redraw if paused or between updates to remove blur for (unsigned i = maxIndex; i--; ) { if (!cells[i].alive) { - uint32_t cellColor = SEGMENT.getPixelColorXY(i % cols, i / cols); + uint32_t cellColor = SEGMENT.getPixelColor(i); if (cellColor != bgColor) { uint32_t newColor; bool needsColor = false; - if (cells[i].faded) { newColor = bgColor; needsColor = true; } + if (cells[i].faded) { newColor = bgColor; needsColor = true; } else { uint32_t blended = color_blend(cellColor, bgColor, 2); if (blended == cellColor) { blended = bgColor; cells[i].faded = 1; } newColor = blended; needsColor = true; } - if (needsColor) SEGMENT.setPixelColorXY(i % cols, i / cols, newColor); + if (needsColor) SEGMENT.setPixelColor(i, newColor); } } } return FRAMETIME; - } // Repeat detection @@ -5286,8 +5285,8 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: unsigned neighbors = 0, aliveParents = 0, parentIdx[3]; // Count alive neighbors - for (int i = 1; i >= -1; i--) for (int j = 1; j >= -1; j--) if (i || j) { - int nX = x + j, nY = y + i; + for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) if (i || j) { + int nX = x + j, nY = y + i; if (cell.edgeCell) { nX = (nX + cols) % cols; nY = (nY + rows) % rows; @@ -5295,8 +5294,8 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: unsigned nIndex = nX + nY * cols; Cell& neighbor = cells[nIndex]; if (neighbor.alive) { - if (++neighbors > 3) break; - if (!neighbor.toggleStatus) { // Alive and not dying + neighbors++; + if (!neighbor.toggleStatus && neighbors < 4) { // Alive and not dying parentIdx[aliveParents++] = nIndex; } } @@ -5304,29 +5303,29 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: uint32_t newColor; bool needsColor = false; - + if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation cell.toggleStatus = 1; if (blur == 255) cell.faded = 1; - newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColorXY(x, y), bgColor, blur); + newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColor(cIndex), bgColor, blur); needsColor = true; } else if (!cell.alive) { - if (neighbors == 3 && (!mutate || hw_random8(128)) || // Normal birth with 1/128 failure chance if mutate - (mutate && neighbors == 2 && !hw_random8(128))) { // Mutation birth with 2 neighbors with 1/128 chance if mutate + byte mutationRoll = mutate ? hw_random8(128) : 1; // if 0: 3 neighbor births fail and 2 neighbor births mutate + if ((neighbors == 3 && mutationRoll) || (mutate && neighbors == 2 && !mutationRoll)) { // Reproduction or Mutation cell.toggleStatus = 1; cell.faded = 0; - + if (aliveParents) { // Set color based on random neighbor unsigned parentIndex = parentIdx[random8(aliveParents)]; - birthColor = SEGMENT.getPixelColorXY(parentIndex % cols, parentIndex / cols); + birthColor = SEGMENT.getPixelColor(parentIndex); } newColor = birthColor; needsColor = true; } else if (!cell.faded) {// No change, fade dead cells - uint32_t cellColor = SEGMENT.getPixelColorXY(x, y); + uint32_t cellColor = SEGMENT.getPixelColor(cIndex); uint32_t blended = color_blend(cellColor, bgColor, blur); if (blended == cellColor) { blended = bgColor; cell.faded = 1; } newColor = blended; @@ -5334,7 +5333,7 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: } } - if (needsColor) SEGMENT.setPixelColorXY(x, y, newColor); + if (needsColor) SEGMENT.setPixelColor(cIndex, newColor); } // Loop through cells, if toggle, swap alive status for (unsigned i = maxIndex; i--; ) { @@ -5344,8 +5343,8 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: if (repeatingOscillator || repeatingSpaceship || emptyGrid) { generation = 0; // reset on next call - SEGENV.step += 1000; // pause final generation for 1 second - } + SEGENV.step += 1024; // pause final generation for ~1 second + } else { ++generation; SEGENV.step = strip.now; From 7e1992fc5c2e792463c3ad127aaff4f58615383f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Oct 2025 07:08:18 +0200 Subject: [PATCH 21/66] adding function to check if a backup exists --- wled00/cfg.cpp | 4 ++++ wled00/fcn_declare.h | 2 ++ wled00/file.cpp | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 840afb51ba..b5a0574403 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -777,6 +777,10 @@ bool verifyConfig() { return validateJsonFile(s_cfg_json); } +bool configBackupExists() { + return checkBackupExists(s_cfg_json); +} + // rename config file and reboot // if the cfg file doesn't exist, such as after a reset, do nothing void resetConfig() { diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index ecd65b7018..e78cf048a7 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -27,6 +27,7 @@ void IRAM_ATTR touchButtonISR(); bool backupConfig(); bool restoreConfig(); bool verifyConfig(); +bool configBackupExists(); void resetConfig(); bool deserializeConfig(JsonObject doc, bool fromFS = false); bool deserializeConfigFromFS(); @@ -103,6 +104,7 @@ inline bool readObjectFromFile(const String &file, const char* key, JsonDocument bool copyFile(const char* src_path, const char* dst_path); bool backupFile(const char* filename); bool restoreFile(const char* filename); +bool checkBackupExists(const char* filename); bool validateJsonFile(const char* filename); void dumpFilesToSerial(); diff --git a/wled00/file.cpp b/wled00/file.cpp index 9f1dd62256..ba406ba3b5 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -557,6 +557,12 @@ bool restoreFile(const char* filename) { return false; } +bool checkBackupExists(const char* filename) { + char backupname[32]; + snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename + return WLED_FS.exists(backupname); +} + bool validateJsonFile(const char* filename) { if (!WLED_FS.exists(filename)) return false; File file = WLED_FS.open(filename, "r"); From 46ff43889b4246091c0af7239a391f1fea6bf7b2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 19 Oct 2025 07:10:05 +0200 Subject: [PATCH 22/66] check config backup as welcome page gate --- wled00/wled.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 923688106d..cd21d8d84e 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -474,7 +474,7 @@ void WLED::setup() if (needsCfgSave) serializeConfigToFS(); // usermods required new parameters; need to wait for strip to be initialised #4752 - if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0) + if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0 && !configBackupExists()) showWelcomePage = true; WiFi.persistent(false); WiFi.onEvent(WiFiEvent); From 0f06535932007fc09cc01a42b642c3c28ef99ac2 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Thu, 25 Sep 2025 10:54:58 +0100 Subject: [PATCH 23/66] Include audioreactive for hub75 examples --- platformio_override.sample.ini | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index d897a494de..8419aba8c7 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -541,12 +541,15 @@ build_flags = ${common.build_flags} -D WLED_ENABLE_HUB75MATRIX -D NO_GFX -D WLED_DEBUG_BUS ; -D WLED_DEBUG + -D SR_DMTYPE=-1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash + lib_deps = ${esp32_idf_V4.lib_deps} https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11 monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = dio +custom_usermods = audioreactive [env:esp32dev_hub75_forum_pinout] extends = env:esp32dev_hub75 @@ -555,10 +558,10 @@ build_flags = ${common.build_flags} -D WLED_ENABLE_HUB75MATRIX -D NO_GFX -D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins -D WLED_DEBUG_BUS + -D SR_DMTYPE=-1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash ; -D WLED_DEBUG - [env:adafruit_matrixportal_esp32s3] ; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75 board = adafruit_matrixportal_esp32s3 @@ -575,6 +578,7 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME= -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips -D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3 -D WLED_DEBUG_BUS + -D SR_DMTYPE=-1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash lib_deps = ${esp32s3.lib_deps} @@ -584,6 +588,7 @@ board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder +custom_usermods = audioreactive [env:esp32S3_PSRAM_HUB75] ;; MOONHUB HUB75 adapter board @@ -601,6 +606,7 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME= -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips -D MOONHUB_S3_PINOUT ;; HUB75 pinout -D WLED_DEBUG_BUS + -D SR_DMTYPE=-1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash lib_deps = ${esp32s3.lib_deps} https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix @@ -609,3 +615,4 @@ board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder +custom_usermods = audioreactive \ No newline at end of file From 8e00e7175c70df449b02b7b41d69974235ee24a1 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Fri, 26 Sep 2025 19:34:18 +0100 Subject: [PATCH 24/66] Include audioreactive for hub75 examples - MOONHUB audio --- platformio_override.sample.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index 8419aba8c7..249b101059 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -606,7 +606,8 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME= -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips -D MOONHUB_S3_PINOUT ;; HUB75 pinout -D WLED_DEBUG_BUS - -D SR_DMTYPE=-1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash + -D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75 + -D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic lib_deps = ${esp32s3.lib_deps} https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix From 5fb37130f825b53802d0e49683e1d5a0d6f72d29 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Fri, 7 Nov 2025 15:42:47 +0000 Subject: [PATCH 25/66] Include esp32 debug build --- platformio.ini | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index c354de2698..6cb401e841 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,25 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover, usermods +default_envs = nodemcuv2 + esp8266_2m + esp01_1m_full + nodemcuv2_160 + esp8266_2m_160 + esp01_1m_full_160 + nodemcuv2_compat + esp8266_2m_compat + esp01_1m_full_compat + esp32dev + esp32dev_debug + esp32_eth + esp32_wrover + lolin_s2_mini + esp32c3dev + esp32s3dev_16MB_opi + esp32s3dev_8MB_opi + esp32s3_4M_qspi + usermods src_dir = ./wled00 data_dir = ./wled00/data @@ -438,6 +456,13 @@ monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = dio +[env:esp32dev_debug] +extends = env:esp32dev +build_unflags = -D WLED_RELEASE_NAME=\"ESP32_V4\" +build_flags = ${env:esp32dev.build_flags} + -D WLED_DEBUG + -D WLED_RELEASE_NAME=\"ESP32_DEBUG\" + [env:esp32dev_8M] board = esp32dev platform = ${esp32_idf_V4.platform} From acd415c522205364897bca46e467ac806538f5be Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Fri, 7 Nov 2025 18:29:03 +0000 Subject: [PATCH 26/66] fix release name for esp32 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 6cb401e841..e0f03e9355 100644 --- a/platformio.ini +++ b/platformio.ini @@ -449,7 +449,7 @@ board = esp32dev platform = ${esp32_idf_V4.platform} build_unflags = ${common.build_unflags} custom_usermods = audioreactive -build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_V4\" #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder From c623b826989b243a69c995efe17edfacc64af6e6 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 8 Nov 2025 15:17:21 +0000 Subject: [PATCH 27/66] improve esp32_dev env --- platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index e0f03e9355..cdf4c2bd32 100644 --- a/platformio.ini +++ b/platformio.ini @@ -458,7 +458,8 @@ board_build.flash_mode = dio [env:esp32dev_debug] extends = env:esp32dev -build_unflags = -D WLED_RELEASE_NAME=\"ESP32_V4\" +upload_speed = 921600 +build_unflags = -D WLED_RELEASE_NAME=\"ESP32\" build_flags = ${env:esp32dev.build_flags} -D WLED_DEBUG -D WLED_RELEASE_NAME=\"ESP32_DEBUG\" From 1afd72cb832f7ddb7537c7cc252ae0d6b5cfeaeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:09:10 +0000 Subject: [PATCH 28/66] Initial plan From 151acb249e97f24d5b4280b0e7b61c3a64fe9bd8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:54:28 +0000 Subject: [PATCH 29/66] Initial plan From 07e26d31f45cc28be0a62231b6fd551054f5088f Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 8 Nov 2025 21:21:32 +0000 Subject: [PATCH 30/66] fix ESP32_DEBUG --- platformio.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index cdf4c2bd32..ec0ed5ce9b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -459,8 +459,7 @@ board_build.flash_mode = dio [env:esp32dev_debug] extends = env:esp32dev upload_speed = 921600 -build_unflags = -D WLED_RELEASE_NAME=\"ESP32\" -build_flags = ${env:esp32dev.build_flags} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_DEBUG -D WLED_RELEASE_NAME=\"ESP32_DEBUG\" From 601bb6f0ca5a514027b9b020ffea898e39756cc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:09:10 +0000 Subject: [PATCH 31/66] Initial plan From 91349234a0435dd11b76cfb4146d679a98d02dd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:54:28 +0000 Subject: [PATCH 32/66] Initial plan From d475d21a79b20f7aa63fce64277a4fc33245453c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:09:10 +0000 Subject: [PATCH 33/66] Initial plan From 670f74d589206049e3f805e1a951e9a5e68d093c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:09:10 +0000 Subject: [PATCH 34/66] Initial plan From 013ecfb1896228172dc4f05384da3ae4e7aac8ea Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 09:15:23 +0000 Subject: [PATCH 35/66] Add ESP32 bootloader upgrade functionality with JSON API support Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com> --- wled00/data/update.htm | 18 ++++++ wled00/fcn_declare.h | 3 + wled00/json.cpp | 3 + wled00/wled_server.cpp | 141 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) diff --git a/wled00/data/update.htm b/wled00/data/update.htm index d8b8876ef2..e93ecde5c0 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -29,6 +29,13 @@ if (data.arch == "esp8266") { toggle('rev'); } + isESP32 = data.arch && data.arch.startsWith('esp32'); + 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); @@ -37,6 +44,7 @@ document.querySelector('.release-name').textContent = 'Unknown'; }); } + function GetV() {/*injected values here*/} - - +

WLED Software Update

Installed version: Loading...
From abfe91d47bc219d60cc5ea52628ab00f4c55d3a9 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 11:31:56 +0000 Subject: [PATCH 58/66] tidy up imports in wled_server.cpp --- wled00/wled_server.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 9245d17f2d..c7b2998e26 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -16,21 +16,10 @@ #include "html_edit.h" #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) - #include - #include +#include #include #include - #include - #include -#endif - -#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) - #include - #include - #include - #include - #include - #include +#include #endif // define flash strings once (saves flash memory) From 8097c7c86d076c72149b389492a6532efea617cc Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 11:41:51 +0000 Subject: [PATCH 59/66] refactor current bootloader reading out of the server ino ota --- wled00/fcn_declare.h | 3 --- wled00/ota_update.cpp | 54 +++++++++++++++++++++++++++++++++++---- wled00/ota_update.h | 18 +++++++++++++ wled00/wled.h | 3 +++ wled00/wled_server.cpp | 57 ------------------------------------------ 5 files changed, 70 insertions(+), 65 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 0b557d4572..01c2c2ec92 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -546,9 +546,6 @@ void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& h void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error); void serveSettings(AsyncWebServerRequest* request, bool post = false); void serveSettingsJS(AsyncWebServerRequest* request); -#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) -String getBootloaderSHA256Hex(); -#endif //ws.cpp void handleWs(); diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index b1e7236647..5fc79db50a 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -5,11 +5,7 @@ #include #include #include -#endif - -// Forward declarations -#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) -void invalidateBootloaderSHA256Cache(); +#include #endif // Platform-specific metadata locations @@ -263,6 +259,54 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, } #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +// Cache for bootloader SHA256 digest +static uint8_t bootloaderSHA256[32]; +static bool bootloaderSHA256Cached = false; + +// Calculate and cache the bootloader SHA256 digest +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((size_t)(bootloaderSize - offset), chunkSize); + 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 +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); +} + +// Invalidate cached bootloader SHA256 (call after bootloader update) +void invalidateBootloaderSHA256Cache() { + bootloaderSHA256Cached = false; +} + // 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 diff --git a/wled00/ota_update.h b/wled00/ota_update.h index 4750ced0bb..82d97d6ce4 100644 --- a/wled00/ota_update.h +++ b/wled00/ota_update.h @@ -52,6 +52,24 @@ std::pair getOTAResult(AsyncWebServerRequest *request); 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 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 c7b2998e26..09bb5139db 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -15,12 +15,6 @@ #include "html_cpal.h" #include "html_edit.h" -#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) -#include - #include - #include -#include -#endif // define flash strings once (saves flash memory) static const char s_redirecting[] PROGMEM = "Redirecting..."; @@ -39,11 +33,6 @@ static const char s_no_store[] PROGMEM = "no-store"; static const char s_expires[] PROGMEM = "Expires"; 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) { @@ -193,52 +182,6 @@ 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((size_t)(bootloaderSize - offset), chunkSize); - 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 -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); -} - -// Invalidate cached bootloader SHA256 (call after bootloader update) -void invalidateBootloaderSHA256Cache() { - bootloaderSHA256Cached = false; -} - -#endif static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) { if (!correctPIN) { From 9474c29946628760b3a18d1b68548af7981d7f2c Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 11:53:42 +0000 Subject: [PATCH 60/66] single definition of BOOTLOADER_OFFSET --- wled00/ota_update.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 5fc79db50a..4e4a31eb68 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -12,6 +12,7 @@ #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 @@ -268,7 +269,6 @@ 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; @@ -280,7 +280,7 @@ void calculateBootloaderSHA256() { for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) { size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize); - if (esp_flash_read(NULL, buffer, bootloaderOffset + offset, readSize) == ESP_OK) { + if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { mbedtls_sha256_update(&ctx, buffer, readSize); } } @@ -329,7 +329,6 @@ bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootload // Offset 23: hash_appended const size_t MIN_IMAGE_HEADER_SIZE = 24; - const size_t BOOTLOADER_OFFSET = 0x1000; // 1. Validate minimum size for header if (len < MIN_IMAGE_HEADER_SIZE) { From ff93a48926462d0707d5972368b8cd7665acacd9 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 11:57:41 +0000 Subject: [PATCH 61/66] optimise fetching of bootloaderSHA256 --- wled00/ota_update.cpp | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 4e4a31eb68..b92ee53cba 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -260,17 +260,18 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, } #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) -// Cache for bootloader SHA256 digest -static uint8_t bootloaderSHA256[32]; -static bool bootloaderSHA256Cached = false; +// Cache for bootloader SHA256 digest as hex string +static String bootloaderSHA256HexCache = ""; -// Calculate and cache the bootloader SHA256 digest +// Calculate and cache the bootloader SHA256 digest as hex string void calculateBootloaderSHA256() { - if (bootloaderSHA256Cached) return; + 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) @@ -285,26 +286,27 @@ void calculateBootloaderSHA256() { } } - mbedtls_sha256_finish(&ctx, bootloaderSHA256); + mbedtls_sha256_finish(&ctx, sha256); mbedtls_sha256_free(&ctx); - bootloaderSHA256Cached = true; -} - -// Get bootloader SHA256 as hex string -String getBootloaderSHA256Hex() { - calculateBootloaderSHA256(); + // Convert to hex string and cache it char hex[65]; for (int i = 0; i < 32; i++) { - sprintf(hex + (i * 2), "%02x", bootloaderSHA256[i]); + sprintf(hex + (i * 2), "%02x", sha256[i]); } hex[64] = '\0'; - return String(hex); + bootloaderSHA256HexCache = String(hex); +} + +// Get bootloader SHA256 as hex string +String getBootloaderSHA256Hex() { + calculateBootloaderSHA256(); + return bootloaderSHA256HexCache; } // Invalidate cached bootloader SHA256 (call after bootloader update) void invalidateBootloaderSHA256Cache() { - bootloaderSHA256Cached = false; + bootloaderSHA256HexCache = ""; } // Verify complete buffered bootloader using ESP-IDF validation approach From a36638ee6dcc9adc46e69ac665d4512084048377 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 12:04:56 +0000 Subject: [PATCH 62/66] Stop processing once an error is detected during bootloader upload --- wled00/ota_update.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index b92ee53cba..60a8330385 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -628,6 +628,10 @@ void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8 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); From 88466c7d1fc3fe5957b8b7d2ba5c6e33b59705ab Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 12:14:32 +0000 Subject: [PATCH 63/66] Truncated bootloader images slip through verification and get flashed --- wled00/ota_update.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 60a8330385..586fa7fbec 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -313,6 +313,7 @@ void invalidateBootloaderSHA256Cache() { // 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; @@ -464,16 +465,22 @@ bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootload // If hash_appended != 0, there's a 32-byte SHA256 hash after the segments uint8_t hashAppended = buffer[23]; if (hashAppended != 0) { - // SHA256 hash is appended (32 bytes) 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 < len) { + 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) @@ -490,7 +497,12 @@ bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootload segmentCount, actualBootloaderSize, len, hashAppended); // 11. Verify we have enough data for all segments + hash + checksum - if (offset > len) { + 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; } From af8c851cc62024b68baddd5a6d88bfa64f540b59 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 12:18:16 +0000 Subject: [PATCH 64/66] Privilege checks must run before bootloader init --- wled00/wled_server.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 09bb5139db..4a833e1636 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -548,11 +548,6 @@ void initServer() } },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){ if (index == 0) { - // Allocate the context structure - if (!initBootloaderOTA(request)) { - return; // Error will be dealt with after upload in response handler, above - } - // Privilege checks IPAddress client = request->client()->remoteIP(); if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) { @@ -571,6 +566,11 @@ void initServer() 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); From c7c379f9622d2cc8924cbe376c3f42db1c854509 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 13:24:15 +0000 Subject: [PATCH 65/66] match all esp32 types --- wled00/data/update.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/data/update.htm b/wled00/data/update.htm index f6591c79ec..976f580c6e 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -29,7 +29,7 @@ if (data.arch == "esp8266") { toggle('rev'); } - const isESP32 = data.arch && data.arch.startsWith('esp32'); + const isESP32 = data.arch && data.arch.toLowerCase().startsWith('esp32'); if (isESP32) { gId('bootloader-section').style.display = 'block'; if (data.bootloaderSHA256) { From 50d33c5bf44afb2e905ad7765122db8ce758a906 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 9 Nov 2025 14:20:30 +0000 Subject: [PATCH 66/66] Only supports ESP32 and ESP32-S2 --- wled00/data/update.htm | 2 +- wled00/ota_update.cpp | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/wled00/data/update.htm b/wled00/data/update.htm index 976f580c6e..e93a113fae 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -29,7 +29,7 @@ if (data.arch == "esp8266") { toggle('rev'); } - const isESP32 = data.arch && data.arch.toLowerCase().startsWith('esp32'); + const isESP32 = data.arch && (data.arch.toLowerCase() === 'esp32' || data.arch.toLowerCase() === 'esp32-s2'); if (isESP32) { gId('bootloader-section').style.display = 'block'; if (data.bootloaderSHA256) { diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index 586fa7fbec..6a5cf29cd3 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -403,32 +403,30 @@ bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String* bootload *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; } - #elif defined(CONFIG_IDF_TARGET_ESP32C2) - if (chipId != 0x000C) { - *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-C2 (0x000C), 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; } - #elif defined(CONFIG_IDF_TARGET_ESP32H2) - if (chipId != 0x0010) { - *bootloaderErrorMsg = "Chip ID mismatch - expected ESP32-H2 (0x0010), 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)