From 774fbc5fabc601f714a38aaf87ac189ba4cfba10 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sun, 29 Mar 2026 22:35:23 +0100 Subject: [PATCH 01/71] 16.0.0-beta --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index de0a4084f7..fe014c0c2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "16.0.0-alpha", + "version": "16.0.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "16.0.0-alpha", + "version": "16.0.0-beta", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", diff --git a/package.json b/package.json index 840dc5006f..2a69c626e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "16.0.0-alpha", + "version": "16.0.0-beta", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { From b293db6af5060be02c747875ce88166249ffee82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:16:05 +0200 Subject: [PATCH 02/71] Clarify instructions to always use the correct source code branch Updated instructions for providing references in analysis results. --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3a2cc14554..9952454001 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -156,7 +156,7 @@ package.json # Node.js dependencies and scripts - **Test web interface manually after any web UI changes** - When reviewing a PR: the PR author does not need to update/commit generated html_*.h files - these files will be auto-generated when building the firmware binary. - **If you are not sure about something, just answer that you are not sure.** Gather more information instead of continuing with a wild guess. -- If asked for an analysis, assessment or web research, **always provide references** to justify your conclusions. +- If asked for an analysis, assessment or web research, **provide relevant references** to justify your conclusions. Ensure your recommendations are based on the correct source code branch or PR. - If updating Web UI files in `wled00/data/`, make use of common functions availeable in `wled00/data/common.js` where possible. - **Use VS Code with PlatformIO extension for best development experience** - **Hardware builds require appropriate ESP32/ESP8266 development board** From 679bb3992267691d36345c41bb6e1552e035fc6c Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 28 Mar 2026 17:22:28 -0400 Subject: [PATCH 03/71] Serialize fxdata without ArduinoJSON Eliminates size limit. Fixes #5458 --- wled00/json.cpp | 65 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index db8e7dfcfd..a653755462 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1160,21 +1160,6 @@ void serializePins(JsonObject root) } } -// deserializes mode data string into JsonArray -void serializeModeData(JsonArray fxdata) -{ - char lineBuffer[256]; - for (size_t i = 0; i < strip.getModeCount(); i++) { - strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1); - lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string - if (lineBuffer[0] != 0) { - char* dataPtr = strchr(lineBuffer,'@'); - if (dataPtr) fxdata.add(dataPtr+1); - else fxdata.add(""); - } - } -} - // deserializes mode names string into JsonArray // also removes effect data extensions (@...) from deserialised names void serializeModeNames(JsonArray arr) @@ -1191,6 +1176,48 @@ void serializeModeNames(JsonArray arr) } } +// Generate a streamed JSON response for the mode data +// This uses sendChunked to send the reply in blocks based on how much fit in the outbound +// packet buffer, minimizing the required state (ie. just the next index to send). This +// allows us to send an arbitrarily large response without using any significant amount of +// memory (so no worries about buffer limits). +void respondModeData(AsyncWebServerRequest* request) { + size_t fx_index = 0; + request->sendChunked(FPSTR(CONTENT_TYPE_JSON), + [fx_index](uint8_t* data, size_t len, size_t) mutable { + size_t bytes_written = 0; + char lineBuffer[256]; + while (fx_index < strip.getModeCount() && (len > 5)) { + strncpy_P(lineBuffer, strip.getModeData(fx_index), sizeof(lineBuffer)/sizeof(char)-1); // Copy to stack buffer for strchr + if (lineBuffer[0] != 0) { + lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string + char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one + size_t mode_bytes; + if (dataPtr) { + mode_bytes = snprintf_P((char*) data, len, PSTR(",\"%s\""), dataPtr + 1); + if (mode_bytes > len) break; // didn't fit; break loop and try again next packet + } else { + strncpy_P((char*)data, PSTR(",\"\""), len); + mode_bytes = 3; + } + if (fx_index == 0) *data = '['; + data += mode_bytes; + len -= mode_bytes; + bytes_written += mode_bytes; + } + ++fx_index; + } + + if ((fx_index == strip.getModeCount()) && (len >= 1)) { + *data = ']'; + ++bytes_written; + ++fx_index; // we're really done + } + + return bytes_written; + }); +} + // Global buffer locking response helper class (to make sure lock is released when AsyncJsonResponse is destroyed) class LockedJsonResponse: public AsyncJsonResponse { bool _holding_lock; @@ -1218,7 +1245,7 @@ class LockedJsonResponse: public AsyncJsonResponse { void serveJson(AsyncWebServerRequest* request) { enum class json_target { - all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config, pins + all, state, info, state_info, nodes, effects, palettes, networks, config, pins }; json_target subJson = json_target::all; @@ -1229,7 +1256,7 @@ void serveJson(AsyncWebServerRequest* request) else if (url.indexOf(F("nodes")) > 0) subJson = json_target::nodes; else if (url.indexOf(F("eff")) > 0) subJson = json_target::effects; else if (url.indexOf(F("palx")) > 0) subJson = json_target::palettes; - else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata; + else if (url.indexOf(F("fxda")) > 0) { respondModeData(request); return; } else if (url.indexOf(F("net")) > 0) subJson = json_target::networks; else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config; else if (url.indexOf(F("pins")) > 0) subJson = json_target::pins; @@ -1254,7 +1281,7 @@ void serveJson(AsyncWebServerRequest* request) } // releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer) // make sure you delete "response" if no "request->send(response);" is made - LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::fxdata || subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary + LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary JsonVariant lDoc = response->getRoot(); @@ -1270,8 +1297,6 @@ void serveJson(AsyncWebServerRequest* request) serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break; case json_target::effects: serializeModeNames(lDoc); break; - case json_target::fxdata: - serializeModeData(lDoc); break; case json_target::networks: serializeNetworks(lDoc); break; case json_target::config: From 56a97a0caa38ded5a636ee41a4ab7478a2b26a46 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 29 Mar 2026 23:13:22 -0400 Subject: [PATCH 04/71] Fix off-by-one in fxdata serializer Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- wled00/json.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index a653755462..53b1723d78 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1195,7 +1195,7 @@ void respondModeData(AsyncWebServerRequest* request) { size_t mode_bytes; if (dataPtr) { mode_bytes = snprintf_P((char*) data, len, PSTR(",\"%s\""), dataPtr + 1); - if (mode_bytes > len) break; // didn't fit; break loop and try again next packet + if (mode_bytes >= len) break; // didn't fit; break loop and try again next packet } else { strncpy_P((char*)data, PSTR(",\"\""), len); mode_bytes = 3; From 82ddefff8737432a07736af1696bc315e2ca0c22 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 29 Mar 2026 23:45:41 -0400 Subject: [PATCH 05/71] Remove declaration of removed function Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- wled00/fcn_declare.h | 1 - 1 file changed, 1 deletion(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 746d6142a6..ffb2c1202f 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -176,7 +176,6 @@ void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true, bool selectedSegmentsOnly = false); void serializeInfo(JsonObject root); void serializeModeNames(JsonArray arr); -void serializeModeData(JsonArray fxdata); void serializePins(JsonObject root); void serveJson(AsyncWebServerRequest* request); #ifdef WLED_ENABLE_JSONLIVE From b5556aa97789bfe7bddb23c8e1ba9b88289759e9 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:40:58 +0200 Subject: [PATCH 06/71] fix for HUB75 builds replaces the last remaining FastLED.h with fstled_slim.h --- wled00/bus_manager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index a71600b44d..21e7fb11a2 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -6,7 +6,7 @@ #include #include -#include +#include "src/dependencies/fastled_slim/fastled_slim.h" #endif /* From a3f730275af60dba9a6e1de190efda5c9da5077d Mon Sep 17 00:00:00 2001 From: Will Miles Date: Mon, 30 Mar 2026 18:58:25 -0400 Subject: [PATCH 07/71] Factor out streaming JSON primitives Co-Authored-By: Claude Sonnet 4.6 --- wled00/json.cpp | 54 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 53b1723d78..a8b37d3757 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1176,6 +1176,42 @@ void serializeModeNames(JsonArray arr) } } +// Writes a JSON-escaped string (with surrounding quotes) into dest[0..maxLen-1]. +// Returns bytes written, or 0 if the buffer was too small. +static size_t writeJSONString(uint8_t* dest, size_t maxLen, const char* src) { + size_t pos = 0; + + auto emit = [&](char c) -> bool { + if (pos >= maxLen) return false; + dest[pos++] = (uint8_t)c; + return true; + }; + + if (!emit('"')) return 0; + + for (const char* p = src; *p; ++p) { + char esc = ARDUINOJSON_NAMESPACE::EscapeSequence::escapeChar(*p); + if (esc) { + if (!emit('\\') || !emit(esc)) return 0; + } else { + if (!emit(*p)) return 0; + } + } + + if (!emit('"')) return 0; + return pos; +} + +// Writes ,"" into dest[0..maxLen-1] (no null terminator). +// Returns bytes written, or 0 if the buffer was too small. +static size_t writeJSONStringElement(uint8_t* dest, size_t maxLen, const char* src) { + if (maxLen == 0) return 0; + dest[0] = ','; + size_t n = writeJSONString(dest + 1, maxLen - 1, src); + if (n == 0) return 0; + return 1 + n; +} + // Generate a streamed JSON response for the mode data // This uses sendChunked to send the reply in blocks based on how much fit in the outbound // packet buffer, minimizing the required state (ie. just the next index to send). This @@ -1187,19 +1223,13 @@ void respondModeData(AsyncWebServerRequest* request) { [fx_index](uint8_t* data, size_t len, size_t) mutable { size_t bytes_written = 0; char lineBuffer[256]; - while (fx_index < strip.getModeCount() && (len > 5)) { - strncpy_P(lineBuffer, strip.getModeData(fx_index), sizeof(lineBuffer)/sizeof(char)-1); // Copy to stack buffer for strchr + while (fx_index < strip.getModeCount()) { + strncpy_P(lineBuffer, strip.getModeData(fx_index), sizeof(lineBuffer)-1); // Copy to stack buffer for strchr if (lineBuffer[0] != 0) { - lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string - char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one - size_t mode_bytes; - if (dataPtr) { - mode_bytes = snprintf_P((char*) data, len, PSTR(",\"%s\""), dataPtr + 1); - if (mode_bytes >= len) break; // didn't fit; break loop and try again next packet - } else { - strncpy_P((char*)data, PSTR(",\"\""), len); - mode_bytes = 3; - } + lineBuffer[sizeof(lineBuffer)-1] = '\0'; // terminate string (only needed if strncpy filled the buffer) + const char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one + size_t mode_bytes = writeJSONStringElement(data, len, dataPtr ? dataPtr + 1 : ""); + if (mode_bytes == 0) break; // didn't fit; break loop and try again next packet if (fx_index == 0) *data = '['; data += mode_bytes; len -= mode_bytes; From e4f2964bd6c1e3df0cbb80c1bc39957805f0c485 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 2 Apr 2026 18:44:19 +0200 Subject: [PATCH 08/71] hotfix: critical bug in candle FX, integer issue in flow FX --- wled00/FX.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c2b36a47b1..18d61b2167 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -3498,11 +3498,14 @@ void candle(bool multi) { if (multi && SEGLEN > 1) { //allocate segment data - unsigned dataSize = sizeof(uint32_t) + max(1, (int)SEGLEN -1) *3; //max. 1365 pixels (ESP8266) + unsigned dataSize = sizeof(uint32_t) + max(1, (int)SEGLEN -1) *3; if (!SEGENV.allocateData(dataSize)) candle(false); //allocation failed + } else { + unsigned dataSize = sizeof(uint32_t); // for last call timestamp + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed } uint32_t* lastcall = reinterpret_cast(SEGENV.data); - uint8_t* candleData = reinterpret_cast(SEGENV.data + sizeof(uint32_t)); + uint8_t* candleData = reinterpret_cast(SEGENV.data + sizeof(uint32_t)); // only used for multi-candle //limit update rate if (strip.now - *lastcall < FRAMETIME_FIXED) return; @@ -4425,7 +4428,8 @@ void mode_flow(void) if (zones & 0x01) zones++; //zones must be even if (zones < 2) zones = 2; int zoneLen = SEGLEN / zones; - zones += 2; //add two extra zones to cover beginning and end of segment (compensate integer truncation) + int requiredZones = (SEGLEN + zoneLen - 1) / zoneLen; + zones = requiredZones + 2; //add extra zones to cover beginning and end of segment (compensate integer truncation) int offset = ((int)SEGLEN - (zones * zoneLen)) / 2; // center the zones on the segment (can not use bit shift on negative number) for (int z = 0; z < zones; z++) From da8bfda2f0e61e86d2297f5b6be01d6a8a3abe0b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 3 Apr 2026 14:21:50 +0200 Subject: [PATCH 09/71] bugfix: do not restrict segment inputs to allow trailing strips --- wled00/data/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 3b79767797..b34260287e 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -310,10 +310,10 @@ function onLoad() sl.addEventListener('touchstart', toggleBubble); sl.addEventListener('touchend', toggleBubble); }); - // limiter for all number inputs: limit inputs instantly + // limiter for all number inputs except segment inputs: limit inputs instantly note: segment inputs are special if matrix is enabled, they allow for trailing strips, need a lot of special cases to handle that d.addEventListener("input", function(e) { const t = e.target; - if (t.tagName === "INPUT" && t.type === "number") { + if (t.tagName === "INPUT" && t.type === "number" && !(t.id && t.id.startsWith("seg"))) { let val = parseFloat(t.value); const max = parseFloat(t.max); const min = parseFloat(t.min); @@ -1182,7 +1182,7 @@ function updateLen(s) let mySD = gId("mkSYD"); if (isM) { // do we have 1D segment *after* the matrix? - if (start >= mw*mh) { + if (start >= mw*mh && s > 0) { if (sY) { sY.value = 0; sY.max = 0; sY.min = 0; } if (eY) { eY.value = 1; eY.max = 1; eY.min = 0; } sX.min = mw*mh; sX.max = ledCount-1; From 227d5354049690355062fd72fc529c3f67406f25 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 4 Apr 2026 15:50:22 -0400 Subject: [PATCH 10/71] Fix leak in blockRouterAdvertisements --- wled00/network.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/network.cpp b/wled00/network.cpp index 0caab86d09..b213b165e3 100644 --- a/wled00/network.cpp +++ b/wled00/network.cpp @@ -377,6 +377,7 @@ bool isWiFiConfigured() { static u8_t blockRouterAdvertisements(void* arg, struct raw_pcb* pcb, struct pbuf* p, const ip_addr_t* addr) { // ICMPv6 type is the first byte of the payload, so we skip the header if (p->len > 0 && (pbuf_get_at(p, sizeof(struct ip6_hdr)) == ICMP6_TYPE_RA)) { + pbuf_free(p); return 1; // claim the packet — lwIP will not pass it further } return 0; // not consumed, pass it on From 8228c7eea4976c70fac6aef4e05e23672fd463d9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 7 Apr 2026 23:29:31 +0200 Subject: [PATCH 11/71] bugfix in pixelforge (no blur by default), fix glitch in animated gifs for C3 --- wled00/data/pixelforge/pixelforge.htm | 2 +- wled00/image_loader.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/wled00/data/pixelforge/pixelforge.htm b/wled00/data/pixelforge/pixelforge.htm index 165eb7a33d..b0c5f2df67 100644 --- a/wled00/data/pixelforge/pixelforge.htm +++ b/wled00/data/pixelforge/pixelforge.htm @@ -1195,7 +1195,7 @@

${esc(t.name)} v${esc(t.ver)}

on:true, seg: cur!==null && cur!==tgt ? [{id:cur,fx:0,n:""},{id:tgt,fx:53,frz:false,sx:128,n:name}] - : {id:tgt,fx:53,frz:false,sx:128,n:name} + : {id:tgt,fx:53,frz:false,sx:128,ix:0,n:name} }; const r=await fetch(getURL('/json/state'),{method:'POST',body:JSON.stringify(j)}); const out=await r.json(); diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 0f4c38893e..7757553928 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -28,6 +28,10 @@ int fileReadCallback(void) { } int fileReadBlockCallback(void * buffer, int numberOfBytes) { + #ifdef CONFIG_IDF_TARGET_ESP32C3 + unsigned t0 = millis(); + while (strip.isUpdating() && (millis() - t0 < 15)) yield(); // be nice, but not too nice. Waits up to 15ms to avoid glitches + #endif return file.read((uint8_t*)buffer, numberOfBytes); } From 19947ec07e404f9124e94b809c3de1b04d9623d8 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Fri, 10 Apr 2026 18:25:31 +0100 Subject: [PATCH 12/71] Merge pull request #5489 from wled/copilot/fix-wled-discovery-issues Fix Alexa/Hue discovery by correcting SSDP response to match UPnP spec --- wled00/src/dependencies/espalexa/Espalexa.h | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/wled00/src/dependencies/espalexa/Espalexa.h b/wled00/src/dependencies/espalexa/Espalexa.h index ae761e9faa..cd9ee5ee42 100644 --- a/wled00/src/dependencies/espalexa/Espalexa.h +++ b/wled00/src/dependencies/espalexa/Espalexa.h @@ -85,6 +85,7 @@ class Espalexa { IPAddress ipMulti; uint32_t mac24; //bottom 24 bits of mac String escapedMac=""; //lowercase mac address + String bridgeId=""; //uppercase EUI-64 bridge ID (16 hex chars) //private member functions const char* modeString(EspalexaColorMode m) @@ -297,13 +298,13 @@ class Espalexa { snprintf_P(buf, sizeof(buf), PSTR("HTTP/1.1 200 OK\r\n" "EXT:\r\n" - "CACHE-CONTROL: max-age=100\r\n" // SSDP_INTERVAL + "CACHE-CONTROL: max-age=86400\r\n" // SSDP_INTERVAL "LOCATION: http://%s:80/description.xml\r\n" "SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.17.0\r\n" // _modelName, _modelNumber "hue-bridgeid: %s\r\n" - "ST: urn:schemas-upnp-org:device:basic:1\r\n" // _deviceType - "USN: uuid:2f402f80-da50-11e1-9b23-%s::upnp:rootdevice\r\n" // _uuid::_deviceType - "\r\n"),s,escapedMac.c_str(),escapedMac.c_str()); + "ST: urn:schemas-upnp-org:device:Basic:1\r\n" // _deviceType + "USN: uuid:2f402f80-da50-11e1-9b23-%s::urn:schemas-upnp-org:device:Basic:1\r\n" // _uuid::_deviceType + "\r\n"),s,bridgeId.c_str(),escapedMac.c_str()); espalexaUdp.beginPacket(espalexaUdp.remoteIP(), espalexaUdp.remotePort()); #ifdef ARDUINO_ARCH_ESP32 @@ -333,6 +334,11 @@ class Espalexa { escapedMac.replace(":", ""); escapedMac.toLowerCase(); + // Compute EUI-64 bridge ID from MAC-48: insert standard "FFFE" padding between + // the first 6 hex chars (OUI/manufacturer) and last 6 hex chars (device), then uppercase + bridgeId = escapedMac.substring(0, 6) + "fffe" + escapedMac.substring(6); + bridgeId.toUpperCase(); + String macSubStr = escapedMac.substring(6, 12); mac24 = strtol(macSubStr.c_str(), 0, 16); From 188beec614a1c6b4521af20f88279509d59d6f26 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 11 Apr 2026 08:20:09 +0100 Subject: [PATCH 13/71] Merge pull request #5465 from netmindz/usage-extended New extended data for usage report --- wled00/data/index.js | 84 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index b34260287e..58b21c32ed 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -3413,13 +3413,23 @@ function showVersionUpgradePrompt(info, oldVersion, newVersion) { function reportUpgradeEvent(info, oldVersion, alwaysReport) { showToast('Reporting upgrade...'); + const IR_TYPES = { + 0: null, // not configured — omit field entirely + 1: "24-key", // white 24-key remote + 2: "24-key-ct", // white 24-key with CW, WW, CT+, CT- keys + 3: "40-key", // blue 40-key remote + 4: "44-key", // white 44-key remote + 5: "21-key", // white 21-key remote + 6: "6-key", // black 6-key learning remote + 7: "9-key", // 9-key remote + 8: "json-remote", // ir.json configurable remote + }; - // Fetch fresh data from /json/info endpoint as requested - fetch(getURL('/json/info'), { - method: 'get' - }) - .then(res => res.json()) - .then(infoData => { + // Reuse the info argument and fetch only /json/cfg (serialize requests to avoid 503s on low-heap devices) + const infoData = info; + fetch(getURL('/json/cfg'), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject(new Error('Failed to fetch /json/cfg'))) + .then(cfgData => { // Map to UpgradeEventRequest structure per OpenAPI spec // Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256 const upgradeData = { @@ -3434,13 +3444,58 @@ function reportUpgradeEvent(info, oldVersion, alwaysReport) { brand: infoData.brand, // Device brand (always present) product: infoData.product, // Product name (always present) flashSize: infoData.flash, // Flash size (always present) - repo: infoData.repo // GitHub repository (always present) - }; + repo: infoData.repo, // GitHub repository (always present) + fsUsed: infoData.fs?.u, // Filesystem used space in kB + fsTotal: infoData.fs?.t, // Filesystem total space in kB + + // LED hardware + busCount: cfgData.hw?.led?.ins?.length ?? 1, + busTypes: (cfgData.hw?.led?.ins ?? []).map(b => busTypeToString(b.type)), + matrixWidth: infoData.leds?.matrix?.w, + matrixHeight: infoData.leds?.matrix?.h, + ledFeatures: [ + ...(infoData.leds?.lc & 0x02 ? ["rgbw"] : []), + ...(infoData.leds?.lc & 0x04 ? ["cct"] : []), + ...((infoData.leds?.maxpwr ?? 0) > 0 ? ["abl"] : []), + ...(cfgData.hw?.led?.cr ? ["cct-from-rgb"] : []), + ...(cfgData.hw?.led?.cct ? ["white-balance"] : []), + ...((cfgData.light?.gc?.col ?? 1.0) > 1.0 || (cfgData.light?.gc?.bri ?? 1.0) > 1.0 ? ["gamma"] : []), + ...(cfgData.light?.aseg ? ["auto-segments"] : []), + ...((cfgData.light?.nl?.mode ?? 0) > 0 ? ["nightlight"] : []), + ], + + // peripherals (note: i2c/spi may reflect board defaults, not user-configured hardware) + peripherals: [ + ...((cfgData.hw?.relay?.pin ?? -1) >= 0 ? ["relay"] : []), + ...((cfgData.hw?.btn?.ins ?? []).filter(b => b.type !== 0).length > 0 ? ["buttons"] : []), + ...((cfgData.eth?.type ?? 0) > 0 ? ["ethernet"] : []), + ...((cfgData.if?.live?.dmx?.inputRxPin ?? 0) > 0 ? ["dmx-input"] : []), + ...((cfgData.hw?.ir?.type ?? 0) > 0 ? ["ir-remote"] : []), + ], + buttonCount: (cfgData.hw?.btn?.ins ?? []).filter(b => b.type !== 0).length, + + // integrations + integrations: [ + ...(cfgData.if?.hue?.en ? ["hue"] : []), + ...(cfgData.if?.mqtt?.en ? ["mqtt"] : []), + ...(cfgData.if?.va?.alexa ? ["alexa"] : []), + ...(cfgData.if?.sync?.send?.en ? ["wled-sync"] : []), + ...(cfgData.nw?.espnow ? ["esp-now"] : []), + ...(cfgData.if?.sync?.espnow ? ["esp-now-sync"] : []), + ], + + // usermods + usermods: Object.keys(cfgData.um ?? {}), + usermodIds: infoData.um ?? [], + }; + + // IR remote — only include if configured + const irType = IR_TYPES[cfgData.hw?.ir?.type ?? 0]; + if (irType) upgradeData.irRemoteType = irType; // Add optional fields if available if (infoData.psrSz !== undefined) upgradeData.psramSize = infoData.psrSz; // Total PSRAM size in MB; can be 0 - // Note: partitionSizes not currently available in /json/info endpoint // Make AJAX call to postUpgradeEvent API return fetch('https://usage.wled.me/api/usage/upgrade', { @@ -3471,6 +3526,17 @@ function reportUpgradeEvent(info, oldVersion, alwaysReport) { }); } +function busTypeToString(t) { + if (t === 0) return "none"; + if (t === 40) return "on-off"; + if (t >= 16 && t <= 39) return "digital"; // WS2812, SK6812, etc. + if (t >= 41 && t <= 47) return "pwm"; // analog RGB/CCT/single + if (t >= 48 && t <= 63) return "digital-spi"; // APA102, WS2801, etc. + if (t >= 64 && t <= 71) return "hub75"; // HUB75 matrix panels + if (t >= 80 && t <= 95) return "network"; // DDP, E1.31, ArtNet + return "unknown"; +} + function updateVersionInfo(version, neverAsk, alwaysReport) { const versionInfo = { version: version, From fadc75da55bb1418f1efb63f22e7180ed6e8551d Mon Sep 17 00:00:00 2001 From: Will Miles Date: Tue, 10 Mar 2026 12:38:52 -0400 Subject: [PATCH 14/71] dynarray: Directly apply linker fix Rather than append a linker file, we edit the upstream supplied ones to add our section to the binaries. Works better on all platforms. Co-Authored-By: Claude --- pio-scripts/dynarray.py | 79 +++++++++++++++++++++++++++------ pio-scripts/validate_modules.py | 3 +- tools/dynarray_espressif32.ld | 10 ----- wled00/dynarray.h | 11 ----- 4 files changed, 67 insertions(+), 36 deletions(-) delete mode 100644 tools/dynarray_espressif32.ld diff --git a/pio-scripts/dynarray.py b/pio-scripts/dynarray.py index 2d3cfa90c5..f918be960a 100644 --- a/pio-scripts/dynarray.py +++ b/pio-scripts/dynarray.py @@ -4,16 +4,69 @@ Import("env") from pathlib import Path -platform = env.get("PIOPLATFORM") -script_file = Path(f"tools/dynarray_{platform}.ld") -if script_file.is_file(): - linker_script = f"-T{script_file}" - if platform == "espressif32": - # For ESP32, the script must be added at the right point in the list - linkflags = env.get("LINKFLAGS", []) - idx = linkflags.index("memory.ld") - linkflags.insert(idx+1, linker_script) - env.Replace(LINKFLAGS=linkflags) - else: - # For other platforms, put it in last - env.Append(LINKFLAGS=[linker_script]) +# Linker script fragment injected into the rodata output section of whichever +# platform we're building for. Placed just before the end-of-rodata marker so +# that the dynarray entries land in flash rodata and are correctly sorted. +DYNARRAY_INJECTION = ( + "\n /* dynarray: WLED dynamic module arrays */\n" + " . = ALIGN(0x10);\n" + " KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))\n" + " " +) + + +def inject_before_marker(path, marker): + """Patch a linker script file in-place, inserting DYNARRAY_INJECTION before marker.""" + original = path.read_text() + path.write_text(original.replace(marker, DYNARRAY_INJECTION + marker, 1)) + + +if env.get("PIOPLATFORM") == "espressif32": + # Find sections.ld on the linker search path (LIBPATH). + sections_ld_path = None + for ld_dir in env.get("LIBPATH", []): + candidate = Path(str(ld_dir)) / "sections.ld" + if candidate.exists(): + sections_ld_path = candidate + break + + if sections_ld_path is not None: + # Inject inside the existing .flash.rodata output section, just before + # _rodata_end. IDF v5 enforces zero gaps between adjacent output + # sections via ASSERT statements, so INSERT AFTER .flash.rodata would + # fail. Injecting inside the section creates no new output section and + # leaves the ASSERTs satisfied. + build_dir = Path(env.subst("$BUILD_DIR")) + patched_path = build_dir / "dynarray_sections.ld" + import shutil + shutil.copy(sections_ld_path, patched_path) + inject_before_marker(patched_path, "_rodata_end = ABSOLUTE(.);") + + # Replace "sections.ld" in LINKFLAGS with an absolute path to our + # patched copy. The flag may appear as a bare token, combined as + # "-Tsections.ld", or split across two tokens ("-T", "sections.ld"). + patched_str = str(patched_path) + new_flags = [] + skip_next = False + for flag in env.get("LINKFLAGS", []): + if skip_next: + new_flags.append(patched_str if flag == "sections.ld" else flag) + skip_next = False + elif flag == "-T": + new_flags.append(flag) + skip_next = True + else: + new_flags.append(flag.replace("sections.ld", patched_str)) + env.Replace(LINKFLAGS=new_flags) + +elif env.get("PIOPLATFORM") == "espressif8266": + # The ESP8266 framework preprocesses eagle.app.v6.common.ld.h into + # local.eagle.app.v6.common.ld in $BUILD_DIR/ld/ at build time. Register + # a post-action on that generated file so the injection happens after + # C-preprocessing but before linking. + build_ld = Path(env.subst("$BUILD_DIR")) / "ld" / "local.eagle.app.v6.common.ld" + + def patch_esp8266_ld(target, source, env): + inject_before_marker(build_ld, "_irom0_text_end = ABSOLUTE(.);") + + env.AddPostAction(str(build_ld), patch_esp8266_ld) diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py index ae098f43cc..99f8c488f9 100644 --- a/pio-scripts/validate_modules.py +++ b/pio-scripts/validate_modules.py @@ -80,8 +80,7 @@ def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]: return found -DYNARRAY_SECTION = ".dtors" if env.get("PIOPLATFORM") == "espressif8266" else ".dynarray" -USERMODS_SECTION = f"{DYNARRAY_SECTION}.usermods.1" +USERMODS_SECTION = f".dynarray.usermods.1" def count_usermod_objects(map_file: list[str]) -> int: """ Returns the number of usermod objects in the usermod list """ diff --git a/tools/dynarray_espressif32.ld b/tools/dynarray_espressif32.ld deleted file mode 100644 index 70ce51f19c..0000000000 --- a/tools/dynarray_espressif32.ld +++ /dev/null @@ -1,10 +0,0 @@ -/* ESP32 linker script fragment to add dynamic array section to binary */ -SECTIONS -{ - .dynarray : - { - . = ALIGN(0x10); - KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*))) - } > default_rodata_seg -} -INSERT AFTER .flash.rodata; diff --git a/wled00/dynarray.h b/wled00/dynarray.h index f9e6de19d4..91fb5240fd 100644 --- a/wled00/dynarray.h +++ b/wled00/dynarray.h @@ -20,15 +20,4 @@ Macros for generating a "dynamic array", a static array of objects declared in d #define DYNARRAY_END(array_name) array_name##_end #define DYNARRAY_LENGTH(array_name) (&DYNARRAY_END(array_name)[0] - &DYNARRAY_BEGIN(array_name)[0]) -#ifdef ESP8266 -// ESP8266 linker script cannot be extended with a unique section for dynamic arrays. -// We instead pack them in the ".dtors" section, as it's sorted and uploaded to the flash -// (but will never be used in the embedded system) -#define DYNARRAY_SECTION ".dtors" - -#else /* ESP8266 */ - -// Use a unique named section; the linker script must be extended to ensure it's correctly placed. #define DYNARRAY_SECTION ".dynarray" - -#endif From c789e3d18713b82e0d52ae752679ede79687cf3c Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 27 Mar 2026 15:32:40 -0400 Subject: [PATCH 15/71] Fix usermod count for LTO --- pio-scripts/validate_modules.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py index 99f8c488f9..81fbce4249 100644 --- a/pio-scripts/validate_modules.py +++ b/pio-scripts/validate_modules.py @@ -80,12 +80,35 @@ def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]: return found -USERMODS_SECTION = f".dynarray.usermods.1" - def count_usermod_objects(map_file: list[str]) -> int: - """ Returns the number of usermod objects in the usermod list """ - # Count the number of entries in the usermods table section - return len([x for x in map_file if USERMODS_SECTION in x]) + """ Returns the number of usermod objects in the usermod list. + + Computes the count from the address span between the .dynarray.usermods.0 + and .dynarray.usermods.99999 sentinel sections. This mirrors the + DYNARRAY_LENGTH macro and is reliable under LTO, where all entries are + merged into a single ltrans partition so counting section occurrences + always yields 1 regardless of the true count. + """ + ENTRY_SIZE = 4 # sizeof(Usermod*) on 32-bit targets + addr_begin = None + addr_end = None + + for i, line in enumerate(map_file): + stripped = line.strip() + if stripped == '.dynarray.usermods.0': + if i + 1 < len(map_file): + m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1]) + if m: + addr_begin = int(m.group(1), 16) + elif stripped == '.dynarray.usermods.99999': + if i + 1 < len(map_file): + m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1]) + if m: + addr_end = int(m.group(1), 16) + + if addr_begin is None or addr_end is None: + return 0 + return (addr_end - addr_begin) // ENTRY_SIZE def validate_map_file(source, target, env): From f994c5e99594339ed53ef41787afbee708d39516 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 27 Mar 2026 18:24:16 -0400 Subject: [PATCH 16/71] validate_modules: Improve performance Use readelf instead of nm for great speed. --- pio-scripts/validate_modules.py | 92 +++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py index 81fbce4249..62f26c00f4 100644 --- a/pio-scripts/validate_modules.py +++ b/pio-scripts/validate_modules.py @@ -13,70 +13,85 @@ def read_lines(p: Path): return f.readlines() -def _get_nm_path(env) -> str: - """ Derive the nm tool path from the build environment """ - if "NM" in env: - return env.subst("$NM") - # Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-nm +def _get_readelf_path(env) -> str: + """ Derive the readelf tool path from the build environment """ + # Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-readelf cc = env.subst("$CC") - nm = re.sub(r'(gcc|g\+\+)$', 'nm', os.path.basename(cc)) - return os.path.join(os.path.dirname(cc), nm) + readelf = re.sub(r'(gcc|g\+\+)$', 'readelf', os.path.basename(cc)) + return os.path.join(os.path.dirname(cc), readelf) def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]: - """ Check which modules have at least one defined symbol placed in the ELF. + """ Check which modules have at least one compilation unit in the ELF. The map file is not a reliable source for this: with LTO, original object file paths are replaced by temporary ltrans.o partitions in all output sections, making per-module attribution impossible from the map alone. - Instead we invoke nm --defined-only -l on the ELF, which uses DWARF debug - info to attribute each placed symbol to its original source file. - - Requires usermod libraries to be compiled with -g so that DWARF sections - are present in the ELF. load_usermods.py injects -g for all WLED modules - via dep.env.AppendUnique(CCFLAGS=["-g"]). + Instead we invoke readelf --debug-dump=info --dwarf-depth=1 on the ELF, + which reads only the top-level compilation-unit DIEs from .debug_info. + Each CU corresponds to one source file; matching DW_AT_comp_dir + + DW_AT_name against the module src_dirs is sufficient to confirm a module + was compiled into the ELF. The output volume is proportional to the + number of source files, not the number of symbols. Returns the set of build_dir basenames for confirmed modules. """ - nm_path = _get_nm_path(env) + readelf_path = _get_readelf_path(env) + secho(f"INFO: Checking for usermod compilation units...") + try: result = subprocess.run( - [nm_path, "--defined-only", "-l", str(elf_path)], + [readelf_path, "--debug-dump=info", "--dwarf-depth=1", str(elf_path)], capture_output=True, text=True, errors="ignore", timeout=120, ) - nm_output = result.stdout + output = result.stdout except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e: - secho(f"WARNING: nm failed ({e}); skipping per-module validation", fg="yellow", err=True) + secho(f"WARNING: readelf failed ({e}); skipping per-module validation", fg="yellow", err=True) return {Path(b.build_dir).name for b in module_lib_builders} # conservative pass - # Match placed symbols against builders as we parse nm output, exiting early - # once all builders are accounted for. - # nm --defined-only still includes debugging symbols (type 'N') such as the - # per-CU markers GCC emits in .debug_info (e.g. "usermod_example_cpp_6734d48d"). - # These live at address 0x00000000 in their debug section — not in any load - # segment — so filtering them out leaves only genuinely placed symbols. - # nm -l appends a tab-separated "file:lineno" location to each symbol line. remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders} found = set() - for line in nm_output.splitlines(): - if not remaining: - break # all builders matched - addr, _, _ = line.partition(' ') - if not addr.lstrip('0'): - continue # zero address — skip debug-section marker - if '\t' not in line: - continue - loc = line.rsplit('\t', 1)[1] - # Strip trailing :lineno (e.g. "/path/to/foo.cpp:42" → "/path/to/foo.cpp") - src_path = Path(loc.rsplit(':', 1)[0]) - # Path.is_relative_to() handles OS-specific separators correctly without - # any regex, avoiding Windows path escaping issues. + def _flush_cu(comp_dir: str | None, name: str | None) -> None: + """Match one completed CU against remaining builders.""" + if not name or not remaining: + return + p = Path(name) + src_path = (Path(comp_dir) / p) if (comp_dir and not p.is_absolute()) else p for src_dir in list(remaining): if src_path.is_relative_to(src_dir): found.add(remaining.pop(src_dir)) break + # readelf emits one DW_TAG_compile_unit DIE per source file. Attributes + # of interest: + # DW_AT_name — source file (absolute, or relative to comp_dir) + # DW_AT_comp_dir — compile working directory + # Both appear as either a direct string or an indirect string: + # DW_AT_name : foo.cpp + # DW_AT_name : (indirect string, offset: 0x…): foo.cpp + # Taking the portion after the *last* ": " on the line handles both forms. + _CU_HEADER = re.compile(r'Compilation Unit @') + _ATTR = re.compile(r'\bDW_AT_(name|comp_dir)\b') + + comp_dir = name = None + for line in output.splitlines(): + if _CU_HEADER.search(line): + _flush_cu(comp_dir, name) + comp_dir = name = None + continue + if not remaining: + break # all builders matched + m = _ATTR.search(line) + if m: + _, _, val = line.rpartition(': ') + val = val.strip() + if m.group(1) == 'name': + name = val + else: + comp_dir = val + _flush_cu(comp_dir, name) # flush the last CU + return found @@ -133,6 +148,7 @@ def validate_map_file(source, target, env): secho(f"INFO: {usermod_object_count} usermod object entries") elf_path = build_dir / env.subst("${PROGNAME}.elf") + confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders) missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules] From fd890b3d580b45e10118df14600f80498ed82bbe Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 27 Mar 2026 22:47:15 -0400 Subject: [PATCH 17/71] dynarray: Support ESP-IDF --- pio-scripts/dynarray.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pio-scripts/dynarray.py b/pio-scripts/dynarray.py index f918be960a..7b580de08d 100644 --- a/pio-scripts/dynarray.py +++ b/pio-scripts/dynarray.py @@ -58,7 +58,15 @@ def inject_before_marker(path, marker): else: new_flags.append(flag.replace("sections.ld", patched_str)) env.Replace(LINKFLAGS=new_flags) - + else: + # Assume sections.ld will be built (ESP-IDF format); add a post-action to patch it + # TODO: consider using ESP-IDF linker fragment (https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/linker-script-generation.html) + # For now, patch after building + sections_ld = Path(env.subst("$BUILD_DIR")) / "sections.ld" + def patch_sections_ld(target, source, env): + inject_before_marker(sections_ld, "_rodata_end = ABSOLUTE(.);") + env.AddPostAction(str(sections_ld), patch_sections_ld) + elif env.get("PIOPLATFORM") == "espressif8266": # The ESP8266 framework preprocesses eagle.app.v6.common.ld.h into # local.eagle.app.v6.common.ld in $BUILD_DIR/ld/ at build time. Register From 48ab88e11e6ccdc73ff2cd53317d1a26c7ed605f Mon Sep 17 00:00:00 2001 From: Will Miles Date: Mon, 30 Mar 2026 19:17:17 -0400 Subject: [PATCH 18/71] Fix up usermod validation again Co-Authored-By: Claude Sonnet 4.6 --- pio-scripts/validate_modules.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py index 62f26c00f4..cd2698b8ea 100644 --- a/pio-scripts/validate_modules.py +++ b/pio-scripts/validate_modules.py @@ -1,11 +1,12 @@ -import os import re import subprocess -from pathlib import Path # For OS-agnostic path manipulation +from pathlib import Path from click import secho from SCons.Script import Action, Exit Import("env") +_ATTR = re.compile(r'\bDW_AT_(name|comp_dir)\b') + def read_lines(p: Path): """ Read in the contents of a file for analysis """ @@ -16,9 +17,8 @@ def read_lines(p: Path): def _get_readelf_path(env) -> str: """ Derive the readelf tool path from the build environment """ # Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-readelf - cc = env.subst("$CC") - readelf = re.sub(r'(gcc|g\+\+)$', 'readelf', os.path.basename(cc)) - return os.path.join(os.path.dirname(cc), readelf) + cc = Path(env.subst("$CC")) + return str(cc.with_name(re.sub(r'(gcc|g\+\+)$', 'readelf', cc.name))) def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]: @@ -51,6 +51,7 @@ def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]: remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders} found = set() + project_dir = Path(env.subst("$PROJECT_DIR")) def _flush_cu(comp_dir: str | None, name: str | None) -> None: """Match one completed CU against remaining builders.""" @@ -58,10 +59,16 @@ def _flush_cu(comp_dir: str | None, name: str | None) -> None: return p = Path(name) src_path = (Path(comp_dir) / p) if (comp_dir and not p.is_absolute()) else p + # In arduino+espidf dual-framework builds the IDF toolchain sets DW_AT_comp_dir + # to the virtual path "/IDF_PROJECT" rather than the real project root, so + # src_path won't match. Pre-compute a fallback using $PROJECT_DIR and check + # both candidates in a single pass. + use_fallback = not p.is_absolute() and comp_dir and Path(comp_dir) != project_dir + src_path_real = project_dir / p if use_fallback else None for src_dir in list(remaining): - if src_path.is_relative_to(src_dir): + if src_path.is_relative_to(src_dir) or (src_path_real and src_path_real.is_relative_to(src_dir)): found.add(remaining.pop(src_dir)) - break + return # readelf emits one DW_TAG_compile_unit DIE per source file. Attributes # of interest: @@ -71,12 +78,10 @@ def _flush_cu(comp_dir: str | None, name: str | None) -> None: # DW_AT_name : foo.cpp # DW_AT_name : (indirect string, offset: 0x…): foo.cpp # Taking the portion after the *last* ": " on the line handles both forms. - _CU_HEADER = re.compile(r'Compilation Unit @') - _ATTR = re.compile(r'\bDW_AT_(name|comp_dir)\b') comp_dir = name = None for line in output.splitlines(): - if _CU_HEADER.search(line): + if 'Compilation Unit @' in line: _flush_cu(comp_dir, name) comp_dir = name = None continue @@ -120,6 +125,8 @@ def count_usermod_objects(map_file: list[str]) -> int: m = re.search(r'0x([0-9a-fA-F]+)', map_file[i + 1]) if m: addr_end = int(m.group(1), 16) + if addr_begin is not None and addr_end is not None: + break if addr_begin is None or addr_end is None: return 0 @@ -129,7 +136,7 @@ def count_usermod_objects(map_file: list[str]) -> int: def validate_map_file(source, target, env): """ Validate that all modules appear in the output build """ build_dir = Path(env.subst("$BUILD_DIR")) - map_file_path = build_dir / env.subst("${PROGNAME}.map") + map_file_path = build_dir / env.subst("${PROGNAME}.map") if not map_file_path.exists(): secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True) @@ -158,7 +165,6 @@ def validate_map_file(source, target, env): fg="red", err=True) Exit(1) - return None env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file')) From ae36f7e20378e34c88e814114b1d28b01a872a97 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 12 Apr 2026 19:06:16 +0200 Subject: [PATCH 19/71] use memory aligned allocations, fix bug in FFT magnitude (integer path) (#5499) * use memory aligned allocations, fix bug in FFT magnitude (integer path) * assign pointer to globals not local copy --- usermods/audioreactive/audio_reactive.cpp | 37 ++++++++++++----------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 757ad35482..5824300289 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -296,17 +296,17 @@ void FFTcode(void * parameter) ArduinoFFT FFT = ArduinoFFT(valFFT, vImag, samplesFFT, SAMPLE_RATE, true); #elif !defined(UM_AUDIOREACTIVE_USE_INTEGER_FFT) // allocate and initialize FFT buffers on first call - // note: free() is never used on these pointers. If it ever is implemented, this implementation can cause memory leaks (need to free raw pointers) if (valFFT == nullptr) { - float* raw_buffer = (float*)heap_caps_malloc((2 * samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT); - if ((raw_buffer == nullptr)) return; // something went wrong - valFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available + valFFT = (float*)heap_caps_aligned_calloc(16, 2 * samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available + if ((valFFT == nullptr)) return; // something went wrong } // create window if (windowFFT == nullptr) { - float* raw_buffer = (float*)heap_caps_malloc((samplesFFT * sizeof(float)) + 16, MALLOC_CAP_8BIT); - if ((raw_buffer == nullptr)) return; // something went wrong - windowFFT = (float*)(((uintptr_t)raw_buffer + 15) & ~15); // SIMD requires aligned memory to 16-byte boundary + windowFFT = (float*)heap_caps_aligned_calloc(16, samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // SIMD requires aligned memory to 16-byte boundary. note in IDF5 there is MALLOC_CAP_SIMD available + if ((windowFFT == nullptr)) { + heap_caps_free(valFFT); valFFT = nullptr; + return; // something went wrong + } } if (dsps_fft2r_init_fc32(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables // create window function for FFT @@ -316,16 +316,20 @@ void FFTcode(void * parameter) dsps_wind_flat_top_f32(windowFFT, samplesFFT); #endif #else - // allocate and initialize integer FFT buffers on first call - if (valFFT == nullptr) valFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT * 2); - if ((valFFT == nullptr)) return; // something went wrong + // use integer FFT - allocate and initialize integer FFT buffers on first call, 4 bytes aligned (just in case, even if not strictly needed for int16_t) + if (valFFT == nullptr) valFFT = (int16_t*) heap_caps_aligned_calloc(4, samplesFFT * 2, sizeof(int16_t), MALLOC_CAP_8BIT); // create window - if (windowFFT == nullptr) windowFFT = (int16_t*) calloc(sizeof(int16_t), samplesFFT); - if ((windowFFT == nullptr)) return; // something went wrong - if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables + if (windowFFT == nullptr) windowFFT = (int16_t*) heap_caps_aligned_calloc(4, samplesFFT, sizeof(int16_t), MALLOC_CAP_8BIT); // create window function for FFT - float *windowFloat = (float*) calloc(sizeof(float), samplesFFT); // temporary buffer for window function - if ((windowFloat == nullptr)) return; // something went wrong + float *windowFloat = (float*) heap_caps_aligned_calloc(4, samplesFFT, sizeof(float), MALLOC_CAP_8BIT); // temporary buffer for window function + if (windowFloat == nullptr || windowFFT == nullptr || valFFT == nullptr) { // something went wrong + if (windowFloat) heap_caps_free(windowFloat); + if (windowFFT) heap_caps_free(windowFFT); windowFFT = nullptr; + if (valFFT) heap_caps_free(valFFT); valFFT = nullptr; + return; + } + if (dsps_fft2r_init_sc16(NULL, samplesFFT) != ESP_OK) return; // initialize FFT tables + #ifdef FFT_PREFER_EXACT_PEAKS dsps_wind_blackman_harris_f32(windowFloat, samplesFFT); #else @@ -335,7 +339,7 @@ void FFTcode(void * parameter) for (int i = 0; i < samplesFFT; i++) { windowFFT[i] = (int16_t)(windowFloat[i] * 32767.0f); } - free(windowFloat); // free temporary buffer + heap_caps_free(windowFloat); // free temporary buffer #endif // see https://www.freertos.org/vtaskdelayuntil.html @@ -468,7 +472,6 @@ void FFTcode(void * parameter) } FFT_Magnitude = FFT_Magnitude_int * 512; // scale to match raw float value FFT_MajorPeak = FFT_MajorPeak_int; - FFT_Magnitude = FFT_Magnitude_int; #endif #endif FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects From e4a7effdb0433b2f8ba2af3328fd9abe1181580e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 12 Apr 2026 19:07:16 +0200 Subject: [PATCH 20/71] add search icon back(#5500) --- wled00/data/icons-ui/demo-files/demo.css | 2 +- wled00/data/icons-ui/demo.html | 16 +++++++++++++++- wled00/data/icons-ui/fonts/wled122.svg | 1 + wled00/data/icons-ui/fonts/wled122.ttf | Bin 5060 -> 5280 bytes wled00/data/icons-ui/fonts/wled122.woff | Bin 5136 -> 5356 bytes wled00/data/icons-ui/fonts/wled122.woff2 | Bin 2816 -> 2928 bytes wled00/data/icons-ui/selection.json | 2 +- wled00/data/icons-ui/style.css | 9 ++++++--- wled00/data/index.css | 2 +- 9 files changed, 25 insertions(+), 7 deletions(-) diff --git a/wled00/data/icons-ui/demo-files/demo.css b/wled00/data/icons-ui/demo-files/demo.css index 731f7b29b6..8fd83c7923 100644 --- a/wled00/data/icons-ui/demo-files/demo.css +++ b/wled00/data/icons-ui/demo-files/demo.css @@ -147,7 +147,7 @@ p { font-size: 16px; } .fs1 { - font-size: 48px; + font-size: 32px; } .fs2 { font-size: 28px; diff --git a/wled00/data/icons-ui/demo.html b/wled00/data/icons-ui/demo.html index c5d596e966..a88b956bbd 100644 --- a/wled00/data/icons-ui/demo.html +++ b/wled00/data/icons-ui/demo.html @@ -9,10 +9,24 @@
-

Font Name: wled122 (Glyphs: 25)

+

Font Name: wled122 (Glyphs: 26)

Grid Size: 16

+
+
+ + i-search +
+
+ + +
+
+ liga: + +
+
diff --git a/wled00/data/icons-ui/fonts/wled122.svg b/wled00/data/icons-ui/fonts/wled122.svg index 082edf54d3..1c0b8ea0b3 100644 --- a/wled00/data/icons-ui/fonts/wled122.svg +++ b/wled00/data/icons-ui/fonts/wled122.svg @@ -11,6 +11,7 @@ + diff --git a/wled00/data/icons-ui/fonts/wled122.ttf b/wled00/data/icons-ui/fonts/wled122.ttf index 84f7719585fc4499ba271eea4e50d45a34a5c1a5..4f238de0c7a392206f5d81a64abec7138aa4f4b4 100644 GIT binary patch delta 527 zcmX@2zCd$=V!c|2CIbVb3=p%VCl(hlFfa%(Ffb_qX^!-q%CruptbQP0gMop6PDW~C ziazV*8w?DBM}YEX89)Jc3FcNH{|}I_l95|dkz)j8F$gIDiHe;3QnO_eZwxo8eh;0MqFZ3X$oB@E0!DF%^-i90ScE}Y!MXsVem zuO#_gd0vU#%EvhlLMGVf&`$()wykZF+-l;IYa0cvBM{E^XM5)=DoMW!Yu z9!&;U1_o7iJw|0a#>o#^MeB`?%*;i^T0GY=5|27v5~kOqqvx; z2s3H48xj0YM8j zHEkre%nDv!jSMSfeL%Jb&=3t}K6(D55-eh(wqkoQ^g+$oJe?(xN5;LxLo=S=<|_j? z3n)Aoo-yVvgwc~l1kD-cCi@5$PTnafWxI$;hiMwq6Xp!&CgwvdZY&Kf*H}YX%UB<= sd9Y=$tzkRBc7Z# zxrr484C)NAK>i;fUm-6sH&w#$<)S$Z3_>6Wv=!tRmoP8`r5J>lOx$sKaxkOJw$u-G7ki96IE1M-7C+j2gPUfM^DVcVeW*Gq)E^%p~M#jk>84V^ev2Rvn zYGT@4&Kk-iGkMk8)OdcIuMFHQAafa>-m!TOqbI)*G-s5ZtRPf4d8&|@{vjqErYfd? y%o5BI%y(GCSaMiivGTB1u-;?+$L7Qq#+Jo)i0vJ_0(%MrBhUod&G&>3F#-S})>SJ2 diff --git a/wled00/data/icons-ui/fonts/wled122.woff b/wled00/data/icons-ui/fonts/wled122.woff index 2fdda4df7d3844d011cd0572b862c46e0e1438bb..abbeea0f4848943b5ba1407e02acb8a1a1cd5388 100644 GIT binary patch delta 567 zcmbQB@kUdk+~3WOfsp|SMBXrPgXsl8KKo=xCXtCcs`Y9en(2wf1q=*KIY3DcC>BW1 zsZ0Zkl`t^y&jH~MrL6vp)Wj491|b2U8Z!{qXT5wQ11Ja-^8oTyK$u;ExiurVqyi`g zG7m^IROA>jFy!PXgVa3%^6fy_M$AGdH?g8Xhk-#Dq)`EkH5g*^5_405VlE5}jBOxn z_;S&lg8X8jLqyI@{BxUe;p7F3(vv?kN@=FcE6IJ8yDZl%`%w0hY@TekY`mFtfnL>_9KgiAIhU!4iAR&cm4QK3U5`=Oj&bsN9K+r->O&f_Vvx1jbBf|<=ABe3kA*rFvC(nOW zf<;W!R%{Q3KByU+AG0L#$henyXvXu~d}ZKf0fra@!!yRbg)n+@f}lC0+~gU8g_Bu@ zq>L9a=`c-WdcvH++{Ao{#f_zbb!HV?K8wl!=A*sieaut%_Wv7ZB)zzD>0 KoBf0iF#-TtGI>k@ delta 376 zcmaE(IYC3B+~3WOfsp|SL#2Q z*bE>aBwmtX#K4e~pA1wd!~^7m^xGJlnC;3;u`0vbnNZvT?FLGVf#_ z%AAsEmuZ#}kl_-SW?*Ju1bS3wasU(e=3J&Grppj+gY)))p XY*}oF*xs=#u&02-Sa!3P@F7M3*1lU+ diff --git a/wled00/data/icons-ui/fonts/wled122.woff2 b/wled00/data/icons-ui/fonts/wled122.woff2 index f049a31affd9d5acf557aedfc28a526d32f5e90a..7fd5f567e3e6cdb802c502dd972efc51225d2689 100644 GIT binary patch delta 2923 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 diff --git a/wled00/data/icons-ui/selection.json b/wled00/data/icons-ui/selection.json index b2c2b14a91..f516914d8e 100644 --- a/wled00/data/icons-ui/selection.json +++ b/wled00/data/icons-ui/selection.json @@ -1 +1 @@ -{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M910.398 765.581l-241.236-241.236c-14.934-14.934-39.371-14.934-54.306 0l-18.102 18.102-147.2-147.2 241.646-241.648h-256.001l-113.645 113.645-11.249-11.247h-54.306v54.306l11.247 11.247-164.848 164.849 127.999 127.999 164.848-164.848 147.2 147.2-18.102 18.102c-14.934 14.934-14.934 39.371 0 54.306l241.236 241.236c14.934 14.934 39.371 14.934 54.306 0l90.509-90.509c14.935-14.934 14.935-39.371 0.002-54.306z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["hammer","tool","fix","make","generate","work","build"],"grid":16},"attrs":[{}],"properties":{"order":1,"id":0,"name":"pixelforge","prevSize":48,"code":59648},"setIdx":0,"setId":3,"iconIdx":0},{"icon":{"paths":["M976.272 538.191c0 11.223-7.016 22.448-14.5 30.867l-157.14 185.202c-27.126 31.802-82.311 57.055-123.469 57.055h-508.837c-16.837 0-40.688-5.146-40.688-26.191 0-11.223 7.016-22.448 14.5-30.867l157.14-185.202c27.126-31.802 82.311-57.055 123.469-57.055h508.837c16.837 0 40.688 5.146 40.688 26.191zM815.856 377.307v74.828h-389.112c-58.461 0-130.952 33.208-168.835 78.104l-159.949 188.009c0-3.74-0.467-7.951-0.467-11.691v-448.977c0-57.523 47.233-104.761 104.761-104.761h149.66c57.523 0 104.761 47.233 104.761 104.761v14.968h254.418c57.523 0 104.761 47.233 104.761 104.761z"],"attrs":[{}],"width":1074,"isMulticolor":false,"isMulticolor2":false,"tags":["folder-open"],"grid":14},"attrs":[{}],"properties":{"order":1,"id":1,"prevSize":28,"name":"editor","code":59649},"setIdx":1,"setId":2,"iconIdx":0},{"icon":{"paths":["M511.573 85.333c235.947 0 427.094 191.147 427.094 426.667s-191.147 426.667-427.094 426.667c-235.52 0-426.24-191.147-426.24-426.667s190.72-426.667 426.24-426.667zM512 853.333c188.587 0 341.333-152.746 341.333-341.333s-152.746-341.333-341.333-341.333-341.333 152.746-341.333 341.333 152.746 341.333 341.333 341.333zM661.333 469.333c-35.413 0-64-28.586-64-64s28.587-64 64-64c35.414 0 64 28.587 64 64s-28.586 64-64 64zM362.667 469.333c-35.414 0-64-28.586-64-64s28.586-64 64-64c35.413 0 64 28.587 64 64s-28.587 64-64 64zM512 746.667c-99.413 0-183.893-62.294-218.027-149.334h436.054c-34.134 87.040-118.614 149.334-218.027 149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE23D"],"defaultCode":57917,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":11,"order":26,"prevSize":32,"code":57917,"name":"pattern"},"setIdx":2,"setId":1,"iconIdx":0},{"icon":{"paths":["M511.573 791.040l314.88-244.907 69.547 54.187-384 298.667-384-298.667 69.12-53.76zM512 682.667l-384-298.667 384-298.667 384 298.667-69.973 54.187z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE34B"],"defaultCode":58187,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":14,"order":35,"ligatures":"","prevSize":32,"code":58187,"name":"segments"},"setIdx":2,"setId":1,"iconIdx":1},{"icon":{"paths":["M288.427 206.507l-60.587 60.16-76.373-76.374 60.16-60.16zM170.667 448v85.333h-128v-85.333h128zM554.667 23.467v125.866h-85.334v-125.866h85.334zM872.533 190.293l-76.373 76.374-60.16-60.16 76.373-76.374zM735.573 774.827l59.734-59.734 76.8 76.374-60.16 60.16zM853.333 448h128v85.333h-128v-85.333zM512 234.667c141.227 0 256 114.773 256 256 0 141.226-114.773 256-256 256s-256-114.774-256-256c0-141.227 114.773-256 256-256zM469.333 957.867v-125.867h85.334v125.867h-85.334zM151.467 791.040l76.373-76.8 60.16 60.16-76.373 76.8z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE333"],"defaultCode":58163,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":40,"order":73,"ligatures":"","prevSize":32,"code":58163,"name":"sun"},"setIdx":2,"setId":1,"iconIdx":2},{"icon":{"paths":["M512 128c212.053 0 384 152.747 384 341.333 0 117.76-95.573 213.334-213.333 213.334h-75.52c-35.414 0-64 28.586-64 64 0 16.213 6.4 31.146 16.213 42.24 10.24 11.52 16.64 26.453 16.64 43.093 0 35.413-28.587 64-64 64-212.053 0-384-171.947-384-384s171.947-384 384-384zM277.333 512c35.414 0 64-28.587 64-64s-28.586-64-64-64c-35.413 0-64 28.587-64 64s28.587 64 64 64zM405.333 341.333c35.414 0 64-28.586 64-64 0-35.413-28.586-64-64-64-35.413 0-64 28.587-64 64 0 35.414 28.587 64 64 64zM618.667 341.333c35.413 0 64-28.586 64-64 0-35.413-28.587-64-64-64-35.414 0-64 28.587-64 64 0 35.414 28.586 64 64 64zM746.667 512c35.413 0 64-28.587 64-64s-28.587-64-64-64c-35.414 0-64 28.587-64 64s28.586 64 64 64z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2B3"],"defaultCode":58035,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":75,"order":75,"prevSize":32,"code":58035,"name":"palette"},"setIdx":2,"setId":1,"iconIdx":3},{"icon":{"paths":["M512 192c213.333 0 395.52 132.693 469.333 320-73.813 187.307-256 320-469.333 320s-395.52-132.693-469.333-320c73.813-187.307 256-320 469.333-320zM512 725.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333-213.333 95.573-213.333 213.333 95.573 213.333 213.333 213.333zM512 384c70.827 0 128 57.173 128 128s-57.173 128-128 128-128-57.173-128-128 57.173-128 128-128z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0E8"],"defaultCode":57576,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":172,"order":74,"prevSize":32,"code":57576,"name":"eye"},"setIdx":2,"setId":1,"iconIdx":4},{"icon":{"paths":["M640 42.667v85.333h-256v-85.333h256zM469.333 597.333v-256h85.334v256h-85.334zM811.947 315.307c52.48 65.706 84.053 148.906 84.053 239.36 0 212.053-171.52 384-384 384s-384-171.947-384-384c0-212.054 171.947-384 384-384 90.453 0 173.653 31.573 239.787 84.48l60.586-60.587c21.76 17.92 41.814 38.4 60.16 60.16zM512 853.333c165.12 0 298.667-133.546 298.667-298.666s-133.547-298.667-298.667-298.667-298.667 133.547-298.667 298.667 133.547 298.666 298.667 298.666z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE325"],"defaultCode":58149,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":370,"order":21,"ligatures":"","prevSize":32,"code":58149,"name":"speed"},"setIdx":2,"setId":1,"iconIdx":5},{"icon":{"paths":["M707.84 366.507l60.16 60.16-256 256-256-256 60.16-60.16 195.84 195.413z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE395"],"defaultCode":58261,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":549,"order":69,"ligatures":"","prevSize":32,"code":58261,"name":"expand"},"setIdx":2,"setId":1,"iconIdx":6},{"icon":{"paths":["M554.667 128v426.667h-85.334v-426.667h85.334zM760.747 220.587c82.773 70.4 135.253 174.506 135.253 291.413 0 212.053-171.947 384-384 384s-384-171.947-384-384c0-116.907 52.48-221.013 135.253-291.413l60.16 60.16c-66.986 54.613-110.080 137.813-110.080 231.253 0 165.12 133.547 298.667 298.667 298.667s298.667-133.547 298.667-298.667c0-93.44-43.094-176.64-110.507-230.827z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE08F"],"defaultCode":57487,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":557,"order":19,"ligatures":"","prevSize":32,"code":57487,"name":"power"},"setIdx":2,"setId":1,"iconIdx":7},{"icon":{"paths":["M816.64 551.936l85.504 67.584c8.192 6.144 10.24 16.896 5.12 26.112l-81.92 141.824c-5.12 9.216-15.872 12.8-25.088 9.216l-101.888-40.96c-20.992 15.872-44.032 29.696-69.12 39.936l-15.36 108.544c-1.024 10.24-9.728 17.408-19.968 17.408h-163.84c-10.24 0-18.432-7.168-20.48-17.408l-15.36-108.544c-25.088-10.24-47.616-23.552-69.12-39.936l-101.888 40.96c-9.216 3.072-19.968 0-25.088-9.216l-81.92-141.824c-4.608-8.704-2.56-19.968 5.12-26.112l86.528-67.584c-2.048-12.8-3.072-26.624-3.072-39.936s1.536-27.136 3.584-39.936l-86.528-67.584c-8.192-6.144-10.24-16.896-5.12-26.112l81.92-141.824c5.12-9.216 15.872-12.8 25.088-9.216l101.888 40.96c20.992-15.872 44.032-29.696 69.12-39.936l15.36-108.544c1.536-10.24 9.728-17.408 19.968-17.408h163.84c10.24 0 18.944 7.168 20.48 17.408l15.36 108.544c25.088 10.24 47.616 23.552 69.12 39.936l101.888-40.96c9.216-3.072 19.968 0 25.088 9.216l81.92 141.824c4.608 8.704 2.56 19.968-5.12 26.112l-86.528 67.584c2.048 12.8 3.072 26.112 3.072 39.936s-1.024 27.136-2.56 39.936zM512 665.6c84.48 0 153.6-69.12 153.6-153.6s-69.12-153.6-153.6-153.6-153.6 69.12-153.6 153.6 69.12 153.6 153.6 153.6z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0A2"],"defaultCode":57506,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":562,"order":29,"ligatures":"","prevSize":32,"code":57506,"name":"settings"},"setIdx":2,"setId":1,"iconIdx":8},{"icon":{"paths":["M556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l-130.133-97.707v384l256-192zM469.333 173.653c-62.293 7.68-119.040 32.427-166.4 69.12l-60.586-61.013c63.146-51.627 141.226-85.76 226.986-94.293v86.186zM242.773 302.933c-36.693 47.36-61.44 104.107-69.12 166.4h-86.186c8.533-85.76 42.666-163.84 94.293-226.986zM173.653 554.667c7.68 62.293 32.427 119.040 69.12 165.973l-61.013 61.013c-51.627-63.146-85.76-141.226-94.293-226.986h86.186zM242.347 842.24l60.586-61.013c47.36 36.693 104.107 61.44 166.4 69.12v86.186c-85.333-8.533-163.84-42.666-226.986-94.293zM938.667 512c0 220.16-167.254 401.92-381.867 424.533v-86.186c167.253-22.187 296.533-165.547 296.533-338.347s-129.28-316.16-296.533-338.347v-86.186c214.613 22.613 381.867 204.373 381.867 424.533z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE139"],"defaultCode":57657,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":595,"order":46,"ligatures":"","prevSize":32,"code":57657,"name":"playlist"},"setIdx":2,"setId":1,"iconIdx":9},{"icon":{"paths":["M386.4 93.333c231.104 0 418.667 187.563 418.667 418.667s-187.563 418.667-418.667 418.667c-43.96 0-85.827-6.699-125.6-19.259 169.979-53.171 293.067-211.845 293.067-399.408s-123.088-346.237-293.067-399.408c39.773-12.56 81.64-19.259 125.6-19.259z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A2"],"defaultCode":58018,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":607,"order":34,"ligatures":"","prevSize":32,"code":58018,"name":"night"},"setIdx":2,"setId":1,"iconIdx":10},{"icon":{"paths":["M512 85.333c235.947 0 426.667 190.72 426.667 426.667s-190.72 426.667-426.667 426.667-426.667-190.72-426.667-426.667 190.72-426.667 426.667-426.667zM725.333 665.173l-153.173-153.173 153.173-153.173-60.16-60.16-153.173 153.173-153.173-153.173-60.16 60.16 153.173 153.173-153.173 153.173 60.16 60.16 153.173-153.173 153.173 153.173z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE38F"],"defaultCode":58255,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":662,"order":50,"ligatures":"","prevSize":32,"code":58255,"name":"cancel"},"setIdx":2,"setId":1,"iconIdx":11},{"icon":{"paths":["M512 170.667c188.587 0 341.333 152.746 341.333 341.333 0 66.987-19.626 129.28-52.906 181.76l-62.294-62.293c19.2-35.414 29.867-76.374 29.867-119.467 0-141.227-114.773-256-256-256v128l-170.667-170.667 170.667-170.666v128zM512 768v-128l170.667 170.667-170.667 170.666v-128c-188.587 0-341.333-152.746-341.333-341.333 0-66.987 19.626-129.28 52.906-181.76l62.294 62.293c-19.2 35.414-29.867 76.374-29.867 119.467 0 141.227 114.773 256 256 256z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE116"],"defaultCode":57622,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":709,"order":17,"ligatures":"","prevSize":32,"code":57622,"name":"sync"},"setIdx":2,"setId":1,"iconIdx":12},{"icon":{"paths":["M384 689.92l451.84-451.413 60.16 60.16-512 512-238.507-238.507 60.587-60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE390"],"defaultCode":58256,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":733,"order":56,"ligatures":"","prevSize":32,"code":58256,"name":"confirm"},"setIdx":2,"setId":1,"iconIdx":13},{"icon":{"paths":["M853.333 370.773l141.227 141.227-141.227 141.227v200.106h-200.106l-141.227 141.227-141.227-141.227h-200.106v-200.106l-141.227-141.227 141.227-141.227v-200.106h200.106l141.227-141.227 141.227 141.227h200.106v200.106zM512 768c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.773-256 256 114.773 256 256 256zM512 341.333c94.293 0 170.667 76.374 170.667 170.667s-76.374 170.667-170.667 170.667-170.667-76.374-170.667-170.667 76.374-170.667 170.667-170.667z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A6"],"defaultCode":58022,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":785,"order":15,"ligatures":"","prevSize":32,"code":58022,"name":"brightness"},"setIdx":2,"setId":1,"iconIdx":14},{"icon":{"paths":["M85.333 725.333v-42.666h128v170.666h-128v-42.666h85.334v-21.334h-42.667v-42.666h42.667v-21.334h-85.334zM128 341.333v-128h-42.667v-42.666h85.334v170.666h-42.667zM85.333 469.333v-42.666h128v38.4l-76.8 89.6h76.8v42.666h-128v-38.4l76.8-89.6h-76.8zM298.667 213.333h597.333v85.334h-597.333v-85.334zM298.667 810.667v-85.334h597.333v85.334h-597.333zM298.667 554.667v-85.334h597.333v85.334h-597.333z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE22D"],"defaultCode":57901,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":797,"order":58,"ligatures":"","prevSize":32,"code":57901,"name":"nodes"},"setIdx":2,"setId":1,"iconIdx":15},{"icon":{"paths":["M810.667 554.667h-256v256h-85.334v-256h-256v-85.334h256v-256h85.334v256h256v85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE18A"],"defaultCode":57738,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":803,"order":59,"ligatures":"","prevSize":32,"code":57738,"name":"add"},"setIdx":2,"setId":1,"iconIdx":16},{"icon":{"paths":["M128 736l471.893-471.893 160 160-471.893 471.893h-160v-160zM883.627 300.373l-78.080 78.080-160-160 78.080-78.080c16.64-16.64 43.52-16.64 60.16 0l99.84 99.84c16.64 16.64 16.64 43.52 0 60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2C6"],"defaultCode":58054,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":834,"order":72,"ligatures":"","prevSize":32,"code":58054,"name":"edit"},"setIdx":2,"setId":1,"iconIdx":17},{"icon":{"paths":["M576 28.587c166.827 133.546 277.333 338.773 277.333 568.746 0 188.587-152.746 341.334-341.333 341.334s-341.333-152.747-341.333-341.334c0-144.213 51.626-276.906 137.813-379.306l-1.28 15.36c0 87.893 66.56 159.146 154.88 159.146 87.893 0 145.493-71.253 145.493-159.146 0-91.734-31.573-204.8-31.573-204.8zM499.627 810.667c113.066 0 204.8-91.734 204.8-204.8 0-59.307-8.534-117.334-25.174-172.374-43.52 58.454-121.6 94.72-197.12 110.080-75.093 15.36-119.893 64-119.893 133.12 0 74.24 61.44 133.974 137.387 133.974z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE409"],"defaultCode":58377,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":871,"order":10,"ligatures":"","prevSize":32,"code":58377,"name":"intensity"},"setIdx":2,"setId":1,"iconIdx":18},{"icon":{"paths":["M938.667 394.24l-232.534 201.813 69.547 299.947-263.68-159.147-263.68 159.147 69.973-299.947-232.96-201.813 306.774-26.027 119.893-282.88 119.893 282.454zM512 657.067l160.853 97.28-42.666-182.614 141.653-122.88-186.88-16.213-72.96-172.373-72.533 171.946-186.88 16.214 141.653 122.88-42.667 182.613z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE410"],"defaultCode":58384,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":927,"order":9,"ligatures":"","prevSize":32,"code":58384,"name":"star"},"setIdx":2,"setId":1,"iconIdx":19},{"icon":{"paths":["M512 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667-426.667-191.147-426.667-426.667 191.147-426.667 426.667-426.667zM554.667 725.333v-256h-85.334v256h85.334zM554.667 384v-85.333h-85.334v85.333h85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE066"],"defaultCode":57446,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":952,"order":62,"ligatures":"","prevSize":32,"code":57446,"name":"info"},"setIdx":2,"setId":1,"iconIdx":20},{"icon":{"paths":["M256 810.667v-512h512v512c0 46.933-38.4 85.333-85.333 85.333h-341.334c-46.933 0-85.333-38.4-85.333-85.333zM810.667 170.667v85.333h-597.334v-85.333h149.334l42.666-42.667h213.334l42.666 42.667h149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE037"],"defaultCode":57399,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":969,"order":55,"ligatures":"","prevSize":32,"code":57399,"name":"del"},"setIdx":2,"setId":1,"iconIdx":21},{"icon":{"paths":["M704 128c131.413 0 234.667 103.253 234.667 234.667 0 161.28-145.067 292.693-364.8 491.946l-61.867 56.32-61.867-55.893c-219.733-199.68-364.8-331.093-364.8-492.373 0-131.414 103.254-234.667 234.667-234.667 74.24 0 145.493 34.56 192 89.173 46.507-54.613 117.76-89.173 192-89.173zM516.267 791.467c203.093-183.894 337.066-305.494 337.066-428.8 0-85.334-64-149.334-149.333-149.334-65.707 0-129.707 42.24-151.893 100.694h-79.787c-22.613-58.454-86.613-100.694-152.32-100.694-85.333 0-149.333 64-149.333 149.334 0 123.306 133.973 244.906 337.066 428.8l4.267 4.266z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE04C"],"defaultCode":57420,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":1034,"order":61,"ligatures":"","prevSize":32,"code":57420,"name":"presets"},"setIdx":2,"setId":1,"iconIdx":22}],"height":1024,"metadata":{"name":"wled122"},"preferences":{"showGlyphs":true,"showCodes":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"i-","metadata":{"fontFamily":"wled122","majorVersion":1,"minorVersion":7},"metrics":{"emSize":1024,"baseline":20,"whitespace":0},"embed":false,"autoHost":true,"noie8":true,"ie7":false,"showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":true},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"name":"icomoon","classSelector":".icon"},"historySize":50,"quickUsageToken":{"wled122":"MDA1MGUxOTY0MyMxNzczNTY5NTMxI0ljcTJCSm9WQnFUUUFOdUZHQzlpaFZ0QkZaOGRFTXFRWFJoU1VjN2VqbmFn"},"showLiga":false,"gridSize":16}} \ No newline at end of file +{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M902.213 804.010l-197.073-167.615c-20.373-18.336-42.161-26.752-59.761-25.941 46.52-54.492 74.622-125.188 74.622-202.455 0-172.313-139.687-312-312-312-172.311 0-312 139.687-312 312s139.687 312 312 312c77.266 0 147.962-28.1 202.455-74.624-0.812 17.6 7.605 39.388 25.941 59.761l167.615 197.073c28.698 31.887 75.58 34.574 104.178 5.976s25.913-75.48-5.974-104.178zM408 616c-114.874 0-208-93.125-208-208s93.125-208 208-208 208 93.125 208 208-93.125 208-208 208z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["search","magnifier","magnifying-glass","inspect","find"],"defaultCode":59782,"grid":16},"attrs":[],"properties":{"ligatures":"search, magnifier","name":"search","id":134,"order":77,"prevSize":32,"code":57505},"setIdx":0,"setId":2,"iconIdx":134},{"icon":{"paths":["M910.398 765.581l-241.236-241.236c-14.934-14.934-39.371-14.934-54.306 0l-18.102 18.102-147.2-147.2 241.646-241.648h-256.001l-113.645 113.645-11.249-11.247h-54.306v54.306l11.247 11.247-164.848 164.849 127.999 127.999 164.848-164.848 147.2 147.2-18.102 18.102c-14.934 14.934-14.934 39.371 0 54.306l241.236 241.236c14.934 14.934 39.371 14.934 54.306 0l90.509-90.509c14.935-14.934 14.935-39.371 0.002-54.306z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["hammer","tool","fix","make","generate","work","build"],"grid":16},"attrs":[{}],"properties":{"order":1,"id":0,"name":"pixelforge","prevSize":32,"code":59648},"setIdx":1,"setId":1,"iconIdx":0},{"icon":{"paths":["M976.272 538.191c0 11.223-7.016 22.448-14.5 30.867l-157.14 185.202c-27.126 31.802-82.311 57.055-123.469 57.055h-508.837c-16.837 0-40.688-5.146-40.688-26.191 0-11.223 7.016-22.448 14.5-30.867l157.14-185.202c27.126-31.802 82.311-57.055 123.469-57.055h508.837c16.837 0 40.688 5.146 40.688 26.191zM815.856 377.307v74.828h-389.112c-58.461 0-130.952 33.208-168.835 78.104l-159.949 188.009c0-3.74-0.467-7.951-0.467-11.691v-448.977c0-57.523 47.233-104.761 104.761-104.761h149.66c57.523 0 104.761 47.233 104.761 104.761v14.968h254.418c57.523 0 104.761 47.233 104.761 104.761z"],"attrs":[{}],"width":1074,"isMulticolor":false,"isMulticolor2":false,"tags":["folder-open"],"grid":14},"attrs":[{}],"properties":{"order":1,"id":1,"prevSize":28,"name":"editor","code":59649},"setIdx":1,"setId":1,"iconIdx":1},{"icon":{"paths":["M511.573 85.333c235.947 0 427.094 191.147 427.094 426.667s-191.147 426.667-427.094 426.667c-235.52 0-426.24-191.147-426.24-426.667s190.72-426.667 426.24-426.667zM512 853.333c188.587 0 341.333-152.746 341.333-341.333s-152.746-341.333-341.333-341.333-341.333 152.746-341.333 341.333 152.746 341.333 341.333 341.333zM661.333 469.333c-35.413 0-64-28.586-64-64s28.587-64 64-64c35.414 0 64 28.587 64 64s-28.586 64-64 64zM362.667 469.333c-35.414 0-64-28.586-64-64s28.586-64 64-64c35.413 0 64 28.587 64 64s-28.587 64-64 64zM512 746.667c-99.413 0-183.893-62.294-218.027-149.334h436.054c-34.134 87.040-118.614 149.334-218.027 149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE23D"],"defaultCode":57917,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":11,"order":26,"prevSize":32,"code":57917,"name":"pattern"},"setIdx":1,"setId":1,"iconIdx":2},{"icon":{"paths":["M511.573 791.040l314.88-244.907 69.547 54.187-384 298.667-384-298.667 69.12-53.76zM512 682.667l-384-298.667 384-298.667 384 298.667-69.973 54.187z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE34B"],"defaultCode":58187,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":14,"order":35,"ligatures":"","prevSize":32,"code":58187,"name":"segments"},"setIdx":1,"setId":1,"iconIdx":3},{"icon":{"paths":["M288.427 206.507l-60.587 60.16-76.373-76.374 60.16-60.16zM170.667 448v85.333h-128v-85.333h128zM554.667 23.467v125.866h-85.334v-125.866h85.334zM872.533 190.293l-76.373 76.374-60.16-60.16 76.373-76.374zM735.573 774.827l59.734-59.734 76.8 76.374-60.16 60.16zM853.333 448h128v85.333h-128v-85.333zM512 234.667c141.227 0 256 114.773 256 256 0 141.226-114.773 256-256 256s-256-114.774-256-256c0-141.227 114.773-256 256-256zM469.333 957.867v-125.867h85.334v125.867h-85.334zM151.467 791.040l76.373-76.8 60.16 60.16-76.373 76.8z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE333"],"defaultCode":58163,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":40,"order":73,"ligatures":"","prevSize":32,"code":58163,"name":"sun"},"setIdx":1,"setId":1,"iconIdx":4},{"icon":{"paths":["M512 128c212.053 0 384 152.747 384 341.333 0 117.76-95.573 213.334-213.333 213.334h-75.52c-35.414 0-64 28.586-64 64 0 16.213 6.4 31.146 16.213 42.24 10.24 11.52 16.64 26.453 16.64 43.093 0 35.413-28.587 64-64 64-212.053 0-384-171.947-384-384s171.947-384 384-384zM277.333 512c35.414 0 64-28.587 64-64s-28.586-64-64-64c-35.413 0-64 28.587-64 64s28.587 64 64 64zM405.333 341.333c35.414 0 64-28.586 64-64 0-35.413-28.586-64-64-64-35.413 0-64 28.587-64 64 0 35.414 28.587 64 64 64zM618.667 341.333c35.413 0 64-28.586 64-64 0-35.413-28.587-64-64-64-35.414 0-64 28.587-64 64 0 35.414 28.586 64 64 64zM746.667 512c35.413 0 64-28.587 64-64s-28.587-64-64-64c-35.414 0-64 28.587-64 64s28.586 64 64 64z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2B3"],"defaultCode":58035,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":75,"order":75,"prevSize":32,"code":58035,"name":"palette"},"setIdx":1,"setId":1,"iconIdx":5},{"icon":{"paths":["M512 192c213.333 0 395.52 132.693 469.333 320-73.813 187.307-256 320-469.333 320s-395.52-132.693-469.333-320c73.813-187.307 256-320 469.333-320zM512 725.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333-213.333 95.573-213.333 213.333 95.573 213.333 213.333 213.333zM512 384c70.827 0 128 57.173 128 128s-57.173 128-128 128-128-57.173-128-128 57.173-128 128-128z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0E8"],"defaultCode":57576,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":172,"order":74,"prevSize":32,"code":57576,"name":"eye"},"setIdx":1,"setId":1,"iconIdx":6},{"icon":{"paths":["M640 42.667v85.333h-256v-85.333h256zM469.333 597.333v-256h85.334v256h-85.334zM811.947 315.307c52.48 65.706 84.053 148.906 84.053 239.36 0 212.053-171.52 384-384 384s-384-171.947-384-384c0-212.054 171.947-384 384-384 90.453 0 173.653 31.573 239.787 84.48l60.586-60.587c21.76 17.92 41.814 38.4 60.16 60.16zM512 853.333c165.12 0 298.667-133.546 298.667-298.666s-133.547-298.667-298.667-298.667-298.667 133.547-298.667 298.667 133.547 298.666 298.667 298.666z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE325"],"defaultCode":58149,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":370,"order":21,"ligatures":"","prevSize":32,"code":58149,"name":"speed"},"setIdx":1,"setId":1,"iconIdx":7},{"icon":{"paths":["M707.84 366.507l60.16 60.16-256 256-256-256 60.16-60.16 195.84 195.413z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE395"],"defaultCode":58261,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":549,"order":69,"ligatures":"","prevSize":32,"code":58261,"name":"expand"},"setIdx":1,"setId":1,"iconIdx":8},{"icon":{"paths":["M554.667 128v426.667h-85.334v-426.667h85.334zM760.747 220.587c82.773 70.4 135.253 174.506 135.253 291.413 0 212.053-171.947 384-384 384s-384-171.947-384-384c0-116.907 52.48-221.013 135.253-291.413l60.16 60.16c-66.986 54.613-110.080 137.813-110.080 231.253 0 165.12 133.547 298.667 298.667 298.667s298.667-133.547 298.667-298.667c0-93.44-43.094-176.64-110.507-230.827z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE08F"],"defaultCode":57487,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":557,"order":19,"ligatures":"","prevSize":32,"code":57487,"name":"power"},"setIdx":1,"setId":1,"iconIdx":9},{"icon":{"paths":["M816.64 551.936l85.504 67.584c8.192 6.144 10.24 16.896 5.12 26.112l-81.92 141.824c-5.12 9.216-15.872 12.8-25.088 9.216l-101.888-40.96c-20.992 15.872-44.032 29.696-69.12 39.936l-15.36 108.544c-1.024 10.24-9.728 17.408-19.968 17.408h-163.84c-10.24 0-18.432-7.168-20.48-17.408l-15.36-108.544c-25.088-10.24-47.616-23.552-69.12-39.936l-101.888 40.96c-9.216 3.072-19.968 0-25.088-9.216l-81.92-141.824c-4.608-8.704-2.56-19.968 5.12-26.112l86.528-67.584c-2.048-12.8-3.072-26.624-3.072-39.936s1.536-27.136 3.584-39.936l-86.528-67.584c-8.192-6.144-10.24-16.896-5.12-26.112l81.92-141.824c5.12-9.216 15.872-12.8 25.088-9.216l101.888 40.96c20.992-15.872 44.032-29.696 69.12-39.936l15.36-108.544c1.536-10.24 9.728-17.408 19.968-17.408h163.84c10.24 0 18.944 7.168 20.48 17.408l15.36 108.544c25.088 10.24 47.616 23.552 69.12 39.936l101.888-40.96c9.216-3.072 19.968 0 25.088 9.216l81.92 141.824c4.608 8.704 2.56 19.968-5.12 26.112l-86.528 67.584c2.048 12.8 3.072 26.112 3.072 39.936s-1.024 27.136-2.56 39.936zM512 665.6c84.48 0 153.6-69.12 153.6-153.6s-69.12-153.6-153.6-153.6-153.6 69.12-153.6 153.6 69.12 153.6 153.6 153.6z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0A2"],"defaultCode":57506,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":562,"order":29,"ligatures":"","prevSize":32,"code":57506,"name":"settings"},"setIdx":1,"setId":1,"iconIdx":10},{"icon":{"paths":["M556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l-130.133-97.707v384l256-192zM469.333 173.653c-62.293 7.68-119.040 32.427-166.4 69.12l-60.586-61.013c63.146-51.627 141.226-85.76 226.986-94.293v86.186zM242.773 302.933c-36.693 47.36-61.44 104.107-69.12 166.4h-86.186c8.533-85.76 42.666-163.84 94.293-226.986zM173.653 554.667c7.68 62.293 32.427 119.040 69.12 165.973l-61.013 61.013c-51.627-63.146-85.76-141.226-94.293-226.986h86.186zM242.347 842.24l60.586-61.013c47.36 36.693 104.107 61.44 166.4 69.12v86.186c-85.333-8.533-163.84-42.666-226.986-94.293zM938.667 512c0 220.16-167.254 401.92-381.867 424.533v-86.186c167.253-22.187 296.533-165.547 296.533-338.347s-129.28-316.16-296.533-338.347v-86.186c214.613 22.613 381.867 204.373 381.867 424.533z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE139"],"defaultCode":57657,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":595,"order":46,"ligatures":"","prevSize":32,"code":57657,"name":"playlist"},"setIdx":1,"setId":1,"iconIdx":11},{"icon":{"paths":["M386.4 93.333c231.104 0 418.667 187.563 418.667 418.667s-187.563 418.667-418.667 418.667c-43.96 0-85.827-6.699-125.6-19.259 169.979-53.171 293.067-211.845 293.067-399.408s-123.088-346.237-293.067-399.408c39.773-12.56 81.64-19.259 125.6-19.259z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A2"],"defaultCode":58018,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":607,"order":34,"ligatures":"","prevSize":32,"code":58018,"name":"night"},"setIdx":1,"setId":1,"iconIdx":12},{"icon":{"paths":["M512 85.333c235.947 0 426.667 190.72 426.667 426.667s-190.72 426.667-426.667 426.667-426.667-190.72-426.667-426.667 190.72-426.667 426.667-426.667zM725.333 665.173l-153.173-153.173 153.173-153.173-60.16-60.16-153.173 153.173-153.173-153.173-60.16 60.16 153.173 153.173-153.173 153.173 60.16 60.16 153.173-153.173 153.173 153.173z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE38F"],"defaultCode":58255,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":662,"order":50,"ligatures":"","prevSize":32,"code":58255,"name":"cancel"},"setIdx":1,"setId":1,"iconIdx":13},{"icon":{"paths":["M512 170.667c188.587 0 341.333 152.746 341.333 341.333 0 66.987-19.626 129.28-52.906 181.76l-62.294-62.293c19.2-35.414 29.867-76.374 29.867-119.467 0-141.227-114.773-256-256-256v128l-170.667-170.667 170.667-170.666v128zM512 768v-128l170.667 170.667-170.667 170.666v-128c-188.587 0-341.333-152.746-341.333-341.333 0-66.987 19.626-129.28 52.906-181.76l62.294 62.293c-19.2 35.414-29.867 76.374-29.867 119.467 0 141.227 114.773 256 256 256z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE116"],"defaultCode":57622,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":709,"order":17,"ligatures":"","prevSize":32,"code":57622,"name":"sync"},"setIdx":1,"setId":1,"iconIdx":14},{"icon":{"paths":["M384 689.92l451.84-451.413 60.16 60.16-512 512-238.507-238.507 60.587-60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE390"],"defaultCode":58256,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":733,"order":56,"ligatures":"","prevSize":32,"code":58256,"name":"confirm"},"setIdx":1,"setId":1,"iconIdx":15},{"icon":{"paths":["M853.333 370.773l141.227 141.227-141.227 141.227v200.106h-200.106l-141.227 141.227-141.227-141.227h-200.106v-200.106l-141.227-141.227 141.227-141.227v-200.106h200.106l141.227-141.227 141.227 141.227h200.106v200.106zM512 768c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.773-256 256 114.773 256 256 256zM512 341.333c94.293 0 170.667 76.374 170.667 170.667s-76.374 170.667-170.667 170.667-170.667-76.374-170.667-170.667 76.374-170.667 170.667-170.667z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A6"],"defaultCode":58022,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":785,"order":15,"ligatures":"","prevSize":32,"code":58022,"name":"brightness"},"setIdx":1,"setId":1,"iconIdx":16},{"icon":{"paths":["M85.333 725.333v-42.666h128v170.666h-128v-42.666h85.334v-21.334h-42.667v-42.666h42.667v-21.334h-85.334zM128 341.333v-128h-42.667v-42.666h85.334v170.666h-42.667zM85.333 469.333v-42.666h128v38.4l-76.8 89.6h76.8v42.666h-128v-38.4l76.8-89.6h-76.8zM298.667 213.333h597.333v85.334h-597.333v-85.334zM298.667 810.667v-85.334h597.333v85.334h-597.333zM298.667 554.667v-85.334h597.333v85.334h-597.333z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE22D"],"defaultCode":57901,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":797,"order":58,"ligatures":"","prevSize":32,"code":57901,"name":"nodes"},"setIdx":1,"setId":1,"iconIdx":17},{"icon":{"paths":["M810.667 554.667h-256v256h-85.334v-256h-256v-85.334h256v-256h85.334v256h256v85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE18A"],"defaultCode":57738,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":803,"order":59,"ligatures":"","prevSize":32,"code":57738,"name":"add"},"setIdx":1,"setId":1,"iconIdx":18},{"icon":{"paths":["M128 736l471.893-471.893 160 160-471.893 471.893h-160v-160zM883.627 300.373l-78.080 78.080-160-160 78.080-78.080c16.64-16.64 43.52-16.64 60.16 0l99.84 99.84c16.64 16.64 16.64 43.52 0 60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2C6"],"defaultCode":58054,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":834,"order":72,"ligatures":"","prevSize":32,"code":58054,"name":"edit"},"setIdx":1,"setId":1,"iconIdx":19},{"icon":{"paths":["M576 28.587c166.827 133.546 277.333 338.773 277.333 568.746 0 188.587-152.746 341.334-341.333 341.334s-341.333-152.747-341.333-341.334c0-144.213 51.626-276.906 137.813-379.306l-1.28 15.36c0 87.893 66.56 159.146 154.88 159.146 87.893 0 145.493-71.253 145.493-159.146 0-91.734-31.573-204.8-31.573-204.8zM499.627 810.667c113.066 0 204.8-91.734 204.8-204.8 0-59.307-8.534-117.334-25.174-172.374-43.52 58.454-121.6 94.72-197.12 110.080-75.093 15.36-119.893 64-119.893 133.12 0 74.24 61.44 133.974 137.387 133.974z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE409"],"defaultCode":58377,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":871,"order":10,"ligatures":"","prevSize":32,"code":58377,"name":"intensity"},"setIdx":1,"setId":1,"iconIdx":20},{"icon":{"paths":["M938.667 394.24l-232.534 201.813 69.547 299.947-263.68-159.147-263.68 159.147 69.973-299.947-232.96-201.813 306.774-26.027 119.893-282.88 119.893 282.454zM512 657.067l160.853 97.28-42.666-182.614 141.653-122.88-186.88-16.213-72.96-172.373-72.533 171.946-186.88 16.214 141.653 122.88-42.667 182.613z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE410"],"defaultCode":58384,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":927,"order":9,"ligatures":"","prevSize":32,"code":58384,"name":"star"},"setIdx":1,"setId":1,"iconIdx":21},{"icon":{"paths":["M512 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667-426.667-191.147-426.667-426.667 191.147-426.667 426.667-426.667zM554.667 725.333v-256h-85.334v256h85.334zM554.667 384v-85.333h-85.334v85.333h85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE066"],"defaultCode":57446,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":952,"order":62,"ligatures":"","prevSize":32,"code":57446,"name":"info"},"setIdx":1,"setId":1,"iconIdx":22},{"icon":{"paths":["M256 810.667v-512h512v512c0 46.933-38.4 85.333-85.333 85.333h-341.334c-46.933 0-85.333-38.4-85.333-85.333zM810.667 170.667v85.333h-597.334v-85.333h149.334l42.666-42.667h213.334l42.666 42.667h149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE037"],"defaultCode":57399,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":969,"order":55,"ligatures":"","prevSize":32,"code":57399,"name":"del"},"setIdx":1,"setId":1,"iconIdx":23},{"icon":{"paths":["M704 128c131.413 0 234.667 103.253 234.667 234.667 0 161.28-145.067 292.693-364.8 491.946l-61.867 56.32-61.867-55.893c-219.733-199.68-364.8-331.093-364.8-492.373 0-131.414 103.254-234.667 234.667-234.667 74.24 0 145.493 34.56 192 89.173 46.507-54.613 117.76-89.173 192-89.173zM516.267 791.467c203.093-183.894 337.066-305.494 337.066-428.8 0-85.334-64-149.334-149.333-149.334-65.707 0-129.707 42.24-151.893 100.694h-79.787c-22.613-58.454-86.613-100.694-152.32-100.694-85.333 0-149.333 64-149.333 149.334 0 123.306 133.973 244.906 337.066 428.8l4.267 4.266z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE04C"],"defaultCode":57420,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":1034,"order":61,"ligatures":"","prevSize":32,"code":57420,"name":"presets"},"setIdx":1,"setId":1,"iconIdx":24}],"height":1024,"metadata":{"name":"wled122"},"preferences":{"showGlyphs":true,"showCodes":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"i-","metadata":{"fontFamily":"wled122","majorVersion":1,"minorVersion":7},"metrics":{"emSize":1024,"baseline":20,"whitespace":0},"embed":false,"autoHost":true,"noie8":true,"ie7":false,"showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":true},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"name":"icomoon","classSelector":".icon"},"historySize":50,"quickUsageToken":{"UntitledProject1":"NmE0ZDcwNGYzYyMxNzc2MDA4Njk4I2J4UGhvcTgwUS9WOElreWNhMkRVS1BlaGlSRGFJVTVqMFR1Ykhqa1lqc1B4"},"showLiga":false,"gridSize":16,"showGrid":true}} \ No newline at end of file diff --git a/wled00/data/icons-ui/style.css b/wled00/data/icons-ui/style.css index 829559fffa..28ad642e32 100644 --- a/wled00/data/icons-ui/style.css +++ b/wled00/data/icons-ui/style.css @@ -1,9 +1,9 @@ @font-face { font-family: 'wled122'; src: - url('fonts/wled122.ttf?yzxblb') format('truetype'), - url('fonts/wled122.woff?yzxblb') format('woff'), - url('fonts/wled122.svg?yzxblb#wled122') format('svg'); + url('fonts/wled122.ttf?2tjc6') format('truetype'), + url('fonts/wled122.woff?2tjc6') format('woff'), + url('fonts/wled122.svg?2tjc6#wled122') format('svg'); font-weight: normal; font-style: normal; font-display: block; @@ -24,6 +24,9 @@ -moz-osx-font-smoothing: grayscale; } +.i-search:before { + content: "\e0a1"; +} .i-pixelforge:before { content: "\e900"; } diff --git a/wled00/data/index.css b/wled00/data/index.css index f3a9f8466a..f1cab8f615 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -1,6 +1,6 @@ @font-face { font-family: "WIcons"; - src: url(data:font/woff2;charset=utf-8;base64,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); + src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAtwAA0AAAAAFzwAAAsYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGhgGYACEHhEICp8QmBQLQAABNgIkA3wEIAWDGweCQBueEhEVqmUn+JmQuYW8aYRI8pyRytrpySK/jJBk9v+nbf19MKDMoI0Jwi77VwXMwsTIUSbEKvy5Veevw/8NG9XGRiobEQNstPsjEh74P+bfFxu1zQc2m+Aa+1eIVHCCRR1PoA82/wK/+v91bBjrxhAh4ktd/2kus/ntTjFbOEASKufInVMvm8JMpgyzruVsD5AssAcF1PIhpKsOUGhW6IGMOOvPDZlh7zJjzjrI26wbrOpZbwBAVfDQuPpH/fDP0TubwXIgpEa+JEjFsKsLM0KLQAQE14LrALAQORH0B0lBAopZoRo0lge+QsIkt7t+gH1ODaQKTBViUo7GA2pIRP8bMf8CyVQwgRDVMu155fOe5wufb3n+5YugFxkvrC9jXua83PHy4MuLo6+NJo1WjC4aXf5K8sr3K+Q2AIDnqTcbt7CFHniq7lfKm8oR5WblLABlM+L3CD7iA8VLxT0AxURRLf9RPiq/DiA/kPvLpQUwrWTSrYAbx6aq0GK5CHxuIQmcHz2lvxChFCIiUFFHKRJF8CoU/zMIYJApEPQffEglRnqtOgL79YqvGCYRhokEXIhkfaAnREw0kRC6CCNxkuxFQzw93OKS1xQKDBMlauQ1UrlcGhaKYZjEUyJRSqViQtRxRbSQ1FPtK/eKlmJsMY5hKgXG5TMXt6G2rDgyAMcjgoMR7poV+WDcbVxqvTsDvAORS+QQM2wZw3diBoGL38dToxuyzsk4nYkHkCVu770sO2Fz5JxyEU0u4z1XZVFkceJWXocPaTOA+92Io/YOLXvTRjscz0HAUTw/CLcyTucgZGFdrosTzUJc0IhwmBFxIUHvjr52qVjv4YUrN7ri+rW113ngEvY7yq4QtoprrqSDyOKsnsOs2IGfciFL4gFn+VXcWnqZj9/HvBpXLrdaRu++oO3foumwpo7cjd2ALMq997KOEraU4Tsx64GTD9xOPzSHXr48kFq2LIhZdmycVzhuXaHidowEmWhkxcpgdsUaZFkbvGorAleulNEr7ZFQBGFbruK2D8tMtL58WSC1fDVwqwLbbK4CKQbOkQsT5J+h+bzToxWcZVyF5zrfbEuz34vbiCwRQ3czj+DW5GO3deuAUwzeyTjcaHMA929BJ7K43rSVXXFcNw8cr6tbuxVZYoecmSM4MCPAAk0h62CVTLUrrnbVpsJvmDE8haaEtPppMQK24RCfXnfKVQLUAy/Ht7py69c7YsiTzrGEyTjsSIGLTv2Ugi+m0KUCTszXkmLkqMCLzykJcKEUeQkJgReLCMA5BNkNdBZQqM90YRZnfcA7YMjsRKx5A0gVV+Zqy/DDG60nkeUUYTsB3HEpdJomq5Px4nk8hLqL46/V3PI1iOqb5TsvEXNoCvA+gyOGRQRLE722LHtdTSzEuazpx3jdQKGtlOucGKNpB9r47WaOh1XHgVstq+BNHM9IQb3tlJAnne14goWMhJH+i7BmugtHUyiGXWmT0cu5OSANzUJCYD5TUGfV5qgybt8d2ptjQRNvkoyxHvBgBjy0U3Vsk5EmGwnAV20Zw46YwcwQ38CaOeKMHWq9mutb0IwI6BjVUp2ddFcP295j7qa66k0xbB3FkDRVy9Kd5touxjilliqq6Dl3au+49vURZaWX8eZYoN7q2qvjAjkH0R2y0Mz6TsrXxBkxwBed54FzzKFMHW2NTGejqZ5mWIahKJqaw6ywBkYxrJQKwkHWp1k6ahkno5fbgtmVlqA53LJAahePS607kGVlMNvvIrxs24EhPeV1lGUFElruEYTtDoBmRUWcO5BS19UMu9dsoRl6xRrnmhVw6lTC5s0/hEUnZq/a2TK1QF+YW1vucrEq1BfkaufOVdmJ0WE/JB0J6V1xUVF2k+n2FRWFXz9ge+bOjc2j6LwEOp0gidFOMOSxVcX2PPi6sclkcH9dXzWmSjVliqrMqK9neChRNQJrYn0xTR8w6D7N0KD56GfoFm40fbWQG1u26hMdqShSGHp7DWUmSb5xxSgUoyJ1n6zKNcYt/MoekjWjWD+tZ6nqYfV6Vl8wTV8c3UwUpE/k53d++613ZKQ5M//jmRTFKj9/5scGAInwr840R072vnlz53n2nIHKnvadXdtKzhD9ptX+JppBtmop95BUD6EKjbGptfPGv2rvjvpGHen9psduz22euz183owM+OfOaudtfcxiC5nCUCGOB0hodycVSuIn5TY05KovAu4+sjS4NGgW9fXXYYYJJwQkgs2Tll9cE6ib0LZa/LtO97t4ddsEHXZMLZVSXp6idgfuZSUPHjY1Da4vSQxv/PjpV9+qUvQ5BsO90RRuz6NkN0dr18R2G6C7f7fZfYqztKOYOTcE6ajlKbX2+u+T439nX3AH1FQVOyF+8gSP9Cq18eB1toItjLiuzUbxrJtjZUZFqbysZ6JhnEFd48l+0dxU09Brys839Zpqav6CjRAjgxyyyC9jhMRXGQCjvDhQmxpuD6dhgrQabZCmjysVs4fui/zjK0yVm7PSVOVwDxjCYJoEYg3r6MxMlYjyTFAMBSXrB33EwwergBt51BdGhYfRYeGGZ0SRAUoa2AWp2bKKqcbQ/EOGstEKsLxkK1SVqomTVF1RBcsSUKLSGmfHPHjwi4GpMre2FhczfAFj+CVwjoGUzo358yjz8uWVzx9XhNqhqJTRewZDjj6l2lYFHyd5T0lnZ4n614H7FFkUZgjrbXzytFgnFn/a1qarDKPCpR+aZ34oDS8K72ArdrzMNX0ZczW/HBhOrd6TyQ1/zHz2mj3OZJgXJ/SNXa6NG9mxadMvB6uqBODKrr7W2XlcDVFJRgPdjwYGhE51s4pqFIYp02xeCyJI0uChfBZZNaQuu9fc9W+uaJY22C59cruX1jNmkuyeptdP7/YRZsaCtemeaTcDyn17ZeW1qByao9275jJzl+4YHPaKyooP+lkdj1rKxVlUdJ5FFukzcvTe5vNQ8FrLIT2wg+Su9wXBHdX+1ZDSpyF/UXLmx94E9Lru5wr5wlWPTdg1F4G+i3Nn7e7ff38Nzt1+MY3bK1eBcUn9Iia2dJE/GUAyTa3gYUkJW03vYqnvva9uXWv7zbugvrmgoAYPq0j5MWA/3poasufX3zQxBH3lnzdTbbNzDclpZDVkvh758opB67FhwgnhTwydEWYPVbWpTXN4/uwRn9KB0v5hn3mzo49A+rLgx5/2qPb4LVny44+wvz+Smtq/bWtfH6Mi/OKgfvqxX9VvMo2oRiAn4J4LDeuGS0pUyv+kIz4Swy7FJWNuxXfjpooPdW3a/RvtQpHwjPaJdgVn+vw/rrjo/8rvSa6/ewdbfbJ/8yQw2NwGgrtZOCqMnqQnanm2tqWDAGsBIGWJKym6KTP0RAMwI+FgRhNHbTSJtNS/CdZpJCqQYkMOJW0qh9JiktoPV/fkRGhP1FClRpLdCCiqvQ0BwmoRD74QCVgUCVkeYax+uAhhphDHbFklLszd3/EoS8g7ASMBCyEhiyeMlQ4XCdImxMY+6Awu2TqY4S3duiRIlAhmbHd3JSReou4oNJuWqm03GfxIeUvA2xpaC6a/l8mmeN0E71BJECsNXL7sYUJsWhCy2KuYOgtzTr4TIG/XlhdY4x1mUSnTaYIqtrCiWHDPZKW9+aKyzgl/7/MoAAQUd6oEKIIgCEEYMCRCYuSBPJFkA/DIpMgLeSMf5Iv8kD8KQIFIhoJQMApBoSgMhSM5UnjIae98IFEfwn4XmrpvPULP5216LPpmNzJ0VXR2N7PD5G3zhL4UvaObbOiS7kvP5y2Ejm6OiiZiCjYBAAAA); } :root { From 1e42ebf852a9a10b1ea375e25684ed976d886996 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 12 Apr 2026 19:49:45 +0200 Subject: [PATCH 21/71] mask out flag bits in DDP type for compatibility --- wled00/e131.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 3a3f6f3b5e..6846e5124e 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -22,7 +22,8 @@ static void handleDDPPacket(e131_packet_t* p) { int lastPushSeq = e131LastSequenceNumber[0]; // reject unsupported color data types (only RGB and RGBW are supported) - if (p->dataType != DDP_TYPE_RGB24 && p->dataType != DDP_TYPE_RGBW32) return; + uint8_t maskedType = p->dataType & 0x3F; // mask out custom and reserved flags, only type bits are relevant + if (maskedType != DDP_TYPE_RGB24 && maskedType != DDP_TYPE_RGBW32) return; // reject status and config packets (not implemented) if (p->destination == DDP_ID_STATUS || p->destination == DDP_ID_CONFIG) return; From c1e5ac5bd18a6cec6726d47492e73f5b30472336 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 17 Apr 2026 15:26:57 +0200 Subject: [PATCH 22/71] bugfixes in PS effects - fix PS Sparkler for large setups: need 32bit random position, 16bit is not enough - fix PS Fireworks 1D: need to `break` if no particles are available or it can lead to stalls on large setups - do not use collisions by default PS Fuzzy Noise: its very slow on larger setups --- wled00/FX.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 18d61b2167..8630d1a53a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8730,7 +8730,7 @@ void mode_particleperlin(void) { PartSys->update(); // update and render } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5"; /* Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with @@ -9836,6 +9836,7 @@ void mode_particleFireworks1D(void) { PartSys->setColorByPosition(false); // disable for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if (idx < 0) break; // no more particles available if(SEGMENT.custom3 > 23) { if(SEGMENT.custom3 == 31) { // highest slider value PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled @@ -9860,7 +9861,7 @@ void mode_particleFireworks1D(void) { PartSys->applyFriction(1); // apply friction to all particles PartSys->update(); // update and render - + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; @@ -9910,7 +9911,7 @@ void mode_particleSparkler(void) { PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; // color saturation if (SEGMENT.speed == 255) // random position at highest speed setting - PartSys->sources[i].source.x = hw_random16(PartSys->maxX); + PartSys->sources[i].source.x = hw_random(PartSys->maxX); else PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler } From 83dd1b078dad57752e02d937eb43b638dd8060d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:36:25 +0200 Subject: [PATCH 23/71] Merge pull request #5516 from kilrah/fix_dmx_ident Add identifier string for DMX realtime mode --- wled00/json.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/json.cpp b/wled00/json.cpp index a8b37d3757..366c74e6d4 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -761,6 +761,7 @@ void serializeInfo(JsonObject root) case REALTIME_MODE_ARTNET: root["lm"] = F("Art-Net"); break; case REALTIME_MODE_TPM2NET: root["lm"] = F("tpm2.net"); break; case REALTIME_MODE_DDP: root["lm"] = F("DDP"); break; + case REALTIME_MODE_DMX: root["lm"] = F("DMX"); break; } root[F("lip")] = realtimeIP[0] == 0 ? "" : realtimeIP.toString(); From e6077acb789b5e6b85cd7aa409f6f3a53a27439a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:19:21 +0200 Subject: [PATCH 24/71] Update rules for PR descriptions and change logs Added a guideline to prevent agents from overwriting PR descriptions (see https://github.com/orgs/community/discussions/187027). --- AGENTS.md | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..634fbb2176 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,184 @@ +# AGENTS.md — WLED Coding Agent Reference + +WLED is C++ firmware for ESP32/ESP8266 microcontrollers controlling addressable LEDs, +with a web UI (HTML/JS/CSS). Built with PlatformIO (Arduino framework) and Node.js tooling. + +See also: `.github/copilot-instructions.md`, `.github/agent-build.instructions.md`, +`docs/cpp.instructions.md`, `docs/web.instructions.md`, `docs/cicd.instructions.md`. + +## Build Commands + +| Command | Purpose | Timeout | +|---|---|---| +| `npm ci` | Install Node.js deps (required first) | 30s | +| `npm run build` | Build web UI into `wled00/html_*.h` / `wled00/js_*.h` | 30s | +| `npm test` | Run test suite (Node.js built-in `node --test`) | 2 min | +| `npm run dev` | Watch mode — auto-rebuilds web UI on changes | continuous | +| `pio run -e esp32dev` | Build firmware (ESP32, most common target) | 30 min | +| `pio run -e nodemcuv2` | Build firmware (ESP8266) | 30 min | + +**Always run `npm ci && npm run build` before `pio run`.** The web UI build generates +required C headers for firmware compilation. + +### Running a Single Test + +Tests use Node.js built-in test runner (`node:test`). The single test file is +`tools/cdata-test.js`. Run it with: + +```sh +npm test # runs all tests via `node --test` +node --test tools/cdata-test.js # run just that file directly +``` + +There are no C++ unit tests. Firmware is validated by successful compilation across +target environments. Always build after code changes: `pio run -e esp32dev`. + +### Common Firmware Environments + +`esp32dev`, `nodemcuv2`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi`, `lolin_s2_mini` + +### Recovery / Troubleshooting + +```sh +npm run build -- -f # force web UI rebuild +rm -f wled00/html_*.h wled00/js_*.h && npm run build # clean + rebuild UI +pio run --target clean # clean PlatformIO build artifacts +rm -rf node_modules && npm ci # reinstall Node.js deps +``` + +## Project Structure + +``` +wled00/ # Main firmware source (C++) + data/ # Web UI source (HTML/JS/CSS) — tabs for indentation + html_*.h, js_*.h # Auto-generated (NEVER edit or commit) + src/ # Sub-modules: fonts, bundled dependencies (ArduinoJSON) +usermods/ # Community usermods (each has library.json + .cpp/.h) +platformio.ini # Build configuration and environments +pio-scripts/ # PlatformIO build scripts (Python) +tools/ # Node.js build tools (cdata.js) and tests +docs/ # Coding convention docs +.github/workflows/ # CI/CD (GitHub Actions) +``` + +## C++ Code Style (wled00/, usermods/) + +### Formatting +- **2-space indentation** (no tabs in C++ files) +- K&R brace style preferred (opening brace on same line) +- Single-statement `if` bodies may omit braces: `if (a == b) doStuff(a);` +- Space after keywords (`if (...)`, `for (...)`), no space before function parens (`doStuff(a)`) +- No enforced line-length limit + +### Naming Conventions +| Kind | Convention | Examples | +|---|---|---| +| Functions, variables | camelCase | `setRandomColor()`, `effectCurrent` | +| Classes, structs | PascalCase | `BusConfig`, `UsermodTemperature` | +| Macros, constants | UPPER_CASE | `WLED_MAX_USERMODS`, `FX_MODE_STATIC` | +| Private members | _camelCase | `_type`, `_bri`, `_len` | +| Enum values | PascalCase | `PinOwner::BusDigital` | + +### Includes +- Include `"wled.h"` as the primary project header +- Project headers first, then platform/Arduino, then third-party +- Platform-conditional includes wrapped in `#ifdef ARDUINO_ARCH_ESP32` / `#ifdef ESP8266` + +### Types and Const +- Prefer `const &` for read-only function parameters +- Mark getter/query methods `const`; use `static` for methods not accessing instance state +- Prefer `constexpr` over `#define` for compile-time constants when possible +- Use `static_assert` over `#if ... #error` +- Use `uint_fast16_t` / `uint_fast8_t` in hot-path code + +### Error Handling +- **No C++ exceptions** — some builds disable them +- Use return codes (`false`, `-1`) and global flags (`errorFlag = ERR_LOW_MEM`) +- Use early returns as guard clauses: `if (!enabled || (strip.isUpdating() && (millis() - last_time < MAX_USERMOD_DELAY))) return;` +- Debug output: `DEBUG_PRINTF()` / `DEBUG_PRINTLN()` (compiled out unless `-D WLED_DEBUG`) + +### Strings and Memory +- Use `F("string")` for string constants (saves RAM on ESP8266) +- Use `PSTR()` with `DEBUG_PRINTF_P()` for format strings +- Avoid `String` in hot paths; acceptable in config/setup code +- Use `d_malloc()` (DRAM-preferred) / `p_malloc()` (PSRAM-preferred) for allocation +- No VLAs — use fixed arrays or heap allocation +- Call `reserve()` on strings/vectors to pre-allocate and avoid fragmentation + +### Preprocessor / Feature Flags +- Feature toggling: `WLED_DISABLE_*` and `WLED_ENABLE_*` flags (exact names matter!) +- `WLED_DISABLE_*`: `2D`, `ADALIGHT`, `ALEXA`, `MQTT`, `OTA`, `INFRARED`, `WEBSOCKETS`, etc. +- `WLED_ENABLE_*`: `DMX`, `GIF`, `HUB75MATRIX`, `JSONLIVE`, `WEBSOCKETS`, etc. +- Platform: `ARDUINO_ARCH_ESP32`, `ESP8266`, `CONFIG_IDF_TARGET_ESP32S3` + +### Comments +- `//` for inline (always space after), `/* */` for block comments +- AI-generated blocks: mark with `// AI: below section was generated by an AI` / `// AI: end` + +### Math Functions +- Use `sin8_t()`, `cos8_t()` — NOT `sin8()`, `cos8()` (removed, won't compile) +- Use `sin_approx()` / `cos_approx()` instead of `sinf()` / `cosf()` +- Replace `inoise8` / `inoise16` with `perlin8` / `perlin16` + +### Hot-Path Code (Pixel Pipeline) +- Use function attributes: `IRAM_ATTR`, `WLED_O2_ATTR`, `__attribute__((hot))` +- Cache class members to locals before loops +- Pre-compute invariants outside loops; use reciprocals to avoid division +- Unsigned range checks: `if ((uint_fast16_t)(pix - start) < len)` + +### ESP32 Tasks +- `delay(1)` in custom FreeRTOS tasks (NOT `yield()`) — feeds IDLE watchdog +- Do not use `delay()` in effects (FX.cpp) or hot pixel path + +## Web UI Code Style (wled00/data/) + +- **Tab indentation** for HTML, JS, and CSS +- camelCase for JS functions/variables +- Reuse helpers from `common.js` — do not duplicate utilities +- After editing, run `npm run build` to regenerate headers +- **Never edit** `wled00/html_*.h` or `wled00/js_*.h` directly + +## Usermod Pattern + +Usermods live in `usermods//` with a `.cpp`, optional `.h`, `library.json`, and `readme.md`. + +```cpp +class MyUsermod : public Usermod { + private: + bool enabled = false; + static const char _name[]; + public: + void setup() override { /* ... */ } + void loop() override { /* ... */ } + void addToConfig(JsonObject& root) override { /* ... */ } + bool readFromConfig(JsonObject& root) override { /* ... */ } + uint16_t getId() override { return USERMOD_ID_MYMOD; } +}; +const char MyUsermod::_name[] PROGMEM = "MyUsermod"; +static MyUsermod myUsermod; +REGISTER_USERMOD(myUsermod); +``` + +- Add usermod IDs to `wled00/const.h` +- Activate via `custom_usermods` in platformio build config +- Base new usermods on `usermods/EXAMPLE/` (never edit the example directly) +- Store repeated strings as `static const char[] PROGMEM` + +## CI/CD + +CI runs on every push/PR via GitHub Actions (`.github/workflows/wled-ci.yml`): +1. `npm test` (web UI build validation) +2. Firmware compilation for all default environments (~22 targets) +3. Post-link validation of usermod linkage (`validate_modules.py`) + +No automated linting is configured. Match existing code style in files you edit. + +## General Rules + +- Repository language is English +- Never edit or commit auto-generated `wled00/html_*.h` / `wled00/js_*.h` +- When updating an existing PR, retain the original description. Only modify it to ensure technical accuracy. Add change logs after the existing description. +- No force-push on open PRs +- Changes to `platformio.ini` require maintainer approval +- Remove dead/unused code — justify or delete it +- Verify feature-flag spelling exactly (misspellings are silently ignored by preprocessor) From 79fde56b0e7df02d6c40a78ad3d99450198d72a9 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:27:32 +0200 Subject: [PATCH 25/71] align AI instructions with ``main`` branch some files were a bit behind - make sure the same instructions apply everywhere. --- .coderabbit.yaml | 113 ++++ .github/agent-build.instructions.md | 120 ++++ .github/copilot-instructions.md | 285 ++++----- CONTRIBUTING.md | 18 +- docs/cicd.instructions.md | 162 ++++++ docs/cpp.instructions.md | 526 +++++++++++++++++ docs/esp-idf.instructions.md | 859 ++++++++++++++++++++++++++++ docs/web.instructions.md | 30 + 8 files changed, 1948 insertions(+), 165 deletions(-) create mode 100644 .coderabbit.yaml create mode 100644 .github/agent-build.instructions.md create mode 100644 docs/cicd.instructions.md create mode 100644 docs/cpp.instructions.md create mode 100644 docs/esp-idf.instructions.md create mode 100644 docs/web.instructions.md diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000000..5be93fa226 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,113 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# +# CodeRabbit configuration — references existing guideline files to avoid +# duplicating conventions. See: +# .github/copilot-instructions.md — project overview & general rules +# docs/cpp.instructions.md — C++ coding conventions +# docs/web.instructions.md — Web UI coding conventions +# docs/cicd.instructions.md — GitHub Actions / CI-CD conventions +# +# NOTE: This file must be committed (tracked by git) for CodeRabbit to read +# it from the repository. If it is listed in .gitignore, CodeRabbit will +# not see it and these settings will have no effect. + +language: en-US + +reviews: + # generic review setting, see https://docs.coderabbit.ai/reference/configuration#reference + auto_apply_labels: true + # abort_on_close: false + high_level_summary: true + review_status: true + collapse_walkthrough: false + poem: false + # sequence_diagrams: false + auto_review: + enabled: true + ignore_title_keywords: + - WIP + + path_instructions: + - path: "**/*.{cpp,h,hpp,ino}" + instructions: > + Follow the C++ coding conventions documented in docs/cpp.instructions.md + and the general project guidelines in .github/copilot-instructions.md. + + Key rules: 2-space indentation (no tabs), camelCase functions/variables, + PascalCase classes, UPPER_CASE macros. No C++ exceptions — use return codes and debug macros. + + Hot-path optimization guidelines (attributes, uint_fast types, caching, + unsigned range checks) apply from pixel set/get operations and strip.show() downward — + NOT to effect functions in FX.cpp, which have diverse contributor styles. +# disabled - the below instruction has no effect +# When initially reviewing a PR, summarize good practices (top 5) and create a prioritized list of suggested improvements (focus on major ones). + + - path: "wled00/data/**" + instructions: > + Follow the web UI conventions documented in docs/web.instructions.md. + + Key rules: indent HTML and JavaScript with tabs, CSS with tabs. + Files here are built into wled00/html_*.h and wled00/js_*.h by tools/cdata.js — never + edit those generated headers directly. +# disabled - the below instruction has no effect +# When initially reviewing a PR, summarize good practices (top 5) and create a prioritized list of suggested improvements (focus on major ones). + + - path: "wled00/html_*.h" + instructions: > + These files are auto-generated from wled00/data/ by tools/cdata.js. + They must never be manually edited or committed. Flag any PR that + includes changes to these files. + + - path: "wled00/js_*.h" + instructions: > + These files are auto-generated from wled00/data/ by tools/cdata.js. + They must never be manually edited or committed. Flag any PR that + includes changes to these files. + + - path: "usermods/**" + instructions: > + Usermods are community add-ons. + Each usermod lives in its own directory under usermods/ and is implemented + as a .cpp file with a dedicated library.json file to manage dependencies. + Follow the same C++ conventions as the core firmware (docs/cpp.instructions.md). +# disabled - the below instruction has no effect +# When initially reviewing a PR, summarize good practices (top 6) and create a prioritized list of suggested improvements (skip minor ones). + + - path: ".github/workflows/*.{yml,yaml}" + instructions: > + Follow the CI/CD conventions documented in docs/cicd.instructions.md. + + Key rules: 2-space indentation, descriptive name: on every workflow/job/step. + Third-party actions must be pinned to a specific version tag — branch pins + such as @main or @master are not allowed. Declare explicit permissions: blocks + scoped to least privilege. Never interpolate github.event.* values directly + into run: steps — pass them through an env: variable to prevent script + injection. Do not use pull_request_target unless fully justified. +# disabled - the below instruction has no effect +# When initially reviewing a PR, summarize good practices (top 6) and create a prioritized list of suggested improvements. + + - path: "**/*.instructions.md" + instructions: | + This file contains both AI-facing rules and human-only reference sections. + Human-only sections are enclosed in `` / + `` HTML comment markers and should not be used as + actionable review criteria. + + When this file is modified in a PR, perform the following alignment check: + 1. For each ` ... ` block, + verify that its examples and guidance are consistent with (and do not + contradict) the AI-facing rules stated in the same file. + 2. Flag any HUMAN_ONLY section whose content has drifted from the surrounding + AI-facing rules due to edits introduced in this PR. + 3. If new AI-facing rules were added without updating a related HUMAN_ONLY + reference section, note this as a suggestion (not a required fix). + + finishing_touches: + # Docstrings | Options for generating Docstrings for your PRs/MRs. + docstrings: + # Docstrings | Allow CodeRabbit to generate docstrings for PRs/MRs. + # default: true - disabled in WLED: has caused confusion in the past + enabled: false + unit_tests: + # default: true - disabled in WLED: we don't have a unit test framework, this option just confuses contributors + enabled: false diff --git a/.github/agent-build.instructions.md b/.github/agent-build.instructions.md new file mode 100644 index 0000000000..c7854fad16 --- /dev/null +++ b/.github/agent-build.instructions.md @@ -0,0 +1,120 @@ +--- +applyTo: "**" +--- +# Agent-Mode Build & Test Instructions + +Detailed build workflow, timeouts, and troubleshooting for making code changes in agent mode. Always reference these instructions first when running builds or validating changes. + +## Build Timing and Timeouts + +Use these timeout values when running builds: + +| Command | Typical Time | Minimum Timeout | Notes | +|---|---|---|---| +| `npm run build` | ~3 s | 30 s | Web UI → `wled00/html_*.h` `wled00/js_*.h` headers | +| `npm test` | ~40 s | 2 min | Validates build system | +| `npm run dev` | continuous | — | Watch mode, auto-rebuilds on changes | +| `pio run -e ` | 15–20 min | 30 min | First build downloads toolchains; subsequent builds are faster | + +**NEVER cancel long-running builds.** PlatformIO downloads and compilation require patience. + +## Development Workflow + +### Code Style Summary +- **C++** files in `wled00/` and `usermods/`: 2-space indentation (no tabs), camelCase functions/variables, PascalCase classes, UPPER_CASE macros. No C++ exceptions — use return codes and debug macros. +- **Web UI** files in `wled00/data`: indent HTML and JavaScript with tabs, CSS with tabs. +- **CI/CD workflows** in `.github/workflows`: 2-space indentation, descriptive `name:` on every workflow/job/step. Third-party actions must be pinned to a specific version tag — branch pins such as `@main` or `@master` are not allowed. SHA pinning recommended. + +### Web UI Changes + +1. Edit files in `wled00/data/` +2. Run `npm run build` to regenerate `wled00/html_*.h` `wled00/js_*.h` headers +3. Test with local HTTP server (see Manual Testing below) +4. Run `npm test` to validate + +### Firmware Changes + +1. Edit files in `wled00/` (but **never** `html_*.h` and `js_*.h` files) +2. Ensure web UI is built first: `npm run build` +3. Build firmware: `pio run -e esp32dev` (set timeout ≥ 30 min) +4. Flash to device: `pio run -e [target] --target upload` + +### Combined Web + Firmware Changes + +1. Always build web UI first +2. Test web interface manually +3. Then build and test firmware + + +## Before Finishing Work - Testing + +**You MUST complete ALL of these before marking work as done:** + +1. **Run tests**: `npm test` — must pass +2. **Build firmware**: `pio run -e esp32dev` — must succeed after source code changes, **never skip this step**. + - Set timeout to 30+ minutes, **never cancel** + - Choose `esp32dev` as a common, representative environment + - If the build fails, fix the issue before proceeding +3. **For web UI changes**: manually test the interface (see below) + +If any step fails, fix the issue. **Do NOT mark work complete with failing builds or tests.** + +## Manual Web UI Testing + +Start a local server: + +```sh +cd wled00/data && python3 -m http.server 8080 +# Open http://localhost:8080/index.htm +``` + +Test these scenarios after every web UI change: + +- **Load**: `index.htm` loads without JavaScript errors (check browser console) +- **Navigation**: switching between main page and settings pages works +- **Color controls**: color picker and brightness controls function correctly +- **Effects**: effect selection and parameter changes work +- **Settings**: form submission and validation work + +## Troubleshooting + +### Common Build Issues + +| Problem | Solution | +|---|---| +| Missing `html_*.h` | Run `npm ci; npm run build` | +| Web UI looks broken | Check browser console for JS errors | +| PlatformIO network errors | Retry — downloads can be flaky | +| Node.js version mismatch | Ensure Node.js 20+ (check `.nvmrc`) | + +### Recovery Steps + +- **Force web UI rebuild**: `npm run build -- -f` +- **Clear generated files**: `rm -f wled00/html_*.h wled00/js_*.h` then `npm run build` +- **Clean PlatformIO build artifacts**: `pio run --target clean` +- **Reinstall Node deps**: `rm -rf node_modules && npm ci` + +## CI/CD Validation + +The GitHub Actions CI workflow will: +1. Install Node.js and Python dependencies +2. Run `npm test` +3. Build web UI (automatic via PlatformIO) +4. Compile firmware for **all** `default_envs` targets + +**To ensure CI success, always validate locally:** +- Run `npm test` and ensure it passes +- Run `pio run -e esp32dev` (or another common firmware environment, see next section) and ensure it completes successfully +- If either fails locally, it WILL fail in CI + +Match this workflow in local development to catch failures before pushing. + +## Important Reminders + +- Always **commit source code** +-  Every pull request MUST include a clear description of *what* changed and *why*. +- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/` +- After modifying source code files, check that any **previous comments have been preserved** or updated to reflect the new behaviour. +- Web UI rebuild is part of the PlatformIO firmware compilation pipeline +- Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi` +- List all PlatformIO targets: `pio run --list-targets` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9952454001..0360a209d9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,175 +4,136 @@ WLED is a fast and feature-rich implementation of an ESP32 and ESP8266 webserver Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. -## Working Effectively - -### Initial Setup -- Install Node.js 20+ (specified in `.nvmrc`): Check your version with `node --version` -- Install dependencies: `npm ci` (takes ~5 seconds) -- Install PlatformIO for hardware builds: `pip install -r requirements.txt` (takes ~60 seconds) - -### Build and Test Workflow -- **ALWAYS build web UI first**: `npm run build` -- takes 3 seconds. NEVER CANCEL. -- **Run tests**: `npm test` -- takes 40 seconds. NEVER CANCEL. Set timeout to 2+ minutes. -- **Development mode**: `npm run dev` -- monitors file changes and auto-rebuilds web UI -- **Hardware firmware build**: `pio run -e [environment]` -- takes 15+ minutes. NEVER CANCEL. Set timeout to 30+ minutes. - -### Build Process Details -The build has two main phases: -1. **Web UI Generation** (`npm run build`): - - Processes files in `wled00/data/` (HTML, CSS, JS) - - Minifies and compresses web content - - Generates `wled00/html_*.h` files with embedded web content - - **CRITICAL**: Must be done before any hardware build - -2. **Hardware Compilation** (`pio run`): - - Compiles C++ firmware for various ESP32/ESP8266 targets - - Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m` - - List all targets: `pio run --list-targets` - -## Before Finishing Work - -**CRITICAL: You MUST complete ALL of these steps before marking your work as complete:** - -1. **Run the test suite**: `npm test` -- Set timeout to 2+ minutes. NEVER CANCEL. - - All tests MUST pass - - If tests fail, fix the issue before proceeding - -2. **Build at least one hardware environment**: `pio run -e esp32dev` -- Set timeout to 30+ minutes. NEVER CANCEL. - - Choose `esp32dev` as it's a common, representative environment - - See "Hardware Compilation" section above for the full list of common environments - - The build MUST complete successfully without errors - - If the build fails, fix the issue before proceeding - - **DO NOT skip this step** - it validates that firmware compiles with your changes - -3. **For web UI changes only**: Manually test the interface - - See "Manual Testing Scenarios" section below - - Verify the UI loads and functions correctly - -**If any of these validation steps fail, you MUST fix the issues before finishing. Do NOT mark work as complete with failing builds or tests.** - -## Validation and Testing - -### Web UI Testing -- **ALWAYS validate web UI changes manually**: - - Start local server: `cd wled00/data && python3 -m http.server 8080` - - Open `http://localhost:8080/index.htm` in browser - - Test basic functionality: color picker, effects, settings pages -- **Check for JavaScript errors** in browser console - -### Code Validation -- **No automated linting configured** - follow existing code style in files you edit -- **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files -- **Language**: The repository language is English (british, american, canadian, or australian). If you find other languages, suggest a translation into English. -- **C++ formatting available**: `clang-format` is installed but not in CI -- **Always run tests before finishing**: `npm test` -- **MANDATORY: Always run a hardware build before finishing** (see "Before Finishing Work" section below) +> **Note for AI review tools**: sections enclosed in +> `` / `` HTML comments contain +> contributor reference material. Do **not** use that content as actionable review +> criteria — treat it as background context only. -### Manual Testing Scenarios -After making changes to web UI, always test: -- **Load main interface**: Verify index.htm loads without errors -- **Navigation**: Test switching between main page and settings pages -- **Color controls**: Verify color picker and brightness controls work -- **Effects**: Test effect selection and parameter changes -- **Settings**: Test form submission and validation +## Setup -## Common Tasks +- Node.js 20+ (see `.nvmrc`) +- Install dependencies: `npm ci` +- PlatformIO (required only for firmware compilation): `pip install -r requirements.txt` -### Repository Structure +## Build and Test + + +| Command | Purpose | Typical Time | +|---|---|---| +| `npm run build` | Build web UI → generates `wled00/html_*.h` and `wled00/js_*.h` headers | ~3 s | +| `npm test` | Run test suite | ~40 s | +| `npm run dev` | Watch mode — auto-rebuilds web UI on file changes | — | +| `pio run -e ` | Build firmware for a hardware target | 15–20 min | + + + +- **Always run `npm run build` before any `pio run`** (and run `npm ci` first on fresh clones or when lockfile/dependencies change). +- The web UI build generates required `wled00/html_*.h` and `wled00/js_*.h` headers for firmware compilation. +- **Build firmware to validate code changes**: `pio run -e esp32dev` — must succeed, never skip this step. +- Common firmware environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`, `esp32c3dev`, `esp32s3dev_8MB_opi` + +For detailed build timeouts, development workflows, troubleshooting, and validation steps, see [agent-build.instructions.md](agent-build.instructions.md). + +### Usermod Guidelines + + - New custom effects can be added into the user_fx usermod. Read the [user_fx documentation](https://github.com/wled/WLED/blob/main/usermods/user_fx/README.md) for guidance. + - Other usermods may be based on the [EXAMPLE usermod](https://github.com/wled/WLED/tree/main/usermods/EXAMPLE). Never edit the example, always create a copy! + - New usermod IDs can be added into [wled00/const.h](https://github.com/wled/WLED/blob/main/wled00/const.h#L160). + - To activate a usermod, a custom build configuration should be used. Add the usermod name to `custom_usermods`. + +## Project Structure Overview + +### Project Branch / Release Structure + + +```text +main # Main development trunk (daily/nightly) 17.0.0-dev + ├── V5 # special branch: code rework for esp-idf 5.5.x (unstable) + ├── V5-C6 # special branch: integration of new MCU types: esp32-c5, esp32-c6, esp32-p4 (unstable) +16_x # current beta, preparations for next release 16.0.0 +0_15_x # maintenance (bugfixes only) for current release 0.15.4 +(tag) v0.14.4 # previous version 0.14.4 (no maintenance) +(tag) v0.13.3 # old version 0.13.3 (no maintenance) +(tag) v0. ... . ... # historical versions 0.12.x and before ``` -wled00/ # Main firmware source (C++) - ├── data/ # Web interface files - │ ├── index.htm # Main UI + + +- ``main``: development trunk (daily/nightly) +- ``V5`` and ``V5-C6``: code rework for esp-idf 5.5.x (unstable) - branched from ``main``. +- ``0_15_x``: bugfixing / maintenance for release 0.15.x + +### Repository Structure + +tl;dr: +* Firmware source: `wled00/` (C++). Web UI source: `wled00/data/`. Build targets: `platformio.ini`. +* Auto-generated headers: `wled00/html_*.h` and `wled00/js_*.h` — **never edit or commit**. +* ArduinoJSON + AsyncJSON: `wled00/src/dependencies/json` (included via `wled.h`). CI/CD: `.github/workflows/`. +* Usermods: `usermods/` (C++, with individual library.json). +* Contributor docs: `docs/` (coding guidelines, etc). + + +Detailed overview: + +```text +wled00/ # Main firmware source (C++) "WLED core" + ├── data/ # Web interface files + │ ├── index.htm # Main UI │ ├── settings*.htm # Settings pages - │ └── *.js/*.css # Frontend resources - ├── *.cpp/*.h # Firmware source files - └── html_*.h # Auto-generated embedded web files (DO NOT EDIT, DO NOT COMMIT) -tools/ # Build tools (Node.js) + │ └── *.js/*.css # Frontend resources + ├── *.cpp/*.h # Firmware source files + ├── html_*.h # Auto-generated embedded web files (DO NOT EDIT, DO NOT COMMIT) + ├── src/ # Modules used by the WLED core (C++) + │ ├── fonts/ # Font libraries for scrolling text effect + └ └── dependencies/ # Utility functions - some of them have their own licensing terms +lib/ # Project specific custom libraries. PlatformIO will compile them to separate static libraries and link them +platformio.ini # Hardware build configuration + +platformio_override.sample.ini # examples for custom build configurations - entries must be copied into platformio_override.ini to use them. + # platformio_override.ini is _not_ stored in the WLED repository! +usermods/ # User-contributed addons to the WLED core, maintained by individual contributors (C++, with individual library.json) +package.json # Node.js dependencies and scripts, release identification +pio-scripts/ # Build tools (PlatformIO) +tools/ # Build tools (Node.js), partition files, and generic utilities ├── cdata.js # Web UI build script └── cdata-test.js # Test suite -platformio.ini # Hardware build configuration -package.json # Node.js dependencies and scripts +docs/ # Contributor docs, coding guidelines .github/workflows/ # CI/CD pipelines ``` -### Key Files and Their Purpose -- `wled00/data/index.htm` - Main web interface -- `wled00/data/settings*.htm` - Configuration pages -- `tools/cdata.js` - Converts web files to C++ headers -- `wled00/wled.h` - Main firmware configuration -- `platformio.ini` - Hardware build targets and settings - -### Development Workflow (applies to agent mode only) -1. **For web UI changes**: - - Edit files in `wled00/data/` - - Run `npm run build` to regenerate headers - - Test with local HTTP server - - Run `npm test` to validate build system - -2. **For firmware changes**: - - Edit files in `wled00/` (but NOT `html_*.h` files) - - Ensure web UI is built first (`npm run build`) - - Build firmware: `pio run -e [target]` - - Flash to device: `pio run -e [target] --target upload` - -3. **For both web and firmware**: - - Always build web UI first - - Test web interface manually - - Build and test firmware if making firmware changes - -## Build Timing and Timeouts - -**IMPORTANT: Use these timeout values when running builds:** - -- **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum -- **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum -- **Hardware builds** (`pio run -e [target]`): 15-20 minutes typical for first build - Set timeout to 1800 seconds (30 minutes) minimum - - Subsequent builds are faster due to caching - - First builds download toolchains and dependencies which takes significant time -- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation require patience - -**When validating your changes before finishing, you MUST wait for the hardware build to complete successfully. Set the timeout appropriately and be patient.** - -## Troubleshooting - -### Common Issues -- **Build fails with missing html_*.h**: Run `npm run build` first -- **Web UI looks broken**: Check browser console for JavaScript errors -- **PlatformIO network errors**: Try again, downloads can be flaky -- **Node.js version issues**: Ensure Node.js 20+ is installed (check `.nvmrc`) - -### When Things Go Wrong -- **Clear generated files**: `rm -f wled00/html_*.h` then rebuild -- **Force web UI rebuild**: `npm run build -- --force` or `npm run build -- -f` -- **Clean PlatformIO cache**: `pio run --target clean` -- **Reinstall dependencies**: `rm -rf node_modules && npm install` - -## Important Notes - -- **Always commit source files** -- **Web UI re-built is part of the platformio firmware compilation** -- **do not commit generated html_*.h files** -- **DO NOT edit `wled00/html_*.h` files** - they are auto-generated. If needed, modify Web UI files in `wled00/data/`. -- **Test web interface manually after any web UI changes** -- When reviewing a PR: the PR author does not need to update/commit generated html_*.h files - these files will be auto-generated when building the firmware binary. -- **If you are not sure about something, just answer that you are not sure.** Gather more information instead of continuing with a wild guess. -- If asked for an analysis, assessment or web research, **provide relevant references** to justify your conclusions. Ensure your recommendations are based on the correct source code branch or PR. -- If updating Web UI files in `wled00/data/`, make use of common functions availeable in `wled00/data/common.js` where possible. -- **Use VS Code with PlatformIO extension for best development experience** -- **Hardware builds require appropriate ESP32/ESP8266 development board** - -## CI/CD Pipeline - -**The GitHub Actions CI workflow will:** -1. Installs Node.js and Python dependencies -2. Runs `npm test` to validate build system (MUST pass) -3. Builds web UI with `npm run build` (automatically run by PlatformIO) -4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all) -5. Uploads build artifacts - -**To ensure CI success, you MUST locally:** -- Run `npm test` and ensure it passes -- Run `pio run -e esp32dev` (or another common environment from "Hardware Compilation" section) and ensure it completes successfully -- If either fails locally, it WILL fail in CI - -**Match this workflow in your local development to ensure CI success. Do not mark work complete until you have validated builds locally.** + +## General Guidelines + +- **Repository language is English.** Suggest translations for non-English content. +- **Use VS Code with PlatformIO extension** for best development experience. +- **Never edit or commit** `wled00/html_*.h` and `wled00/js_*.h` — auto-generated from `wled00/data/`. +- If updating Web UI files in `wled00/data/`, **make use of common functions in `wled00/data/common.js` whenever possible**. +- **When unsure, say so.** Gather more information rather than guessing. +- **Acknowledge good patterns** when you see them. Positive feedback always helps. +- **Provide references** when making analyses or recommendations. Base them on the correct branch or PR. +- **Highlight user-visible breaking changes and ripple effects**. Ask for confirmation that these were introduced intentionally. +- **Unused / dead code must be justified or removed**. This helps to keep the codebase clean, maintainable and readable. +- **Verify feature-flag names.** Every `WLED_ENABLE_*` / `WLED_DISABLE_*` flag must exactly match one of the names below — misspellings are silently ignored by the preprocessor (e.g. `WLED_IR_DISABLE` instead of `WLED_DISABLE_INFRARED`), causing silent build variations. Flag unrecognised names as likely typos and suggest the correct spelling. +
**`WLED_DISABLE_*`**: `2D`, `ADALIGHT`, `ALEXA`, `BROWNOUT_DET`, `ESPNOW`, `FILESYSTEM`, `HUESYNC`, `IMPROV_WIFISCAN`, `INFRARED`, `LOXONE`, `MQTT`, `OTA`, `PARTICLESYSTEM1D`, `PARTICLESYSTEM2D`, `PIXELFORGE`, `WEBSOCKETS` +
**`WLED_ENABLE_*`**: `ADALIGHT`, `AOTA`, `DMX`, `DMX_INPUT`, `DMX_OUTPUT`, `FS_EDITOR`, `GIF`, `HUB75MATRIX`, `JSONLIVE`, `LOXONE`, `MQTT`, `PIXART`, `PXMAGIC`, `USERMOD_PAGE`, `WEBSOCKETS`, `WPA_ENTERPRISE` +- **C++ formatting available**: `clang-format` is installed but not in CI +- No automated linting is configured — match existing code style in files you edit. + +Refer to `docs/cpp.instructions.md` and `docs/web.instructions.md` for language-specific conventions, and `docs/cicd.instructions.md` for GitHub Actions workflows. + +### Attribution for AI-generated code +Using AI-generated code can hide the source of the inspiration / knowledge / sources it used. +- Document attribution of inspiration / knowledge / sources used in the code, e.g. link to GitHub repositories or other websites describing the principles / algorithms used. +- When a larger block of code is generated by an AI tool, embed it into `// AI: below section was generated by an AI` ... `// AI: end` comments (see C++ guidelines). +- Every non-trivial AI-generated function should have a brief comment describing what it does. Explain parameters when their names alone are not self-explanatory. +- AI-generated code must be well documented with meaningful comments that explain intent, assumptions, and non-obvious logic. Do not rephrase source code; explain concepts and reasoning. + +### Pull Request Expectations + +- **No force-push on open PRs.** Once a pull request is open and being reviewed, do not force-push (`git push --force`) to the branch. Force-pushing rewrites history that reviewers may have already commented on, making it impossible to track incremental changes. Use regular commits or `git merge` to incorporate feedback; the branch will be squash-merged when it is accepted. +- **Modifications to ``platformio.ini`` MUST be approved explicitly** by a *maintainer* or *WLED organisation Member*. Modifications to the global build environment may break github action builds. Always flag them. +- **Document your changes in the PR.** Every pull request should include a clear description of *what* changed and *why*. If the change affects user-visible behavior, describe the expected impact. Link to related issues where applicable. Provide screenshots to showcase new features. + +### Supporting Reviews and Discussions +- **For "is it worth doing?" debates** about proposed reliability, safety, or data-integrity mechanisms (CRC checks, backups, power-loss protection): suggest a software **FMEA** (Failure Mode and Effects Analysis). + Clarify the main feared events, enumerate failure modes, assess each mitigation's effectiveness per failure mode, note common-cause failures, and rate credibility for the typical WLED use case. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cd77ecd14..c65639b3a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,14 @@ We'll work with you to refine your contribution, but we'll also push back if som Here are a few suggestions to make it easier for you to contribute: +### Important Developer Infos +* [Project Structure, Files and Directories](.github/copilot-instructions.md#project-structure-overview) (in our AI instructions) +* [Instructions for creating usermods](.github/copilot-instructions.md#usermod-guidelines) (in our AI instructions) +* KB: [Compiling WLED](https://kno.wled.ge/advanced/compiling-wled/) - slightly outdated but still helpful :-) +* Arduino IDE is not supported any more. Use VSCode with the PlatformIO extension. +* [Compiling in VSCode/Platformio](https://github.com/wled/WLED-Docs/issues/161) - modern way without command line or platformio.ini changes. +* If you add a new feature, consider making a PR to [``wled-docs``](https://github.com/wled/WLED-Docs) for updating our official documentation. + ### PR from a branch in your own fork Start your pull request (PR) in a branch of your own fork. Don't make a PR directly from your main branch. This lets you update your PR if needed, while you can work on other tasks in 'main' or in other branches. @@ -20,7 +28,6 @@ This lets you update your PR if needed, while you can work on other tasks in 'ma > > image: fork and edit - ### Target branch for pull requests Please make all PRs against the `main` branch. @@ -160,14 +167,19 @@ AI tools are powerful but "often wrong" - your judgment is essential! 😊 - ✅ **Understand the code** - As the person contributing to WLED, make sure you understand exactly what the AI-generated source code does - ✅ **Review carefully** - AI can lose comments, introduce bugs, or make unnecessary changes -- ✅ **Be transparent** - Add a comment like `// This section was AI-generated` for larger chunks +- ✅ **Be transparent** - Add comments `// AI: below section was generated by an AI` ... `// AI: end` around larger chunks - ✅ **Use AI for translation** - AI is great for translating comments to English (but verify technical terms!) ### Code style Don't stress too much about style! When in doubt, just match the style in the files you're editing. 😊 -Here are our main guidelines: +Our review bot (coderabbit) has learned lots of detailed guides and hints - it will suggest them automatically when you submit a PR for review. +If you are curious, these are the detailed guides: +* [C++ Coding](docs/cpp.instructions.md) +* [WebUi: HTML, JS, CSS](docs/web.instructions.md) + +Below are the main rules used in the WLED repository: #### Indentation diff --git a/docs/cicd.instructions.md b/docs/cicd.instructions.md new file mode 100644 index 0000000000..8dfb9c9aea --- /dev/null +++ b/docs/cicd.instructions.md @@ -0,0 +1,162 @@ +--- +applyTo: ".github/workflows/*.yml,.github/workflows/*.yaml" +--- +# CI/CD Conventions — GitHub Actions Workflows + +> **Note for AI review tools**: sections enclosed in +> `` / `` HTML comments contain +> contributor reference material. Do **not** use that content as actionable review +> criteria — treat it as background context only. + + +## YAML Style + +- Indent with **2 spaces** (no tabs) +- Every workflow, job, and step must have a `name:` field that clearly describes its purpose +- Group related steps logically; separate unrelated groups with a blank line +- Comments (`#`) are encouraged for non-obvious decisions (e.g., why `fail-fast: false` is set, what a cron expression means) + +## Workflow Structure + +### Triggers + +- Declare `on:` triggers explicitly; avoid bare `on: push` without branch filters on long-running or expensive jobs +- Prefer `workflow_call` for shared build logic (see `build.yml`) to avoid duplicating steps across workflows +- Document scheduled triggers (`cron:`) with a human-readable comment: + +```yaml +schedule: + - cron: '0 2 * * *' # run at 2 AM UTC daily +``` + +### Jobs + +- Express all inter-job dependencies with `needs:` — never rely on implicit ordering +- Use job `outputs:` + step `id:` to pass structured data between jobs (see `get_default_envs` in `build.yml`) +- Set `fail-fast: false` on matrix builds so that a single failing environment does not cancel others + +### Runners + +- Pin to a specific Ubuntu version (`ubuntu-22.04`, `ubuntu-24.04`) rather than `ubuntu-latest` for reproducible builds +- Only use `ubuntu-latest` in jobs where exact environment reproducibility is not required (e.g., trivial download/publish steps) + +### Tool and Language Versions + +- Pin tool versions explicitly: + ```yaml + python-version: '3.12' + ``` +- Do not rely on the runner's pre-installed tool versions — always install via a versioned setup action + +### Caching + +- Always cache package managers and build tool directories when the job installs dependencies: + ```yaml + - uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ``` +- Include the environment name or a relevant identifier in cache keys when building multiple targets + +### Artifacts + +- Name artifacts with enough context to be unambiguous (e.g., `firmware-${{ matrix.environment }}`) +- Avoid uploading artifacts that will never be consumed downstream + + +--- + +## Security + +Important: Several current workflows still violate parts of the baseline below - migration is in progress. + +### Permissions — Least Privilege + +Declare explicit `permissions:` blocks. The default token permissions are broad; scope them to the minimum required: + +```yaml +permissions: + contents: read # for checkout +``` + +For jobs that publish releases or write to the repository: + +```yaml +permissions: + contents: write # create/update releases +``` + +A common safe baseline for build-only jobs: + +```yaml +permissions: + contents: read +``` + +### Supply Chain — Action Pinning + +**Third-party actions** (anything outside the `actions/` and `github/` namespaces) should be pinned to a specific release tag. Branch pins (`@main`, `@master`) are **not allowed** — they can be updated by the action author at any time without notice: + +```yaml +# ✅ Acceptable — specific version tag. SHA pinning recommended for more security, as @v2 is still a mutable tag. +uses: softprops/action-gh-release@v2 + +# ❌ Not acceptable — mutable branch reference +uses: andelf/nightly-release@main +``` + +SHA pinning (e.g., `uses: someorg/some-action@abc1234`) is the most secure option for third-party actions; it is recommended when auditing supply-chain risk is a priority. At minimum, always use a specific version tag. + +**First-party actions** (`actions/checkout`, `actions/cache`, `actions/upload-artifact`, etc.) pinned to a major version tag (e.g., `@v4`) are acceptable because GitHub maintains and audits these. + +When adding a new third-party action: +1. Check that the action's repository is actively maintained +2. Review the action's source before adding it +3. Prefer well-known, widely-used actions over obscure ones + +### Credentials and Secrets + +- Use `${{ secrets.GITHUB_TOKEN }}` for operations within the same repository — it is automatically scoped and rotated +- Never commit secrets, tokens, or passwords into workflow files or any tracked file +- Never print secrets in `run:` steps, even with `echo` — GitHub masks known secrets but derived values are not automatically masked +- Scope secrets to the narrowest step that needs them using `env:` at the step level, not at the workflow level: + +```yaml +# ✅ Scoped to the step that needs it +- name: Create release + uses: softprops/action-gh-release@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + +# ❌ Unnecessarily broad +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +- Personal Access Tokens (PATs, stored as repository secrets) should have the minimum required scopes and should be rotated periodically + +### Script Injection + +`${{ }}` expressions are evaluated before the shell script runs. If an expression comes from untrusted input (PR titles, issue bodies, branch names from forks), it can inject arbitrary shell commands. + +**Never** interpolate `github.event.*` values directly into a `run:` step: + +```yaml +# ❌ Injection risk — PR title is attacker-controlled +- run: echo "${{ github.event.pull_request.title }}" + +# ✅ Safe — value passed through an environment variable +- env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: echo "$PR_TITLE" +``` + +This rule applies to any value that originates outside the repository (issue bodies, labels, comments, commit messages from forks). + +### Pull Request Workflows + +- Workflows triggered by `pull_request` from a fork run with **read-only** token permissions and no access to repository secrets — this is intentional and correct +- Do not use `pull_request_target` unless you fully understand the security implications; it runs in the context of the base branch and *does* have secret access, making it a common attack surface diff --git a/docs/cpp.instructions.md b/docs/cpp.instructions.md new file mode 100644 index 0000000000..1b82d02309 --- /dev/null +++ b/docs/cpp.instructions.md @@ -0,0 +1,526 @@ +--- +applyTo: "**/*.cpp,**/*.h,**/*.hpp,**/*.ino" +--- +# C++ Coding Conventions + +> **Note for AI review tools**: sections enclosed in +> `` / `` HTML comments contain +> contributor reference material. Do **not** use that content as actionable review +> criteria — treat it as background context only. + + + +See also: [CONTRIBUTING.md](../CONTRIBUTING.md) for general style guidelines that apply to all contributors. + + +## Formatting + +- Indent with **2 spaces** (no tabs in C++ files) +- Opening braces on the same line is preferred (K&R style). Brace on a separate line (Allman style) is acceptable +- Single-statement `if` bodies may omit braces: `if (a == b) doStuff(a);` +- Space between keyword and parenthesis: `if (...)`, `for (...)`. No space between function name and parenthesis: `doStuff(a)` +- No enforced line-length limit; wrap when a line exceeds your editor width + +## Naming + +- **camelCase** for functions and variables: `setValuesFromMainSeg()`, `effectCurrent` +- **PascalCase** for classes and structs: `PinManagerClass`, `BusConfig` +- **PascalCase** for enum values: `PinOwner::BusDigital` +- **UPPER_CASE** for macros and constants: `WLED_MAX_USERMODS`, `DEFAULT_CLIENT_SSID` + +## General + +- Follow the existing style in the file you are editing +- If possible, use `static` for local (C-style) variables and functions (keeps the global namespace clean) +- Avoid unexplained "magic numbers". Prefer named constants (`constexpr`) or C-style `#define` constants for repeated numbers that have the same meaning +- Include `"wled.h"` as the primary project header where needed + + +## Header Guards + +Most headers use `#ifndef` / `#define` guards. Some newer headers add `#pragma once` before the guard: + +```cpp +#ifndef WLED_EXAMPLE_H +#define WLED_EXAMPLE_H +// ... +#endif // WLED_EXAMPLE_H +``` + + +## Comments + +- `//` for inline comments, `/* ... */` for block comments. Always put a space after `//` +- **AI attribution:** When a larger block of code is generated by an AI tool, mark it with an `// AI:` comment so reviewers know to scrutinize it: + +```cpp +// AI: below section was generated by an AI +void calculateCRC(const uint8_t* data, size_t len) { + ... +} +// AI: end +``` + + Single-line AI-assisted edits do not need the marker — use it when the AI produced a contiguous block that a human did not write line-by-line. + + + +- **Function & feature comments:** Every non-trivial function should have a brief comment above it describing what it does. Include a note about each parameter when the names alone are not self-explanatory: + +```cpp +/* ***** + * Apply gamma correction to a single color channel. + * @param value raw 8-bit channel value (0–255) + * @param gamma gamma exponent (typically 2.8) + * @return corrected 8-bit value + ***** */ +uint8_t gammaCorrect(uint8_t value, float gamma); +``` + + + Short accessor-style functions (getters/setters, one-liners) may skip this if their purpose is obvious from the name. + +## Preprocessor & Feature Flags + +- Prefer compile-time feature flags (`#ifdef` / `#ifndef`) over runtime checks where possible +- Platform differentiation: `ARDUINO_ARCH_ESP32` vs `ESP8266` +- PSRAM availability: `BOARD_HAS_PSRAM` + +## Error Handling + +- `DEBUG_PRINTF()` / `DEBUG_PRINTLN()` for developer diagnostics (compiled out unless `-D WLED_DEBUG`) +- Don't rely on C++ exceptions — use return codes (`-1` / `false` for errors) and global flags (e.g. `errorFlag = ERR_LOW_MEM`). Some builds don't support C++ exceptions. + +## Strings + +- Use `const char*` for temporary/parsed strings +- Avoid `String` (Arduino heap-allocated string) in hot paths; acceptable in config/setup code +- Use `F("string")` for string constants (major RAM win on ESP8266; mostly overload/type compatibility on ESP32) +- Store repeated strings as `static const char[] PROGMEM` + + + On **ESP8266** this explicitly stores the string in flash (PROGMEM), saving precious RAM — every byte counts on that platform. + On **ESP32**, `PROGMEM` is a no-op and string literals already reside in flash/rodata, so `F()` yields little RAM benefit but remains harmless (it satisfies `__FlashStringHelper*` overloads that some APIs expect). + + +```cpp + DEBUG_PRINTLN(F("WS client connected.")); // string stays in flash, not RAM + DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); // format string stays in flash +``` + +## Memory + +- **PSRAM-aware allocation**: use `d_malloc()` (prefer DRAM), `p_malloc()` (prefer PSRAM) from `fcn_declare.h` +- **Avoid Variable Length Arrays (VLAs)**: FreeRTOS task stacks are typically 2–8 KB. A runtime-sized VLA can silently exhaust the stack. Use fixed-size arrays or heap allocation (`d_malloc` / `p_malloc`). Any VLA must be explicitly justified in source or PR. + +GCC/Clang support VLAs as an extension (they are not part of the C++ standard), so they look like a legitimate feature — but they are allocated on the stack at runtime. On ESP32/ESP8266, a VLA whose size depends on a runtime parameter (segment dimensions, pixel counts, etc.) can silently exhaust the stack and cause the program to behave in unexpected ways or crash. + +- **Larger buffers** (LED data, JSON documents) should use PSRAM when available and technically feasible +- **Hot-path**: some data should stay in DRAM or IRAM for performance reasons +- Memory efficiency matters, but is less critical on boards with PSRAM + +Heap fragmentation is a concern: + + - Fragmentation can lead to crashes, even when the overall amount of available heap is still good. The C++ runtime doesn't do any "garbage collection". + + - Avoid frequent `d_malloc` and `d_free` inside a function, especially for small sizes. + - Avoid frequent creation / destruction of objects. + - Allocate buffers early, and try to re-use them. + - Instead of incrementally appending to a `String`, reserve the expected max buffer upfront by using the `reserve()` method. + + +```cpp + String result; + result.reserve(65); // pre-allocate to avoid realloc fragmentation +``` + +```cpp + // prefer DRAM; falls back gracefully and enforces MIN_HEAP_SIZE guard + _ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len)); +``` + +```cpp + _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation - does not increase size() + _modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation - does not increase size() +``` + + +## `const` and `constexpr` + +`const` is a promise to the compiler that a value (or object) will not change - a function declared with a `const char* message` parameter is not allowed to modify the content of `message`. +This pattern enables optimizations and makes intent clear to reviewers. + +`constexpr` allows to define constants that are *guaranteed* to be evaluated by the compiler (zero run-time costs). + + +- For function parameters that are read-only, prefer `const &` or `const`. + +### `const` locals + +* Adding `const` to a local variable that is only assigned once is optional, but *not* strictly necessary. + +* In hot-path code, `const` on cached locals may help the compiler keep values in registers. + ```cpp + const uint_fast16_t cols = vWidth(); + const uint_fast16_t rows = vHeight(); + ``` + +### `const` references to avoid copies +- Pass objects by `const &` (or `&`) instead of copying them implicitly. +- Use `const &` (or `&`) inside loops - This avoids constructing temporary objects on every access. + + +```cpp +const auto &m = _mappings[i]; // reference, not a copy (bus_manager.cpp) +Segment& sourcesegment = strip.getSegment(sourceid); // alias — avoids creating a temporary Segment instance +``` + +For function parameters that are read-only, prefer `const &`: +```cpp +BusManager::add(const BusConfig &bc, bool placeholder) { +``` + + +- Class **Data Members:** Avoid reference data members (`T&` or `const T&`) in a class. + A reference member can outlive the object it refers to, causing **dangling reference** bugs that are hard to diagnose. Prefer value storage or use a pointer and document the expected lifetime. + + + +### `constexpr` over `#define` + +- Prefer `constexpr` for compile-time constants. Unlike `#define`, `constexpr` respects scope and type safety, keeping the global namespace clean. + +```cpp +// Prefer: +constexpr uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; +constexpr int WLED_MAX_BUSSES = WLED_MAX_DIGITAL_CHANNELS + WLED_MAX_ANALOG_CHANNELS; + +// Avoid (when possible): +#define TWO_CHANNEL_MASK 0x00FF00FF +``` + + +### `static_assert` over `#error` + +- Use `static_assert` instead of the C-style `#if … #error … #endif` pattern when validating compile-time constants. It provides a clear message and works with `constexpr` values. +- `#define` and `#if ... #else ... #endif` is still needed for conditional-compilation guards and build-flag-overridable values. + + +```cpp +// Prefer: +constexpr int WLED_MAX_BUSSES = WLED_MAX_DIGITAL_CHANNELS + WLED_MAX_ANALOG_CHANNELS; +static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); + +// Avoid: +#if (WLED_MAX_BUSSES > 32) + #error "WLED_MAX_BUSSES exceeds hard limit" +#endif +``` + +```cpp + // using static_assert() to validate enumerated types (zero cost at runtime) + static_assert(0u == static_cast(PinOwner::None), + "PinOwner::None must be zero, so default array initialization works as expected"); +``` + + +### `static` and `const` class methods + +#### `const` member functions + +Marking a member function `const` tells the compiler that it does not modify the object's state: + +```cpp +uint16_t length() const { return _len; } +bool isActive() const { return _active; } +``` + +Benefits for GCC/Xtensa/RISC-V: +- The compiler knows the method cannot write to `this`, so it is free to **keep member values in registers** across the call and avoid reload barriers. +- `const` methods can be called on `const` objects and `const` references — essential when passing large objects as `const &` to avoid copying. +- `const` allows the compiler to **eliminate redundant loads**: if a caller already has a member value cached, the compiler can prove the `const` call cannot invalidate it. + + +Declare getter, query, or inspection methods `const`. If you need to mark a member `mutable` to work around this (e.g. for a cache or counter), document the reason. + +#### `static` member functions + + +A `static` member function has no implicit `this` pointer. This has two distinct advantages: + +1. **Smaller code, faster calls**: no `this` is passed in a register. On Xtensa and RISC-V, this removes one register argument from every call site and prevents the compiler from emitting `this`-preservation code around inlined blocks. +2. **Better inlining**: GCC can inline a `static` method with more certainty because it cannot be overridden by a derived class (no virtual dispatch ambiguity) and has no aliasing concern through `this`. + +Use `static` for any method that does not need access to instance members: + +```cpp +// Factory / utility — no instance needed: +static BusConfig fromJson(JsonObject obj); + +// Pure computation helpers: +static uint8_t gamma8(uint8_t val); +static uint32_t colorBalance(uint32_t color, uint8_t r, uint8_t g, uint8_t b); +``` + + +`static` communicates intent clearly: a reviewer immediately knows the method is stateless and safe to call without a fully constructed object. + +> **Rule of thumb**: if a method does not read or write any member variable, make it `static`. If it only reads member variables, make it `const`. Note: `static` methods cannot also be `const`-qualified because there is no implicit `this` pointer to be const — just use `static`. Both qualifiers reduce coupling and improve generated code on all ESP32 targets. + +--- + +## Hot-Path Optimization + +The hot path is the per-frame pixel pipeline: **Segment → Strip → BusManager → Bus(Digital,HUB75,Network) or PolyBus → LED driver, plus ``WS2812FX::show()`` and below**. +Speed is the priority here. The patterns below are taken from existing hot-path code (`FX_fcn.cpp`, `FX_2Dfcn.cpp`, `bus_manager.cpp`, `colors.cpp`) and should be followed when modifying these files. + +Note: `FX.cpp` (effect functions) is written by many contributors and has diverse styles — that is acceptable. + +### Function Attributes + +Stack the appropriate attributes on hot-path functions. Defined in `const.h`: + +| Attribute | Meaning | When to use | +|---|---|---| +| `__attribute__((hot))` | Branch-prediction hint | hot-path functions with complex logic | +| `IRAM_ATTR` | Place in fast IRAM (ESP32) | Critical per-pixel functions (e.g. `BusDigital::setPixelColor`) | +| `IRAM_ATTR_YN` | IRAM on ESP32, no-op on ESP8266 | Hot functions that ESP8266 can't fit in IRAM | +| `WLED_O2_ATTR` | Force `-O2` optimization | Most hot-path functions | +| `WLED_O3_ATTR` | Force `-O3,fast-math` | Innermost color math (e.g. `color_blend`) | +| `[[gnu::hot]] inline` | Modern C++ attribute + inline | Header-defined accessors (e.g. `progress()`, `currentBri()`) | + +Note: `WLED_O3_ATTR` sometimes causes performance loss compared to `WLED_O2_ATTR`. Choose optimization levels based on test results. + +Example signature: + +```cpp +void IRAM_ATTR_YN WLED_O2_ATTR __attribute__((hot)) Segment::setPixelColor(unsigned i, uint32_t c) +``` + + +### Cache Members to Locals Before Loops + +Copy class members and virtual-call results to local variables before entering a loop: + +```cpp +uint_fast8_t count = numBusses; // avoid repeated member access +for (uint_fast8_t i = 0; i < count; i++) { + Bus* const b = busses[i]; // const pointer hints to compiler + uint_fast16_t bstart = b->getStart(); + uint_fast16_t blen = b->getLength(); + ... +} +``` + + +### Unsigned Range Check + +Replace two-comparison range tests with a single unsigned subtraction: + +```cpp +// Instead of: if (pix >= bstart && pix < bstart + blen) +if ((uint_fast16_t)(pix - bstart) < blen) // also catches negative pix via unsigned underflow +``` + +### Early Returns + +Guard every hot-path function with the cheapest necessary checks first: + +```cpp +if (!isActive()) return; // inactive segment +if (unsigned(i) >= vLength()) return; // bounds check (catches negative i too) +``` + +### Avoid Nested Calls — Fast Path / Complex Path + +Avoid calling non-inline functions or making complex decisions inside per-pixel hot-path code. When a function has both a common simple case and a rare complex case, split it into two variants and choose once per frame rather than per pixel. + +General rules: +- Keep fast-path functions free of non-inline calls, multi-way branches, and complex switch-case decisions. +- Hoist per-frame decisions (e.g. simple vs. complex segment) out of the per-pixel loop. +- Code duplication between fast/slow variants is acceptable to keep the fast path lean. + +### Function Pointers to Eliminate Repeated Decisions + +When the same decision (e.g. "which drawing routine?") would be evaluated for every pixel, assign the chosen variant to a function pointer once and let the inner loop call through the pointer. This removes the branch entirely — the calling code (e.g. the GIF decoder loop) only ever invokes one function per frame, with no per-pixel decision. + + +`image_loader.cpp` demonstrates the pattern: `calculateScaling()` picks the best drawing callback once per frame based on segment dimensions and GIF size, then passes it to the decoder via `setDrawPixelCallback()`: + +```cpp +// calculateScaling() — called once per frame +if ((perPixelX < 2) && (perPixelY < 2)) + decoder.setDrawPixelCallback(drawPixelCallbackDownScale2D); // downscale-only variant +else + decoder.setDrawPixelCallback(drawPixelCallback2D); // full-scaling variant +``` + +Each callback is a small, single-purpose function with no internal branching — the decoder's per-pixel loop never re-evaluates which strategy to use. + + +### Template Specialization (Advanced) + +Templates can eliminate runtime decisions by generating separate code paths at compile time. For example, a pixel setter could be templated on color order or channel count so the compiler removes dead branches and produces tight, specialized machine code: + +```cpp +template +void setChannel(uint8_t* out, uint32_t col) { + out[0] = R(col); out[1] = G(col); out[2] = B(col); + if constexpr (hasWhite) out[3] = W(col); // compiled out when hasWhite==false +} +``` + +Use sparingly — each instantiation duplicates code in flash. On ESP8266 and small-flash ESP32 boards this can exhaust IRAM/flash. Prefer templates only when the hot path is measurably faster and the number of instantiations is small (2–4). + +### RAII Lock-Free Synchronization (Advanced) + +Where contention is rare and the critical section is short, consider replacing mutex-based locking with lock-free techniques using `std::atomic` and RAII scoped guards. A scoped guard sets a flag on construction and clears it on destruction, guaranteeing cleanup even on early return: + +```cpp +struct ScopedBusyFlag { + std::atomic& flag; + bool acquired; + ScopedBusyFlag(std::atomic& f) : flag(f), acquired(false) { + bool expected = false; + acquired = flag.compare_exchange_strong(expected, true); + } + ~ScopedBusyFlag() { if (acquired) flag.store(false); } + explicit operator bool() const { return acquired; } +}; + +// Usage +static std::atomic busySending{false}; +ScopedBusyFlag guard(busySending); +if (!guard) return; // another task is already sending +// ... do work — flag auto-clears when guard goes out of scope +``` + +This avoids FreeRTOS semaphore overhead and the risk of forgetting to return a semaphore. There are no current examples of this pattern in the codebase — consult with maintainers before introducing it in new code, to ensure it aligns with the project's synchronization conventions. + + +### Pre-Compute Outside Loops + +Move invariant calculations before the loop. Pre-compute reciprocals to replace division with multiplication. + +```cpp +const uint_fast16_t cols = virtualWidth(); +const uint_fast16_t rows = virtualHeight(); +uint_fast8_t fadeRate = (255U - rate) >> 1; +float mappedRate_r = 1.0f / (float(fadeRate) + 1.1f); // reciprocal — avoid division inside loop +``` + + +### Parallel Channel Processing + +Process R+B and W+G channels simultaneously using the two-channel mask pattern: + +```cpp +constexpr uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; +uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; +uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK; +return rb | wg; +``` + +### Bit Shifts Over Division (mainly for RISC-V boards) + +ESP32 and ESP32-S3 (Xtensa core) have a fast "integer divide" instruction, so manual shifts rarely help. +On RISC-V targets (ESP32-C3/C6/P4) and ESP8266, prefer explicit bit-shifts for power-of-two arithmetic — the compiler does **not** always convert divisions to shifts. +Always use unsigned operands for right shifts; signed right-shift is implementation-defined. + + +On RISC-V-based boards (ESP32-C3, ESP32-C6, ESP32-C5) explicit shifts can be beneficial. +```cpp +position >> 3 // instead of position / 8 +(255U - rate) >> 1 // instead of (255 - rate) / 2 +i & 0x0007 // instead of i % 8 +``` + +**Important**: The bit-shifted expression should be unsigned. On some MCUs, "signed right-shift" is implemented by an "arithmetic shift right" that duplicates the sign bit: ``0b1010 >> 1 = 0b1101``. + + +### Static Caching for Expensive Computations + +Cache results in static locals when the input rarely changes between calls: + +```cpp +static uint16_t lastKelvin = 0; +static byte correctionRGB[4] = {255,255,255,0}; +if (lastKelvin != kelvin) { + colorKtoRGB(kelvin, correctionRGB); // expensive — only recalculate when input changes + lastKelvin = kelvin; +} +``` + +### Inlining Strategy + +- Move frequently-called small functions to headers for inlining (e.g. `Segment::setPixelColorRaw` is in `FX.h`) +- Use `static inline` for file-local helpers + +### Math & Trigonometric Functions + +- WLED uses a custom `fastled_slim` library. The old FastLED trig aliases (`sin8`, `cos8`, `sin16`, `cos16`) **no longer exist and cause a compile error** — use `sin8_t()`, `cos8_t()`, `sin16_t()`, `cos16_t()` instead. For float approximations use `sin_approx()` / `cos_approx()` instead of `sinf()` / `cosf()`. Replace FastLED noise aliases (`inoise8`, `inoise16`) with `perlin8`, `perlin16`. + + +| ❌ Do not use (compile error) | ✅ Use instead | Source | +|---|---|---| +| `sin8()`, `cos8()` | `sin8_t()`, `cos8_t()` | `fastled_slim.h` → `wled_math.cpp` | +| `sin16()`, `cos16()` | `sin16_t()`, `cos16_t()` | `fastled_slim.h` → `wled_math.cpp` | +| `sinf()`, `cosf()` | `sin_approx()`, `cos_approx()` | `wled_math.cpp` | +| `atan2f()`, `atan2()` | `atan2_t()` | `wled_math.cpp` | +| `sqrt()` on integers | `sqrt32_bw()` | `fcn_declare.h` → `wled_math.cpp` | +| `sqrtf()` on floats | `sqrtf()` (acceptable) | — no WLED replacement | + + +--- + +## `delay()` vs `yield()` in ESP32 Tasks + +* On ESP32, `delay(ms)` calls `vTaskDelay(ms / portTICK_PERIOD_MS)`, which **suspends only the calling task**. The FreeRTOS scheduler immediately runs all other ready tasks. +* The Arduino `loop()` function runs inside `loopTask`. Calling `delay()` there does *not* block the network stack, audio FFT, LED DMA, nor any other FreeRTOS task. +* This differs from ESP8266, where `delay()` stalls the entire system unless `yield()` was called inside. + + +- On ESP32, `delay()` is generally allowed, as it helps to efficiently manage CPU usage of all tasks. +- On ESP8266, only use `delay()` and `yield()` in the main `loop()` context. If not sure, protect with `if (can_yield()) ...`. +- Do *not* use `delay()` in effects (FX.cpp) or in the hot pixel path. +- `delay()` on the bus-level is allowed, it might be needed to achieve exact timing in LED drivers. + +### IDLE Watchdog and Custom Tasks on ESP32 + +- In arduino-esp32, `yield()` calls `vTaskDelay(0)`, which only switches to tasks at equal or higher priority — the IDLE task (priority 0) is never reached. +- **Do not use `yield()` to pace ESP32 tasks or assume it feeds any watchdog**. +- **Custom `xTaskCreate()` tasks must call `delay(1)` in their loop, not `yield()`.** Without a real blocking call, the IDLE task is starved. The IDLE watchdog panic is the first visible symptom — but the damage starts earlier: deleted task memory leaks, software timers stop firing, light sleep is disabled, and Wi-Fi/BT idle hooks don't run. Structure custom tasks like this: +```cpp +// WRONG — IDLE task is never scheduled; yield() does not feed the idle task watchdog. +void myTask(void*) { + for (;;) { + doWork(); + yield(); + } +} + +// CORRECT — delay(1) suspends the task for ≥1 ms, IDLE task runs, IDLE watchdog is fed +void myTask(void*) { + for (;;) { + doWork(); + delay(1); // DO NOT REMOVE — lets IDLE(0) run and feeds its watchdog + } +} +``` + +- Prefer blocking FreeRTOS primitives (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) over `delay(1)` polling where precise timing or event-driven behaviour is needed. +- **Watchdog note.** WLED disables the Task Watchdog by default (`WLED_WATCHDOG_TIMEOUT 0` in `wled.h`). When enabled, `esp_task_wdt_reset()` is called at the end of each `loop()` iteration. Long blocking operations inside `loop()` — such as OTA downloads or slow file I/O — must call `esp_task_wdt_reset()` periodically, or be restructured so the main loop is not blocked for longer than the configured timeout. + +## Caveats and Pitfalls + +- **LittleFS filenames**: File paths passed to `file.open()` must not exceed 255 bytes (`LFS_NAME_MAX`). Validate constructed paths (e.g., `/ledmap_` + segment name + `.json`) stay within this limit (assume standard configurations, like WLED_MAX_SEGNAME_LEN = 64). + +- **Float-to-unsigned conversion is undefined behavior when the value is out of range.** Converting a negative `float` directly to an unsigned integer type (`uint8_t`, `uint16_t`, …) is UB per the C++ standard — the Xtensa (ESP32) toolchain may silently wrap, but RISC-V (ESP32-C3/C5/C6/P4) can produce different results due to clamping. Cast through a signed integer first: + ```cpp + // Undefined behavior — avoid: + uint8_t angle = 40.74f * atan2f(dy, dx); // negative float → uint8_t is UB + + // Correct — cast through int first: + // atan2f returns [-π..+π], scaled ≈ [-128..+128] as int; uint8_t wraps negative ints via 2's complement (e.g. -1 → 255) + uint8_t angle = int(40.74f * atan2f(dy, dx)); // float→int (defined), int→uint8_t (defined) + ``` diff --git a/docs/esp-idf.instructions.md b/docs/esp-idf.instructions.md new file mode 100644 index 0000000000..3eff122b49 --- /dev/null +++ b/docs/esp-idf.instructions.md @@ -0,0 +1,859 @@ +--- +applyTo: "**/*.cpp,**/*.h,**/*.hpp,**/*.ino" +--- +# ESP-IDF Coding Guide (within arduino-esp32) + +WLED runs on the Arduino-ESP32 framework, which wraps ESP-IDF. Understanding the ESP-IDF layer is essential when writing chip-specific code, managing peripherals, or preparing for the IDF v5.x migration. This guide documents patterns already used in the codebase and best practices derived from Espressif's official examples. + +> **Scope**: This file is an optional review guideline. It applies when touching chip-specific code, peripheral drivers, memory allocation, or platform conditionals. + +> **Note for AI review tools**: sections enclosed in +> `` / `` HTML comments contain +> contributor reference material. Do **not** use that content as actionable review +> criteria — treat it as background context only. + +--- + + +## Identifying the Build Target: `CONFIG_IDF_TARGET_*` + +Use `CONFIG_IDF_TARGET_*` macros to gate chip-specific code at compile time. These are set by the build system and are mutually exclusive — exactly one is defined per build. + +| Macro | Chip | Architecture | Notes | +|---|---|---|---| +| `CONFIG_IDF_TARGET_ESP32` | ESP32 (classic) | Xtensa dual-core | Primary target. Has DAC, APLL, I2S ADC mode | +| `CONFIG_IDF_TARGET_ESP32S2` | ESP32-S2 | Xtensa single-core | Limited peripherals. 13-bit ADC | +| `CONFIG_IDF_TARGET_ESP32S3` | ESP32-S3 | Xtensa dual-core | Preferred for large installs. Octal PSRAM, USB-OTG | +| `CONFIG_IDF_TARGET_ESP32C3` | ESP32-C3 | RISC-V single-core | Minimal peripherals. RISC-V clamps out-of-range float→unsigned casts | +| `CONFIG_IDF_TARGET_ESP32C5` | ESP32-C5 | RISC-V single-core | Wi-Fi 2.4Ghz + 5Ghz, Thread/Zigbee. Future target | +| `CONFIG_IDF_TARGET_ESP32C6` | ESP32-C6 | RISC-V single-core | Wi-Fi 6, Thread/Zigbee. Future target | +| `CONFIG_IDF_TARGET_ESP32P4` | ESP32-P4 | RISC-V dual-core | High performance. Future target | + + +### Build-time validation +WLED validates at compile time that exactly one target is defined and that it is a supported chip (`wled.cpp` lines 39–61). Follow this pattern when adding new chip-specific branches: + + +```cpp +#if defined(CONFIG_IDF_TARGET_ESP32) + // classic ESP32 path +#elif defined(CONFIG_IDF_TARGET_ESP32S3) + // S3-specific path +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4) + // RISC-V common path +#else + #warning "Untested chip — review peripheral availability" +#endif +``` + + +### Guidelines + +- **Always test on the actual chip** before claiming support. Simulators and cross-compilation can hide peripheral differences. +- **Prefer `#elif` chains** over nested `#ifdef` for readability. +- **Do not use `CONFIG_IDF_TARGET_*` for feature detection.** Use `SOC_*` capability macros instead (see next section). For example, use `SOC_I2S_SUPPORTS_ADC` instead of `CONFIG_IDF_TARGET_ESP32` to check for I2S ADC support. +- When a feature must be disabled on certain chips, use explicit `static_assert()` or `#warning` directives so the build clearly reports what is missing. + +--- + +## Hardware Capability Detection: `SOC_*` Macros + +`SOC_*` macros (from `soc/soc_caps.h`) describe what the current chip supports. They are the correct way to check for peripheral features — they stay accurate when new chips are added, unlike `CONFIG_IDF_TARGET_*` checks. + + +### Important `SOC_*` macros used in WLED + +| Macro | Type | Used in | Purpose | +|---|---|---|---| +| `SOC_I2S_NUM` | `int` | `audio_source.h` | Number of I2S peripherals (1 or 2) | +| `SOC_I2S_SUPPORTS_ADC` | `bool` | `usermods/audioreactive/audio_source.h` | I2S ADC sampling mode (ESP32 only) | +| `SOC_I2S_SUPPORTS_APLL` | `bool` | `usermods/audioreactive/audio_source.h` | Audio PLL for precise sample rates | +| `SOC_I2S_SUPPORTS_PDM_RX` | `bool` | `usermods/audioreactive/audio_source.h` | PDM microphone input | +| `SOC_ADC_MAX_BITWIDTH` | `int` | `util.cpp` | ADC resolution (12 or 13 bits). Renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH` in IDF v5 | +| `SOC_ADC_CHANNEL_NUM(unit)` | `int` | `pin_manager.cpp` | ADC channels per unit | +| `SOC_UART_NUM` | `int` | `dmx_input.cpp` | Number of UART peripherals | +| `SOC_DRAM_LOW` / `SOC_DRAM_HIGH` | `addr` | `util.cpp` | DRAM address boundaries for validation | + + +### Key pitfall +`SOC_ADC_MAX_BITWIDTH` (ADC resolution 12 or 13 bits) was renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH` in IDF v5. + + +### Less commonly used but valuable + +| Macro | Purpose | +|---|---| +| `SOC_RMT_TX_CANDIDATES_PER_GROUP` | Number of RMT TX channels (varies 2–8 by chip) | +| `SOC_LEDC_CHANNEL_NUM` | Number of LEDC (PWM) channels | +| `SOC_GPIO_PIN_COUNT` | Total GPIO pin count | +| `SOC_DAC_SUPPORTED` | Whether the chip has a DAC (ESP32/S2 only) | +| `SOC_SPIRAM_SUPPORTED` | Whether PSRAM interface exists | +| `SOC_CPU_CORES_NUM` | Core count (1 or 2) — useful for task pinning decisions | + + + +### Best practices + +```cpp +// Good: feature-based detection +#if SOC_I2S_SUPPORTS_PDM_RX + _config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); +#else + #warning "PDM microphones not supported on this chip" +#endif + +// Avoid: chip-name-based detection +#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3) + // happens to be correct today, but breaks when a new chip adds PDM support +#endif +``` + + +### PSRAM capability macros + +For PSRAM presence, mode, and DMA access patterns: + +| Macro | Meaning | +|---|---| +| `CONFIG_SPIRAM` / `BOARD_HAS_PSRAM` | PSRAM is present in the build configuration | +| `CONFIG_SPIRAM_MODE_QUAD` | Quad-SPI PSRAM (standard, used on ESP32 classic and some S2/S3 boards) | +| `CONFIG_SPIRAM_MODE_OCT` | Octal-SPI PSRAM — 8 data lines, DTR mode. Used on ESP32-S3 with octal PSRAM (e.g. N8R8 / N16R8 modules). Reserves GPIO 33–37 for the PSRAM bus — **do not allocate these pins** when this macro is defined. `wled.cpp` uses this to gate GPIO reservation. | +| `CONFIG_SPIRAM_MODE_HEX` | Hex-SPI (16-line) PSRAM — future interface on ESP32-P4 running at up to 200 MHz. Used in `json.cpp` to report the PSRAM mode. | +| `CONFIG_SOC_PSRAM_DMA_CAPABLE` | PSRAM buffers can be used with DMA (ESP32-S3 with octal PSRAM) | +| `CONFIG_SOC_MEMSPI_FLASH_PSRAM_INDEPENDENT` | SPI flash and PSRAM on separate buses (no speed contention) | + + + +#### Detecting octal/hex flash + +On ESP32-S3 modules with OPI flash (e.g. N8R8 modules where the SPI flash itself runs in Octal-PI mode), the build system sets: + +| Macro | Meaning | +|---|---| +| `CONFIG_ESPTOOLPY_FLASHMODE_OPI` | Octal-PI flash mode. On S3, implies GPIO 33–37 are used by the flash/PSRAM interface — the same GPIO block as octal PSRAM. `wled.cpp` uses `CONFIG_ESPTOOLPY_FLASHMODE_OPI \|\| (CONFIG_SPIRAM_MODE_OCT && BOARD_HAS_PSRAM)` to decide whether to reserve these GPIOs. `json.cpp` uses this to report the flash mode string as `"🚀OPI"`. | +| `CONFIG_ESPTOOLPY_FLASHMODE_HEX` | Hex flash mode (ESP32-P4). Reported as `"🚀🚀HEX"` in `json.cpp`. | + +**Pattern used in WLED** (from `wled.cpp`) to reserve the octal-bus GPIOs on S3: +```cpp +#if defined(CONFIG_IDF_TARGET_ESP32S3) + #if CONFIG_ESPTOOLPY_FLASHMODE_OPI || (CONFIG_SPIRAM_MODE_OCT && defined(BOARD_HAS_PSRAM)) + // S3: GPIO 33-37 are used by the octal PSRAM/flash bus + managed_pin_type pins[] = { {33, true}, {34, true}, {35, true}, {36, true}, {37, true} }; + pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); + #endif +#endif +``` + +--- + +## ESP-IDF Version Conditionals + + +### Checking the IDF version + +```cpp +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + // IDF v5+ code path +#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) + // IDF v4.4+ code path +#else + // Legacy IDF v3/v4.x path +#endif +``` + +### Key ESP-IDF version thresholds for WLED + +| Version | What changed | +|---|---| +| **4.0.0** | Filesystem API (`SPIFFS`/`LittleFS`), GPIO driver overhaul | +| **4.2.0** | ADC/GPIO API updates; `esp_adc_cal` introduced | +| **4.4.0** | I2S driver refactored (legacy API remains); `adc_deprecated.h` headers appear for newer targets | +| **4.4.4–4.4.8** | Known I2S channel-swap regression on ESP32 (workaround in `audio_source.h`) | +| **5.0.0** | **Major breaking changes** — RMT, I2S, ADC, SPI flash APIs replaced (see migration section) | +| **5.1.0** | Matter protocol support; new `esp_flash` API stable | +| **5.3+** | arduino-esp32 v3.x compatibility; C6/P4 support | + + +### Guidelines + +- When adding a version guard, **always include a comment** explaining *what* changed and *why* the guard is needed. +- Avoid version ranges that silently break — prefer `>=` over exact version matches. +- Known regressions should use explicit range guards: + ```cpp + // IDF 4.4.4–4.4.8 swapped I2S left/right channels (fixed in 4.4.9) + #if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 4)) && \ + (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 8)) + #define I2S_CHANNELS_SWAPPED + #endif + ``` + +--- + +## Migrating from ESP-IDF v4.4.x to v5.x + +The jump from IDF v4.4 (arduino-esp32 v2.x) to IDF v5.x (arduino-esp32 v3.x) is the largest API break in ESP-IDF history. This section documents the critical changes and recommended migration patterns based on the upstream WLED `V5-C6` branch (`https://github.com/wled/WLED/tree/V5-C6`). Note: WLED has not yet migrated to IDF v5 — these patterns prepare for the future migration. + + +### Compiler changes + +IDF v5.x ships a much newer GCC toolchain. Key versions: + +| ESP-IDF | GCC | C++ default | Notes | +|---|---|---|---| +| 4.4.x (current) | **8.4.0** | C++17 (gnu++17) | Xtensa + RISC-V | +| 5.1–5.3 | **13.2** | C++20 (gnu++2b) | Significant warning changes | +| 5.4–5.5 | **14.2** | C++23 (gnu++2b) | Latest; stricter diagnostics | + +Notable behavioral differences: + +| Change | Impact | Action | +|---|---|---| +| Stricter `-Werror=enum-conversion` | Implicit int-to-enum casts now error | Use explicit `static_cast<>` or typed enums | +| C++20/23 features available | `consteval`, `concepts`, `std::span`, `std::expected` | Use judiciously — ESP8266 builds still require GCC 10.x with C++17 | +| `-Wdeprecated-declarations` enforced | Deprecated API calls become warnings/errors | Migrate to new APIs (see below) | +| `-Wdangling-reference` (GCC 13+) | Warns when a reference binds to a temporary that will be destroyed | Fix the lifetime issue; do not suppress the warning | +| `-fno-common` default (GCC 12+) | Duplicate tentative definitions across translation units cause linker errors | Use `extern` declarations in headers, define in exactly one `.cpp` | +| RISC-V codegen improvements | C3/C6/P4 benefit from better register allocation | No action needed — automatic | + +### C++ language features: GCC 8 → GCC 14 + +The jump from GCC 8.4 to GCC 14.2 spans six major compiler releases. This section lists features that become available and patterns that need updating. + +#### Features safe to use after migration + +These work in GCC 13+/14+ but **not** in GCC 8.4. Guard with `#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)` if the code must compile on both IDF v4 and v5. + +| Feature | Standard | Example | Benefit | +|---|---|---|---| +| Designated initializers (C++20) | C++20 | `gpio_config_t cfg = { .mode = GPIO_MODE_OUTPUT };` | Already used as a GNU extension in GCC 8; becomes standard and portable in C++20 | +| `[[likely]]` / `[[unlikely]]` | C++20 | `if (err != ESP_OK) [[unlikely]] { ... }` | Hints for branch prediction; useful in hot paths | +| `[[nodiscard("reason")]]` | C++20 | `[[nodiscard("leak if ignored")]] void* allocBuffer();` | Enforces checking return values — helpful for `esp_err_t` wrappers | +| `std::span` | C++20 | `void process(std::span buf)` | Safe, non-owning view of contiguous memory — replaces raw pointer + length pairs | +| `consteval` | C++20 | `consteval uint32_t packColor(...)` | Guarantees compile-time evaluation; useful for color constants | +| `constinit` | C++20 | `constinit static int counter = 0;` | Prevents static initialization order fiasco | +| Concepts / `requires` | C++20 | `template requires std::integral` | Clearer constraints than SFINAE; improves error messages | +| Three-way comparison (`<=>`) | C++20 | `auto operator<=>(const Version&) const = default;` | Less boilerplate for comparable types | +| `std::bit_cast` | C++20 | `float f = std::bit_cast(uint32_val);` | Type-safe reinterpretation — replaces `memcpy` or `union` tricks | +| `if consteval` | C++23 | `if consteval { /* compile-time */ } else { /* runtime */ }` | Cleaner than `std::is_constant_evaluated()` | +| `std::expected` | C++23 | `std::expected readSensor()` | Monadic error handling — cleaner than returning error codes | +| `std::to_underlying` | C++23 | `auto val = std::to_underlying(myEnum);` | Replaces `static_cast(myEnum)` | + +#### Features already available in GCC 8 (C++17) + +These work on both IDF v4.4 and v5.x — prefer them now: + +| Feature | Example | Notes | +|---|---|---| +| `if constexpr` | `if constexpr (sizeof(T) == 4) { ... }` | Compile-time branching; already used in WLED | +| `std::optional` | `std::optional pin;` | Nullable value without sentinel values like `-1` | +| `std::string_view` | `void log(std::string_view msg)` | Non-owning, non-allocating string reference | +| Structured bindings | `auto [err, value] = readSensor();` | Useful with `std::pair` / `std::tuple` returns | +| Fold expressions | `(addSegment(args), ...);` | Variadic template expansion | +| Inline variables | `inline constexpr int MAX_PINS = 50;` | Avoids ODR issues with header-defined constants | +| `[[maybe_unused]]` | `[[maybe_unused]] int debug_only = 0;` | Suppresses unused-variable warnings cleanly | +| `[[fallthrough]]` | `case 1: doA(); [[fallthrough]]; case 2:` | Documents intentional switch fallthrough | +| Nested namespaces | `namespace wled::audio { }` | Shorter than nested `namespace` blocks | + +#### Patterns that break or change behavior + +| Pattern | GCC 8 behavior | GCC 14 behavior | Fix | +|---|---|---|---| +| `int x; enum E e = x;` | Warning (often ignored) | Error with `-Werror=enum-conversion` | `E e = static_cast(x);` | +| `int g;` in two `.cpp` files | Both compile, linker merges (tentative definition) | Error: multiple definitions (`-fno-common`) | `extern int g;` in header, `int g;` in one `.cpp` | +| `const char* ref = std::string(...).c_str();` | Silent dangling pointer | Warning (`-Wdangling-reference`) | Extend lifetime: store the `std::string` in a local variable | +| `register int x;` | Accepted (ignored) | Warning or error (`register` removed in C++17) | Remove `register` keyword | +| Narrowing in aggregate init | Warning | Error | Use explicit cast or wider type | +| Implicit `this` capture in lambdas | Accepted in `[=]` | Deprecated warning; error in C++20 mode | Use `[=, this]` or `[&]` | + + +#### Recommendations + +- **Do not raise the minimum C++ standard yet.** WLED must still build on IDF v4.4 (GCC 8.4, C++17). Use `#if __cplusplus > 201703L` to gate C++20 features. +- **Mark intentional fallthrough** with `[[fallthrough]]` — GCC 14 warns on unmarked fallthrough by default. + +- **Prefer `std::optional` over sentinel values** (e.g., `-1` for "no pin") in new code — it works on both compilers. +- **Use `std::string_view`** for read-only string parameters instead of `const char*` or `const String&` — zero-copy and works on GCC 8+. +- **Avoid raw `union` type punning** — prefer `memcpy` (GCC 8) or `std::bit_cast` (GCC 13+) for strict-aliasing safety. + + +### Deprecated and removed APIs + +#### RMT (Remote Control Transceiver) + +The legacy `rmt_*` functions are removed in IDF v5. Do not introduce new legacy RMT calls. + + +The new API is channel-based: + +| IDF v4 (legacy) | IDF v5 (new) | Notes | +|---|---|---| +| `rmt_config()` + `rmt_driver_install()` | `rmt_new_tx_channel()` / `rmt_new_rx_channel()` | Channels are now objects | +| `rmt_write_items()` | `rmt_transmit()` with encoder | Requires `rmt_encoder_t` | +| `rmt_set_idle_level()` | Configure in channel config | Set at creation time | +| `rmt_item32_t` | `rmt_symbol_word_t` | Different struct layout | + + +**WLED impact**: NeoPixelBus LED output and IR receiver both use legacy RMT. The upstream `V5-C6` branch adds `-D WLED_USE_SHARED_RMT` and disables IR until the library is ported. + +#### I2S (Inter-IC Sound) + +Legacy `i2s_driver_install()` + `i2s_read()` API is deprecated. When touching audio source code, wrap legacy I2S init and reading in `#if ESP_IDF_VERSION_MAJOR < 5` / `#else`. + + + The new API uses channel handles: +| IDF v4 (legacy) | IDF v5 (new) | Notes | +|---|---|---| +| `i2s_driver_install()` | `i2s_channel_init_std_mode()` | Separate STD/PDM/TDM modes | +| `i2s_set_pin()` | Pin config in `i2s_std_gpio_config_t` | Set at init time | +| `i2s_read()` | `i2s_channel_read()` | Uses channel handle | +| `i2s_set_clk()` | `i2s_channel_reconfig_std_clk()` | Reconfigure running channel | +| `i2s_config_t` | `i2s_std_config_t` | Separate config for each mode | + +**Migration pattern** (from Espressif examples): +```cpp +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + #include "driver/i2s_std.h" + i2s_chan_handle_t rx_handle; + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + i2s_new_channel(&chan_cfg, NULL, &rx_handle); + + i2s_std_config_t std_cfg = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(22050), + .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = { .din = GPIO_NUM_32, .mclk = I2S_GPIO_UNUSED, ... }, + }; + i2s_channel_init_std_mode(rx_handle, &std_cfg); + i2s_channel_enable(rx_handle); +#else + // Legacy i2s_driver_install() path +#endif +``` + +**WLED impact**: The audioreactive usermod (`audio_source.h`) heavily uses legacy I2S. Migration requires rewriting the `I2SSource` class for channel-based API. + + +#### ADC (Analog-to-Digital Converter) + +Legacy `adc1_get_raw()` and `esp_adc_cal_*` are deprecated: + +| IDF v4 (legacy) | IDF v5 (new) | Notes | +|---|---|---| +| `adc1_config_width()` + `adc1_get_raw()` | `adc_oneshot_new_unit()` + `adc_oneshot_read()` | Object-based API | +| `esp_adc_cal_characterize()` | `adc_cali_create_scheme_*()` | Calibration is now scheme-based | +| `adc_continuous_*` (old) | `adc_continuous_*` (restructured) | Config struct changes | + +#### SPI Flash + +| IDF v4 (legacy) | IDF v5 (new) | +|---|---| +| `spi_flash_read()` | `esp_flash_read()` | +| `spi_flash_write()` | `esp_flash_write()` | +| `spi_flash_erase_range()` | `esp_flash_erase_region()` | + +WLED already has a compatibility shim in `ota_update.cpp` that maps old names to new ones. + +#### GPIO + +| IDF v4 (legacy) | IDF v5 (recommended) | +|---|---| +| `gpio_pad_select_gpio()` | `esp_rom_gpio_pad_select_gpio()` (or use `gpio_config()`) | +| `gpio_set_direction()` + `gpio_set_pull_mode()` | `gpio_config()` with `gpio_config_t` struct | + +### Features disabled in IDF v5 builds + +The upstream `V5-C6` branch explicitly disables features with incompatible library dependencies: + +```ini +# platformio.ini [esp32_idf_V5] +-D WLED_DISABLE_INFRARED # IR library uses legacy RMT +-D WLED_DISABLE_MQTT # AsyncMqttClient incompatible with IDF v5 +-D ESP32_ARDUINO_NO_RGB_BUILTIN # Prevents RMT driver conflict with built-in LED +-D WLED_USE_SHARED_RMT # Use new shared RMT driver for NeoPixel output +``` + + +### Migration checklist for new code + +1. **Never use a removed API without a version guard.** Always provide both old and new paths, or disable the feature on IDF v5. +2. **Test on both IDF v4.4 and v5.x builds** if the code must be backward-compatible. +3. **Prefer the newer API** when writing new code — wrap the old API in an `#else` block. +4. **Mark migration TODOs** with `// TODO(idf5):` so they are easy to find later. + +--- + +## Memory Management: `heap_caps_*` Best Practices + +ESP32 has multiple memory regions with different capabilities. Using the right allocator is critical for performance and stability. + + +### Memory regions + +| Region | Flag | Speed | DMA | Size | Use for | +|---|---|---|---|---|---| +| DRAM | `MALLOC_CAP_INTERNAL \| MALLOC_CAP_8BIT` | Fast | Yes (ESP32) | 200–320 KB | Hot-path buffers, task stacks, small allocations | +| IRAM | `MALLOC_CAP_EXEC` | Fastest | No | 32–128 KB | Code (automatic via `IRAM_ATTR`) | +| PSRAM (SPIRAM) | `MALLOC_CAP_SPIRAM \| MALLOC_CAP_8BIT` | Slower | Chip-dependent | 2–16 MB | Large buffers, JSON documents, image data | +| RTC RAM | `MALLOC_CAP_RTCRAM` | Moderate | No | 8 KB | Data surviving deep sleep; small persistent buffers | + + + +### WLED allocation wrappers + +WLED provides convenience wrappers with automatic fallback. **Always prefer these over raw `heap_caps_*` calls**: + +| Function | Allocation preference | Use case | +|---|---|---| +| `d_malloc(size)` | RTC → DRAM → PSRAM | General-purpose; prefers fast memory | +| `d_calloc(n, size)` | Same as `d_malloc`, zero-initialized | Arrays, structs | +| `p_malloc(size)` | PSRAM → DRAM | Large buffers; prefers abundant memory | +| `p_calloc(n, size)` | Same as `p_malloc`, zero-initialized | Large arrays | +| `d_malloc_only(size)` | RTC → DRAM (no PSRAM fallback) | DMA buffers, time-critical data | + +### PSRAM guidelines + +- **Check availability**: always test `psramFound()` before assuming PSRAM is present. +- **DMA compatibility**: on ESP32 (classic), PSRAM buffers are **not DMA-capable** — use `d_malloc_only()` to allocate DMA buffers in DRAM only. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), PSRAM buffers *can* be used with DMA when `CONFIG_SOC_PSRAM_DMA_CAPABLE` is defined. +- **JSON documents**: use the `PSRAMDynamicJsonDocument` allocator (defined in `wled.h`) to put large JSON documents in PSRAM: + ```cpp + PSRAMDynamicJsonDocument doc(16384); // allocated in PSRAM if available + ``` +- **Fragmentation**: PSRAM allocations fragment less than DRAM because the region is larger. But avoid mixing small and large allocations in PSRAM — small allocations waste the MMU page granularity. +- **Heap validation**: use `d_measureHeap()` and `d_measureContiguousFreeHeap()` to monitor remaining DRAM. Allocations that would drop free DRAM below `MIN_HEAP_SIZE` should go to PSRAM instead. +- **Performance**: Keep hot-path data in DRAM. Prefer PSRAM for capacity-oriented buffers and monitor contiguous DRAM headroom. + + PSRAM access is up to 15× slower than DRAM on ESP32, 3–10× slower than DRAM on ESP32-S3/-S2 with quad-SPI bus. On ESP32-S3 with octal PSRAM (`CONFIG_SPIRAM_MODE_OCT`), the penalty is smaller (~2×) because the 8-line DTR bus can transfer 8 bits in parallel at 80 MHz (120 MHz is possible with CONFIG_SPIRAM_SPEED_120M, which requires enabling experimental ESP-IDF features). On ESP32-P4 with hex PSRAM (`CONFIG_SPIRAM_MODE_HEX`), the 16-line bus runs at 200 MHz which brings it on-par with DRAM. Keep hot-path data in DRAM regardless, but consider that ESP32 often crashes when the largest DRAM chunk gets below 10 KB. + + + +### Pattern: preference-based allocation + +When you need a buffer that works on boards with or without PSRAM: + +```cpp +// Prefer PSRAM for large buffers, fall back to DRAM +uint8_t* buf = (uint8_t*)heap_caps_malloc_prefer(bufSize, 2, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, // first choice: PSRAM + MALLOC_CAP_DEFAULT); // fallback: any available +// Or simply: +uint8_t* buf = (uint8_t*)p_malloc(bufSize); +``` + + +--- + +## I2S Audio: Best Practices + +The audioreactive usermod uses I2S for microphone input. Key patterns: + +### Port selection + +```cpp +constexpr i2s_port_t AR_I2S_PORT = I2S_NUM_0; +// I2S_NUM_1 has limitations: no MCLK routing, no ADC support, no PDM support +``` + +Always use `I2S_NUM_0` unless you have a specific reason and have verified support on all target chips. + +### DMA buffer tuning + +DMA buffer size controls latency vs. reliability: + +| Scenario | `dma_buf_count` | `dma_buf_len` | Latency | Notes | +|---|---|---|---|---| +| With HUB75 matrix | 18 | 128 | ~100 ms | Higher count prevents I2S starvation during matrix DMA | +| Without PSRAM | 24 | 128 | ~140 ms | More buffers compensate for slower interrupt response | +| Default | 8 | 128 | ~46 ms | Acceptable for most setups | + +### Interrupt priority + +Choose interrupt priority based on coexistence with other drivers: + +```cpp +#ifdef WLED_ENABLE_HUB75MATRIX + .intr_alloc_flags = ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1, // level 1 (lowest) to avoid starving HUB75 +#else + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_LEVEL3, // accept level 2 or 3 (allocator picks available) +#endif +``` + +### APLL (Audio PLL) usage + +The ESP32 has an audio PLL for precise sample rates. Rules: + +- Enable APLL when an MCLK pin is provided and precision matters. +- **Disable APLL** when Ethernet or HUB75 is active — they also use the APLL. +- APLL is broken on ESP32 revision 0 silicon. +- Not all chips have APLL — gate with `SOC_I2S_SUPPORTS_APLL`. + +```cpp +#if !defined(SOC_I2S_SUPPORTS_APLL) + _config.use_apll = false; +#elif defined(WLED_USE_ETHERNET) || defined(WLED_ENABLE_HUB75MATRIX) + _config.use_apll = false; // APLL conflict +#endif +``` + +### PDM microphone caveats + +- Not supported on ESP32-C3 (`SOC_I2S_SUPPORTS_PDM_RX` not defined). +- ESP32-S3 PDM has known issues: sample rate at 50% of expected, very low amplitude. + - **16-bit data width**: Espressif's IDF documentation states that in PDM mode the data unit width is always 16 bits, regardless of the configured `bits_per_sample`. + - See [espressif/esp-idf#8660](https://github.com/espressif/esp-idf/issues/8660) for the upstream issue. + - **Flag `bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT` in PDM mode** — this causes the S3 low-amplitude symptom. +- No clock pin (`I2S_CKPIN = -1`) triggers PDM mode in WLED. + +--- + +## HUB75 LED Matrix: Best Practices + +WLED uses the `ESP32-HUB75-MatrixPanel-I2S-DMA` library for HUB75 matrix output. + +### Chip-specific panel limits + +```cpp +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM) + maxChainLength = 6; // S3 + PSRAM: up to 6 panels (DMA-capable PSRAM) +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + maxChainLength = 2; // S2: limited DMA channels +#else + maxChainLength = 4; // Classic ESP32: default +#endif +``` + +### Color depth vs. pixel count + +The driver dynamically reduces color depth for larger displays to stay within DMA buffer limits: + +| Pixel count | Color depth | Bits per pixel | +|---|---|---| +| ≤ `MAX_PIXELS_10BIT` | 10-bit (30-bit color) | High quality (experimental) | +| ≤ `MAX_PIXELS_8BIT` | 8-bit (24-bit color) | Full quality | +| ≤ `MAX_PIXELS_6BIT` | 6-bit (18-bit color) | Slight banding | +| ≤ `MAX_PIXELS_4BIT` | 4-bit (12-bit color) | Visible banding | +| larger | 3-bit (9-bit color) | Minimal color range | + +### Resource conflicts + +- **APLL**: HUB75 I2S DMA uses the APLL. Disable APLL in the audio I2S driver when HUB75 is active. +- **I2S peripheral**: HUB75 uses `I2S_NUM_1` (or `I2S_NUM_0` on single-I2S chips). Audio must use the other port. +- **Pin count**: HUB75 requires 13–14 GPIO pins. On ESP32-S2 this severely limits remaining GPIO. +- **Reboot required**: on ESP32-S3, changing HUB75 driver options requires a full reboot — the I2S DMA cannot be reconfigured at runtime. + +--- + + +## GPIO Best Practices + +### Prefer `gpio_config()` over individual calls + +```cpp +// Preferred: single struct-based configuration +gpio_config_t io_conf = { + .pin_bit_mask = (1ULL << pin), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, +}; +gpio_config(&io_conf); + +// Avoid: multiple separate calls (more error-prone, deprecated in IDF v5) +gpio_set_direction(pin, GPIO_MODE_OUTPUT); +gpio_set_pull_mode(pin, GPIO_FLOATING); +``` + + +### Pin manager integration + +Always allocate pins through WLED's `pinManager` before using GPIO APIs: + +```cpp +if (!pinManager.allocatePin(myPin, true, PinOwner::UM_MyUsermod)) { + return; // pin in use by another module +} +// Now safe to configure +``` + +--- + +## Timer Best Practices + +### Microsecond timing + +For high-resolution timing, prefer `esp_timer_get_time()` (microsecond resolution, 64-bit) over `millis()` or `micros()`. + + +```cpp +#include +int64_t now_us = esp_timer_get_time(); // monotonic, not affected by NTP +``` + +> **Note**: In arduino-esp32, both `millis()` and `micros()` are thin wrappers around `esp_timer_get_time()` — they share the same monotonic clock source. Prefer the direct call when you need the full 64-bit value or ISR-safe access without truncation: +> ```cpp +> // arduino-esp32 internals (cores/esp32/esp32-hal-misc.c): +> // unsigned long micros() { return (unsigned long)(esp_timer_get_time()); } +> // unsigned long millis() { return (unsigned long)(esp_timer_get_time() / 1000ULL); } +> ``` + + +### Periodic timers + +For periodic tasks with sub-millisecond precision, use `esp_timer`: + +```cpp +esp_timer_handle_t timer; +esp_timer_create_args_t args = { + .callback = myCallback, + .arg = nullptr, + .dispatch_method = ESP_TIMER_TASK, // run in timer task (not ISR) + .name = "my_timer", +}; +esp_timer_create(&args, &timer); +esp_timer_start_periodic(timer, 1000); // 1 ms period +``` + + +Always prefer `ESP_TIMER_TASK` dispatch over `ESP_TIMER_ISR` unless you need ISR-level latency — ISR callbacks have severe restrictions (no logging, no heap allocation, no FreeRTOS API calls). + +### Precision waiting: coarse delay then spin-poll + +When waiting for a precise future deadline (e.g., FPS limiting, protocol timing), avoid spinning the entire duration — that wastes CPU and starves other tasks. Instead, yield to FreeRTOS while time allows, then spin only for the final window. + +```cpp +// Wait until 'target_us' (a micros() / esp_timer_get_time() timestamp) +long time_to_wait = (long)(target_us - micros()); +// Coarse phase: yield to FreeRTOS while we have more than ~2 ms remaining. +// vTaskDelay(1) suspends the task for one RTOS tick, letting other task run freely. +while (time_to_wait > 2000) { + vTaskDelay(1); + time_to_wait = (long)(target_us - micros()); +} +// Fine phase: busy-poll the last ≤2 ms for microsecond accuracy. +// micros() wraps esp_timer_get_time() so this is low-overhead. +while ((long)(target_us - micros()) > 0) { /* spin */ } +``` + + +> The threshold (2000 µs as an example) should be at least one RTOS tick (default 1 ms on ESP32) plus some margin. A value of 1500–3000 µs works well in practice. + +--- + +## ADC Best Practices + + +### Version-aware ADC code + +ADC is one of the most fragmented APIs across IDF versions: + +```cpp +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + // IDF v5: oneshot driver + #include "esp_adc/adc_oneshot.h" + #include "esp_adc/adc_cali.h" + adc_oneshot_unit_handle_t adc_handle; + adc_oneshot_unit_init_cfg_t unit_cfg = { .unit_id = ADC_UNIT_1 }; + adc_oneshot_new_unit(&unit_cfg, &adc_handle); +#else + // IDF v4: legacy driver + #include "driver/adc.h" + #include "esp_adc_cal.h" + adc1_config_width(ADC_WIDTH_BIT_12); + int raw = adc1_get_raw(ADC1_CHANNEL_0); +#endif +``` + + +### Bit width portability + +Not all chips have 12-bit ADC. `SOC_ADC_MAX_BITWIDTH` reports the maximum resolution (12 or 13 bits). Note that in IDF v5, this macro was renamed to `CONFIG_SOC_ADC_RTC_MAX_BITWIDTH`. Write version-aware guards: + +```cpp +// IDF v4: SOC_ADC_MAX_BITWIDTH IDF v5: CONFIG_SOC_ADC_RTC_MAX_BITWIDTH +#if defined(CONFIG_SOC_ADC_RTC_MAX_BITWIDTH) // IDF v5+ + #define MY_ADC_MAX_BITWIDTH CONFIG_SOC_ADC_RTC_MAX_BITWIDTH +#elif defined(SOC_ADC_MAX_BITWIDTH) // IDF v4 + #define MY_ADC_MAX_BITWIDTH SOC_ADC_MAX_BITWIDTH +#else + #define MY_ADC_MAX_BITWIDTH 12 // safe fallback +#endif + +#if MY_ADC_MAX_BITWIDTH == 13 + adc1_config_width(ADC_WIDTH_BIT_13); // ESP32-S2 +#else + adc1_config_width(ADC_WIDTH_BIT_12); // ESP32, S3, C3, etc. +#endif +``` + +WLED's `util.cpp` uses the IDF v4 form (`SOC_ADC_MAX_BITWIDTH`) — this will need updating when the codebase migrates to IDF v5. + +--- + + +## RMT Best Practices + +### Current usage in WLED + +RMT drives NeoPixel LED output (via NeoPixelBus) and IR receiver input. Both use the legacy API that is removed in IDF v5. + +### Migration notes + +- The upstream `V5-C6` branch uses `-D WLED_USE_SHARED_RMT` to switch to the new RMT driver for NeoPixel output. +- IR is disabled on IDF v5 until the IR library is ported. +- New chips (C6, P4) have different RMT channel counts — use `SOC_RMT_TX_CANDIDATES_PER_GROUP` to check availability. +- The new RMT API requires an "encoder" object (`rmt_encoder_t`) to translate data formats — this is more flexible but requires more setup code. + + +--- + +## Espressif Best Practices (from official examples) + +### Error handling + +Always check `esp_err_t` return values. Use `ESP_ERROR_CHECK()` in initialization code, but handle errors gracefully in runtime code. + +```cpp +// Initialization — crash early on failure +ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &config, 0, nullptr)); + +// Runtime — log and recover +esp_err_t err = i2s_read(I2S_NUM_0, buf, len, &bytes_read, portMAX_DELAY); +if (err != ESP_OK) { + DEBUGSR_PRINTF("I2S read failed: %s\n", esp_err_to_name(err)); + return; +} +``` + + +For situations between these two extremes — where you want the `ESP_ERROR_CHECK` formatted log message (file, line, error name) but must not abort — use `ESP_ERROR_CHECK_WITHOUT_ABORT()`. + + +```cpp +// Logs in the same format as ESP_ERROR_CHECK, but returns the error code instead of aborting. +// Useful for non-fatal driver calls where you want visibility without crashing. +esp_err_t err = ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_set_clk(AR_I2S_PORT, rate, bits, ch)); +if (err != ESP_OK) return; // handle as needed +``` + + +### Logging + +WLED uses its own logging macros — **not** `ESP_LOGx()`. For application-level code, always use the WLED macros defined in `wled.h`: + +| Macro family | Defined in | Controlled by | Use for | +|---|---|---|---| +| `DEBUG_PRINT` / `DEBUG_PRINTLN` / `DEBUG_PRINTF` | `wled.h` | `WLED_DEBUG` build flag | Development/diagnostic output; compiled out in release builds | + +All of these wrap `Serial` output through the `DEBUGOUT` / `DEBUGOUTLN` / `DEBUGOUTF` macros. + +**Exception — low-level driver code**: When writing code that interacts directly with ESP-IDF APIs (e.g., I2S initialization, RMT setup), use `ESP_LOGx()` macros instead. They support tag-based filtering and compile-time log level control: + + +```cpp +static const char* TAG = "my_module"; +ESP_LOGI(TAG, "Initialized with %d buffers", count); +ESP_LOGW(TAG, "PSRAM not available, falling back to DRAM"); +ESP_LOGE(TAG, "Failed to allocate %u bytes", size); +``` + +### Task creation and pinning + + +On dual-core chips (ESP32, S3, P4), pin latency-sensitive tasks to a specific core: + +```cpp +xTaskCreatePinnedToCore( + audioTask, // function + "audio", // name + 4096, // stack size + nullptr, // parameter + 5, // priority (higher = more important) + &audioTaskHandle, // handle + 0 // core ID (0 = protocol core, 1 = app core) +); +``` + + +Guidelines: +- Pin network/protocol tasks to core 0 (where Wi-Fi runs). +- Pin real-time tasks (audio, LED output) to core 1. +- On single-core chips (S2, C3, C5, C6), only core 0 exists — pinning to core 1 will fail. Use `SOC_CPU_CORES_NUM > 1` guards or `tskNO_AFFINITY`. +- Use `SOC_CPU_CORES_NUM` to conditionally pin tasks: + ```cpp + #if SOC_CPU_CORES_NUM > 1 + xTaskCreatePinnedToCore(audioTask, "audio", 4096, nullptr, 5, &handle, 1); + #else + xTaskCreate(audioTask, "audio", 4096, nullptr, 5, &handle); + #endif + ``` + +**Tip: use xTaskCreateUniversal()** - from arduino-esp32 - to avoid the conditional on `SOC_CPU_CORES_NUM`. This function has the same signature as ``xTaskCreatePinnedToCore()``, but automatically falls back to ``xTaskCreate()`` on single-core MCUs. + +### `delay()`, `yield()`, and the IDLE task + +FreeRTOS on ESP32 is **preemptive** — all tasks are scheduled by priority regardless of `yield()` calls. This is fundamentally different from ESP8266 cooperative multitasking. + + +| Call | What it does | Reaches IDLE (priority 0)? | +|---|---|---| +| `delay(ms)` / `vTaskDelay(ticks)` | Suspends calling task; scheduler runs all other ready tasks | ✅ Yes | +| `yield()` / `vTaskDelay(0)` | Hint to switch to tasks at **equal or higher** priority only | ❌ No | +| `taskYIELD()` | Same as `vTaskDelay(0)` | ❌ No | +| Blocking API (`xQueueReceive`, `ulTaskNotifyTake`, `vTaskDelayUntil`) | Suspends task until event or timeout; IDLE runs freely | ✅ Yes | + + + +**`delay()` in `loopTask` is safe.** Arduino's `loop()` runs inside `loopTask`. Calling `delay()` suspends only `loopTask` — all other FreeRTOS tasks (Wi-Fi stack, audio FFT, LED DMA) continue uninterrupted on either core. + +**`yield()` does not yield to IDLE.** Any task that loops with only `yield()` calls will starve the IDLE task, causing the IDLE watchdog to fire. Always use `delay(1)` (or a blocking FreeRTOS call) in tight task loops. Note: WLED redefines `yield()` as an empty macro on ESP32 WLEDMM_FASTPATH builds. + +#### Why the IDLE task is not optional + + +The FreeRTOS IDLE task (one per core on dual-core ESP32 and ESP32-S3; single instance on single-core chips) is not idle in the casual sense — it performs essential system housekeeping: + +- **Frees deleted task memory**: when a task calls `vTaskDelete()`, the IDLE task reclaims its TCB and stack. Without IDLE running, deleted tasks leak memory permanently. +- **Runs the idle hook**: when `configUSE_IDLE_HOOK = 1`, the IDLE task calls `vApplicationIdleHook()` on every iteration — some ESP-IDF components register low-priority background work here. +- **Implements tickless idle / light sleep**: on battery-powered devices, IDLE is the entry point for low-power sleep. A permanently starved IDLE task disables light sleep entirely. +- **Runs registered idle hooks**: ESP-IDF components register callbacks via `esp_register_freertos_idle_hook()` (e.g., Wi-Fi background maintenance, Bluetooth housekeeping). These only fire when IDLE runs. + + + +In short: **starving IDLE corrupts memory cleanup, breaks background activities, disables low-power sleep, and prevents Wi-Fi/BT maintenance.** The IDLE watchdog panic is a symptom — the real damage happens before the watchdog fires. + +### Watchdog management + +Long-running operations may trigger the task watchdog. Feed it explicitly: + +```cpp +#include +esp_task_wdt_reset(); // feed the watchdog in long loops +``` + +For tasks that intentionally block for extended periods, consider subscribing/unsubscribing from the TWDT: + +```cpp +esp_task_wdt_delete(NULL); // remove current task from TWDT (IDF v4.4) +// ... long blocking operation ... +esp_task_wdt_add(NULL); // re-register +``` + +> **IDF v5 note**: In IDF v5, `esp_task_wdt_add()` and `esp_task_wdt_delete()` require an explicit `TaskHandle_t`. Use `xTaskGetCurrentTaskHandle()` instead of `NULL`. + + +--- + +## Quick Reference: IDF v4 → v5 API Mapping + +| Component | IDF v4 Header | IDF v5 Header | Key Change | +|---|---|---|---| +| I2S | `driver/i2s.h` | `driver/i2s_std.h` | Channel-based API | +| ADC (oneshot) | `driver/adc.h` | `esp_adc/adc_oneshot.h` | Unit/channel handles | +| ADC (calibration) | `esp_adc_cal.h` | `esp_adc/adc_cali.h` | Scheme-based calibration | +| RMT | `driver/rmt.h` | `driver/rmt_tx.h` / `rmt_rx.h` | Encoder-based transmit | +| SPI Flash | `spi_flash.h` | `esp_flash.h` | `esp_flash_*` functions | +| GPIO | `driver/gpio.h` | `driver/gpio.h` | `gpio_pad_select_gpio()` removed | +| Timer | `driver/timer.h` | `driver/gptimer.h` | General-purpose timer handles | +| PCNT | `driver/pcnt.h` | `driver/pulse_cnt.h` | Handle-based API | + diff --git a/docs/web.instructions.md b/docs/web.instructions.md new file mode 100644 index 0000000000..87c9b74531 --- /dev/null +++ b/docs/web.instructions.md @@ -0,0 +1,30 @@ +--- +applyTo: "wled00/data/**" +--- +# Web UI Coding Conventions + +## Formatting + +- Indent **HTML and JavaScript** with **tabs** +- Indent **CSS** with **tabs** + +## JavaScript Style + +- **camelCase** for functions and variables: `gId()`, `selectedFx`, `currentPreset` +- Abbreviated helpers are common: `d` for `document`, `gId()` for `getElementById()` + +## Key Files + +- `index.htm` — main interface +- `index.js` — functions that manage / update the main interface +- `settings*.htm` — configuration pages +- `*.css` — stylesheets (inlined during build) +- `common.js` — helper functions + +**Reuse shared helpers from `common.js` whenever possible** instead of duplicating utilities in page-local scripts. + +## Build Integration + +Files in this directory are processed by `tools/cdata.js` into generated headers +(`wled00/html_*.h`, `wled00/js_*.h`). +Run `npm run build` after any change. **Never edit generated headers directly.** From 6394bec21ba94c88ecc31ad68fce74a93ed24d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:23:06 +0200 Subject: [PATCH 26/71] Highlight target branch for pull requests Emphasize the importance of targeting the main branch for PRs. --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c65639b3a4..91f1aedfe8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,8 @@ This lets you update your PR if needed, while you can work on other tasks in 'ma ### Target branch for pull requests -Please make all PRs against the `main` branch. +> [!IMPORTANT] +> Please make all PRs against the `main` branch. ### Describing your PR From 96b0c7b4aeacb4050cdb6e937b52b4a22a4e5ef3 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 20 Apr 2026 18:58:09 +0200 Subject: [PATCH 27/71] better packet queuing & pacing for custom palette live preview (#5515) * better packet queuing / pacing for custom palette live preview * fix leak * remove window prefix from variables --- wled00/data/cpal/cpal.htm | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/wled00/data/cpal/cpal.htm b/wled00/data/cpal/cpal.htm index 4e41757a94..7c3ca44ac7 100644 --- a/wled00/data/cpal/cpal.htm +++ b/wled00/data/cpal/cpal.htm @@ -71,6 +71,8 @@ let copyColor = '#000'; let ws = null; let maxCol; // max colors to send out in one chunk, ESP8266 is limited to ~50 (500 bytes), ESP32 can do ~128 (1340 bytes) + let _applySeq = 0; // incremented each time applyLED fires; used to cancel stale in-flight previews + let _httpQueue = [], _httpRun = 0; // load external resources in sequence to avoid 503 errors if heap is low, repeats indefinitely until loaded (function loadFiles() { @@ -621,21 +623,16 @@ async function requestJson(cmd) { - if (ws && ws.readyState == 1) { + if (ws && ws.readyState == 1 && ws.bufferedAmount < 32768) { try { ws.send(JSON.stringify(cmd)); + await new Promise(r => setTimeout(r, 15)); // short delay to give ESP time to process (fewer packets dropped) return 1; } catch (e) {} } - if (!window._httpQueue) { - window._httpQueue = []; - window._httpRun = 0; - } - if (_httpQueue.length >= 5) { - return Promise.resolve(-1); // reject if too many queued requests - } - + // HTTP fallback + if (_httpQueue.length >= 5) return -1; // queue full; applyLED cancels stale queues before sending return new Promise(resolve => { _httpQueue.push({ cmd, resolve }); (async function run() { @@ -650,7 +647,7 @@ cache: 'no-store' }); } catch (e) {} - await new Promise(r => setTimeout(r, 120)); + await new Promise(r => setTimeout(r, 120)); // delay between requests (go slow, this is the http fallback if WS fails) q.resolve(0); } _httpRun = 0; @@ -662,8 +659,12 @@ async function applyLED() { if (!palCache.length) return; + const seq = ++_applySeq; + // discard pending HTTP chunks from any previous preview so stale data doesn't drain slowly + while (_httpQueue.length) _httpQueue.shift().resolve(-1); // resolve dropped entries so their awaiters can observe the seq change and exit try { let st = await (await fetch(getURL('/json/state'), { cache: 'no-store' })).json(); + if (seq !== _applySeq) return; // superseded by a newer preview request if (!st.seg || !st.seg.length) return; // get selected segments, use main segment if none selected @@ -680,6 +681,7 @@ arr.push(palCache[len > 1 ? Math.round(i * 255 / (len - 1)) : 0]); // send colors in chunks for (let j = 0; j < arr.length; j += maxCol) { + if (seq !== _applySeq) return; // superseded mid-send let chunk = [s.start + j, ...arr.slice(j, j + maxCol)]; await requestJson({ seg: { id: s.id, i: chunk } }); } From 974c15c1d3183be60857261a02d4ebdc522e42a3 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 20 Apr 2026 19:06:33 +0200 Subject: [PATCH 28/71] Fix blending style options list filter for iOS (#5513) * fix blending style options list filter for iOS * fix default fallback --- wled00/data/index.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 58b21c32ed..63ed5661ca 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -25,6 +25,7 @@ var pN = "", pI = 0, pNum = 0; var pmt = 1, pmtLS = 0; var lastinfo = {}; var isM = false, mw = 0, mh=0; +var bsOpts = null; // blending style options snapshot, used for dynamic filtering based on matrix mode (iOS compatibility) var ws, wsRpt=0; var cfg = { theme:{base:"dark", bg:{url:"", rnd: false, rndGrayscale: false, rndBlur: false}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}}, @@ -660,12 +661,14 @@ function parseInfo(i) { mw = i.leds.matrix ? i.leds.matrix.w : 0; mh = i.leds.matrix ? i.leds.matrix.h : 0; isM = mw>0 && mh>0; + if (!bsOpts) bsOpts = Array.from(gId('bs').options).map(o => o.cloneNode(true)); // snapshot all options on first call + const bsSel = gId('bs'); + // note: style.display='none' for option elements is not supported on all browsers (notably iOS) + bsSel.replaceChildren(...bsOpts.filter(o => isM || o.dataset.type !== "2D").map(o => o.cloneNode(true))); // allow all in matrix mode, filter 2D blends otherwise if (!isM) { - gId("filter2D").classList.add('hide'); - gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='none';}); + gId("filter2D").classList.add('hide'); // hide 2D effects in non-matrix mode } else { - gId("filter2D").classList.remove('hide'); - gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='';}); + gId("filter2D").classList.remove('hide'); } gId("updBt").style.display = (i.opt & 1) ? '':'none'; // if (i.noaudio) { @@ -1457,7 +1460,9 @@ function readState(s,command=false) tr = s.transition; gId('tt').value = tr/10; - gId('bs').value = s.bs || 0; + const bsSel = gId('bs'); + bsSel.value = s.bs || 0; // assign blending style + if (!bsSel.value) bsSel.value = 0; // fall back to Fade if option does not exist if (tr===0) gId('bsp').classList.add('hide') else gId('bsp').classList.remove('hide') From 6afc990397f5c8c457a750b3ca30c03c327f1ed7 Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Wed, 22 Apr 2026 00:11:59 -0400 Subject: [PATCH 29/71] GoL missing return. (#5523) --- wled00/FX.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8630d1a53a..19af445681 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5451,6 +5451,7 @@ void mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://na } } } + return; } // Repeat detection From 7f292b9ecd33127ea905a4f0ec9bf323310a46dc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 22 Apr 2026 09:56:57 +0200 Subject: [PATCH 30/71] fix indentation --- wled00/data/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 63ed5661ca..a3545981ba 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -666,9 +666,9 @@ function parseInfo(i) { // note: style.display='none' for option elements is not supported on all browsers (notably iOS) bsSel.replaceChildren(...bsOpts.filter(o => isM || o.dataset.type !== "2D").map(o => o.cloneNode(true))); // allow all in matrix mode, filter 2D blends otherwise if (!isM) { - gId("filter2D").classList.add('hide'); // hide 2D effects in non-matrix mode + gId("filter2D").classList.add('hide'); // hide 2D effects in non-matrix mode } else { - gId("filter2D").classList.remove('hide'); + gId("filter2D").classList.remove('hide'); } gId("updBt").style.display = (i.opt & 1) ? '':'none'; // if (i.noaudio) { From 41e4bbf2bdee7115ba3b34c372f39b02574f51ca Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 25 Apr 2026 17:08:22 +0100 Subject: [PATCH 31/71] Merge pull request #5473 from DedeHai/pin_dropdowns Use drop-downs for pins --- wled00/data/common.js | 135 ++++++++++++++++++++ wled00/data/settings_leds.htm | 203 ++++++++++++++----------------- wled00/data/settings_pininfo.htm | 35 +----- wled00/data/settings_sync.htm | 40 ++++-- wled00/data/settings_um.htm | 43 ++----- wled00/dmx_input.cpp | 4 +- wled00/dmx_input.h | 10 +- wled00/pin_manager.h | 2 +- wled00/set.cpp | 2 +- wled00/wled.h | 6 +- wled00/xml.cpp | 12 ++ 11 files changed, 294 insertions(+), 198 deletions(-) diff --git a/wled00/data/common.js b/wled00/data/common.js index 03103798c2..e6cea4d526 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -222,3 +222,138 @@ function sendDDP(ws, start, len, colors) { } return true; } + +// Pin utilities +function getOwnerName(o,t,n) { + // Use firmware-provided name if available + if(n) return n; + if(!o) return "System"; // no owner provided + if(o===0x85){ return getBtnTypeName(t); } // button pin + return "UM #"+o; +} +function getBtnTypeName(t) { + var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"]; + var label = n[t] || "?"; + return 'Button '+label+''; +} +function getCaps(p,c) { + var r=[]; + // Use touch info from settings endpoint + if(d.touch && d.touch.includes(p)) r.push("Touch"); + if(d.ro_gpio && d.ro_gpio.includes(p)) r.push("Input Only"); + // Use other caps from JSON (Analog, Boot, Input Only) + if(c&0x02) r.push("Analog"); + if(c&0x08) r.push("Flash Boot"); + if(c&0x10) r.push("Bootstrap"); + return r.length?r.join(", "):"-"; +} + +// Fetch GPIO caps (/settings/s.js?p=11) then pin occupancy (/json/pins) with retry. +// Caches result in d.pinsData. Calls cb() when ready (or on failure). +// If page already loaded its own s.js (d.max_gpio set), skips caps load and goes straight to pins fetch. +function fetchPinInfo(cb, retries=5) { + if (d.pinsData) { cb&&cb(); return; } + var done=false, fr=retries; + function doFetch() { + fetch(getURL('/json/pins')) + .then(r=>r.json()) + .then(j=>{ if(!done){done=true; d.pinsData=j.pins||[]; cb&&cb();} }) + .catch(()=>{ fr-->0 ? setTimeout(doFetch,100) : (!done&&(done=true,d.pinsData=[],cb&&cb())); }); + } + if (d.max_gpio) { doFetch(); return; } + // Load GPIO caps from s.js?p=11 first (sets d.rsvd/ro_gpio/max_gpio/touch/adc/um_p) + d.max_gpio=50; d.rsvd=[]; d.ro_gpio=[]; d.touch=[]; d.adc=[]; d.um_p=[]; + var cr=retries; + function tryCaps() { + var s=cE("script"); s.src=getURL('/settings/s.js?p=11'); + d.body.appendChild(s); + s.onload=function(){ GetV(); doFetch(); }; + s.onerror=function(){ cr-->0 ? setTimeout(tryCaps,100) : doFetch(); }; + } + tryCaps(); +} + +// Pin dropdown utilities +// Create or rebuild a pin or existing back to +function unmakePinSelect(name) { + let sel = gN(name); + if (!sel || sel.tagName !== "SELECT") return null; + let inp = cE('input'); + inp.type = "number"; + inp.name = sel.name; + inp.value = sel.value; + inp.className = "s"; + if (sel.required) inp.required = true; + let oc = sel.getAttribute("onchange"); + if (oc) inp.setAttribute("onchange", oc); + sel.parentElement.replaceChild(inp, sel); + return inp; +} +// Add option to select, auto-select matching data-val +function addOption(sel, txt, val) { + if (!sel) return null; + let opt = cE("option"); + opt.value = val; + opt.text = txt; + sel.appendChild(opt); + if (sel.dataset.val !== undefined) { + for (let i = 0; i < sel.options.length; i++) { + if (sel.options[i].value == sel.dataset.val) { sel.selectedIndex = i; break; } + } + } + return opt; +} diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index c10affc545..67abbefae3 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -49,7 +49,7 @@ setABL(); d.Sf.addEventListener("submit", trySubmit); if (d.um_p[0]==-1) d.um_p.shift(); - pinDropdowns(); + fetchPinInfo(pinDropdowns); }); // If we set async false, file is loaded and executed, then next statement is processed if (loc) d.Sf.action = getURL('/settings/leds'); } @@ -70,9 +70,10 @@ function isS2() { return chipID == 2; } function isS3() { return chipID == 3; } function is32() { return chipID == 4; } + function pinsOK() { var ok = true; - var nList = d.Sf.querySelectorAll("#mLC input[name^=L]"); + var nList = Array.from(d.Sf.querySelectorAll("#mLC input[name^=L], #mLC select.pin[name^=L]")); // include both input types (numeric & dropdown) nList.forEach((LC,i)=>{ if (!ok) return; // prevent iteration after conflict let nm = LC.name.substring(0,2); // field name : /L./ @@ -84,19 +85,28 @@ } // ignore IP address if (isNet(t)) return; + // LED pin(s) must be assigned for non-virtual types + let pIdx = parseInt(LC.name.charAt(1)); // determine pin index (0 for L0, 1 for L1, etc.) + if (pIdx < numPins(t) && !isVir(t) && (LC.value === "" || LC.value === "-1")) { + LC.focus(); + ok = false; + return; + } //check for pin conflicts if (LC.value!="" && LC.value!="-1") { let p = d.rsvd.concat(d.um_p); // used pin array - d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay + //d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay + // only non-LED selects (buttons, IR, relay) — LED pins check against each other below + d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(!/^L[0-4]/.test(e.name) && e.value>-1)p.push(parseInt(e.value));}) if (p.some((e)=>e==parseInt(LC.value))) { alert(`Sorry, pins ${JSON.stringify(p)} can't be used.`); - LC.value=""; + if (LC.tagName==="SELECT") LC.value="-1"; else LC.value=""; LC.focus(); ok = false; return; } else if (d.ro_gpio.some((e)=>e==parseInt(LC.value))) { alert(`Sorry, pins ${JSON.stringify(d.ro_gpio)} are input only.`); - LC.value=""; + if (LC.tagName==="SELECT") LC.value="-1"; else LC.value=""; LC.focus(); ok = false; return; @@ -107,10 +117,9 @@ let m = nList[j].name.substring(2,3); // bus number (0-Z) let t2 = parseInt(gN("LT"+m).value, 10); if (isVir(t2)) continue; - if (nList[j].value!="" && nList[i].value==nList[j].value) { + if (nList[j].value!="" && nList[j].value!="-1" && LC.value==nList[j].value) { alert(`Pin conflict between ${LC.name}/${nList[j].name}!`); - nList[j].value=""; - nList[j].focus(); + if (nList[j].tagName==="SELECT") nList[j].value="-1"; else { nList[j].value=""; nList[j].focus(); } ok = false; return; } @@ -303,14 +312,28 @@ gId("p0d"+n).innerText = p0d; gId("p1d"+n).innerText = p1d; gId("off"+n).innerText = off; - // secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off) - let pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 4*isHub75(t); // fixes network pins to 4 + // convert pin fields so show/hide applies to the correct element type + for (let p=0; p<5; p++) { + let nm2 = "L"+p+n; + let el = d.Sf[nm2]; + if (!el) continue; + if (isVir(t) || isHub75(t)) { + if (el.tagName === "SELECT") unmakePinSelect(nm2); // see common.js + } else { + if (el.tagName === "INPUT" && el.type === "number") { + makePinSelect(nm2, 1); // see common.js + d.pinUpdPending = true; + } + } + } + // show/hide secondary pins on whatever element type now exists + let pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 4*isHub75(t); for (let p=1; p<5; p++) { var LK = d.Sf["L"+p+n]; if (!LK) continue; LK.style.display = (p < pins) ? "inline" : "none"; LK.required = (p < pins); - if (p >= pins) LK.value=""; + if (p >= pins) LK.value = (LK.tagName === "SELECT") ? "-1" : ""; } } @@ -415,25 +438,6 @@ LC.style.color="#fff"; return; // do not check conflicts } - // check for pin conflicts & color fields - if (nm.search(/^L[0-4]/) == 0) // pin fields - if (LC.value!="" && LC.value!="-1") { - let p = d.rsvd.concat(d.um_p); // used pin array - d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay - for (j=0; je==parseInt(LC.value))) LC.style.color = "red"; - else LC.style.color = d.ro_gpio.some((e)=>e==parseInt(LC.value)) ? "orange" : "#fff"; - } else LC.style.color = "#fff"; }); // Use helper function to calculate channel usage @@ -510,6 +514,11 @@ gId('fpsNone').style.display = (d.Sf.FR.value == 0) ? 'block':'none'; gId('fpsWarn').style.display = (d.Sf.FR.value == 0) || (d.Sf.FR.value >= 80) ? 'block':'none'; gId('fpsHigh').style.display = (d.Sf.FR.value >= 80) ? 'block':'none'; + + if (d.pinUpdPending) { + d.pinUpdPending = false; + d.Sf.querySelectorAll("select.pin").forEach((e) => { pinUpd(e); }); + } } function lastEnd(i) { @@ -671,11 +680,18 @@ gId("com_rem").style.display = (i>0) ? "inline":"none"; } + // get pin dropdown flags for button type: touch=2, ADC=4, any=0 + function btnPinFlags(t) { return (t==6||t==9) ? 2 : (t==7||t==8) ? 4 : 0; } + function btnPinDd(s) { + let t = parseInt(d.Sf["BE"+s].value); + makePinSelect("BT"+s, btnPinFlags(t)); + d.Sf.querySelectorAll("select.pin").forEach(e => pinUpd(e)); + } function addBtn(i,p,t) { var b = gId("btns"); var s = chrID(i); var c = `
#${i} GPIO: `; - c += ` ` c += ``; c += ``; c += ``; @@ -805,101 +821,70 @@ } } function pinDropdowns() { - let fields = ["IR","RL"]; // IR & relay - gId("btns").querySelectorAll('input[type="number"]').forEach((e)=>{fields.push(e.name);}) // buttons - for (let i of d.Sf.elements) { - if (i.type === "number" && fields.includes(i.name)) { //select all pin select elements - let v = parseInt(i.value); - let sel = addDropdown(i.name,0); - for (var j = -1; j < d.max_gpio; j++) { - if (d.rsvd.includes(j)) continue; - let foundPin = d.um_p.indexOf(j); - let txt = (j === -1) ? "unused" : `${j}`; - if (foundPin >= 0 && j !== v) txt += ` used`; // already reserved pin - if (d.ro_gpio.includes(j)) txt += " (R/O)"; - let opt = addOption(sel, txt, j); - if (j === v) opt.selected = true; // this is "our" pin - else if (d.um_p.includes(j) && j > -1) opt.disabled = true; // someone else's pin - } - } - } - // update select options - d.Sf.querySelectorAll("select.pin").forEach((e)=>{pinUpd(e);}); - // add dataset values for LED GPIO pins - d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i)=>{ - if (i.value!=="" && i.value>=0) + // Rebuild LED GPIO selects now that d.pinsData is available. + d.Sf.querySelectorAll(".iST input.s[name^=L], .iST select.pin[name^=L]").forEach((e) => { + let n = e.name.substring(2, 3); + let t = parseInt(d.Sf["LT"+n].value, 10); + if (!isVir(t) && !isHub75(t)) makePinSelect(e.name, 1); + }); + // IR (any pin including input-only) + let irSel = makePinSelect("IR", 0); + if (irSel) irSel.onchange = function() { UI(); pinUpd(this); }; + // Relay (output required) + let rlSel = makePinSelect("RL", 1); + if (rlSel) rlSel.onchange = function() { UI(); pinUpd(this); }; + // Buttons (flags depend on button type: touch=2, ADC=4) + gId("btns").querySelectorAll('input[type="number"], select.pin[name^=BT]').forEach((e) => { + let s = e.name.substring(2); + let t = parseInt(d.Sf["BE"+s]?.value) || 0; + let bSel = makePinSelect(e.name, btnPinFlags(t)); + if (bSel) bSel.onchange = function() { UI(); pinUpd(this); }; + }); + // cross-update all pin selects + d.Sf.querySelectorAll("select.pin").forEach((e) => { pinUpd(e); }); + // add dataset values for remaining LED GPIO inputs (virtual/HUB75) + d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i) => { + if (i.value !== "" && parseInt(i.value, 10) >= 0) i.dataset.val = i.value; }); } function pinUpd(e) { - // update changed select options across all usermods + // update changed select options across all pin selects let oldV = parseInt(e.dataset.val); e.dataset.val = e.value; - let txt = e.name; + let label = /^L[0-4].$/.test(e.name) ? 'LED' : e.name; let pins = []; - d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i)=>{ - if (i.value!=="" && i.value>=0 && i.max<255) - pins.push(i.value); + // collect LED bus pin values from remaining inputs (virtual/HUB75) + d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i) => { + let busN = i.name.substring(2, 3); + let t = parseInt(d.Sf["LT"+busN].value, 10); + let p = parseInt(i.name.charAt(1), 10); + if (isVir(t) || isHub75(t) || p >= numPins(t)) return; + if (i.value !== "" && parseInt(i.value, 10) >= 0) pins.push(i.value); + }); + // collect LED bus pin values from selects + d.Sf.querySelectorAll(".iST select.pin[name^=L]").forEach((s) => { + if (s !== e && parseInt(s.value) >= 0) + pins.push(s.value); }); let selects = d.Sf.querySelectorAll("select.pin"); for (let sel of selects) { - if (sel == e) continue - Array.from(sel.options).forEach((i)=>{ + if (sel == e) continue; + Array.from(sel.options).forEach((i) => { + if (i.value == sel.dataset.val) return; // skip sel's own selected pin let led = pins.includes(i.value); - if (!(i.value==oldV || i.value==e.value || led)) return; - if (i.value == -1) { - i.text = "unused"; - return - } + if (!(i.value == oldV || i.value == e.value || led)) return; + if (i.value == -1) { i.text = "unused"; return; } i.text = i.value; - if (i.value==oldV) { - i.disabled = false; - } - if (i.value==e.value || led) { + if (i.value == oldV) { i.disabled = false; } + if (i.value == e.value || led) { i.disabled = true; - i.text += ` ${led?'LED':txt}`; + i.text += ` ${led ? 'LED' : label}`; } - if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)"; + //if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)"; // read only pin note: removed as pin is not shown for outputs }); } } - // https://stackoverflow.com/questions/39729741/javascript-change-input-text-to-select-option - function addDropdown(field) { - let sel = cE('select'); - sel.classList.add("pin"); - let inp = d.getElementsByName(field)[0]; - if (inp && inp.tagName === "INPUT" && (inp.type === "text" || inp.type === "number")) { // may also use nodeName - let v = inp.value; - let n = inp.name; - // copy the existing input element's attributes to the new select element - for (var i = 0; i < inp.attributes.length; ++ i) { - var att = inp.attributes[i]; - // type and value don't apply, so skip them - // ** you might also want to skip style, or others -- modify as needed ** - if (att.name != 'type' && att.name != 'value' && att.name != 'class' && att.name != 'style') { - sel.setAttribute(att.name, att.value); - } - } - sel.setAttribute("data-val", v); - sel.setAttribute("onchange", "pinUpd(this)"); - // finally, replace the old input element with the new select element - inp.parentElement.replaceChild(sel, inp); - return sel; - } - return null; - } - function addOption(sel,txt,val) { - if (sel===null) return; // select object missing - let opt = cE("option"); - opt.value = val; - opt.text = txt; - sel.appendChild(opt); - for (let i=0; i{ - d.um_p = []; - d.rsvd = []; - d.ro_gpio = []; - d.max_gpio = 50; - d.touch = []; - }, ()=>{ - // Load extended GPIO info and start pin polling - loadPins(); - pinsTimer = setInterval(loadPins, 250); - }); + fetchPinInfo(()=>{ loadPins(); pinsTimer=setInterval(loadPins,250); }); } function B(){window.open(getURL('/settings'),'_self');} // back button - function getOwnerName(o,t,n) { - // Use firmware-provided name if available - if(n) return n; - if(!o) return "System"; // no owner provided - if(o===0x85){ return getBtnTypeName(t); } // button pin - return "UM #"+o; - } - function getBtnTypeName(t) { - var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"]; - var label = n[t] || "?"; - return 'Button '+label+''; - } - function getCaps(p,c) { - var r=[]; - // Use touch info from settings endpoint - if(d.touch && d.touch.includes(p)) r.push("Touch"); - if(d.ro_gpio && d.ro_gpio.includes(p)) r.push("Input Only"); - // Use other caps from JSON (Analog, Boot, Input Only) - if(c&0x02) r.push("Analog"); - if(c&0x08) r.push("Flash Boot"); - if(c&0x10) r.push("Bootstrap"); - return r.length?r.join(", "):"-"; - } function loadPins() { fetch(getURL('/json/pins'),{method:'get'}) .then(r=>r.json()) diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index 43baaf6718..12eb3e8d07 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -42,12 +42,30 @@ function SetVal(){switch(parseInt(d.Sf.EP.value)){case 5568: d.Sf.DI.value = 5568; break; case 6454: d.Sf.DI.value = 6454; break; case 4048: d.Sf.DI.value = 4048; break; }; SP();FC();} function S(){ getLoc(); - loadJS(getURL('/settings/s.js?p=4'), false, undefined, ()=>{SetVal();}); // If we set async false, file is loaded and executed, then next statement is processed + loadJS(getURL('/settings/s.js?p=4'), false, undefined, ()=>{SetVal(); fetchPinInfo(pinDropdowns);}); // If we set async false, file is loaded and executed, then next statement is processed if (loc) d.Sf.action = getURL('/settings/sync'); } function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; } + const dmxNs = ["IDMR","IDMT","IDME"]; + let dmxOwnPins; // track dmx pins to update dropdown restrictions on pin change + function pinDropdowns() { + dmxNs.forEach((n,i) => { const s = makePinSelect(n, i?1:0); if (s) s.onchange = dmxPinUpd; }); // note on the i?1:0 -> RX pin is allowed input only but TX/EN pins must be GPIO + dmxOwnPins = dmxNs.map(n => parseInt(gN(n)?.value??-1)); + dmxPinUpd(); + } + function dmxPinUpd() { + const vs = dmxNs.map(n => parseInt(gN(n)?.value??-1)); + dmxNs.forEach((n,i) => { + for (const o of gN(n)?.options??[]) { + const p = parseInt(o.value); if (p<0) continue; + // unlock old selected pin, lock newly selected one + o.disabled = (p!==vs[i] && !dmxOwnPins.includes(p) && !!d.pinsData?.find(pi=>pi.p===p)?.a) || vs.some((v,j)=>j!==i&&v===p); + if (!o.disabled) o.text = String(p); // clear owner name i.e. (DMX input) + } + }); + } function hideDMXInput(){gId("dmxInput").style.display="none";} function hideNoDMXInput(){gId("dmxInputOff").style.display="none";} function hideNoDMXOutput(){gId("dmxOnOffOutput").style.display="none";} @@ -126,8 +144,8 @@

Send

Send notifications on button press or IR:
Send Alexa notifications:
Send Philips Hue change notifications:
-UDP packet retransmissions:

-Reboot required to apply changes. +UDP packet retransmissions:
+Reboot required to apply changes.

Instance List

@@ -139,7 +157,7 @@

Realtime

Receive UDP realtime:
Use main segment only:
Respect LED Maps:

-Network DMX input
+

Network DMX input


Type:
Realtime LED offset:
-

Wired DMX Input Pins

- DMX RX: RO
- DMX TX: DI
- DMX Enable: RE+DE
+
+

Wired DMX Input

+ DMX RX Pin: RO
+ DMX TX Pin: DI
+ DMX Enable Pin: RE+DE
DMX Port:
-
+ Reboot required to apply changes. +

This firmware build does not include DMX Input support.
@@ -223,7 +243,7 @@

MQTT

Group Topic:
Publish on button press:
Retain brightness & color messages:
-Reboot required to apply changes. MQTT info +Reboot required to apply changes. MQTT info
diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index bb2113ddbc..0a795088d7 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -7,7 +7,6 @@