Compare commits

...

14 Commits

Author SHA1 Message Date
Frank Möhle c02c5ca775 Merge branch 'main' into backport_5435 2026-06-20 16:51:17 +02:00
Frank Möhle 3a46f44495 remove audioreactive from esp01_1m_full_160 build
* AR needs 7kb program size, bringing us too close to the size limit (99.8% used)
* all other esp01_1m builds don't have audioreactive support either
2026-06-20 16:50:25 +02:00
Will Tatam 360853bc28 Add missing javascript function from WLED-MM 2026-06-20 15:29:25 +01:00
Will Tatam 99c3ea0ba9 Add missing javascript function from WLED-MM 2026-06-20 15:28:58 +01:00
Will Tatam 56b3345f6f Pull new ERR_ values into upstream 2026-06-20 15:28:33 +01:00
Frank 779cfa1904 implement review feedback
* clarify #ifdef condition in waitForLEDs()
* move strip.waitForLEDs() before file exists checks  (exists() can cause flickering)
* use MEDUIM timeout in image loader
* correct misunderstood comment in wled.cpp
2026-06-20 16:19:12 +02:00
Frank 01e81c286a short wait in sendDataWs()
getFreeHeapSize() can cause glitches.
2026-06-19 23:04:20 +02:00
Frank 098908d994 fix ancient typo 2026-06-19 23:03:23 +02:00
Frank 16d94e7b87 minor improvements (suggested by the bunny)
* space before comment
* use isUpdating() - not strip.isUpdating() - in waitForLEDs()
2026-06-19 22:46:26 +02:00
Frank adbd8e8ee3 wait for LEDs after beginStrip()
prevents flickering at startup
2026-06-19 22:45:04 +02:00
Frank 0be9f2302d fox comment
comment should match real timeout
2026-06-19 21:20:59 +02:00
Frank 71ca244b46 modernization for 16.0.0
* use constants instead of raw timeouts
* simplify usage - in many cases we don't need #ifdef any more
* missed one "wait" on image_loader
2026-06-19 21:19:38 +02:00
Frank 37de1ce2bd overlooked one 2026-06-19 20:32:05 +02:00
Frank Möhle b4bb5fe4bd (0.15.x) wait for LEDs output to finish before OS calls that potentially suspend interrupts (#5435)
* adding strip.waitForLEDs(waitMS) function
* wait for LEDs output to complete before file writing
* wait before ESP.getFreeHeap() - main loop
* wait before file close (upload) and before  getFreeHeap() (json info)
* avoid losing "trigger" events due to strip.suspend
---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-06-19 20:26:17 +02:00
12 changed files with 171 additions and 13 deletions
+1 -1
View File
@@ -490,7 +490,7 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=
-D WLED_DISABLE_PARTICLESYSTEM1D
-D WLED_DISABLE_PARTICLESYSTEM2D
-D WLED_DISABLE_PIXELFORGE
custom_usermods = audioreactive
;; custom_usermods = audioreactive ;; pushed program flash size over the limits
[env:esp32dev]
extends = esp32
+10 -1
View File
@@ -810,6 +810,11 @@ class Segment {
friend class ParticleSystem1D;
};
// max wait times when waiting until !strip.isUpdating() - use with waitForLEDs(...)
constexpr unsigned STRIP_WAIT_VERYSHORT = 25; // 25 ms - when risk of flickering is low but delays should be avoided
constexpr unsigned STRIP_WAIT_SHORT = 50; // 50 ms - for cases where fluent animations are most important, and risk of flickering is low
constexpr unsigned STRIP_WAIT_MEDIUM = 150; // 150 ms - good balance to avoid flickering on -C3 (good up to 4000 ws2812b LEDs per pin)
// main "strip" class (108 bytes)
class WS2812FX {
typedef void (*mode_ptr)(); // pointer to mode function
@@ -910,6 +915,11 @@ class WS2812FX {
{ if (_segments.size() < getMaxSegments()) _segments.emplace_back(sStart,sStop,sStartY,sStopY); }
inline void suspend() { _suspend = true; } // will suspend (and canacel) strip.service() execution
inline void resume() { _suspend = false; } // will resume strip.service() execution
inline bool isSuspended() const { return _suspend; } // true if strip.service() execution is suspended
// be nice, but not too nice - wait until LEDs are idle, or maxWaitMS have passed
// on 8266 this call will _not_ wait outside of the main loop context
// returns isUpdating() status after waiting
bool waitForLEDs(unsigned maxWaitMS, bool always = false) const;
void restartRuntime();
void setTransitionMode(bool t);
@@ -923,7 +933,6 @@ class WS2812FX {
inline bool isServicing() const { return _isServicing; } // returns true if strip.service() is executing
inline bool hasWhiteChannel() const { return _hasWhiteChannel; } // returns true if strip contains separate white chanel
inline bool isOffRefreshRequired() const { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset)
inline bool isSuspended() const { return _suspend; } // returns true if strip.service() execution is suspended
inline bool needsUpdate() const { return _triggered; } // returns true if strip received a trigger() request
// uint8_t paletteBlend; // obsolete - use global paletteBlend instead of strip.paletteBlend
+27
View File
@@ -1837,6 +1837,33 @@ void WS2812FX::waitForIt() {
#endif
};
/**
* Be nice, but not too nice - wait until LEDs are idle, or maxWaitMS milliseconds have passed.
* always=true enforces waiting even when using the RMTHI driver.
* On 8266 this call will _not_ wait outside the main loop context.
* Function returns isUpdating() status after waiting.
**/
bool WS2812FX::waitForLEDs(unsigned maxWaitMS, bool always) const {
#ifdef ARDUINO_ARCH_ESP32
#if !defined(WLED_USE_SHARED_RMT) && !defined(__riscv)
// shortcut: don't wait if we have the RMTHI driver, unless requested with "always = true"
if (!always) return isUpdating();
#endif
unsigned long waitStart = millis();
while (isUpdating() && (millis() - waitStart < maxWaitMS)) delay(1);
#else
if (can_yield()) {
// If we are in a yieldable context (main loop), wait until the LEDs output finishes
yield();
unsigned long waitStart = millis();
while (isUpdating() && (millis() - waitStart < maxWaitMS)) delay(1);
yield();
}
#endif
return isUpdating();
}
void WS2812FX::setTargetFps(unsigned fps) {
if (fps <= 250) _targetFps = fps;
if (_targetFps > 0) _frametime = 1000 / _targetFps;
+2
View File
@@ -472,6 +472,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define ERR_OVERTEMP 30 // An attached temperature sensor has measured above threshold temperature (not implemented)
#define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented)
#define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented)
#define ERR_REBOOT_NEEDED 98 // reboot needed after changing hardware setting
#define ERR_POWEROFF_NEEDED 99 // power-cycle needed after changing hardware setting
// JSON buffer lock owners
#define JSON_LOCK_UNKNOWN 255
+98 -1
View File
@@ -47,6 +47,7 @@
d.rsvd = [];
d.ro_gpio = [];
d.extra = [];
d.a_pins = []; // analog pins array
}, ()=>{
if (d.um_p[0]==-1) d.um_p.shift(); // remove filler
d.Sf.SDA.max = d.Sf.SCL.max = d.Sf.MOSI.max = d.Sf.SCLK.max = d.Sf.MISO.max = d.max_gpio;
@@ -194,7 +195,11 @@
um += ":"+fld;
} else if (typeof(fld) === "number") sel.classList.add("pin"); // a hack to add a class
let arr = d.getElementsByName(um);
let idx = arr[0].type==="hidden"?1:0; // ignore hidden field
if (!arr || arr.length === 0) {
console.log("addDD: No elements found for name:", um);
return null; // no elements found
}
let idx = (arr[0] && arr[0].type==="hidden")?1:0; // ignore hidden field
if (arr.length > 1+idx) {
// we have array of values (usually pins)
for (let i of arr) {
@@ -256,6 +261,98 @@
e.preventDefault();
if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
}
// TODO: rename this function, needs to be in sync with the now out of tree mod
function aOpt(name,el) {
let obj = d.getElementsByName(name);
if (!obj || obj.length === 0) return; // No elements found
var select = obj;
if (obj[el]) select = obj[el];
// Check if it's actually a select element with options
if (!select.options || !select.options.length) return;
for (let i=0; i<select.options.length; i++) {
let c = select.options[i];
let found = false;
for (let jj=0; jj<d.a_pins.length; jj++) if (d.a_pins[jj] == c.value) found = true; //value -1 or analog pins
if (c.value != -1 && !found) {
select.removeChild(c);
i--; //decrease i by one because the index has been adjusted
}
//https://www.javascripttutorial.net/javascript-dom/javascript-add-remove-options/
//https://www.javascripttutorial.net/javascript-dom/javascript-remove-items-from-a-select-conditionally/
}
}
function rOpt(name,el,txt,val) {
let obj = d.getElementsByName(name);
if (!obj || obj.length === 0) return; // No elements found
var select = obj;
if (obj[el]) select = obj[el];
// Check if element has childNodes
if (!select.childNodes || !select.childNodes.length) return;
for (let i=0; i<select.childNodes.length; i++) {
let c = select.childNodes[i];
if (c.value == val) c.text = txt;
}
}
function xOpt(name,el,txt,val) {
let obj = d.getElementsByName(name);
if (!obj || obj.length === 0) return; // No elements found
var select = obj;
if (obj[el]) select = obj[el];
// Check if element has childNodes
if (!select.childNodes || !select.childNodes.length) return;
for (let i=0; i<select.childNodes.length; i++) {
let c = select.childNodes[i];
if (c.value == val) c.text += txt;
}
}
function dOpt(name,el,valFrom,valTo) {
let obj = d.getElementsByName(name);
if (!obj || obj.length === 0) return; // No elements found
var select = obj;
if (obj[el]) select = obj[el];
// Check if it's actually a select element with options
if (!select.options || !select.options.length) return;
for (let i=0; i<select.options.length; i++) {
let c = select.options[i];
if (c.value >= valFrom && c.value <= valTo) {
select.removeChild(c);
i--; //decrease i by one because the index has been adjusted
}
//https://www.javascripttutorial.net/javascript-dom/javascript-add-remove-options/
//https://www.javascripttutorial.net/javascript-dom/javascript-remove-items-from-a-select-conditionally/
}
}
function dRO(name,el) {
// Initialize d.ro_gpio if not already set
if (!d.ro_gpio) d.ro_gpio = [];
let obj = d.getElementsByName(name);
if (!obj || obj.length === 0) return; // No elements found
var select = obj;
if (obj[el]) select = obj[el];
// Check if it's actually a select element with options
if (!select.options || !select.options.length) return;
// console.log("dRO", name, el, obj, "s", select, d.ro_gpio);
for (let i=0; i<select.options.length; i++) {
let c = select.options[i];
// console.log("dRO option", c, c.value, d.ro_gpio.includes(c.value));
for (let j=0; j<d.ro_gpio.length; j++) if (d.ro_gpio[j] == c.value) c.disabled=true; //if (d.ro_gpio.includes(c.value))
}
}
</script>
</head>
+16 -1
View File
@@ -34,13 +34,24 @@ static File f; // don't export to other cpp files
//wrapper to find out how long closing takes
void closeFile() {
if (!doCloseFile || !f) { doCloseFile = false; return; } // file not open, or no request to close -> nothing to do, nothing to wait
#ifdef WLED_DEBUG_FS
DEBUGFS_PRINT(F("Close -> "));
uint32_t s = millis();
#endif
doCloseFile = false; // consume flag early, to reduce the time window for concurrent closing attempts from several tasks.
// f.close() may enter flash critical sections (interrupts/cache paused), so we wait for LED transmission to finish first to avoid WS281x glitches
// This is most relevant on ESP32-C3/C5/C6, where the RMT driver is very sensitive to interrupt timing.
bool haveSuspended = false;
#if defined(WLED_USE_SHARED_RMT) || defined(__riscv) || !defined(ARDUINO_ARCH_ESP32)
if (!strip.isSuspended()) { strip.suspend(); haveSuspended = true; } // prevent that a new strip.show() starts after waiting
strip.waitForLEDs(STRIP_WAIT_MEDIUM); // be nice, but not too nice. Waits up to 150ms
#endif
f.close(); // "if (f)" check is aleady done inside f.close(), and f cannot be nullptr -> no need for double checking before closing the file handle.
if (haveSuspended) strip.resume(); // end of critical section - new LEDs updates are allowed again
DEBUGFS_PRINTF("took %lu ms\n", millis() - s);
doCloseFile = false;
}
//find() that reads and buffers data from file stream in 256-byte blocks.
@@ -437,6 +448,7 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){
}
}
#endif
strip.waitForLEDs(STRIP_WAIT_SHORT); // wait for LEDs before file access (not using strip.suspend(), to avoid effect stuttering)
if(WLED_FS.exists(path) || WLED_FS.exists(path + ".gz")) {
request->send(request->beginResponse(WLED_FS, path, {}, request->hasArg(F("download")), {}));
return true;
@@ -447,6 +459,7 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){
// copy a file, delete destination file if incomplete to prevent corrupted files
bool copyFile(const char* src_path, const char* dst_path) {
DEBUG_PRINTF("copyFile from %s to %s\n", src_path, dst_path);
strip.waitForLEDs(STRIP_WAIT_MEDIUM, true); // wait for LEDs before file access (not using strip.suspend(), to avoid effect stuttering)
if(!WLED_FS.exists(src_path)) {
DEBUG_PRINTLN(F("file not found"));
return false;
@@ -485,6 +498,7 @@ bool copyFile(const char* src_path, const char* dst_path) {
// compare two files, return true if identical
bool compareFiles(const char* path1, const char* path2) {
DEBUG_PRINTF("compareFile %s and %s\n", path1, path2);
strip.waitForLEDs(STRIP_WAIT_SHORT); // wait for LEDs before file access (not using strip.suspend(), to avoid effect stuttering)
if (!WLED_FS.exists(path1) || !WLED_FS.exists(path2)) {
DEBUG_PRINTLN(F("file not found"));
return false;
@@ -543,6 +557,7 @@ bool restoreFile(const char* filename) {
char backupname[32];
snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename
strip.waitForLEDs(STRIP_WAIT_MEDIUM); // wait for LEDs before file existence checking
if (!WLED_FS.exists(backupname)) {
DEBUG_PRINTLN(F("no backup found"));
return false;
+2 -4
View File
@@ -28,10 +28,7 @@ int fileReadCallback(void) {
}
int fileReadBlockCallback(void * buffer, int numberOfBytes) {
#ifdef CONFIG_IDF_TARGET_ESP32C3
unsigned t0 = millis();
while (strip.isUpdating() && (millis() - t0 < 150)) yield(); // be nice, but not too nice. Waits up to 150ms to avoid glitches
#endif
strip.waitForLEDs(STRIP_WAIT_MEDIUM);
return file.read((uint8_t*)buffer, numberOfBytes);
}
@@ -137,6 +134,7 @@ byte renderImageToSegment(Segment &seg) {
DEBUG_PRINTF_P(PSTR("GIF decoder unsupported file: %s\n"), lastFilename);
return IMAGE_ERROR_UNSUPPORTED_FORMAT;
}
strip.waitForLEDs(STRIP_WAIT_MEDIUM);
if (file) file.close();
if (!openGif(lastFilename)) {
gifDecodeFailed = true;
+2
View File
@@ -849,6 +849,8 @@ void serializeInfo(JsonObject root)
root[F("lwip")] = LWIP_VERSION_MAJOR;
#endif
// calling ESP.getFreeHeap() during led update causes glitches on C3 and possibly on 8266, too
strip.waitForLEDs(STRIP_WAIT_SHORT); // be nice, but not too nice. Waits up to 50ms. No need to suspend effects - ESP.getFreeHeap() will not need much time
root[F("freeheap")] = getFreeHeapSize();
#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
// Report PSRAM information
+2
View File
@@ -239,6 +239,7 @@ static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
strip.suspend();
strip.waitForLEDs(STRIP_WAIT_MEDIUM, true); // always wait for LED transmissions to finish
backupConfig(); // backup current config in case the update ends badly
strip.resetSegments(); // free as much memory as you can
context->needsRestart = true;
@@ -759,6 +760,7 @@ bool initBootloaderOTA(AsyncWebServerRequest *request) {
#endif
lastEditTime = millis(); // make sure PIN does not lock during update
strip.suspend();
strip.waitForLEDs(STRIP_WAIT_SHORT, true); // be sure that LED transmissions have finished
strip.resetSegments();
// Check available heap before attempting allocation
+3 -5
View File
@@ -174,12 +174,9 @@ void WLED::loop()
#ifdef ESP8266
uint32_t heap = getFreeHeapSize(); // ESP8266 needs ~8k of free heap for UI to work properly
#else
#ifdef CONFIG_IDF_TARGET_ESP32C3
// calling getContiguousFreeHeap() during led update causes glitches on C3
// this can (probably) be removed once RMT driver for C3 is fixed
unsigned t0 = millis();
while (strip.isUpdating() && (millis() - t0 < 150)) delay(1); // be nice, but not too nice. Waits up to 150ms
#endif
strip.waitForLEDs(STRIP_WAIT_MEDIUM); // be nice, but not too nice. Waits up to 150ms - we are in the main loop, so a new strip.show() cannot start while waiting
uint32_t heap = getContiguousFreeHeap(); // ESP32 family needs ~10k of contiguous free heap for UI to work properly
#endif
if (heap < MIN_HEAP_SIZE - 1024) heapDanger+=5; // allow 1k of "wiggle room" for things that do not respect min heap limits
@@ -494,6 +491,7 @@ void WLED::setup()
DEBUG_PRINTLN(F("Initializing strip"));
beginStrip();
strip.waitForLEDs(STRIP_WAIT_MEDIUM); // prevent flickering - beginStrip() calls strip.show()
DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize());
DEBUG_PRINTLN(F("Usermods setup"));
@@ -598,7 +596,7 @@ void WLED::setup()
#endif
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); //enable brownout detector
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); //re-enable brownout detector
#endif
markOTAvalid();
}
+7
View File
@@ -212,7 +212,14 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
request->_tempFile.write(data,len);
}
if (isFinal) {
bool haveSuspended = false;
#if defined(WLED_USE_SHARED_RMT) || defined(__riscv) || !defined(ARDUINO_ARCH_ESP32)
if (!strip.isSuspended()) { strip.suspend(); haveSuspended = true; } // prevent that a new strip.show() starts after waiting
strip.waitForLEDs(STRIP_WAIT_SHORT, true); // calling file.close() during LEDs sendout can cause glitches on C3 and on 8266
#endif
request->_tempFile.close();
if (haveSuspended) strip.resume();
if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash
doReboot = true;
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Config restore ok.\nRebooting..."));
+1
View File
@@ -153,6 +153,7 @@ void sendDataWs(AsyncWebSocketClient * client)
DEBUG_PRINTF_P(PSTR("JSON buffer size: %u for WS request (%u).\n"), pDoc->memoryUsage(), len);
// the following may no longer be necessary as heap management has been fixed by @willmmiles in AWS
strip.waitForLEDs(STRIP_WAIT_VERYSHORT); // wait for LEDs ouptut to finish - prevents glitches on -C3
size_t heap1 = getFreeHeapSize();
DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize());
AsyncWebSocketBuffer buffer(len);