Compare commits

..

12 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
f706c6cbed Rename ignoreRelease to skipValidation in OTA validation system
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 16:16:42 +00:00
copilot-swe-agent[bot]
18dfc7081c Address review feedback: unify structures, remove code duplication, improve efficiency
Co-authored-by: willmmiles <6540455+willmmiles@users.noreply.github.com>
2025-09-14 15:05:15 +00:00
copilot-swe-agent[bot]
5c0c84eb6b Add ESP8266 support to OTA release compatibility system using .ver_number section
Co-authored-by: willmmiles <6540455+willmmiles@users.noreply.github.com>
2025-09-14 14:34:31 +00:00
copilot-swe-agent[bot]
691c058ae8 Fix runtime release name replacement - move to build-time
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 14:14:11 +00:00
copilot-swe-agent[bot]
7d550baf94 Replace metadata header approach with ESP-IDF custom description section
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 13:51:48 +00:00
copilot-swe-agent[bot]
42ff73ffe7 Implement metadata-based OTA release checking system
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 13:11:24 +00:00
copilot-swe-agent[bot]
2d8edfcb24 Remove build artifacts from repository
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:56:12 +00:00
copilot-swe-agent[bot]
fb077ecadc Improve OTA release checking with scoring-based candidate selection
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:55:18 +00:00
copilot-swe-agent[bot]
caf3d900fd Fix OTA release checking to use flexible string search instead of hardcoded patterns
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:50:47 +00:00
copilot-swe-agent[bot]
e920d2e101 Complete OTA release compatibility system with comprehensive testing
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:32:52 +00:00
copilot-swe-agent[bot]
54746c9730 Implement OTA release compatibility checking system
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-14 12:28:47 +00:00
copilot-swe-agent[bot]
8225a2a07c Initial plan 2025-09-14 12:13:03 +00:00
11 changed files with 315 additions and 72 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,2 @@
github: [Aircoookie,blazoncek,DedeHai,lost-hope,willmmiles]
github: [Aircoookie,blazoncek]
custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek']
thanks_dev: u/gh/netmindz

View File

@@ -1,5 +1,6 @@
Import("env")
import shutil
import os
node_ex = shutil.which("node")
# Check if Node.js is installed and present in PATH if it failed, abort the build
@@ -12,6 +13,21 @@ else:
print('\x1b[6;33;42m' + 'Installing node packages' + '\x1b[0m')
env.Execute("npm ci")
# Extract the release name from build flags
release_name = "Custom"
build_flags = env.get("BUILD_FLAGS", [])
for flag in build_flags:
if 'WLED_RELEASE_NAME=' in flag:
# Extract the release name, remove quotes and handle different formats
parts = flag.split('WLED_RELEASE_NAME=')
if len(parts) > 1:
release_name = parts[1].split()[0].strip('\"\\')
break
# Set environment variable for cdata.js to use
os.environ['WLED_RELEASE_NAME'] = release_name
print(f'Building web UI with release name: {release_name}')
# Call the bundling script
exitCode = env.Execute("npm run build")

View File

