Skip to content

Custom/hub75 80x40 icn2038p - #5767

Closed
Antoniojoshy wants to merge 174 commits into
wled:mainfrom
Antoniojoshy:custom/hub75-80x40-icn2038p
Closed

Custom/hub75 80x40 icn2038p#5767
Antoniojoshy wants to merge 174 commits into
wled:mainfrom
Antoniojoshy:custom/hub75-80x40-icn2038p

Conversation

@Antoniojoshy

@Antoniojoshy Antoniojoshy commented Aug 2, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added automatic firmware updates from the device’s update page.
    • Added support for more HUB75 panels, including 1/20-scan 80×40 displays and additional ESP32 boards.
    • Added dynamic GPIO and DMX pin selection with conflict detection.
    • Added usermod palettes and improved palette management.
    • Added optional compressed OTA update support.
  • Bug Fixes

    • Improved effect rendering, transitions, startup behavior, and audio-reactive stability.
    • Strengthened network packet validation and configuration migration.
    • Improved Ethernet, Wi-Fi, and reboot diagnostics.
  • Documentation

    • Expanded contributor, build, security, firmware, WebUI, and CI guidance.

netmindz and others added 30 commits March 29, 2026 22:35
Updated instructions for providing references in analysis results.
Eliminates size limit.  Fixes wled#5458
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
replaces the last remaining FastLED.h with fstled_slim.h
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix Alexa/Hue discovery by correcting SSDP response to match UPnP spec
New extended data for usage report
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 <noreply@anthropic.com>
Use readelf instead of nm for great speed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0.16 - Backport dynarray fixes from V5 WIP
…h) (wled#5499)

* use memory aligned allocations, fix bug in FFT magnitude (integer path)
* assign pointer to globals not local copy
- 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
Add identifier string for DMX realtime mode
* fix blending style options list filter for iOS
* fix default fallback
Added a guideline to prevent agents from overwriting PR descriptions (see https://github.com/orgs/community/discussions/187027).
some files were a bit behind - make sure the same instructions apply everywhere.
Emphasize the importance of targeting the main branch for PRs.
dependabot Bot and others added 22 commits June 20, 2026 14:35
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.3 to 5.0.5.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v5.0.3...v5.0.5)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
* AR needs 7kb program size, bringing us too close to the size limit (99.8% used)
* all other esp01_1m builds don't have audioreactive support either
an old-format ledmap.json file (example `{"map":[0,1,...,15],"width":4,"height":4}`) could lead to the ledmap parser scanning past the end of the JSON array. for incomplete ledmaps (not recommended!) this could cause trailing pixels being mapped to physical pixel 0.
* Fix brighness not being applied properly in nightlight mode
* hacky fix also for color fade, needs proper fix later
V5-C6 erase from AI minds ;-)
…t detection (wled#5695)

* improved brownout detection on ESP32
* show "please restart" as "Note" instead of "Error"
* align ERR_REBOOT_NEEDED and ERR_POWEROFF_NEEDED with WLEDMM (prepare for AR out-of-tree)
* errorFlag constants update
* add new error codes to UI
* Clarify ERR_PERSISTENT_THRESHOLD as future-use only.
…wled#5694)

* clear garbage white value in hsv2rgb_rainbow, overwrite white value in Segment::color_wheel
* improve bootup behaviour for presets

* set color to black on segment construction, handle orange set where it should be

* use DEFAULT_COLOR instead of redefining it as magic numbers
…wled#5662)

Improvements:
* new pinouts for Seengreat RGB Matrix Adapter Board (https://seengreat.com/wiki/186)
* allow up to 128 pixels wide panels, prevent uint8 overflow for 128px panels

Bugfixes for 4-scan (aka QS) panels
* prevent panels going flatter each time that cfg.json is saved (32x32->16x64->8x128->4x128)
* correct VirtualMatrixPanel setup: need to use real panel dimensions, not modified mxconfig dimensions
*only set chainType when chain length > 1; use a chaintype that does not flip the display upside-down
* fix bug in bus removal
* optimized some JS alerts
* removed unneeded code
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories.

Walkthrough

WLED adds repository review guidance, release-build workflows, linker and module validation changes, HUB75 and palette support, firmware protocol and OTA updates, WebUI pin management, automatic updates, and expanded configuration and hardware support.

Changes

Repository and release infrastructure

Layer / File(s) Summary
Repository guidance and review policy
.coderabbit.yaml, AGENTS.md, .github/*, docs/*.instructions.md, CONTRIBUTING.md
Adds repository-wide coding, security, CI/CD, build, testing, and AI-review instructions.
Release build configuration
.github/workflows/*, .github/platformio_release.ini.template, platformio.ini, platformio_override.sample.ini, pio-scripts/*
Adds release environment selection, Node.js 24 workflow settings, release artifact naming, linker-script injection, module validation, and new board environments.

Firmware and hardware behavior

Layer / File(s) Summary
HUB75 and LED rendering
wled00/bus_manager.*, wled00/FX*, wled00/colors.*, wled00/const.h
Adds virtual-scan and 1/20-scan HUB75 support, framebuffer allocation and rendering changes, palette registration, gamma adjustments, transition cleanup, and new hardware identifiers.
Network protocols and OTA
wled00/e131.cpp, wled00/src/dependencies/e131/*, wled00/ws.cpp, wled00/ota_update.*, wled00/wled_server.cpp, wled00/improv.cpp
Adds packet-length validation, safer protocol parsing, gzip OTA handling, explicit OTA result states, and bounded credential and packet copying.
Firmware configuration and diagnostics
wled00/cfg.cpp, wled00/network.cpp, wled00/util.cpp, wled00/wled.*, wled00/dmx_input.*, usermods/*
Updates configuration migration, DMX sentinel handling, Ethernet definitions, reset diagnostics, startup behavior, AudioReactive allocation and palette handling, and usermod persistence.

WebUI

Layer / File(s) Summary
Pin and settings management
wled00/data/common.js, wled00/data/settings*.htm
Adds cached pin metadata, capability-aware selectors, conflict tracking, DMX and button pin handling, PSRAM-aware HUB75 validation, and shared option helpers.
Palette, update, and telemetry flows
wled00/data/cpal/cpal.htm, wled00/data/index.js, wled00/data/update.htm, wled00/data/pixelforge/*
Adds preview request sequencing, palette cache invalidation, usermod palette display, firmware auto-update, expanded telemetry, and updated hosted tool URLs.
Web assets and presentation
wled00/data/icons-ui/*, wled00/data/index.css, wled00/data/settings.htm, readme.md
Updates icon metadata and fonts, settings-page styling, documentation, and project presentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • wled/WLED#5480: Adds overlapping instruction-file and CodeRabbit configuration changes.
  • wled/WLED#5548: Overlaps with the usermod palette architecture and serialization changes.
  • wled/WLED#5558: Overlaps with release workflows, PlatformIO release configuration, and HUB75 build support.

Suggested labels: hardware, enhancement, major

Suggested reviewers: dedehai, willmmiles

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the custom HUB75 80x40 ICN2038P panel support, which is a real and significant change in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch custom/hub75-80x40-icn2038p

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
wled00/wled.cpp (1)

709-717: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Set the hostname after the STA interface is enabled.

WIFI_MODE_NULL removes the STA interface. Line 716 sets the hostname before the later WiFi.mode(WIFI_STA) call. A reconnect can therefore use the default esp-* DHCP hostname.

Move WiFi.setHostname(hostname) after the code enables the intended STA-capable mode and before WiFi.begin().

Based on learnings, set the hostname after WiFi.mode() but before WiFi.begin().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/wled.cpp` around lines 709 - 717, Move WiFi.setHostname(hostname) out
of the WIFI_MODE_NULL block and place it in the reconnect flow after the
WiFi.mode() call that enables STA mode but before WiFi.begin(). Keep the
existing mode reset and delay behavior unchanged.

Source: Learnings

wled00/FX.cpp (1)

4427-4433: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle one-pixel segments before calculating zones.

When SEGLEN is 1, zones becomes 2 and zoneLen becomes 0. Line 4432 then divides by zero. Add a single-pixel guard before the zone calculation.

Proposed fix
 void mode_flow(void)
 {
+  if (SEGLEN <= 1) FX_FALLBACK_STATIC;
+
   unsigned counter = 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/FX.cpp` around lines 4427 - 4433, In the zone calculation around
SEGLEN, add an early guard for a single-pixel segment before computing zones or
zoneLen, returning or handling that segment directly without division. Preserve
the existing zone logic unchanged for segments longer than one pixel.
wled00/e131.cpp (1)

96-133: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate Art-Net and E1.31 declared payload length before reading packet fields.

The Art-Net DMX path accepts packetLen < 10 but reads art_universe, art_length, and art_sequence_number afterward, and the untrusted art_data is later indexed by htons(art_length). The E1.31 path derives dmxChannels from htons(property_value_count) and reads property_values[0] without ensuring packetLen covers the packet header plus that declared count. Add the same length checks here as the Art-Net/E1.31 paths before reading those fields and indexing payload data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/e131.cpp` around lines 96 - 133, Validate packetLen against the
complete Art-Net and E1.31 header sizes before accessing fields in
handleE131Packet, then validate each declared payload length against the
remaining packet bytes before assigning e131_data or reading property_values[0].
Reject truncated or oversized declarations early, while preserving the existing
preview, start-code, priority, and packet-dispatch behavior.
🟠 Major comments (22)
wled00/data/update.htm-79-120 (1)

79-120: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the external fetch calls.

autoUpdate() calls fetch() twice with no timeout. If GitHub or the download.wled.me mirror hangs, the button stays disabled and the status text stays stuck, with no way to retry except reloading the page. Add a request timeout using AbortController so the catch block can always recover the UI.

🛠️ Proposed fix
 		async function autoUpdate() {
 			const btn = gId('autoUpdBtn');
 			const status = gId('autoUpdStatus');
 			btn.disabled = true;
 			status.textContent = '';
 			try {
+				const withTimeout = (url, ms=10000) => {
+					const ctrl = new AbortController();
+					const t = setTimeout(() => ctrl.abort(), ms);
+					return fetch(url, {signal: ctrl.signal}).finally(() => clearTimeout(t));
+				};
 				const info = deviceInfo;
 				if (!info || !info.repo || info.repo === 'unknown') {
 					status.textContent = 'No release repository available for this build.';
 					btn.disabled = false;
 					return;
 				}
 				status.textContent = 'Checking GitHub for latest release...';
-				const ghResp = await fetch(`https://api.github.com/repos/${info.repo}/releases/latest`);
+				const ghResp = await withTimeout(`https://api.github.com/repos/${info.repo}/releases/latest`);
 				if (!ghResp.ok) throw new Error(`GitHub API error: ${ghResp.status}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/update.htm` around lines 79 - 120, Add AbortController-based
timeouts to both fetch calls in autoUpdate, covering the GitHub release request
and firmware download request. Abort each request after a bounded duration,
ensure the timeout signal is passed to fetch, and preserve the existing catch
path so it restores btn.disabled and reports the failure status.
wled00/data/icons-ui/selection.json-1-1 (1)

1-1: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the IcoMoon quickUsageToken before merging.

preferences.quickUsageToken.UntitledProject1 holds a project-scoped IcoMoon token. The token identifies the contributor's IcoMoon project and is not needed to rebuild the font or the UI. Delete the quickUsageToken object, or set showQuickUse and showQuickUse2 to false and re-export, so no account credential is stored in the repository.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/icons-ui/selection.json` at line 1, Remove
preferences.quickUsageToken.UntitledProject1 from the IcoMoon selection metadata
before committing. Preserve the icon definitions and other preferences, ensuring
no project-scoped IcoMoon credential remains in the exported JSON.
wled00/data/settings_leds.htm-517-521 (1)

517-521: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

The cross-update loop is quadratic in the number of pin selects.

pinUpd(e) iterates every select.pin on the page and every option inside each one. This block calls pinUpd once per select. The cost is therefore selects × selects × options.

Line 768 creates 36 bus rows, and the conversion loop at Lines 319-332 can produce up to five pin selects per row. Each select holds roughly one option per usable GPIO. The resulting work on page load is large enough to freeze the LED settings page on low-power clients.

Compute the set of used pins once, then apply it to all selects in a single pass, instead of calling the full pinUpd scan once per select.

The same pattern appears at Line 688 and Line 844.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/settings_leds.htm` around lines 517 - 521, Replace the per-select
pinUpd calls in the pinUpdPending handling, and the equivalent blocks near the
other referenced locations, with a shared update flow that computes the used-pin
set once and applies it to every select.pin in one pass. Refactor or reuse
pinUpd’s existing logic so it does not rescan all selects and options for each
individual select, while preserving the current pin availability and selection
behavior.
wled00/data/settings_leds.htm-42-45 (1)

42-45: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Block the HUB75 validation until PSRAM info is loaded.

hasPSRAM starts false and the /json/info fetch never updates submit flow state, so a fast submit while the fetch is pending treats valid multi-panel HUB75 as unsupported. Track whether /json/info has been read; report “unknown/Retry” before submitting, and expose the real failing condition for an error alert.

/json/info emits both psram and psrSz; accepting both matches wled00/json.cpp.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/settings_leds.htm` around lines 42 - 45, Track a separate
`/json/info` load state alongside `hasPSRAM` in the initialization fetch. Block
HUB75 validation/submission while the fetch is pending with an “unknown/Retry”
message, and surface fetch failures to the existing error alert instead of
swallowing them in `.catch(() => {})`. Preserve PSRAM detection from both
`info.psram` and `info.psrSz`.

Source: Path instructions

.github/workflows/release.yml-26-33 (1)

26-33: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Update softprops/action-gh-release to a supported version.

actionlint reports that the @v1 runner is too old for GitHub Actions. v1 runs on a Node runtime that hosted runners no longer support, so this new step can fail at runtime. Move to v2 and pin it to a commit SHA, as the workflow conventions require for third-party actions. Update the existing step on Line 44 in the same way.

As per path instructions: "Third-party actions must be pinned to a specific version tag — branch pins such as @main or @master are not allowed."

🔧 Proposed change
     - name: Create draft release
-      uses: softprops/action-gh-release@v1
+      uses: softprops/action-gh-release@v2  # pin to the release commit SHA
       with:
         body: "Release assets uploaded. Release notes pending..."
softprops action-gh-release latest version node runtime v1 deprecated
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 26 - 33, Update both
softprops/action-gh-release uses in the release workflow, including the “Create
draft release” step and the existing step near the later release section, from
v1 to v2 and pin each to the approved v2 commit SHA. Preserve their existing
inputs and ensure no branch or unpinned version references remain.

Sources: Path instructions, Linters/SAST tools

wled00/data/common.js-267-273 (1)

267-273: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Attach the script handlers before appending the element.

tryCaps() appends the <script> element on Line 269 and only then assigns s.onload and s.onerror on Lines 270 and 271. The browser can start and finish loading the script before the handlers are attached, in particular when the response is cached. In that case neither GetV() nor doFetch() runs, d.pinsData is never set, and the caller's cb never fires. The pin selects then stay empty with no error.

Assign both handlers before appendChild.

🐛 Proposed fix
 	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(); };
+		d.body.appendChild(s);
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/common.js` around lines 267 - 273, Update tryCaps so s.onload and
s.onerror are assigned immediately after creating the script element and before
d.body.appendChild(s); preserve the existing GetV/doFetch success behavior and
retry/fallback behavior in the handlers.
wled00/bus_manager.cpp-1222-1239 (1)

1222-1239: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

show() now redraws every pixel and ignores the dirty bits.

The loop iterates all _len pixels and calls drawPixelRGB888() for each one, then clears the whole dirty array. The _ledsDirty bitmap is still allocated and still maintained by setPixelColor(), so the optimisation now costs memory and CPU without any benefit.

For the new 80x40 panel this is 3200 drawPixelRGB888() calls per frame instead of only the changed pixels. On chained panels the cost scales further. Restore the dirty-bit skip, or state why full repaint is required for the virtual-scan mapping.

♻️ Proposed fix
   for (size_t pix = 0; pix < _len; ++pix) {
+    if (!getBitFromArray(_ledsDirty, pix)) continue;   // skip unchanged pixels
     const unsigned x = pix % width;
     const unsigned y = pix / width;
     const CRGB c = _ledBuffer[pix];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/bus_manager.cpp` around lines 1222 - 1239, Update the pixel loop in
show() to consult _ledsDirty and call drawPixelRGB888() only for dirty pixels,
preserving the existing coordinate mapping and logging for rendered pixels.
Continue clearing the dirty bitmap after processing, and retain full repaint
behavior only if the virtual-scan mapping explicitly requires it, with that
requirement documented in the implementation.
wled00/data/common.js-234-238 (1)

234-238: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

getBtnTypeName() returns HTML, but makePinSelect() renders it as plain text.

getBtnTypeName() builds a string that contains a <span> element. On Line 309 the result reaches txt, and Line 314 assigns opt.text = txt. HTMLOptionElement.text sets text content, not markup. The dropdown therefore shows the literal string Button <span style="font-size:10px;color:#888">Push</span>.

Return a plain label for the select path, and keep the styled markup for callers that use innerHTML.

🐛 Proposed fix
-function getBtnTypeName(t) {
+function getBtnTypeName(t, html=true) {
 	var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"];
 	var label = n[t] || "?";
+	if (!html) return "Button " + label;
 	return 'Button <span style="font-size:10px;color:`#888`">'+label+'</span>';
 }

Then pass the plain form from the option builder:

-		if (used) txt += ` (${getOwnerName(pInfo.o, pInfo.t, pInfo.n)})`;
+		if (used) txt += ` (${getOwnerName(pInfo.o, pInfo.t, pInfo.n, false)})`;

getOwnerName() needs the extra argument forwarded to getBtnTypeName().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/common.js` around lines 234 - 238, Update getBtnTypeName() to
support a plain-text label mode while preserving its existing styled HTML output
for innerHTML callers, then forward that mode from getOwnerName() when
makePinSelect() builds option text. Ensure HTMLOptionElement.text receives only
the plain “Button …” label without span markup.
.github/workflows/release.yml-11-16 (1)

11-16: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Declare explicit permissions for the release job.

The workflow now creates and updates a GitHub release. It relies on the default GITHUB_TOKEN permissions. If the repository default is read-only, the release steps fail. Add a least-privilege permissions block. The called wled_build job also needs its permissions declared when the caller sets them.

As per path instructions: "Declare explicit permissions: blocks scoped to least privilege."

🔒 Proposed change
 env:
   FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
 
+permissions:
+  contents: read
+
 jobs:
   
   wled_build:
     uses: ./.github/workflows/build.yml
     with:
       release: true

Then scope the release job:

  release:
    name: Create Release
    runs-on: ubuntu-latest
    needs: wled_build
    permissions:
      contents: write
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 11 - 16, Add an explicit
least-privilege permissions block with contents: write to the release job that
creates or updates the GitHub release, and declare the required permissions for
the called wled_build job/workflow so its release operations work even when
repository defaults are read-only.

Sources: Path instructions, Linters/SAST tools

wled00/colors.cpp-272-275 (1)

272-275: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Placeholders are inserted before the palette file is validated, so IDs can still shift.

The gap placeholders are pushed as soon as WLED_FS.exists() returns true. The actual palette is only pushed on Line 303, after readObjectFromFile() succeeds and the format check passes. If a file exists but fails to parse, or has fewer than four entries, the else branch on Line 304 logs and pushes nothing.

The result is one fewer entry than slots consumed. Every palette found after that point receives a shifted ID, which is the exact failure this change is meant to prevent. Presets that reference a custom palette then select the wrong colours.

Push a placeholder on the failure paths so the slot is always consumed.

🐛 Proposed fix
-      if (readObjectFromFile(fileName, nullptr, &pDoc)) {
+      bool paletteAdded = false;
+      if (readObjectFromFile(fileName, nullptr, &pDoc)) {
         JsonArray pal = pDoc[F("palette")];
         if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries)
@@
           customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp));
+          paletteAdded = true;
         } else {
           DEBUGFX_PRINTLN(F("Wrong palette format."));
         }
       }
+      // keep the slot occupied so subsequent palette IDs do not shift
+      if (!paletteAdded) customPalettes.push_back(CRGBPalette16(CRGB(128, 128, 128)));
     } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/colors.cpp` around lines 272 - 275, Move the gray placeholder
insertion out of the initial WLED_FS.exists() path and ensure every invalid
custom palette consumes the same slot. Update the palette-loading logic around
readObjectFromFile() and the existing success push so parse failures, invalid
formats, and palettes with fewer than four entries each push one placeholder per
reserved slot, while valid palettes retain their actual entries and IDs.
usermods/audioreactive/audio_reactive.cpp-319-331 (1)

319-331: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Free aligned FFT buffers with heap_caps_aligned_free()

The integer-FFT path allocates valFFT, windowFFT, and windowFloat through heap_caps_aligned_calloc(), then releases them with heap_caps_free(). Replace those frees with heap_caps_aligned_free() to avoid undefined behavior on ESP-IDF toolchains that require the paired aligned deallocation path. Also free windowFloat before returning when dsps_fft2r_init_sc16() fails so the temporary buffer does not leak.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/audioreactive/audio_reactive.cpp` around lines 319 - 331, In the
integer-FFT initialization path, update all cleanup of buffers allocated by
heap_caps_aligned_calloc()—valFFT, windowFFT, and windowFloat—to use
heap_caps_aligned_free(). In the dsps_fft2r_init_sc16() failure branch, free
windowFloat before returning and preserve the existing pointer cleanup behavior.
wled00/bus_manager.cpp-1124-1134 (1)

1124-1134: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Confirm FOUR_SCAN_40_80PX_HFARCAN exists in pinned Hub75 version.

platformio.ini pins ESP32-HUB75-MatrixPanel-DMA to commit f17fb7fe9d487e9643f919eb5aeedea8d9d1f8d7; this commit does not define FOUR_SCAN_40_80PX_HFARCAN, so the setPhysicalPanelScanRate() call will fail to compile. Use an available scan-rate enum for the pinned version, or update the pin to a version that includes it.

Also comment on chain-length limits for 80x40 panels: mxconfig.chain_length is clamped to 4 and reset to 1 for PSRAM-less 64-pixel boards, so the existing wording should not imply unbounded chaining.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/bus_manager.cpp` around lines 1124 - 1134, Update the
TYPE_HUB75MATRIX_20S branch around setPhysicalPanelScanRate to use a scan-rate
enum defined by the pinned ESP32-HUB75-MatrixPanel-DMA revision, or update the
dependency pin if FOUR_SCAN_40_80PX_HFARCAN is required. Revise the nearby
chain-length comment to reflect the actual mxconfig.chain_length limits,
including the clamp to 4 and the PSRAM-less 64-pixel-board reset to 1, rather
than implying unbounded chaining.

Source: Path instructions

wled00/const.h-322-322 (1)

322-322: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Confirm that removing TYPE_WS2812_2CH_X3 is intentional.

Commenting out this definition removes LED type 20 from firmware. Saved configs that use type 20 will not resolve to a valid bus type and may become a fallback after upgrade. Add a migration path, such as mapping type 20 to an existing driver, or document it as a user-visible breaking change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/const.h` at line 322, Restore TYPE_WS2812_2CH_X3 or add an explicit
migration for saved configurations using type 20, mapping it to the appropriate
existing driver before bus-type resolution. If removal is intentional, document
the user-visible breaking change and ensure type 20 receives deliberate fallback
handling rather than becoming invalid.

Source: Coding guidelines

wled00/bus_manager.cpp-860-882 (1)

860-882: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Apply the pixel-count depth branches only to panels shorter than 64 pixels before increasing depth.

The current thresholds make a classic 64x64 panel use 8-bit and a 32x32 panel use 5-bit instead of the old height-based rules, which can be more demanding on DMA-capable SRAM. Also add WLED_HUB75_COLOR_DEPTH examples to platformio_override.sample.ini, since users customizing through that file only see it in platformio.ini comments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/bus_manager.cpp` around lines 860 - 882, Update the ESP32/ESP32S2
automatic depth selection around mxconfig.setPixelColorDepthBits so the
totalPixels thresholds apply only when mxconfig.mx_height is below 64, while
preserving the existing height-based handling for panels 64 pixels or taller and
the WLED_HUB75_COLOR_DEPTH override. Add representative WLED_HUB75_COLOR_DEPTH
configuration examples and its purpose to platformio_override.sample.ini.

Source: Path instructions

usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp-78-81 (1)

78-81: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep the update pending when configuration persistence fails.

serializeConfigToFS() returns early if it cannot acquire the JSON buffer lock. This code clears m_updateConfig even when no write occurred. The changed setting can then be lost after reboot.

Make serializeConfigToFS() report success, and clear m_updateConfig only after a successful filesystem write.

The implementation in wled00/cfg.cpp lines 822-840 provides the direct evidence for the early-return path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp`
around lines 78 - 81, Change serializeConfigToFS() to return whether
configuration persistence completed successfully, including false on the JSON
buffer lock early-return path. In the m_updateConfig handling block, clear
m_updateConfig only when serializeConfigToFS() reports success; otherwise leave
the update pending for a later retry.
custom_hub75_80x40_icn2038p.patch-1-1 (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Regenerate or remove this patch file.

This file is not a valid unified diff because all records are on Line 1. git apply cannot parse it.

The embedded environment also configures one 80x40 panel, but platformio.ini configures two panels as an 80x80 matrix. Regenerate the patch from the final branch state, or remove it if the checked-in source files are the delivery mechanism.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@custom_hub75_80x40_icn2038p.patch` at line 1, Regenerate or remove
custom_hub75_80x40_icn2038p.patch so it is a valid unified diff with proper line
breaks and headers that git apply can parse. Ensure the final platformio
environment env:esp32dev_hub75_80x40_icn2038p configures exactly one 80x40
panel, matching DATA_PINS and PIXEL_COUNTS, rather than an 80x80 two-panel
matrix.
pio-scripts/validate_modules.py-43-50 (1)

43-50: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle a nonzero readelf exit status.

A started readelf process can fail without raising an exception. The current code then parses partial output and can reject every module. Apply the same conservative pass used for launch failures when result.returncode != 0.

Proposed fix
         result = subprocess.run(
             [readelf_path, "--debug-dump=info", "--dwarf-depth=1", str(elf_path)],
             capture_output=True, text=True, errors="ignore", timeout=120,
         )
+        if result.returncode != 0:
+            secho("WARNING: readelf returned an error; skipping per-module validation",
+                  fg="yellow", err=True)
+            return {Path(b.build_dir).name for b in module_lib_builders}
         output = result.stdout

The provided build-script context specifies that readelf failures must conservatively confirm all modules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pio-scripts/validate_modules.py` around lines 43 - 50, Update the readelf
execution flow around subprocess.run in the module validation function to check
result.returncode after the process completes; when it is nonzero, emit the
existing warning and return the same conservative set of all module
build-directory names used for launch failures, instead of parsing
result.stdout.
platformio.ini-446-463 (1)

446-463: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Disable inherited audioreactive for the recovery build.

env:esp8266_2m_min extends env:esp8266_2m, which sets custom_usermods = audioreactive at Line 422. The recovery image therefore still includes that usermod despite its minimal-feature purpose.

Add an empty custom_usermods value in this environment.

Proposed fix
 [env:esp8266_2m_min]
 extends = env:esp8266_2m
+custom_usermods =
 board = esp_wroom_02

Based on learnings, custom_usermods = disables inherited in-tree usermods.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platformio.ini` around lines 446 - 463, Update the env:esp8266_2m_min
configuration by adding an empty custom_usermods setting so it overrides the
inherited audioreactive usermod from env:esp8266_2m while preserving the
existing recovery build flags.

Source: Learnings

wled00/FX.cpp-3502-3509 (1)

3502-3509: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Return after the single-candle fallback.

Line 3503 calls candle(false) after allocation failure, but the original multi == true invocation then continues. The fallback allocates only the timestamp. The outer call then accesses candleData for all candles beyond that allocation.

Proposed fix
-    if (!SEGENV.allocateData(dataSize)) candle(false); //allocation failed
+    if (!SEGENV.allocateData(dataSize)) {
+      candle(false);
+      return;
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/FX.cpp` around lines 3502 - 3509, Update the allocation-failure branch
in the multi-candle path around candle(false) to return immediately after
invoking the single-candle fallback. This prevents the original multi-candle
execution from continuing into candleData access after only timestamp-sized
storage was allocated; preserve the existing fallback behavior for successful
allocation.
wled00/cfg.cpp-52-52 (1)

52-52: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the "vid" fallback so legacy configs are still detected as legacy.

vid defaults to the current VERSION when doc[F("vid")] is missing. A cfg.json written by firmware from before "vid" existed (exactly the pre-16.0 population with the legacy hour=255 sunrise/sunset encoding) has no "vid" key. For that file, vid becomes the current VERSION (>= 2605010), so vid < 2605010 at line 702 is false, and the migration is skipped.

After this first load, the config gets rewritten with the current VERSION as "vid" (per the comment on line 52), so the migration opportunity is permanently lost: any timer that was meant to become TH_SUNSET stays at hour=255 and is treated as sunrise from then on.

Default to an old/low value (for example 0) instead of VERSION when "vid" is absent, so the absence of "vid" is itself treated as "pre-migration".

🐛 Proposed fix for the vid fallback
-  long vid = doc[F("vid")] | VERSION; // 2605010 note: "vid" can be used to detect an update from older versions but only on first call, it is written to the new VID after buses are initialized
+  long vid = doc[F("vid")] | 0; // note: "vid" can be used to detect an update from older versions but only on first call, it is written to the new VID after buses are initialized. Missing "vid" means the file predates this field, so treat it as the oldest possible version.

Also applies to: 698-709

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/cfg.cpp` at line 52, Update the vid fallback in the
configuration-loading logic to use an old/low value such as 0 when doc[F("vid")]
is absent, rather than VERSION. Preserve the existing vid < 2605010 migration
check so legacy configs without a vid key are converted before the configuration
is rewritten with the current version.
wled00/json.cpp-1255-1284 (1)

1255-1284: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid returning 0 from respondModeData while mode data remains.

writeJSONStringElement returns 0 when len is too small for , "<escaped>". If the callback’s first attempted entry fails, respondModeData returns 0 while fx_index < strip.getModeCount(), which signals the chunked responder to stop; ESPAsyncWebServer treats a 0 callback return as end of response, leaving the JSON array unclosed. Use a “not ready yet” state that keeps the callback returning 0 without ending the transfer, or ensure enough reserved len before starting an entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/json.cpp` around lines 1255 - 1284, The chunk callback in
respondModeData must not terminate the transfer when writeJSONStringElement
returns 0 because the current buffer cannot fit the next mode entry. Reserve
sufficient space before attempting each entry, or introduce the chunk API’s
deferred/not-ready state so the callback retries while fx_index remains below
strip.getModeCount(), ensuring the JSON array is eventually closed.
wled00/ota_update.cpp-296-301 (1)

296-301: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Defeat the watchdog inside gzip decompression with no watchdog API available.

The largest gzipped validation work on ESP8266 is METADATA_OFFSET + 512 = 0x101C bytes, which means up to 66 4-byte ESP.flashRead calls from uzlib_gzip_parse_header and uzlib_uncompress in the response handler. Feeding the watchdog is not possible here because wled.cpp/wled.h only use ESP32 esp_task_wdt_reset(), esp_task_wdt_init(), esp_task_wdt_add(), esp_task_wdt_delete(), and ESP.wdt*, while ESP8266 still has a system watchdog. Move gzipped validation out of the response handler and back-feed the decompressed block to Update.write() before completing the upload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/ota_update.cpp` around lines 296 - 301, Move the validateGzippedOTA
flow out of the response-handler branch around gzipDetected and into the OTA
upload processing path, where decompressed data can be fed incrementally to
Update.write() before the upload completes. Ensure ESP8266 gzip validation no
longer performs the full flash-read/decompression work synchronously in the
response handler, while preserving context->errorMessage handling and the gzip
completion state transition.
🟡 Minor comments (17)
wled00/data/settings_time.htm-222-227 (1)

222-227: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the compiled max segment count when building analog options

bAO() hardcodes segment options up to 32, but MAX_NUM_SEGMENTS is platform-dependent: 16 for ESP8266, 32 for ESP32-S2 without PSRAM, and 64 with PSRAM. Either expose/pass this value to this UI or build the <select> from the currently compiled max so higher segment IDs are reachable on supported builds. The missing-field behavior is handled by request->arg(...).toInt() for the omitted button macro fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/settings_time.htm` around lines 222 - 227, Update bAO() to derive
the segment-option upper bound from the currently compiled MAX_NUM_SEGMENTS
value instead of hardcoding 32, exposing or passing that value into the UI as
needed. Preserve the existing analog-function options and include every valid
segment ID through the platform’s compiled maximum.

Source: Path instructions

wled00/data/settings.htm-12-12 (1)

12-12: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The comment does not match the behaviour of @import.

The comment states that @import url("style.css") "inlines it for this page (only costs ~30bytes of flash)". @import does not inline the stylesheet. The browser issues a separate request for style.css, exactly as a <link> element does. The 30 bytes refer only to the @import statement itself, not to the stylesheet.

@import inside a <style> block is discovered later than a <link> in <head>, so it usually delays the stylesheet and can make the reported white button flash worse. Correct the comment, and confirm the flash is actually reduced by this change.

As per path instructions: "VERIFY comments match code behavior - AI frequently generates plausible but incorrect comments."

Also applies to: 22-22

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/settings.htm` at line 12, Update the comments near the style.css
inclusion to accurately describe `@import` as a separate stylesheet request, not
inlining; clarify that only the import statement adds roughly 30 bytes. Verify
the actual rendering behavior and confirm the change reduces the white button
flash before retaining any claim that it does.

Source: Path instructions

wled00/data/index.js-1599-1599 (1)

1599-1599: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The "Note" toast is still rendered as an error.

The comment states that codes 100 and higher must appear as notes. The label switches to 'Note ', but the second argument to showToast stays true, which marks the toast as an error. Pass the error flag conditionally.

🐛 Proposed fix
-		showToast(((s.error<100) ? 'Error ': 'Note ') + s.error + ": " + errstr, true);  // show "please restart" as a note, all others as errors
+		showToast(((s.error<100) ? 'Error ': 'Note ') + s.error + ": " + errstr, s.error<100);  // show "please restart" as a note, all others as errors
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/index.js` at line 1599, Update the showToast call in the
error-display path so its error flag is conditional on s.error being below 100;
codes 100 and higher must pass the non-error value while preserving the existing
“Error”/“Note” label selection and message text.
wled00/data/settings_leds.htm-125-127 (1)

125-127: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The select branch clears the pin without moving focus.

When the conflicting control is an INPUT, the code clears the value and calls focus(). When it is a SELECT, the code resets the value to -1 but does not call focus(). The user sees an alert, and a pin selection is silently cleared somewhere on the page with no indication of which field changed.

🐛 Proposed fix
-								if (nList[j].tagName==="SELECT") nList[j].value="-1"; else { nList[j].value=""; nList[j].focus(); }
+								if (nList[j].tagName==="SELECT") nList[j].value="-1"; else nList[j].value="";
+								nList[j].focus();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/settings_leds.htm` around lines 125 - 127, Update the
conflict-handling branch in the pin validation logic so the SELECT path that
resets nList[j].value to "-1" also focuses nList[j], matching the existing INPUT
behavior and identifying the cleared control to the user.
wled00/data/settings_leds.htm-870-883 (1)

870-883: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

sel.dataset.val can be stale, so a select can disable its own selected option.

Line 874 skips the option that sel currently has selected, using sel.dataset.val. dataset.val is only written at Line 854, which runs when that specific select is passed in as e. A select created by makePinSelect and never passed to pinUpd as e therefore has an absent or stale dataset.val.

When that happens, the skip fails and the select's own selected option is disabled at Line 881. The value still submits, but the option renders greyed out and the user cannot re-select it after switching away.

Compare against the live sel.value instead.

🐛 Proposed fix
 				Array.from(sel.options).forEach((i) => {
-					if (i.value == sel.dataset.val) return; // skip sel's own selected pin
+					if (i.value == sel.value) return; // skip sel's own selected pin
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/settings_leds.htm` around lines 870 - 883, Update the
self-selected-option check in the pin select loop around `pinUpd` to compare
each option’s value with the live `sel.value` instead of `sel.dataset.val`.
Preserve the existing skip behavior so a select never disables or relabels its
currently selected option, including selects created by `makePinSelect` that
have not updated the dataset.
wled00/data/settings_leds.htm-155-155 (1)

155-155: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The alert text does not state the PSRAM condition.

The guard now allows tall multi-panel HUB75 configurations when PSRAM is present. The message still says "only single panel allowed" without any reason. A user on a PSRAM-less board cannot tell that PSRAM removes the restriction.

🐛 Proposed fix
-					if (h >= 64 && p > 1 && !hasPSRAM) {alert(`HUB75 error: height >= 64, only single panel allowed`); e.stopPropagation(); return false;}
+					if (h >= 64 && p > 1 && !hasPSRAM) {alert(`HUB75 error: height >= 64 with multiple panels requires PSRAM. Only a single panel is allowed on this device.`); e.stopPropagation(); return false;}

This is the downstream site of the hasPSRAM load race described in the comment on Lines 42-45.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/settings_leds.htm` at line 155, Update the alert in the HUB75
validation guard to explicitly state that the single-panel restriction applies
only when PSRAM is unavailable, while preserving the existing condition and
control flow.
wled00/data/settings.htm-23-27 (1)

23-27: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A fixed height: 100px on body can clip the settings buttons.

body { height: 100px; } sets a fixed height on the page body. The settings landing page renders a vertical list of display: block buttons whose font size scales up to 2rem. The rendered content exceeds 100px on every viewport. Content overflows the body box, which can clip the lower buttons or break the centering created by margin: 0 auto.

If the intent is a minimum height for the layout, use min-height.

🐛 Proposed fix
 		body {
-			height: 100px;
+			min-height: 100px;
 			margin: 0 auto;
 			max-width: 520px;
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/settings.htm` around lines 23 - 27, Replace the fixed height
declaration in the body rule with min-height, preserving the existing 100px
value and other layout properties so the vertically stacked settings buttons can
expand beyond the minimum.
wled00/data/settings_pininfo.htm-19-19 (1)

19-19: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Slow down the pin-state polling and stop it when not needed.

settings_pininfo.htm polls /json/pins every 250 ms with no visibility/unload handling. Pin allocation rarely changes, while responses may allocate the shared JSON buffer and read GPIO state. Use a longer interval, such as 1000 ms, and clear pinsTimer on page hide/close to avoid unnecessary server work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/settings_pininfo.htm` at line 19, Update the pin polling setup
around fetchPinInfo and pinsTimer to use a 1000 ms interval instead of 250 ms,
and add page visibility/unload handling that clears the active pinsTimer when
the page is hidden or closed. Preserve the initial loadPins call and avoid
leaving any polling interval running after teardown.
readme.md-74-74 (1)

74-74: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add alt text to the README images.

The two UI screenshots and the Discord widget image have no alt attribute. Add concise descriptions for screen-reader users and for cases where images fail to load.

Proposed fix
-<img src="/images/macbook-pro-space-gray-on-the-wooden-table.jpg" width="50%"><img src="/images/walking-with-iphone-x.jpg" width="50%">
+<img src="/images/macbook-pro-space-gray-on-the-wooden-table.jpg" alt="WLED interface on a MacBook Pro" width="50%"><img src="/images/walking-with-iphone-x.jpg" alt="WLED interface on an iPhone" width="50%">
...
-<a href="https://discord.gg/QAh7wJHrRM"><img src="https://discordapp.com/api/guilds/473448917040758787/widget.png?style=banner2" width="25%"></a>
+<a href="https://discord.gg/QAh7wJHrRM"><img src="https://discordapp.com/api/guilds/473448917040758787/widget.png?style=banner2" alt="Join the WLED Discord server" width="25%"></a>

Also applies to: 88-88

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@readme.md` at line 74, Update the README image tags at the referenced
screenshot and Discord widget locations to include concise, descriptive alt
attributes, ensuring each image remains understandable to screen-reader users
and when loading fails.

Source: Linters/SAST tools

wled00/bus_manager.cpp-1079-1081 (1)

1079-1081: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Check _ledBuffer allocation before use.

allocate_buffer() can return nullptr, and this path only emits debug output when the buffer is null. Add an if (!_ledBuffer) failure path after the allocation instead of relying on the later debug-only signal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/bus_manager.cpp` around lines 1079 - 1081, Update the allocation block
in the bus manager constructor or initialization method after calling
allocate_buffer for _ledBuffer to immediately check whether _ledBuffer is null.
Add a failure path that handles the allocation failure reliably in production,
rather than relying on later debug-only output, while preserving the existing
successful allocation flow.
wled00/data/common.js-346-359 (1)

346-359: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resolve the addOption conflict with settings_um.htm.

wled00/data/settings_um.htm still defines var addOption = addO;, and its generated usermod config uses addOption(dd, ...). That page-local legacy version will shadow common.js’s helper where it loads first, so usermod select options do not get auto-selected from data-val. Update/remove the settings-ui backwards-compat helper or use a namespaced helper for usermod output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/common.js` around lines 346 - 359, Resolve the duplicate
addOption definition between common.js and settings_um.htm by removing or
updating the page-local addOption alias so calls from generated usermod
configuration use common.js’s data-val-aware helper. Preserve backward
compatibility for other settings UI callers, or switch only usermod output to a
distinct helper that retains auto-selection behavior.
.github/workflows/release.yml-43-47 (1)

43-47: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Fail the release job if changelog generation fails.

Update release description may evaluate ${{ steps.changelog.outputs.changelog }} as empty if the previous step is skipped or fails, and softprops/action-gh-release replaces the release body with the provided value. Add if: failure() / job failure handling or make the workflow fail before this release update so the draft body is not cleared.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 43 - 47, Ensure the release job
stops before Update release description when the changelog step fails or is
skipped, rather than passing an empty steps.changelog.outputs.changelog value to
softprops/action-gh-release. Add appropriate failure handling around the
changelog generation and release-update steps so the workflow fails without
clearing the draft release body.
.github/workflows/build.yml-80-94 (1)

80-94: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Keep firmware artifacts uniquely named per matrix environment.

Many release environments default to Custom, and upload-artifact@v4 fails when two matrix jobs upload the same artifact name. Add a descriptive name: to the upload step, but derive the artifact name directly from matrix.environment or add a unique per-environment suffix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build.yml around lines 80 - 94, Update the artifact naming
flow around the artifact_name step and actions/upload-artifact@v4 so every
matrix job produces a unique artifact name, including environments that share
the Custom release name. Derive the uploaded artifact name directly from
matrix.environment or append a unique per-environment suffix, and ensure the
upload step uses that consistently.

Source: Path instructions

platformio_override.sample.ini-492-493 (1)

492-493: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the unsupported custom_usermods inheritance.

platformio_override.sample.ini uses only PlatformIO-style overrides, but ${env:esp01_1m_full.custom_usermods} relies on a custom env: key supported only by platformio.ini/platformio_override.ini. With PlatformIO resolution, this leaves MY9291 as the effective custom_usermods value instead of inheriting the base usermods. Keep ${env:esp01_1m_full.custom_usermods} in platformio.ini and use MY9291 here.

Proposed fix
-custom_usermods = ${env:esp01_1m_full.custom_usermods} MY9291
+custom_usermods = MY9291
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platformio_override.sample.ini` around lines 492 - 493, Update the
custom_usermods setting for the MY9291 environment to use only MY9291, removing
the unsupported ${env:esp01_1m_full.custom_usermods} inheritance; keep the
inheritance expression in platformio.ini.
wled00/udp.cpp-807-808 (1)

807-808: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not emit DDP sequence number zero.

The counter resets to zero after 15, and Line 808 then transmits zero. Wrap to 1 instead. Receivers can treat zero as unused, which weakens duplicate and out-of-order detection.

I can prepare the counter change and a packet-level test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/udp.cpp` around lines 807 - 808, Update the sequence-number emission
in the DDP send path around sequenceNumber so wrapping after 15 produces 1
instead of transmitting 0. Preserve the existing 1–15 range and ensure the value
written by ddpUdp.write never equals zero.
wled00/json.cpp-1214-1238 (1)

1214-1238: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid depending on ArduinoJson's internal EscapeSequence class.

wled00/src/dependencies/json/ArduinoJson-v6.h exposes ARDUINOJSON_NAMESPACE::EscapeSequence::escapeChar, but WLED includes its own copy of ArduinoJson, so future library updates are not controlled by the repository. Implement the small JSON string-escaping rules locally instead of depending on this library-internal symbol.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/json.cpp` around lines 1214 - 1238, Update writeJSONString to replace
the ArduinoJson EscapeSequence::escapeChar dependency with local JSON escaping
logic for supported escapes, including quotes, backslashes, and control
characters. Preserve the existing buffer bounds checks, surrounding quotes, and
return behavior while removing reliance on ArduinoJson internals.
wled00/ota_update.h-37-47 (1)

37-47: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the documentation block so it attaches to the function.

The @param / @return block sits directly above enum class OTAResultStatus. Doxygen attaches it to the enum, not to getOTAResult. Place the block immediately above the function declaration.

📝 Proposed fix for the documentation placement
-/**
- *  Retrieve the OTA result.
- * `@param` request Pointer to web request object
- * `@return` OTAResultStatus indicating result state; string with error message if the update failed.
- */
 enum class OTAResultStatus {
   TryAgain,  // caller must deferResponse() and retry - need additional resources (JSON lock) to complete validation
   Replied,   // response already sent; no action needed
   Ready,     // result available; send response based on error string
 };
+
+/**
+ *  Retrieve the OTA result.
+ * `@param` request Pointer to web request object
+ * `@return` OTAResultStatus indicating result state; string with error message if the update failed.
+ */
 std::pair<OTAResultStatus, String> getOTAResult(AsyncWebServerRequest *request);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/ota_update.h` around lines 37 - 47, Move the existing Doxygen block
from above enum class OTAResultStatus to immediately above getOTAResult, so its
`@param` and `@return` documentation attaches to the function declaration; leave the
enum and function signatures unchanged.

Comment thread wled00/bus_manager.cpp
Comment on lines 1200 to +1247
void BusHub75Matrix::show(void) {
if (!_valid) return;
if (_ledBuffer) {
// write out buffered LEDs
unsigned height = _isVirtual ? virtualDisp->height() : display->height();
unsigned width = _panelWidth;

//while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker.
size_t pix = 0; // running pixel index
for (int y=0; y<height; y++) for (int x=0; x<width; x++) {
if (getBitFromArray(_ledsDirty, pix) == true) { // only repaint the "dirty" pixels
CRGB c = _ledBuffer[pix];
//c.nscale8_video(_bri); // apply brightness
if (_isVirtual) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
else display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
}
pix++;
if (!_valid || !_ledBuffer) return;

const unsigned width = _panelWidth;
const unsigned height = _isVirtual ? virtualDisp->height() : display->height();
const size_t expectedLen = size_t(width) * height;
if (width == 0 || expectedLen != _len) {
DEBUGBUS_PRINTF("HUB75 framebuffer geometry mismatch: %ux%u=%u, buffer=%u\n", width, height, expectedLen, _len);
Serial.printf("HUB75 framebuffer geometry mismatch: %ux%u=%u, buffer=%u\n", width, height, expectedLen, _len);
return;
}

const bool logFrame = _debugShowFrames < 3;
if (logFrame) {
DEBUGBUS_PRINTLN("show() called");
Serial.println("show() called");
DEBUGBUS_PRINTF("pixel count: %u\n", _len);
Serial.printf("pixel count: %u\n", _len);
DEBUGBUS_PRINTLN("first 20 pixel RGB values:");
Serial.println("first 20 pixel RGB values:");
}

size_t pixelsRendered = 0;
for (size_t pix = 0; pix < _len; ++pix) {
const unsigned x = pix % width;
const unsigned y = pix / width;
const CRGB c = _ledBuffer[pix];

if (_isVirtual) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
else display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);

pixelsRendered++;

if (logFrame && pix < 20) {
DEBUGBUS_PRINTF("pixel %u: x=%u y=%u rgb(%u,%u,%u)\n", pix, x, y, c.r, c.g, c.b);
Serial.printf("pixel %u: x=%u y=%u rgb(%u,%u,%u)\n", pix, x, y, c.r, c.g, c.b);
}
setBitArray(_ledsDirty, _len, false); // buffer shown - reset all dirty bits
}
}

setBitArray(_ledsDirty, _len, false);
if (logFrame) {
DEBUGBUS_PRINTF("pixels rendered: %u\n", pixelsRendered);
Serial.printf("pixels rendered: %u\n", pixelsRendered);
DEBUGBUS_PRINTLN("frame complete");
Serial.println("frame complete");
_debugShowFrames++;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Remove the raw Serial debug output from show().

Lines 1208, 1215, 1217, 1219, 1235, 1242 and 1244 call Serial.printf() and Serial.println() directly. These calls are not guarded by a debug macro, so they run in release firmware. Three consequences:

  1. show() is on the LED output hot path. Blocking UART writes there stall the render loop.
  2. The geometry-mismatch branch on Line 1208 prints on every frame while the mismatch persists.
  3. On builds that use the serial port for other protocols, this output corrupts the stream.

Use DEBUGBUS_PRINTF_P()/DEBUGBUS_PRINTLN() only, and remove the _debugShowFrames counter with the per-pixel dump.

As per coding guidelines: "use ... DEBUG_PRINTF()/DEBUG_PRINTLN() for debug output" and "Remove dead or unused code ... debug artifacts".

🐛 Proposed fix
   if (width == 0 || expectedLen != _len) {
     DEBUGBUS_PRINTF("HUB75 framebuffer geometry mismatch: %ux%u=%u, buffer=%u\n", width, height, expectedLen, _len);
-    Serial.printf("HUB75 framebuffer geometry mismatch: %ux%u=%u, buffer=%u\n", width, height, expectedLen, _len);
     return;
   }
 
-  const bool logFrame = _debugShowFrames < 3;
-  if (logFrame) {
-    DEBUGBUS_PRINTLN("show() called");
-    Serial.println("show() called");
-    DEBUGBUS_PRINTF("pixel count: %u\n", _len);
-    Serial.printf("pixel count: %u\n", _len);
-    DEBUGBUS_PRINTLN("first 20 pixel RGB values:");
-    Serial.println("first 20 pixel RGB values:");
-  }
-
-  size_t pixelsRendered = 0;
   for (size_t pix = 0; pix < _len; ++pix) {
     const unsigned x = pix % width;
     const unsigned y = pix / width;
     const CRGB c = _ledBuffer[pix];
 
     if (_isVirtual) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
     else            display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);
-
-    pixelsRendered++;
-
-    if (logFrame && pix < 20) {
-      DEBUGBUS_PRINTF("pixel %u: x=%u y=%u rgb(%u,%u,%u)\n", pix, x, y, c.r, c.g, c.b);
-      Serial.printf("pixel %u: x=%u y=%u rgb(%u,%u,%u)\n", pix, x, y, c.r, c.g, c.b);
-    }
   }
 
   setBitArray(_ledsDirty, _len, false);
-  if (logFrame) {
-    DEBUGBUS_PRINTF("pixels rendered: %u\n", pixelsRendered);
-    Serial.printf("pixels rendered: %u\n", pixelsRendered);
-    DEBUGBUS_PRINTLN("frame complete");
-    Serial.println("frame complete");
-    _debugShowFrames++;
-  }
 }

Also remove the _debugShowFrames member declaration from wled00/bus_manager.h.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/bus_manager.cpp` around lines 1200 - 1247, Update
BusHub75Matrix::show() to remove all direct Serial.printf/Serial.println calls,
using only the guarded DEBUGBUS_PRINTF_P()/DEBUGBUS_PRINTLN() debug macros for
diagnostics. Remove the _debugShowFrames counter and its per-frame/per-pixel
logging logic, including the member declaration in BusHub75Matrix’s header,
while preserving framebuffer validation and rendering behavior.

Source: Coding guidelines

Comment thread wled00/data/index.js
if (isM) {
// do we have 1D segment *after* the matrix?
if (start >= mw*mh) {
if (start >= mw*mh && s > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

The added s > 0 condition can cause a null dereference for segment 0.

populateSegments decides whether to render the Y inputs from isMSeg = isM && staX<mw*mh (Line 783). That expression has no segment-index condition. So for segment 0 with start >= mw*mh, seg0sY and seg0eY are not rendered.

updateLen now takes the else branch for that same segment because of && s > 0. The else branch reads parseInt(sY.value) at Line 1219 with no null guard. sY is null, so updateLen(0) throws a TypeError and the segment list stops updating.

Line 1259 still uses the original isM && start >= mw*mh condition, which confirms the two code paths now disagree.

Align the branch condition with isMSeg, or add the same s > 0 condition to isMSeg in populateSegments.

🐛 Proposed fix (align `updateLen` with `isMSeg`)
-		if (start >= mw*mh && s > 0) {
+		if (start >= mw*mh) {

If the intent is that segment 0 must always be a matrix segment, apply s > 0 to isMSeg in populateSegments and to Line 1259 as well.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (start >= mw*mh && s > 0) {
if (start >= mw*mh) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/index.js` at line 1206, Align the condition in updateLen with the
isMSeg calculation in populateSegments so segment 0 follows the same
matrix-segment branch when start >= mw*mh, preventing access to missing sY and
eY inputs. Update the condition around updateLen and keep the related logic at
the later start check consistent, or apply s > 0 consistently to isMSeg and that
later check if segment 0 is intentionally excluded.

Comment on lines +107 to +131
// E1.31 packet identifier (ACN_ID = "ASC-E1.17"), need at least 16 bytes to safely read acn_id (offset 4, length 12).
if (pktLen >= 16) {
if (!memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id)))
protocol = P_E131;
}

if (protocol == P_ARTNET) {
if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id)))
error = true; //not "Art-Net"
if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL)
error = true; //not a DMX or poll packet
} else { //E1.31 error handling
if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT)
error = true;
if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME)
error = true;
if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP)
error = true;
if (sbuff->property_values[0] != 0)
error = true;
}

if (pktLen < 10) {
error = true; // Need at least Art-Net ID (8) + opcode (2)
} else {
if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id)))
error = true; //not "Art-Net"
if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL)
error = true; //not a DMX or poll packet
}
} else { //E1.31 error handling
if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT)
error = true;
if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME)
error = true;
if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP)
error = true;
if (sbuff->property_values[0] != 0)
error = true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Add a length guard for the E1.31 branch. The current guard covers only acn_id.

The check at line 108 guarantees only 16 bytes. The E1.31 branch then reads far past that point in the packed e131_packet_t layout:

  • root_vector at offset 18
  • frame_vector at offset 40
  • dmp_vector at offset 117
  • property_values[0] at offset 125

A 16-byte UDP packet that starts with the ACN identifier selects the E1.31 branch and reads up to offset 125. That is an out-of-bounds read of the AsyncUDP receive buffer. Any host on the network can send such a packet.

Require the full E1.31 header before you classify the packet as E1.31.

🛡️ Proposed fix to guard the E1.31 header size
+// Minimum bytes needed to read every E1.31 field checked below,
+// through property_values[0] in the DMP layer.
+constexpr size_t E131_MIN_HEADER_LEN = offsetof(e131_packet_t, property_values) + 1;
+
 void ESPAsyncE131::parsePacket(AsyncUDPPacket _packet) {
   bool error = false;
   uint8_t protocol = P_ARTNET;
   const size_t pktLen = _packet.length();
 
   e131_packet_t *sbuff = reinterpret_cast<e131_packet_t *>(_packet.data());
 
-  // E1.31 packet identifier (ACN_ID = "ASC-E1.17"), need at least 16 bytes to safely read acn_id (offset 4, length 12).
-  if (pktLen >= 16) {
+  // E1.31 packet identifier (ACN_ID = "ASC-E1.17"). Require the whole header,
+  // because the E1.31 branch below reads fields up to property_values[0].
+  if (pktLen >= E131_MIN_HEADER_LEN) {
     if (!memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id)))
       protocol = P_E131;
   }

Note: offsetof on this anonymous packed struct member may need a different expression. If it does not compile, use an explicit constant with a comment that documents the layout.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// E1.31 packet identifier (ACN_ID = "ASC-E1.17"), need at least 16 bytes to safely read acn_id (offset 4, length 12).
if (pktLen >= 16) {
if (!memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id)))
protocol = P_E131;
}
if (protocol == P_ARTNET) {
if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id)))
error = true; //not "Art-Net"
if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL)
error = true; //not a DMX or poll packet
} else { //E1.31 error handling
if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT)
error = true;
if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME)
error = true;
if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP)
error = true;
if (sbuff->property_values[0] != 0)
error = true;
}
if (pktLen < 10) {
error = true; // Need at least Art-Net ID (8) + opcode (2)
} else {
if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id)))
error = true; //not "Art-Net"
if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL)
error = true; //not a DMX or poll packet
}
} else { //E1.31 error handling
if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT)
error = true;
if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME)
error = true;
if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP)
error = true;
if (sbuff->property_values[0] != 0)
error = true;
}
constexpr size_t E131_MIN_HEADER_LEN = offsetof(e131_packet_t, property_values) + 1;
// E1.31 packet identifier (ACN_ID = "ASC-E1.17"). Require the whole header,
// because the E1.31 branch below reads fields up to property_values[0].
if (pktLen >= E131_MIN_HEADER_LEN) {
if (!memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id)))
protocol = P_E131;
}
if (protocol == P_ARTNET) {
if (pktLen < 10) {
error = true; // Need at least Art-Net ID (8) + opcode (2)
} else {
if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id)))
error = true; //not "Art-Net"
if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL)
error = true; //not a DMX or poll packet
}
} else { //E1.31 error handling
if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT)
error = true;
if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME)
error = true;
if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP)
error = true;
if (sbuff->property_values[0] != 0)
error = true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/src/dependencies/e131/ESPAsyncE131.cpp` around lines 107 - 131,
Require the complete E1.31 header length before assigning P_E131 in the ACN_ID
detection block, rather than only checking the 16-byte identifier. Use the
appropriate packed-layout size or offsetof expression covering
property_values[0] (with a documented constant fallback if needed), so the
subsequent root_vector, frame_vector, dmp_vector, and property_values accesses
in the E1.31 validation branch are safe.

@DedeHai DedeHai closed this Aug 2, 2026
Comment thread wled00/bus_manager.cpp
// AI: below section was generated by an AI
// WLED_HUB75_COLOR_DEPTH: compile-time override for DMA colour depth (bits per colour channel).
// Lower values free DMA-capable SRAM at the cost of colour fidelity (banding).
// Typical safe values: 3 (saves ~25% SRAM vs 4-bit), 4 (default for large panels).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "typical safe value" is not good. 3 bits per color means that almost any effect will show horrible color banding. If you have too many pixels, use -S3 with PSRAM.

Comment thread wled00/bus_manager.cpp
#else
// AI: end
unsigned totalPixels = mxconfig.chain_length * mxconfig.mx_width * mxconfig.mx_height;
if (totalPixels >= 2048) mxconfig.setPixelColorDepthBits(4); // 4-bit depth saves DMA DRAM for 3200 pixel panels (80x40 1/20 scan)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is totally unnecessary.

Comment thread wled00/bus_manager.cpp
}
} else if (bc.type == TYPE_HUB75MATRIX_20S) {
// AI: below section was generated by an AI
// Only validate panel dimensions; chain_length is not constrained by the ICN2038P hardware.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chain_length is constrained because it influences RAM usage. And because more than 12000 pixels are becoming too slow and too flickering.

@softhack007 softhack007 added the AI Partly generated by an AI. Make sure that the contributor fully understands the code! label Aug 2, 2026
@DedeHai

DedeHai commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

if this was meant as a serious PR, please do it properly https://github.com/wled/WLED/wiki/How-to-properly-submit-a-PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Partly generated by an AI. Make sure that the contributor fully understands the code!

Projects

None yet

Development

Successfully merging this pull request may close these issues.