Compare commits

..

11 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
8590538272 Use min() to prevent increasing bus count and maintain user configuration
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-15 06:26:11 +00:00
copilot-swe-agent[bot]
f75b13a29a Simplify bus creation logic as requested - replace complex binary search with simple default count fallback
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-15 06:15:20 +00:00
copilot-swe-agent[bot]
1b19c35625 Add comprehensive UI error handling for empty segment lists
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-15 05:45:21 +00:00
copilot-swe-agent[bot]
2dfea5c1e2 Improve bus creation with memory-aware truncation and UI error handling
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-15 05:10:24 +00:00
copilot-swe-agent[bot]
a441ee81b6 Implement bus creation fallback when memory limit exceeded
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-15 04:52:35 +00:00
copilot-swe-agent[bot]
6c0c875fe6 Initial plan 2025-09-15 04:43:20 +00:00
Will Tatam
9d706010f5 Update WLED app links for Android and iOS 2025-09-14 19:05:47 +01:00
Will Tatam
97b20438fd Fix typo in 'Originally' in readme.md 2025-09-14 19:03:21 +01:00
Will Tatam
65913f990d Update project title and add creator attribution 2025-09-14 18:24:19 +01:00
Will Tatam
56d00357d3 testing thanks.dev 2025-09-14 18:12:03 +01:00
netmindz
1864e550e6 Update funding to include currently active maintainers 2025-09-14 15:32:37 +01:00
11 changed files with 72 additions and 315 deletions

3
.github/FUNDING.yml vendored
View File

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

View File

@@ -1,6 +1,5 @@
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
@@ -13,21 +12,6 @@ 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,10 +10,12 @@
</p>
# Welcome to my project WLED! ✨
# Welcome to 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
@@ -32,7 +34,7 @@ A fast and feature-rich implementation of an ESP32 and ESP8266 webserver to cont
- 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=com.aircoookie.WLED) and [iOS](https://apps.apple.com/us/app/wled/id1475695033)
- 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)
- JSON and HTTP request APIs
- MQTT
- E1.31, Art-Net, DDP and TPM2.net

View File

@@ -95,11 +95,6 @@ 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,6 +13,10 @@
#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!
@@ -1173,12 +1177,22 @@ void WS2812FX::finalizeInit() {
// create buses/outputs
unsigned mem = 0;
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);
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;
}
busConfigs.clear();
busConfigs.shrink_to_fit();

View File

@@ -668,9 +668,20 @@ function parseInfo(i) {
if (loc) name = "(L) " + name;
d.title = name;
simplifiedUI = i.simplifiedui;
ledCount = i.leds.count;
// 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
}
//syncTglRecv = i.str;
maxSeg = i.leds.maxseg;
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
}
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";
@@ -912,12 +923,24 @@ function populateSegments(s)
gId(`segr${i}`).classList.add("hide");
}
if (segCount < 2) {
gId(`segd${lSeg}`).classList.add("hide"); // hide delete if only one segment
if (parseInt(gId("seg0bri").value)==255) gId(`segp0`).classList.add("hide");
// 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");
// hide segment controls if there is only one segment in simplified UI
if (simplifiedUI) gId("segcont").classList.add("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");
// 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");
}
gId('segutil2').style.display = (segCount > 1) ? "block":"none"; // rsbtn parent
if (Array.isArray(li.maps) && li.maps.length>1) {
@@ -2253,7 +2276,9 @@ function rptSeg(s)
var rev = gId(`seg${s}rev`).checked;
var mi = gId(`seg${s}mi`).checked;
var sel = gId(`seg${s}sel`).checked;
var pwr = gId(`seg${s}pwr`).classList.contains('act');
// 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 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);
@@ -2380,7 +2405,13 @@ function setGrp(s, g)
function setSegPwr(s)
{
var pwr = gId(`seg${s}pwr`).classList.contains('act');
// 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 obj = {"seg": {"id": s, "on": !pwr}};
requestJson(obj);
}

View File

@@ -28,13 +28,10 @@
<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

@@ -1,122 +0,0 @@
#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

@@ -1,64 +0,0 @@
#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

@@ -1,31 +0,0 @@
#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,7 +6,6 @@
#else
#include <Update.h>
#endif
#include "ota_release_check.h"
#endif
#include "html_ui.h"
#include "html_settings.h"
@@ -425,81 +424,32 @@ 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
// 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());
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"));
strip.resume();
UsermodManager::onUpdateBegin(false);
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
#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
}
}
}
});