@@ -10,12 +10,10 @@
</p>
# Welcome to WLED! ✨
# Welcome to my project WLED! ✨
A fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102!
Originally created by [Aircoookie](https://github.com/Aircoookie)
## ⚙️ Features
- WS2812FX library with more than 100 special effects
- FastLED noise effects and 50 palettes
@@ -34,7 +32,7 @@ Originally created by [Aircoookie](https://github.com/Aircoookie)
- Filesystem-based config for easier backup of presets and settings
## 💡 Supported light control interfaces
- WLED app for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239)
- WLED app for [Android](https://play.google.com/store/apps/details?id=com.aircoookie.WLED) and [iOS](https://apps.apple.com/us/app/wled/id1475695033)
- JSON and HTTP request APIs
- MQTT
- E1.31, Art-Net, DDP and TPM2.net

View File

@@ -95,6 +95,11 @@ function adoptVersionAndRepo(html) {
if (version) {
html = html.replaceAll("##VERSION##", version);
}
// Replace ##RELEASE## with the actual release name from build environment
const releaseName = process.env.WLED_RELEASE_NAME || 'Custom';
html = html.replaceAll("##RELEASE##", releaseName);
return html;
}

View File

@@ -13,10 +13,6 @@
#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h?
#include "palettes.h"
#ifndef DEFAULT_LED_COLOR_ORDER
#define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB
#endif
/*
Custom per-LED mapping has moved!
@@ -1177,22 +1173,12 @@ void WS2812FX::finalizeInit() {
// create buses/outputs
unsigned mem = 0;
for (auto bus : busConfigs) {
// Calculate what this bus would use with its current configuration
unsigned busMemUsage = bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount : 0);
// If memory exceeds limit, set count to minimum of current count and default length
if (mem + busMemUsage > MAX_LED_MEMORY) {
bus.count = min(bus.count, DEFAULT_LED_COUNT);
DEBUG_PRINTF_P(PSTR("Bus %d memory usage exceeds limit, setting count to %d\n"), (int)bus.type, bus.count);
}
if (BusManager::add(bus) != -1) {
mem += bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount : 0);
if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) digitalCount++;
} else break;
for (const auto &bus : busConfigs) {
mem += bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // includes global buffer
if (mem <= MAX_LED_MEMORY) {
if (BusManager::add(bus) == -1) break;
} else DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)bus.type, (int)bus.count, digitalCount);
}
busConfigs.clear();
busConfigs.shrink_to_fit();

View File

@@ -668,20 +668,9 @@ function parseInfo(i) {
if (loc) name = "(L) " + name;
d.title = name;
simplifiedUI = i.simplifiedui;
// Add safety checks for LED count data to prevent UI crashes
if (i.leds && typeof i.leds.count !== 'undefined') {
ledCount = i.leds.count;
} else {
console.warn('LED count data missing, using fallback value');
ledCount = 30; // Fallback value matching firmware default
}
ledCount = i.leds.count;
//syncTglRecv = i.str;
if (i.leds && typeof i.leds.maxseg !== 'undefined') {
maxSeg = i.leds.maxseg;
} else {
console.warn('Max segment data missing, using fallback value');
maxSeg = 16; // Reasonable fallback for max segments
}
maxSeg = i.leds.maxseg;
pmt = i.fs.pmt;
if (pcMode && !i.wifi.ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide");
gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none";
@@ -923,24 +912,12 @@ function populateSegments(s)
gId(`segr${i}`).classList.add("hide");
}
if (segCount < 2) {
// Add safety check for segment elements to prevent UI crashes
const segdElement = gId(`segd${lSeg}`);
if (segdElement) segdElement.classList.add("hide"); // hide delete if only one segment
const seg0briElement = gId("seg0bri");
const segp0Element = gId(`segp0`);
if (seg0briElement && segp0Element && parseInt(seg0briElement.value)==255) segp0Element.classList.add("hide");
gId(`segd${lSeg}`).classList.add("hide"); // hide delete if only one segment
if (parseInt(gId("seg0bri").value)==255) gId(`segp0`).classList.add("hide");
// hide segment controls if there is only one segment in simplified UI
if (simplifiedUI) gId("segcont").classList.add("hide");
}
// Add safety checks for segment control elements
const segSElement = gId(`seg${lSeg}s`);
const segEElement = gId(`seg${lSeg}e`);
const segrElement = gId(`segr${lSeg}`);
if (!isM && !noNewSegs && segSElement && segEElement && segrElement) {
const segLen = cfg.comp.seglen ? parseInt(segSElement.value) : 0;
const segEnd = parseInt(segEElement.value);
if (segLen + segEnd < ledCount) segrElement.classList.remove("hide");
}
if (!isM && !noNewSegs && (cfg.comp.seglen?parseInt(gId(`seg${lSeg}s`).value):0)+parseInt(gId(`seg${lSeg}e`).value)<ledCount) gId(`segr${lSeg}`).classList.remove("hide");
gId('segutil2').style.display = (segCount > 1) ? "block":"none"; // rsbtn parent
if (Array.isArray(li.maps) && li.maps.length>1) {
@@ -2276,9 +2253,7 @@ function rptSeg(s)
var rev = gId(`seg${s}rev`).checked;
var mi = gId(`seg${s}mi`).checked;
var sel = gId(`seg${s}sel`).checked;
// Add safety check for segment power element to prevent UI crashes
const segPwrElement = gId(`seg${s}pwr`);
var pwr = segPwrElement ? segPwrElement.classList.contains('act') : false;
var pwr = gId(`seg${s}pwr`).classList.contains('act');
var obj = {"seg": {"id": s, "n": name, "start": start, "stop": (cfg.comp.seglen?start:0)+stop, "rev": rev, "mi": mi, "on": pwr, "bri": parseInt(gId(`seg${s}bri`).value), "sel": sel}};
if (gId(`seg${s}grp`)) {
var grp = parseInt(gId(`seg${s}grp`).value);
@@ -2405,13 +2380,7 @@ function setGrp(s, g)
function setSegPwr(s)
{
// Add safety check for segment power element to prevent UI crashes
const segPwrElement = gId(`seg${s}pwr`);
if (!segPwrElement) {
console.warn('Segment power element not found, skipping power toggle');
return;
}
var pwr = segPwrElement.classList.contains('act');
var pwr = gId(`seg${s}pwr`).classList.contains('act');
var obj = {"seg": {"id": s, "on": !pwr}};
requestJson(obj);
}

View File

@@ -28,10 +28,13 @@
<h2>WLED Software Update</h2>
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
Installed version: <span class="sip">WLED ##VERSION##</span><br>
Release: <span class="sip">##RELEASE##</span><br>
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
style="vertical-align: text-bottom; display: inline-flex;">
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
<input type='checkbox' name='skipValidation' id='skipValidation'>
<label for='skipValidation'>Ignore firmware validation</label><br>
<button type="submit">Update!</button><br>
<hr class="sml">
<button id="rev" type="button" onclick="cR()">Revert update</button><br>

View File

@@ -0,0 +1,122 @@
#include "ota_release_check.h"
#include "wled.h"
#ifdef ESP32
#include <esp_app_format.h>
#include <esp_ota_ops.h>
#endif
// Same hash function used at compile time (must match wled_custom_desc.cpp)
static uint32_t djb2_hash(const char* str) {
uint32_t hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) + *str++;
}
return hash;
}
bool extractReleaseFromCustomDesc(const uint8_t* binaryData, size_t dataSize, char* extractedRelease) {
if (!binaryData || !extractedRelease || dataSize < 64) {
return false;
}
// Search in first 8KB only - ESP32 .rodata.wled_desc and ESP8266 .ver_number
// sections appear early in binary. 8KB should be sufficient for metadata discovery
// while minimizing processing time for large firmware files.
const size_t search_limit = min(dataSize, (size_t)8192);
for (size_t offset = 0; offset <= search_limit - sizeof(wled_custom_desc_t); offset++) {
const wled_custom_desc_t* custom_desc = (const wled_custom_desc_t*)(binaryData + offset);
// Check for magic number
if (custom_desc->magic == WLED_CUSTOM_DESC_MAGIC) {
// Found potential match, validate version
if (custom_desc->version != WLED_CUSTOM_DESC_VERSION) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
offset, custom_desc->version);
continue;
}
// Validate hash using same algorithm as compile-time
uint32_t expected_hash = djb2_hash(custom_desc->release_name);
if (custom_desc->crc32 != expected_hash) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
continue;
}
// Valid structure found
strncpy(extractedRelease, custom_desc->release_name, WLED_RELEASE_NAME_MAX_LEN - 1);
extractedRelease[WLED_RELEASE_NAME_MAX_LEN - 1] = '\0';
#ifdef ESP32
DEBUG_PRINTF_P(PSTR("Extracted ESP32 release name from .rodata.wled_desc section at offset %u: '%s'\n"),
offset, extractedRelease);
#else
DEBUG_PRINTF_P(PSTR("Extracted ESP8266 release name from .ver_number section at offset %u: '%s'\n"),
offset, extractedRelease);
#endif
return true;
}
}
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
return false;
}
bool validateReleaseCompatibility(const char* extractedRelease) {
if (!extractedRelease || strlen(extractedRelease) == 0) {
return false;
}
// Simple string comparison - releases must match exactly
bool match = strcmp(releaseString, extractedRelease) == 0;
DEBUG_PRINTF_P(PSTR("Release compatibility check: current='%s', uploaded='%s', match=%s\n"),
releaseString, extractedRelease, match ? "YES" : "NO");
return match;
}
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool skipValidation, char* errorMessage) {
// Clear error message
if (errorMessage) {
errorMessage[0] = '\0';
}
// Ensure our custom description structure is referenced (prevents optimization)
const wled_custom_desc_t* local_desc = getWledCustomDesc();
(void)local_desc; // Suppress unused variable warning
// If user chose to ignore release check, allow OTA
if (skipValidation) {
DEBUG_PRINTLN(F("OTA release check bypassed by user"));
return true;
}
// Try to extract release name directly from binary data
char extractedRelease[WLED_RELEASE_NAME_MAX_LEN];
bool hasCustomDesc = extractReleaseFromCustomDesc(binaryData, dataSize, extractedRelease);
if (!hasCustomDesc) {
// No custom description - this could be a legacy binary
if (errorMessage) {
strcpy(errorMessage, "Binary has no release compatibility metadata. Check 'Ignore validation' to proceed.");
}
DEBUG_PRINTLN(F("OTA blocked: No custom description found"));
return false;
}
// Validate compatibility using extracted release name
if (!validateReleaseCompatibility(extractedRelease)) {
if (errorMessage) {
snprintf(errorMessage, 127, "Release mismatch: current='%s', uploaded='%s'. Check 'Ignore validation' to proceed.",
releaseString, extractedRelease);
}
DEBUG_PRINTF_P(PSTR("OTA blocked: Release mismatch current='%s', uploaded='%s'\n"),
releaseString, extractedRelease);
return false;
}
DEBUG_PRINTLN(F("OTA allowed: Release names match"));
return true;
}

View File

@@ -0,0 +1,64 @@
#ifndef WLED_OTA_RELEASE_CHECK_H
#define WLED_OTA_RELEASE_CHECK_H
/*
* OTA Release Compatibility Checking using ESP-IDF Custom Description Section
* Functions to extract and validate release names from uploaded binary files using embedded metadata
*/
#include <Arduino.h>
#ifdef ESP32
#include <esp_app_format.h>
#endif
#define WLED_CUSTOM_DESC_MAGIC 0x57535453 // "WSTS" (WLED System Tag Structure)
#define WLED_CUSTOM_DESC_VERSION 1
#define WLED_RELEASE_NAME_MAX_LEN 48
/**
* WLED Custom Description Structure
* This structure is embedded in platform-specific sections at a fixed offset
* in ESP32/ESP8266 binaries, allowing extraction without modifying the binary format
*/
typedef struct {
uint32_t magic; // Magic number to identify WLED custom description
uint32_t version; // Structure version for future compatibility
char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)
uint32_t crc32; // CRC32 of the above fields for integrity check
} __attribute__((packed)) wled_custom_desc_t;
/**
* Extract release name from binary using ESP-IDF custom description section
* @param binaryData Pointer to binary file data
* @param dataSize Size of binary data in bytes
* @param extractedRelease Buffer to store extracted release name (should be at least WLED_RELEASE_NAME_MAX_LEN bytes)
* @return true if release name was found and extracted, false otherwise
*/
bool extractReleaseFromCustomDesc(const uint8_t* binaryData, size_t dataSize, char* extractedRelease);
/**
* Validate if extracted release name matches current release
* @param extractedRelease Release name from uploaded binary
* @return true if releases match (OTA should proceed), false if they don't match
*/
bool validateReleaseCompatibility(const char* extractedRelease);
/**
* 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 skipValidation If true, skip release validation
* @param errorMessage Buffer to store error message if validation fails (should be at least 128 bytes)
* @return true if OTA should proceed, false if it should be blocked
*/
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool skipValidation, char* errorMessage);
/**
* Get pointer to the embedded custom description structure
* This ensures the structure is referenced and not optimized out
* @return pointer to the custom description structure
*/
const wled_custom_desc_t* getWledCustomDesc();
#endif // WLED_OTA_RELEASE_CHECK_H

