From 6aaab571b31f032b139845f3023ee29f550f091b Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 22 Nov 2025 12:18:36 +0000 Subject: [PATCH 01/14] Upgrade reporting --- wled00/data/index.js | 176 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/wled00/data/index.js b/wled00/data/index.js index 2c1f143ae1..bd27f95250 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -2224,6 +2224,7 @@ function requestJson(command=null) if (json.info) { let i = json.info; parseInfo(i); + checkVersionUpgrade(i); // Check for version upgrade populatePalettes(i); if (isInfo) populateInfo(i); } @@ -3681,6 +3682,181 @@ function mergeDeep(target, ...sources) } return mergeDeep(target, ...sources); } +// Version reporting feature +var versionCheckDone = false; + +function checkVersionUpgrade(info) { + // Only check once per page load + if (versionCheckDone) return; + versionCheckDone = true; + + // Fetch version-info.json using existing /edit endpoint + fetch('/edit?edit=version-info.json', { + method: 'get' + }) + .then(res => { + if (res.status === 404) { + // File doesn't exist - first install, show install prompt + showVersionUpgradePrompt(info, null, info.ver); + return null; + } + if (!res.ok) { + throw new Error('Failed to fetch version-info.json'); + } + return res.json(); + }) + .then(versionInfo => { + if (!versionInfo) return; // 404 case already handled + + // Check if user opted out + if (versionInfo.neverAsk) return; + + // Check if version has changed + const currentVersion = info.ver; + const storedVersion = versionInfo.version || ''; + + if (storedVersion && storedVersion !== currentVersion) { + // Version has changed, show upgrade prompt + showVersionUpgradePrompt(info, storedVersion, currentVersion); + } else if (!storedVersion) { + // Empty version in file, show install prompt + showVersionUpgradePrompt(info, null, currentVersion); + } + }) + .catch(e => { + console.log('Failed to load version-info.json', e); + // On error, save current version for next time + if (info && info.ver) { + updateVersionInfo(info.ver, false); + } + }); +} + +function showVersionUpgradePrompt(info, oldVersion, newVersion) { + // Determine if this is an install or upgrade + const isInstall = !oldVersion; + + // Create overlay and dialog + const overlay = d.createElement('div'); + overlay.id = 'versionUpgradeOverlay'; + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;'; + + const dialog = d.createElement('div'); + dialog.style.cssText = 'background:var(--c-1);border-radius:10px;padding:25px;max-width:500px;margin:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3);'; + + // Build contextual message based on install vs upgrade + const title = isInstall + ? '🎉 Thank you for installing WLED!' + : '🎉 WLED Upgrade Detected!'; + + const description = isInstall + ? `You are now running WLED ${newVersion}.` + : `Your WLED has been upgraded from ${oldVersion} to ${newVersion}.`; + + const question = 'Would you like to help the WLED development team by reporting your installation? This helps us understand what hardware and versions are being used.' + + dialog.innerHTML = ` +

${title}

+

${description}

+

${question}

+
+ + + +
+ `; + + overlay.appendChild(dialog); + d.body.appendChild(overlay); + + // Add event listeners + gId('versionReportYes').addEventListener('click', () => { + reportUpgradeEvent(oldVersion, newVersion); + d.body.removeChild(overlay); + }); + + gId('versionReportNo').addEventListener('click', () => { + // Don't update version, will ask again on next load + d.body.removeChild(overlay); + }); + + gId('versionReportNever').addEventListener('click', () => { + updateVersionInfo(newVersion, true); + d.body.removeChild(overlay); + showToast('You will not be asked again.'); + }); +} + +function reportUpgradeEvent(oldVersion, newVersion) { + showToast('Reporting upgrade...'); + + // Fetch fresh data from /json/info endpoint as requested + fetch('/json/info', { + method: 'get' + }) + .then(res => res.json()) + .then(infoData => { + // Map to UpgradeEventRequest structure per OpenAPI spec + // Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256 + const upgradeData = { + deviceId: infoData.deviceId, + version: infoData.ver || '', // Current version string + previousVersion: oldVersion || '', // Previous version from version-info.json + releaseName: infoData.release, + chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc) + ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs + isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup + bootloaderSHA256: infoData.bootloaderSHA256 || '' // Bootloader SHA256 hash + }; + + // Make AJAX call to postUpgradeEvent API + return fetch('https://usage.wled.me/api/usage/upgrade', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(upgradeData) + }); + }) + .then(res => { + if (res.ok) { + showToast('Thank you for reporting!'); + updateVersionInfo(newVersion, false); + } else { + showToast('Report failed. Please try again later.', true); + // Do NOT update version info on failure - user will be prompted again + } + }) + .catch(e => { + console.log('Failed to report upgrade', e); + showToast('Report failed. Please try again later.', true); + // Do NOT update version info on error - user will be prompted again + }); +} + +function updateVersionInfo(version, neverAsk) { + const versionInfo = { + version: version, + neverAsk: neverAsk + }; + + // Create a Blob with JSON content and use /upload endpoint + const blob = new Blob([JSON.stringify(versionInfo)], { type: 'application/json' }); + const formData = new FormData(); + formData.append('data', blob, 'version-info.json'); + + fetch('/upload', { + method: 'POST', + body: formData + }) + .then(res => res.text()) + .then(data => { + console.log('Version info updated', data); + }) + .catch(e => { + console.log('Failed to update version-info.json', e); + }); +} size(); _C.style.setProperty('--n', N); From d2dab32f1023f2a9f2614d39b2214870e79200ef Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 28 Nov 2025 22:58:15 +0100 Subject: [PATCH 02/14] WLED-MM branding for update message box --- wled00/data/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 68b722092a..924e2260e3 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -3751,12 +3751,12 @@ function showVersionUpgradePrompt(info, oldVersion, newVersion) { // Build contextual message based on install vs upgrade const title = isInstall - ? '🎉 Thank you for installing WLED!' - : '🎉 WLED Upgrade Detected!'; + ? '🎉 Thank you for installing WLED-MM!' + : '🎉 WLED-MM Upgrade Detected!'; const description = isInstall - ? `You are now running WLED ${newVersion}.` - : `Your WLED has been upgraded from ${oldVersion} to ${newVersion}.`; + ? `You are now running WLED-MM ${newVersion}.` + : `Your WLED-MM has been upgraded from ${oldVersion} to ${newVersion}.`; const question = 'Would you like to help the WLED development team by reporting your installation? This helps us understand what hardware and versions are being used.' From 23864c70fd5bbf1b60773b4fd77eaecdadb1a0f7 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 28 Nov 2025 22:59:05 +0100 Subject: [PATCH 03/14] update dialog: align data with upstream --- wled00/data/index.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 924e2260e3..29aa4a99aa 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -3804,14 +3804,17 @@ function reportUpgradeEvent(oldVersion, newVersion) { // Map to UpgradeEventRequest structure per OpenAPI spec // Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256 const upgradeData = { - deviceId: infoData.deviceId, - version: infoData.ver || '', // Current version string - previousVersion: oldVersion || '', // Previous version from version-info.json - releaseName: infoData.release, - chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc) - ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs - isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup - bootloaderSHA256: infoData.bootloaderSHA256 || '' // Bootloader SHA256 hash + deviceId: infoData.deviceId, // Use anonymous unique device ID + version: infoData.ver || '', // Current version string + previousVersion: oldVersion || '', // Previous version from version-info.json + releaseName: infoData.release || '', // Release name (e.g., "WLED 0.15.0") + chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc) + ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs + isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup + bootloaderSHA256: infoData.bootloaderSHA256 || '', // Bootloader SHA256 hash - not yet availeable in WLEDMM + brand: infoData.brand, // Device brand (always present) + product: infoData.product, // Product name (always present) + flashSize: infoData.flash // Flash size (always present) }; // Make AJAX call to postUpgradeEvent API From a9670435cf2b2ab6a13a1b864162db435722470c Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 28 Nov 2025 22:59:54 +0100 Subject: [PATCH 04/14] update message: report total PSRAM, instead of unused PSRAM space --- wled00/data/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 29aa4a99aa..52938892db 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -3816,7 +3816,11 @@ function reportUpgradeEvent(oldVersion, newVersion) { product: infoData.product, // Product name (always present) flashSize: infoData.flash // Flash size (always present) }; - + // Add optional fields if available + if (infoData.tpram !== undefined) upgradeData.psramSize = Math.round(infoData.tpram / (1024 * 1024)); // convert bytes to MB - tpram is MM specific + // Note: partitionSizes not currently available in /json/info endpoint + // it is availeable in WLEDMM => infoData.t = total FS size in bytes + // Make AJAX call to postUpgradeEvent API return fetch('https://usage.wled.me/api/usage/upgrade', { method: 'POST', From 260f26dadbaf458b8c50e0a0a861c99fb8b06d71 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:08:43 -0500 Subject: [PATCH 05/14] Fix stale UI after firmware updates (#5120) Add WEB_BUILD_TIME to html_ui.h and use it for ETag generation Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com> Co-authored-by: Aircoookie <21045690+Aircoookie@users.noreply.github.com> --- tools/cdata.js | 13 ++++++- wled00/wled_server.cpp | 79 ++++++++++++++++++++++++------------------ 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/tools/cdata.js b/tools/cdata.js index d9664551ef..3cc6f8ecac 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -113,6 +113,11 @@ function filter(str, type) { } } +// Generate build timestamp as UNIX timestamp (seconds since epoch) +function generateBuildTime() { + return Math.floor(Date.now() / 1000); +} + function writeHtmlGzipped(sourceFile, resultFile, page) { console.info("Reading " + sourceFile); new inliner(sourceFile, function (error, html) { @@ -141,7 +146,13 @@ function writeHtmlGzipped(sourceFile, resultFile, page) { * Please see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui * to find out how to easily modify the web UI source! */ - + +// Automatically generated build time for cache busting (UNIX timestamp) +#ifdef WEB_BUILD_TIME // avoid duplicate defintions +#undef WEB_BUILD_TIME +#endif +#define WEB_BUILD_TIME ${generateBuildTime()} + // Autogenerated from ${sourceFile}, do not edit!! const uint16_t PAGE_${page}_L = ${result.length}; const uint8_t PAGE_${page}[] PROGMEM = { diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 0a0dd50fd0..361b5b5804 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -11,18 +11,56 @@ #endif #include "html_cpal.h" -/* - * Integrated HTTP web server page declarations - */ - -bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request); -void setStaticContentCacheHeaders(AsyncWebServerResponse *response); - // define flash strings once (saves flash memory) static const char s_redirecting[] PROGMEM = "Redirecting..."; static const char s_content_enc[] PROGMEM = "Content-Encoding"; static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security settings!"; static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!"; +static const char s_cache_control[] PROGMEM = "Cache-Control"; +static const char s_no_store[] PROGMEM = "no-store"; +static const char s_expires[] PROGMEM = "Expires"; + +/* + * Integrated HTTP web server page declarations + */ + +static void generateEtag(char *etag, uint16_t eTagSuffix) { + sprintf_P(etag, PSTR("%u-%02x-%04x"), WEB_BUILD_TIME, cacheInvalidate, eTagSuffix); +} + +static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code=200, uint16_t eTagSuffix = 0) { + // Only send ETag for 200 (OK) responses + if (code != 200) return; + + // https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c + #ifndef WLED_DEBUG + // this header name is misleading, "no-cache" will not disable cache, + // it just revalidates on every load using the "If-None-Match" header with the last ETag value + response->addHeader(FPSTR(s_cache_control), F("no-cache")); + #else + response->addHeader(FPSTR(s_cache_control), F("no-store,max-age=0")); // prevent caching if debug build + #endif + char etag[32] = {'\0'}; + generateEtag(etag, eTagSuffix); + response->addHeader(F("ETag"), etag); +} + +static bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest *request, int code=200, uint16_t eTagSuffix = 0) { + // Only send 304 (Not Modified) if response code is 200 (OK) + if (code != 200) return false; + + AsyncWebHeader *header = request->getHeader(F("If-None-Match")); + char etag[32] = {'\0'}; + generateEtag(etag, eTagSuffix); + if (header && header->value() == etag) { + AsyncWebServerResponse *response = request->beginResponse(304); + setStaticContentCacheHeaders(response, code, eTagSuffix); + request->send(response); + return true; + } + return false; +} + //Is this an IP? bool isIp(String str) { @@ -451,7 +489,7 @@ void initServer() AsyncWebServerResponse *response = request->beginResponse_P(404, "text/html", PAGE_404, PAGE_404_length); #endif response->addHeader(FPSTR(s_content_enc),"gzip"); - setStaticContentCacheHeaders(response); + setStaticContentCacheHeaders(response, 404); request->send(response); //request->send_P(404, "text/html", PAGE_404); }); @@ -467,31 +505,6 @@ void serveIndexOrWelcome(AsyncWebServerRequest *request) } } -bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request) -{ - AsyncWebHeader* header = request->getHeader("If-None-Match"); - if (header && header->value() == String(VERSION)) { - request->send(304); - return true; - } - return false; -} - -void setStaticContentCacheHeaders(AsyncWebServerResponse *response) -{ - char tmp[12]; - // https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c - #ifndef WLED_DEBUG - //this header name is misleading, "no-cache" will not disable cache, - //it just revalidates on every load using the "If-None-Match" header with the last ETag value - response->addHeader(F("Cache-Control"),"no-cache"); - #else - response->addHeader(F("Cache-Control"),"no-store,max-age=0"); // prevent caching if debug build - #endif - sprintf_P(tmp, PSTR("%8d-%02x"), VERSION, cacheInvalidate); - response->addHeader(F("ETag"), tmp); -} - void serveIndex(AsyncWebServerRequest* request) { if (handleFileRead(request, "/index.htm")) return; From 00e026ce081a630f41a5dbc7182902457585e8e6 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 29 Nov 2025 15:18:11 +0000 Subject: [PATCH 06/14] Merge pull request #5126 from wled/copilot/backport-version-reporting-0-15-x Backport version reporting (PR #5093 and #5111) to 0.15.x --- .gitignore | 1 + wled00/json.cpp | 6 +- wled00/ota_update.cpp | 323 ++++++++++++++++++++++++++++++++++++++++++ wled00/ota_update.h | 72 ++++++++++ wled00/util.cpp | 11 +- 5 files changed, 407 insertions(+), 6 deletions(-) create mode 100644 wled00/ota_update.cpp create mode 100644 wled00/ota_update.h diff --git a/.gitignore b/.gitignore index 4134164812..c3e06ea53b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ compile_commands.json /wled00/Release /wled00/wled00.ino.cpp /wled00/html_*.h +_codeql_detected_source_root diff --git a/wled00/json.cpp b/wled00/json.cpp index 599a0a4af8..03986fdf62 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1,4 +1,5 @@ #include "wled.h" +#include "ota_update.h" #include "palettes.h" @@ -938,7 +939,7 @@ void serializeInfo(JsonObject root) //root[F("cn")] = F(WLED_CODENAME); //WLEDMM removed root[F("release")] = FPSTR(releaseString); root[F("rel")] = FPSTR(releaseString); //WLEDMM to add bin name - + //root[F("repo")] = repoString; // WLEDMM not availeable root[F("deviceId")] = getDeviceId(); JsonObject leds = root.createNestedObject("leds"); @@ -1083,6 +1084,9 @@ void serializeInfo(JsonObject root) root[F("lwip")] = 0; //deprecated root[F("totalheap")] = ESP.getHeapSize(); //WLEDMM + #ifndef WLED_DISABLE_OTA + root[F("bootloaderSHA256")] = getBootloaderSHA256Hex(); + #endif #else root[F("arch")] = "esp8266"; root[F("core")] = ESP.getCoreVersion(); diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp new file mode 100644 index 0000000000..e0a3c3c85d --- /dev/null +++ b/wled00/ota_update.cpp @@ -0,0 +1,323 @@ +#include "ota_update.h" +#include "wled.h" + +#ifdef ESP32 +#include +#include +#include +#endif + +// Platform-specific metadata locations +#ifdef ESP32 +constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata +#define UPDATE_ERROR errorString + +// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB +// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064 +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) +constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6 +constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size +#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5) +constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5 +constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size +#else +constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2 +constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size +#endif + +#elif defined(ESP8266) +constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset +#define UPDATE_ERROR getErrorString +#endif +constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes + + +/** + * Check if OTA should be allowed based on release compatibility using custom description + * @param binaryData Pointer to binary file data (not modified) + * @param dataSize Size of binary data in bytes + * @param errorMessage Buffer to store error message if validation fails + * @param errorMessageLen Maximum length of error message buffer + * @return true if OTA should proceed, false if it should be blocked + */ + +static bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) { + // Clear error message + if (errorMessage && errorMessageLen > 0) { + errorMessage[0] = '\0'; + } + + // Try to extract WLED structure directly from binary data + wled_metadata_t extractedDesc; + bool hasDesc = findWledMetadata(binaryData, dataSize, &extractedDesc); + + if (hasDesc) { + return shouldAllowOTA(extractedDesc, errorMessage, errorMessageLen); + } else { + // No custom description - this could be a legacy binary + if (errorMessage && errorMessageLen > 0) { + strncpy_P(errorMessage, PSTR("This firmware file is missing compatibility metadata."), errorMessageLen - 1); + errorMessage[errorMessageLen - 1] = '\0'; + } + return false; + } +} + +struct UpdateContext { + // State flags + // FUTURE: the flags could be replaced by a state machine + bool replySent = false; + bool needsRestart = false; + bool updateStarted = false; + bool uploadComplete = false; + bool releaseCheckPassed = false; + String errorMessage; + + // Buffer to hold block data across posts, if needed + std::vector releaseMetadataBuffer; +}; + + +static void endOTA(AsyncWebServerRequest *request) { + UpdateContext* context = reinterpret_cast(request->_tempObject); + request->_tempObject = nullptr; + + DEBUG_PRINTF_P(PSTR("EndOTA %x --> %x (%d)\n"), (uintptr_t)request,(uintptr_t) context, context ? context->uploadComplete : 0); + if (context) { + if (context->updateStarted) { // We initialized the update + // We use Update.end() because not all forms of Update() support an abort. + // 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; + } + } + + if (context->needsRestart) { + strip.resume(); + UsermodManager::onUpdateBegin(false); + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().enableWatchdog(); + #endif + } + delete context; + } +}; + +static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context) +{ + #ifdef ESP8266 + Update.runAsync(true); + #endif + + if (Update.isRunning()) { + request->send(503); + setOTAReplied(request); + return false; + } + + #if WLED_WATCHDOG_TIMEOUT > 0 + WLED::instance().disableWatchdog(); + #endif + UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) + + 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); + + auto skipValidationParam = request->getParam("skipValidation", true); + if (skipValidationParam && (skipValidationParam->value() == "1")) { + context->releaseCheckPassed = true; + DEBUG_PRINTLN(F("OTA validation skipped by user")); + } + + // Begin update with the firmware size from content length + size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); + if (!Update.begin(updateSize)) { + context->errorMessage = Update.UPDATE_ERROR(); + DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), context->errorMessage.c_str()); + return false; + } + + context->updateStarted = true; + return true; +} + +// Create an OTA context object on an AsyncWebServerRequest +// Returns true if successful, false on failure. +bool initOTA(AsyncWebServerRequest *request) { + // Allocate update context + UpdateContext* context = new (std::nothrow) UpdateContext {}; + if (context) { + request->_tempObject = context; + request->onDisconnect([=]() { endOTA(request); }); // ensures we restart on failure + }; + + DEBUG_PRINTF_P(PSTR("OTA Update init, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context); + return (context != nullptr); +} + +void setOTAReplied(AsyncWebServerRequest *request) { + UpdateContext* context = reinterpret_cast(request->_tempObject); + if (!context) return; + context->replySent = true; +}; + +// Returns pointer to error message, or nullptr if OTA was successful. +std::pair getOTAResult(AsyncWebServerRequest* request) { + UpdateContext* context = reinterpret_cast(request->_tempObject); + if (!context) return { true, F("OTA context unexpectedly missing") }; + if (context->replySent) return { false, {} }; + if (context->errorMessage.length()) return { true, context->errorMessage }; + + if (context->updateStarted) { + // Release the OTA context now. + endOTA(request); + if (Update.hasError()) { + return { true, Update.UPDATE_ERROR() }; + } else { + return { true, {} }; + } + } + + // Should never happen + return { true, F("Internal software failure") }; +} + + + +void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) +{ + UpdateContext* context = reinterpret_cast(request->_tempObject); + if (!context) return; + + //DEBUG_PRINTF_P(PSTR("HandleOTAData: %d %d %d\n"), index, len, isFinal); + + if (context->replySent || (context->errorMessage.length())) return; + + if (index == 0) { + if (!beginOTA(request, context)) return; + } + + // Perform validation if we haven't done it yet and we have reached the metadata offset + if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) { + // Current chunk contains the metadata offset + size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET; + + DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset); + + if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) { + // We have enough data to validate, one way or another + const uint8_t* search_data = data; + size_t search_len = len; + + // If we have saved data, use that instead + if (context->releaseMetadataBuffer.size()) { + // Add this data + context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len); + search_data = context->releaseMetadataBuffer.data(); + search_len = context->releaseMetadataBuffer.size(); + } + + // Do the checking + char errorMessage[128]; + bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage)); + + // Release buffer if there was one + context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){}; + + if (!OTA_ok) { + DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage); + context->errorMessage = errorMessage; + context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway."); + return; + } else { + DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed")); + context->releaseCheckPassed = true; + } + } else { + // Store the data we just got for next pass + context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len); + } + } + + // Check if validation was still pending (shouldn't happen normally) + // This is done before writing the last chunk, so endOTA can abort + if (isFinal && !context->releaseCheckPassed) { + DEBUG_PRINTLN(F("OTA failed: Validation never completed")); + // Don't write the last chunk to the updater: this will trip an error later + context->errorMessage = F("Release check data never arrived?"); + return; + } + + // Write chunk data to OTA update (only if release check passed or still pending) + if (!Update.hasError()) { + if (Update.write(data, len) != len) { + DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR()); + } + } + + if(isFinal) { + DEBUG_PRINTLN(F("OTA Update End")); + // Upload complete + context->uploadComplete = true; + } +} + +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) +static String bootloaderSHA256HexCache = ""; + +// Calculate and cache the bootloader SHA256 digest as hex string +void calculateBootloaderSHA256() { + if (!bootloaderSHA256HexCache.isEmpty()) return; + + // 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 < BOOTLOADER_SIZE; offset += chunkSize) { + size_t readSize = min((size_t)(BOOTLOADER_SIZE - offset), chunkSize); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { // use esp_flash_read for V4 framework (-S2, -S3, -C3) +#else + if (spi_flash_read(BOOTLOADER_OFFSET + offset, buffer, readSize) == ESP_OK) { // use spi_flash_read for old V3 framework (legacy esp32) +#endif + 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 = hex; +} + +// Get bootloader SHA256 as hex string +String getBootloaderSHA256Hex() { + calculateBootloaderSHA256(); + return bootloaderSHA256HexCache; +} + +// Invalidate cached bootloader SHA256 (call after bootloader update) +void invalidateBootloaderSHA256Cache() { + bootloaderSHA256HexCache = ""; +} +#endif \ No newline at end of file diff --git a/wled00/ota_update.h b/wled00/ota_update.h new file mode 100644 index 0000000000..6513e9750c --- /dev/null +++ b/wled00/ota_update.h @@ -0,0 +1,72 @@ +// WLED OTA update interface + +#include +#ifdef ESP8266 + #include +#else + #include +#endif + +#pragma once + +// Platform-specific metadata locations +#ifdef ESP32 +#define BUILD_METADATA_SECTION ".rodata_custom_desc" +#elif defined(ESP8266) +#define BUILD_METADATA_SECTION ".ver_number" +#endif + + +class AsyncWebServerRequest; + +/** + * Create an OTA context object on an AsyncWebServerRequest + * @param request Pointer to web request object + * @return true if allocation was successful, false if not + */ +bool initOTA(AsyncWebServerRequest *request); + +/** + * Indicate to the OTA subsystem that a reply has already been generated + * @param request Pointer to web request object + */ +void setOTAReplied(AsyncWebServerRequest *request); + +/** + * Retrieve the 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 getOTAResult(AsyncWebServerRequest *request); + +/** + * Process a block of OTA data. This is a passthrough of an ArUploadHandlerFunction. + * Requires that initOTA 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 + * @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(); +#endif diff --git a/wled00/util.cpp b/wled00/util.cpp index f489be7394..e92c4a3ffc 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -7,6 +7,7 @@ #else #include "mbedtls/sha1.h" // for SHA1 on ESP32 #include "esp_efuse.h" +#include "esp_adc_cal.h" #endif //helper to get int value at a position in string @@ -703,18 +704,17 @@ String computeSHA1(const String& input) { } #ifdef ESP32 -#include "esp_adc_cal.h" String generateDeviceFingerprint() { uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint esp_chip_info_t chip_info; esp_chip_info(&chip_info); esp_efuse_mac_get_default((uint8_t*)fp); fp[1] ^= ESP.getFlashChipSize(); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 3) + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 4) fp[0] ^= chip_info.full_revision | (chip_info.model << 16); -#else + #else fp[0] ^= chip_info.revision | (chip_info.model << 16); -#endif + #endif // mix in ADC calibration data: esp_adc_cal_characteristics_t ch; #if SOC_ADC_MAX_BITWIDTH == 13 // S2 has 13 bit ADC @@ -739,6 +739,7 @@ String generateDeviceFingerprint() { sprintf(fp_string, "%08X%08X", fp[1], fp[0]); return String(fp_string); } + #else // ESP8266 String generateDeviceFingerprint() { uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint @@ -771,4 +772,4 @@ String getDeviceId() { cachedDeviceId = firstHash + secondHash.substring(38); return cachedDeviceId; -} \ No newline at end of file +} From 23ce580a28e40ad2f4ec1bce8806138b492e0133 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 29 Nov 2025 21:26:24 +0100 Subject: [PATCH 07/14] post-merge * reduce memory footprint by removing all unneeded functions in ota_update.cpp * don't compile ota_update.cpp when WLED_DISABLE_OTA is defined --- wled00/ota_update.cpp | 17 ++++++++++++----- wled00/ota_update.h | 8 ++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/wled00/ota_update.cpp b/wled00/ota_update.cpp index e0a3c3c85d..f108c06dac 100644 --- a/wled00/ota_update.cpp +++ b/wled00/ota_update.cpp @@ -1,3 +1,5 @@ +#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) // WLEDMM we only want getBootloaderSHA256Hex() + #include "ota_update.h" #include "wled.h" @@ -31,6 +33,9 @@ constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4 #endif constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes +#endif + +#if 0 // WLEDMM not needed - we only want getBootloaderSHA256Hex(); /** * Check if OTA should be allowed based on release compatibility using custom description @@ -75,7 +80,7 @@ struct UpdateContext { // Buffer to hold block data across posts, if needed std::vector releaseMetadataBuffer; -}; +} static void endOTA(AsyncWebServerRequest *request) { @@ -106,7 +111,7 @@ static void endOTA(AsyncWebServerRequest *request) { } delete context; } -}; +} static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context) { @@ -271,11 +276,13 @@ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, } } +#endif + #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) static String bootloaderSHA256HexCache = ""; // Calculate and cache the bootloader SHA256 digest as hex string -void calculateBootloaderSHA256() { +static void calculateBootloaderSHA256() { if (!bootloaderSHA256HexCache.isEmpty()) return; // Calculate SHA256 @@ -317,7 +324,7 @@ String getBootloaderSHA256Hex() { } // Invalidate cached bootloader SHA256 (call after bootloader update) -void invalidateBootloaderSHA256Cache() { +static void invalidateBootloaderSHA256Cache() { bootloaderSHA256HexCache = ""; } -#endif \ No newline at end of file +#endif diff --git a/wled00/ota_update.h b/wled00/ota_update.h index 6513e9750c..1181e43f3b 100644 --- a/wled00/ota_update.h +++ b/wled00/ota_update.h @@ -9,6 +9,8 @@ #pragma once +#if 0 // WLEDMM not needed - we only want getBootloaderSHA256Hex(); + // Platform-specific metadata locations #ifdef ESP32 #define BUILD_METADATA_SECTION ".rodata_custom_desc" @@ -51,12 +53,14 @@ std::pair getOTAResult(AsyncWebServerRequest *request); */ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal); +#endif + #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(); +static void calculateBootloaderSHA256(); /** * Get bootloader SHA256 as hex string @@ -68,5 +72,5 @@ String getBootloaderSHA256Hex(); * Invalidate cached bootloader SHA256 (call after bootloader update) * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex */ -void invalidateBootloaderSHA256Cache(); +static void invalidateBootloaderSHA256Cache(); #endif From aff7bbfefebb4aed683146c08d48ab668eb78c84 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 29 Nov 2025 21:27:09 +0100 Subject: [PATCH 08/14] tpram => tpsram small update for code readability --- wled00/data/index.js | 8 ++++---- wled00/data/simple.js | 4 ++-- wled00/json.cpp | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 52938892db..7283e65e59 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -717,11 +717,11 @@ ${inforow("Uptime",getRuntimeStr(i.uptime))} ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB, " +Math.round(i.fs.u*100/i.fs.t) + "%")} ${theap>0?inforow("Heap ☾",((i.totalheap-i.freeheap)/1000).toFixed(0)+"/"+theap.toFixed(0)+" kB",", "+Math.round((i.totalheap-i.freeheap)/(10*theap))+"%"):inforow("Free heap",heap," kB")} ${i.minfreeheap?inforow("Max used heap ☾",((i.totalheap-i.minfreeheap)/1000).toFixed(0)+" kB",", "+Math.round((i.totalheap-i.minfreeheap)/(10*theap))+"%"):""} -${i.psram?inforow("PSRAM ☾",((i.tpram-i.psram)/1024).toFixed(0)+"/"+(i.tpram/1024).toFixed(0)+" kB",", "+((i.tpram-i.psram)*100.0/i.tpram).toFixed(1)+"%"):""} -${i.psusedram?inforow("Max used PSRAM ☾",((i.tpram-i.psusedram)/1024).toFixed(0)+" kB",", "+((i.tpram-i.psusedram)*100.0/i.tpram).toFixed(1)+"%"):""} +${i.psram?inforow("PSRAM ☾",((i.tpsram-i.psram)/1024).toFixed(0)+"/"+(i.tpsram/1024).toFixed(0)+" kB",", "+((i.tpsram-i.psram)*100.0/i.tpsram).toFixed(1)+"%"):""} +${i.psusedram?inforow("Max used PSRAM ☾",((i.tpsram-i.psusedram)/1024).toFixed(0)+" kB",", "+((i.tpsram-i.psusedram)*100.0/i.tpsram).toFixed(1)+"%"):""} ${i.freestack?inforow("Free stack ☾",(i.freestack/1000).toFixed(3)," kB"):""}
-${i.tpram?inforow("PSRAM " + (i.psrmode?"("+i.psrmode+" mode) ":"") + " ☾",(i.tpram/1024/1024).toFixed(0)," MB"):inforow("NO PSRAM found.", "")} +${i.tpsram?inforow("PSRAM " + (i.psrmode?"("+i.psrmode+" mode) ":"") + " ☾",(i.tpsram/1024/1024).toFixed(0)," MB"):inforow("NO PSRAM found.", "")} ${i.e32flash?inforow("Flash mode "+i.e32flashmode+i.e32flashtext + " ☾",i.e32flash+" MB, "+i.e32flashspeed," Mhz"):""} ${i.e32model?inforow(i.e32model + " ☾",i.e32cores +" core(s),"," "+i.e32speed+" Mhz"):""} ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} @@ -3817,7 +3817,7 @@ function reportUpgradeEvent(oldVersion, newVersion) { flashSize: infoData.flash // Flash size (always present) }; // Add optional fields if available - if (infoData.tpram !== undefined) upgradeData.psramSize = Math.round(infoData.tpram / (1024 * 1024)); // convert bytes to MB - tpram is MM specific + if (infoData.tpsram !== undefined) upgradeData.psramSize = Math.round(infoData.tpsram / (1024 * 1024)); // convert bytes to MB - tpsram is MM specific // Note: partitionSizes not currently available in /json/info endpoint // it is availeable in WLEDMM => infoData.t = total FS size in bytes diff --git a/wled00/data/simple.js b/wled00/data/simple.js index 1ab2a3110f..50d06ae1ca 100644 --- a/wled00/data/simple.js +++ b/wled00/data/simple.js @@ -528,8 +528,8 @@ ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i. ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} ${theap>0?inforow("Total heap",theap," kB"):""} ${i.minfreeheap?inforow("Max used heap",((i.totalheap-i.minfreeheap)/1000).toFixed(1)," kB"):""} -${i.tpram?inforow("Total PSRAM",(i.tpram/1024).toFixed(1)," kB"):""} -${i.psusedram?((i.tpram-i.psusedram)>16383?inforow("Max Used PSRAM",((i.tpram-i.psusedram)/1024).toFixed(1)," kB"):inforow("Max Used PSRAM",(i.tpram-i.psusedram)," B")):""} +${i.tpsram?inforow("Total PSRAM",(i.tpsram/1024).toFixed(1)," kB"):""} +${i.psusedram?((i.tpsram-i.psusedram)>16383?inforow("Max Used PSRAM",((i.tpsram-i.psusedram)/1024).toFixed(1)," kB"):inforow("Max Used PSRAM",(i.tpsram-i.psusedram)," B")):""} ${i.e32model?inforow(i.e32model,i.e32cores +" core(s)"," "+i.e32speed+" Mhz"):""} ${i.e32flash?inforow("Flash "+i.e32flash+" MB"+", mode "+i.e32flashmode+i.e32flashtext,i.e32flashspeed," Mhz"):""} diff --git a/wled00/json.cpp b/wled00/json.cpp index 03986fdf62..511a74e1b8 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1108,7 +1108,7 @@ void serializeInfo(JsonObject root) #endif #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (psramFound()) { - root[F("tpram")] = ESP.getPsramSize(); //WLEDMM + root[F("tpsram")] = ESP.getPsramSize(); //WLEDMM root[F("psram")] = ESP.getFreePsram(); root[F("psusedram")] = ESP.getMinFreePsram(); #if CONFIG_ESP32S3_SPIRAM_SUPPORT // WLEDMM -S3 has "qspi" or "opi" PSRAM mode @@ -1121,7 +1121,7 @@ void serializeInfo(JsonObject root) } #else // for testing - // root[F("tpram")] = 4194304; //WLEDMM + // root[F("tpsram")] = 4194304; //WLEDMM // root[F("psram")] = 4193000; // root[F("psusedram")] = 3083000; #endif From de9c6aed48c16e885694c5c501fb11ce3771b523 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 29 Nov 2025 21:27:58 +0100 Subject: [PATCH 09/14] remove anumartix and artifx from V3 legacy builds flash size limits exceeded --- platformio.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index cd2e7e41e1..412bbc0c20 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1252,8 +1252,11 @@ monitor_filters = esp32_exception_decoder extends = esp32_4MB_S_base build_flags = ${common.build_flags} ${esp32_legacy.build_flags} ${common_mm.build_flags_S} ${common_mm.build_flags_M} ;; we don't want common_mm.build_disable_sync_interfaces, so we cannot inherit from esp32_4MB_S_base build_unflags = ${esp32_4MB_S_base.build_unflags} + {common_mm.animartrix_build_flags} ;; exceeds flash limits in V3 builds + -DUSERMOD_ARTIFX ;; exceeds flash limits in V3 builds lib_deps = ${esp32_4MB_S_base.lib_deps} ${common_mm.lib_deps_M} - +lib_ignore = ${esp32_4MB_S_base.lib_ignore} + {common_mm.animartrix_lib_ignore} [esp32_4MB_XL_base] extends = esp32_4MB_M_base build_flags = ${esp32_4MB_M_base.build_flags} ${common_mm.build_flags_XL} From 0bcece32a3e0cb27b6ca7acc0f6fe430207f3766 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 29 Nov 2025 21:41:03 +0100 Subject: [PATCH 10/14] fix wrong syntax in platformio.ini --- platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 412bbc0c20..b1b3e5e654 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1252,11 +1252,11 @@ monitor_filters = esp32_exception_decoder extends = esp32_4MB_S_base build_flags = ${common.build_flags} ${esp32_legacy.build_flags} ${common_mm.build_flags_S} ${common_mm.build_flags_M} ;; we don't want common_mm.build_disable_sync_interfaces, so we cannot inherit from esp32_4MB_S_base build_unflags = ${esp32_4MB_S_base.build_unflags} - {common_mm.animartrix_build_flags} ;; exceeds flash limits in V3 builds + ${common_mm.animartrix_build_flags} ;; exceeds flash limits in V3 builds -DUSERMOD_ARTIFX ;; exceeds flash limits in V3 builds lib_deps = ${esp32_4MB_S_base.lib_deps} ${common_mm.lib_deps_M} lib_ignore = ${esp32_4MB_S_base.lib_ignore} - {common_mm.animartrix_lib_ignore} + ${common_mm.animartrix_lib_ignore} [esp32_4MB_XL_base] extends = esp32_4MB_M_base build_flags = ${esp32_4MB_M_base.build_flags} ${common_mm.build_flags_XL} From 49bd6300b86c51491fed96627f2b3d2b32456531 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 29 Nov 2025 16:21:42 +0000 Subject: [PATCH 11/14] fix loading version-info to use older edit api --- wled00/data/index.js | 70 +++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 7283e65e59..d61f25e166 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -3696,45 +3696,41 @@ function checkVersionUpgrade(info) { versionCheckDone = true; // Fetch version-info.json using existing /edit endpoint - fetch('/edit?edit=version-info.json', { + fetch('/edit?edit=/version-info.json', { method: 'get' }) - .then(res => { - if (res.status === 404) { - // File doesn't exist - first install, show install prompt - showVersionUpgradePrompt(info, null, info.ver); - return null; - } - if (!res.ok) { - throw new Error('Failed to fetch version-info.json'); - } - return res.json(); - }) - .then(versionInfo => { - if (!versionInfo) return; // 404 case already handled - - // Check if user opted out - if (versionInfo.neverAsk) return; - - // Check if version has changed - const currentVersion = info.ver; - const storedVersion = versionInfo.version || ''; - - if (storedVersion && storedVersion !== currentVersion) { - // Version has changed, show upgrade prompt - showVersionUpgradePrompt(info, storedVersion, currentVersion); - } else if (!storedVersion) { - // Empty version in file, show install prompt - showVersionUpgradePrompt(info, null, currentVersion); - } - }) - .catch(e => { - console.log('Failed to load version-info.json', e); - // On error, save current version for next time - if (info && info.ver) { - updateVersionInfo(info.ver, false); - } - }); + .then(res => { + if (res.status === 404) { + // File doesn't exist - first install, show install prompt + showVersionUpgradePrompt(info, null, info.ver); + return null; + } + if (!res.ok) { + throw new Error('Failed to fetch version-info.json'); + } + return res.json(); + }) + .then(versionInfo => { + if (!versionInfo) return; // 404 case already handled + + // Check if user opted out + if (versionInfo.neverAsk) return; + + // Check if version has changed + const currentVersion = info.ver; + const storedVersion = versionInfo.version || ''; + + if (storedVersion && storedVersion !== currentVersion) { + // Version has changed, show upgrade prompt + showVersionUpgradePrompt(info, storedVersion, currentVersion); + } else if (!storedVersion) { + // Empty version in file, show install prompt + showVersionUpgradePrompt(info, null, currentVersion); + } + }) + .catch(e => { + console.log('Failed to load version-info.json', e); + }); } function showVersionUpgradePrompt(info, oldVersion, newVersion) { From aa8148e02a3e19eebcaad5f04ffabf89846ff3c2 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 29 Nov 2025 22:12:56 +0100 Subject: [PATCH 12/14] fix for athom music build --- platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio.ini b/platformio.ini index b1b3e5e654..c74222d0da 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2869,6 +2869,7 @@ board_build.partitions = ${esp32.extreme_partitions} ;; WLED extended for 16MB f [env:athom_music_esp32_4MB_M] extends = esp32_4MB_M_base build_unflags = ${esp32_legacy.build_unflags} + ${common_mm.animartrix_build_flags} ;; exceeds flash limits in V3 builds -D USERMOD_ARTIFX ;; disabled to save some program space in flash -D USERMOD_DALLASTEMPERATURE ;; disabled - flash space is too tight for this -D USERMOD_ROTARY_ENCODER_UI ;; see above From 9a4e7a60499f4d7d1b6b0f0b3fef2956763a0a55 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sat, 29 Nov 2025 23:16:43 +0100 Subject: [PATCH 13/14] bugfix for reboot loop affecting V3 "legacy" builds * ``-DCONFIG_LITTLEFS_FOR_IDF_3_2 -DLFS_THREADSAFE`` caused a crash when mounting the filesystem * added "extends = " to MM legacy buildenvs * make sure that flash_mode is always well-defined --- platformio.ini | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index c74222d0da..652f5c38a5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -333,6 +333,7 @@ platform = espressif32@3.5.0 platform_packages = framework-arduinoespressif32 @ https://github.com/Aircoookie/arduino-esp32.git#1.0.6.4 build_unflags = ${common.build_unflags} -Wshadow=compatible-local ;; not supported by older compilers + ;; -D CORE_DEBUG_LEVEL=0 -DNDEBUG ;; enable core debugging build_flags = -g -Wno-unused-variable -Wno-unused-function ;; removes noise -DARDUINO_ARCH_ESP32 @@ -340,7 +341,8 @@ build_flags = -g -D CONFIG_ASYNC_TCP_TASK_STACK_SIZE=9472 ;; WLEDMM increase stack by 1.25Kb, as audioreactive needs bigger SETTINGS_STACK_BUF_SIZE -D CONFIG_ASYNC_TCP_STACK_SIZE=9472 -D LOROL_LITTLEFS ;; use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x - -DCONFIG_LITTLEFS_FOR_IDF_3_2 -DLFS_THREADSAFE + ;; -D CORE_DEBUG_LEVEL=5 ;; enable core debug messages + ;; -DDEBUG -DWLED_DEBUG ;; enable WLED debug messages lib_deps = esp32async/AsyncTCP @ 3.4.7 ; https://github.com/lorol/LITTLEFS.git @@ -348,11 +350,12 @@ lib_deps = makuna/NeoPixelBus @ 2.7.5 ;; makuna/NeoPixelBus @ 2.7.9 ;; experimental ${env.lib_deps} +monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs default_partitions = ${esp32.default_partitions} ;; backwards compatibility board_build.f_flash = 80000000L board_build.flash_mode = dout ;; avoid dio/quot/qio - these are broken in arduino-esp32 1.0.6.x - +;;board_build.flash_mode = dio ;; standard platform for esp32 [esp32] @@ -376,6 +379,8 @@ monitor_filters = esp32_exception_decoder AR_build_flags = ${common_mm.AR_build_flags} AR_lib_deps = ${common_mm.AR_lib_deps} ;; optimized version, 10% faster on -S2/-C3 board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs +board_build.f_flash = 80000000L +board_build.flash_mode = dio ;; WLEDMM begin @@ -449,6 +454,9 @@ lib_deps = ;; makuna/NeoPixelBus @ 2.7.9 ;; experimental ${env.lib_deps} board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs +monitor_filters = esp32_exception_decoder +board_build.f_flash = 80000000L +board_build.flash_mode = dio [esp32s2] ;; generic definitions for all ESP32-S2 boards @@ -637,6 +645,7 @@ extends = env:esp32_4MB_PSRAM_S ;; legacy build for OTA compatibility with upstream 0.15.x, slow but safe [env:esp32dev_compat] +extends = esp32_legacy board = esp32dev platform = ${esp32_legacy.platform} platform_packages = ${esp32_legacy.platform_packages} @@ -652,6 +661,7 @@ monitor_filters = esp32_exception_decoder ;; legacy build for OTA compatibility with upstream 0.15.x, faster due to qio mode [env:esp32dev_qio80_compat] +extends = esp32_legacy board = esp32dev platform = ${esp32_legacy.platform} platform_packages = ${esp32_legacy.platform_packages} @@ -686,6 +696,7 @@ extends = env:esp32_4MB_M_eth ;; legacy build for OTA compatibility with upstream 0.15.x ;; --> use [env:esp32_4MB_M_eth] (4MB) or [env:esp32_16MB_M_eth] [env:esp32_eth_compat] +extends = esp32_legacy board = esp32-poe platform = ${esp32_legacy.platform} platform_packages = ${esp32_legacy.platform_packages} @@ -1227,6 +1238,7 @@ lib_deps_XL = ; common defaults for all MM environments [esp32_4MB_S_base] +extends = esp32_legacy board = esp32dev ;; legacy V3 platform platform = ${esp32_legacy.platform} From 47a1b62fce314d5933e828fc5e5087d723afe91f Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 30 Nov 2025 14:10:33 +0100 Subject: [PATCH 14/14] tiny optimization small flash optimization by re-using string constants (string merging) --- wled00/wled_server.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 912428cf80..5e6eb4b2f1 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -17,8 +17,8 @@ static const char s_content_enc[] PROGMEM = "Content-Encoding"; static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security settings!"; static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!"; static const char s_cache_control[] PROGMEM = "Cache-Control"; -static const char s_no_store[] PROGMEM = "no-store"; -static const char s_expires[] PROGMEM = "Expires"; +//static const char s_no_store[] PROGMEM = "no-store"; +//static const char s_expires[] PROGMEM = "Expires"; /* * Integrated HTTP web server page declarations @@ -619,7 +619,7 @@ void serveSettingsJS(AsyncWebServerRequest* request) AsyncWebServerResponse *response; response = request->beginResponse(200, "application/javascript", buf); - response->addHeader(F("Cache-Control"),"no-store"); + response->addHeader(FPSTR(s_cache_control),F("no-store")); response->addHeader(F("Expires"),"0"); request->send(response); }