View File

@@ -0,0 +1,31 @@
#include "ota_release_check.h"
#include "wled.h"
// Simple compile-time hash function for release name validation
constexpr uint32_t djb2_hash(const char* str) {
uint32_t hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) + *str++;
}
return hash;
}
// Single structure definition for both platforms
#ifdef ESP32
const wled_custom_desc_t __attribute__((section(".rodata.wled_desc"))) wled_custom_description = {
#elif defined(ESP8266)
const wled_custom_desc_t __attribute__((section(".ver_number"))) wled_custom_description = {
#endif
WLED_CUSTOM_DESC_MAGIC, // magic
WLED_CUSTOM_DESC_VERSION, // version
WLED_RELEASE_NAME, // release_name
djb2_hash(WLED_RELEASE_NAME) // crc32 - computed at compile time
};
// Single reference to ensure it's not optimized away
const wled_custom_desc_t* __attribute__((used)) wled_custom_desc_ref = &wled_custom_description;
// Function to ensure the structure is referenced by code
const wled_custom_desc_t* getWledCustomDesc() {
return &wled_custom_description;
}

View File

@@ -6,6 +6,7 @@
#else
#include <Update.h>
#endif
#include "ota_release_check.h"
#endif
#include "html_ui.h"
#include "html_settings.h"
@@ -424,32 +425,81 @@ void initServer()
return;
}
if (!correctPIN || otaLock) return;
// Static variable to track release check status across chunks
static bool releaseCheckPassed = false;
if(!index){
DEBUG_PRINTLN(F("OTA Update Start"));
// Check if user wants to ignore release check
bool skipValidation = request->hasParam("skipValidation", true);
// Validate OTA release compatibility using the first chunk data directly
char errorMessage[128];
releaseCheckPassed = shouldAllowOTA(data, len, skipValidation, errorMessage);
if (!releaseCheckPassed) {
DEBUG_PRINTF_P(PSTR("OTA blocked: %s\n"), errorMessage);
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), errorMessage);
return;
}
DEBUG_PRINTLN(F("Release check passed, starting OTA update"));
#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)
lastEditTime = millis(); // make sure PIN does not lock during update
// Start the actual OTA update
strip.suspend();
backupConfig(); // backup current config in case the update ends badly
strip.resetSegments(); // free as much memory as you can
#ifdef ESP8266
Update.runAsync(true);
#endif
Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
}
if(!Update.hasError()) Update.write(data, len);
if(isFinal){
if(Update.end(true)){
DEBUG_PRINTLN(F("Update Success"));
} else {
DEBUG_PRINTLN(F("Update Failed"));
// 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)) {
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), Update.getErrorString().c_str());
strip.resume();
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
UsermodManager::onUpdateBegin(false);
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
#ifdef ESP32
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), String("Update.begin failed: ") + Update.errorString());
#else
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), String("Update.begin failed: ") + Update.getErrorString());
#endif
return;
}
}
// Write chunk data to OTA update (only if release check passed)
if (releaseCheckPassed && !Update.hasError()) {
if (Update.write(data, len) != len) {
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.getErrorString().c_str());
}
}
if(isFinal){
DEBUG_PRINTLN(F("OTA Update End"));
if (releaseCheckPassed) {
if(Update.end(true)){
DEBUG_PRINTLN(F("Update Success"));
} else {
DEBUG_PRINTLN(F("Update Failed"));
strip.resume();
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
}
}
}
});