Skip to content

Conversation

@DedeHai
Copy link
Collaborator

@DedeHai DedeHai commented Sep 16, 2025

  • add all buses in config even - divert to "dummy" bus if RAM limit is exceeded (thx @willmmiles )

  • improved heap checker in main loop: checks heap once every second and takes action only if limit is exceeded continuously (prevents false triggers)

  • added a variable to prevent wifi re-init during OTA

  • edit: retracted index.js changes in favour of Use sequential loading and requests for all UI resources #5013 added some safety checks for UI to not throw errors if JSON parameters are missing to prevent UI being fully stuck

fixes #4936

this is mostly what codepilot came up with.

Summary by CodeRabbit

  • New Features

    • UI now shows placeholder LED buses when channel or memory limits are exceeded so configurations remain visible and editable.
    • Staged low‑memory protection with progressive remediation (simpler effects, recreate segments, full reset and reconnect) and more robust periodic heap checks for improved stability across architectures.
  • Bug Fixes

    • Ensures at least one default segment is recreated if segments are lost due to low memory.
    • Safer enumeration and serialization of buses/settings to avoid invalid accesses during low‑resource conditions.

✏️ Tip: You can customize this high-level summary in your review settings.

- added some safety checks for UI to not throw errors if JSCON parameters are missing
- add all buses in config even if they exceed RAM limit, truncates long buses
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 16, 2025

Walkthrough

Adds a BusPlaceholder type and opt-in placeholder creation when buses would exceed memory/channel limits; WS2812FX init/segment flows now tolerate non-ok buses (stop only on null), and finalizeInit uses per-bus mem accounting with placeholder fallback; staged, architecture-aware heap monitoring with multi-threshold recovery actions; tiny web UI line tweak.

Changes

Cohort / File(s) Summary of changes
WS2812FX init & segments
wled00/FX_fcn.cpp
finalizeInit() now iterates busConfigs by value, computes per-bus memory, uses BusManager::add(..., placeholder) and may add BusPlaceholder when memory/channel limits would be exceeded (preserves processing instead of aborting); updates mem accounting and digitalCount; clears busConfigs. resetSegments() ensures at least one default segment (ERR_NORAM_PX safeguard). makeAutoSegments() stops only on null bus (removed isOk() early-exit).
Bus manager & placeholder
wled00/bus_manager.h, wled00/bus_manager.cpp
New BusPlaceholder class (stores bus config, no-op pixel/show, exposes pins/getters, isPlaceholder() true, getBusSize()), added to bus list when over-limit; changed BusManager::add(const BusConfig&)add(const BusConfig&, bool placeholder); runtime guards updated to check isOk() where appropriate.
Serialization & settings loops
wled00/cfg.cpp, wled00/xml.cpp
Relaxed bus iteration exit condition from `if (!bus
Heap monitoring & staged recovery
wled00/wled.cpp
Replaced single-threshold heap tracking with cadence-based checks using heapDanger counter and architecture-aware heap queries; staged actions at ~15/30/45 consecutive low-heap seconds (purge/static → resetSegments → full reset + WiFi reconnect); timing and heap querying logic updated for ESP8266/ESP32 variants.
Web UI minor change
wled00/data/index.js
Trivial/semantically no-op edit to a touchend event listener line.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • willmmiles
  • blazoncek
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.00% 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 'Fix for cfg exceeding LED limit' directly addresses the main issue: preventing configs that request more LEDs than MAX_LED_MEMORY from failing entirely. It clearly summarizes the primary change without being vague or generic.
Linked Issues check ✅ Passed The PR implements all core objectives from #4936: introduces a BusPlaceholder to preserve configuration when memory limits are exceeded, improves heap checking with per-second sampling, ensures UI robustness by handling missing JSON parameters gracefully, and prevents silent bus loss.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the LED limit overflow issue and improving heap management: BusPlaceholder implementation, memory checking in finalizeInit, heap-based recovery thresholds, and UI safety checks. Changes to bus iteration conditions align with placeholder support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
wled00/data/index.js (2)

926-944: populateSegments guards — solid hardening, tiny nit

Good null checks around seg deletion/power controls. Consider explicit radix in parseInt for clarity across engines.

-    if (seg0briElement && segp0Element && parseInt(seg0briElement.value)==255) segp0Element.classList.add("hide");
+    if (seg0briElement && segp0Element && parseInt(seg0briElement.value, 10) === 255) segp0Element.classList.add("hide");

2409-2415: Typo in warning; keep message clear

Fix “elemen” → “element”.

-  console.warn('No power elemen');
+  console.warn('No power element');
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 76cb2e9 and db310c6.

📒 Files selected for processing (2)
  • wled00/FX_fcn.cpp (1 hunks)
  • wled00/data/index.js (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
wled00/data/**/*.{htm,html,css,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use tabs for indentation in web UI files (.htm/.html/.css/.js) under wled00/data

Files:

  • wled00/data/index.js
wled00/**/*.{cpp,h}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use spaces (2 per level) for C++ firmware files

Files:

  • wled00/FX_fcn.cpp
🧠 Learnings (4)
📚 Learning: 2025-04-24T09:31:06.879Z
Learnt from: blazoncek
PR: wled/WLED#4658
File: wled00/led.cpp:90-91
Timestamp: 2025-04-24T09:31:06.879Z
Learning: In the WLED codebase, the `getSegment()` function guards against out-of-bounds segment IDs, and `getFirstSelectedSegId()` falls back to `getMainSegmentId()` if no segments are selected, ensuring no crashes when used through the `setValuesFromFirstSelectedSeg()` macro.

Applied to files:

  • wled00/data/index.js
📚 Learning: 2025-04-24T09:31:06.879Z
Learnt from: blazoncek
PR: wled/WLED#4658
File: wled00/led.cpp:90-91
Timestamp: 2025-04-24T09:31:06.879Z
Learning: In the WLED codebase, the `getSegment()` function guards against out-of-bounds segment IDs by falling back to the main segment ID, and `getFirstSelectedSegId()` falls back to `getMainSegmentId()` if no segments are selected, ensuring no crashes when used through the `setValuesFromFirstSelectedSeg()` macro.

Applied to files:

  • wled00/data/index.js
📚 Learning: 2025-09-12T17:29:43.793Z
Learnt from: DedeHai
PR: wled/WLED#4923
File: wled00/FX.cpp:4883-4901
Timestamp: 2025-09-12T17:29:43.793Z
Learning: In WLED’s web UI, only one slider value (e.g., SEGMENT.intensity or SEGMENT.custom1) changes at a time; code relying on this may use simplified change guards, though presets/JSON can still update multiple fields atomically.

Applied to files:

  • wled00/data/index.js
📚 Learning: 2025-08-26T11:51:21.817Z
Learnt from: DedeHai
PR: wled/WLED#4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR #4798, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Applied to files:

  • wled00/FX_fcn.cpp
🧬 Code graph analysis (1)
wled00/FX_fcn.cpp (2)
wled00/bus_manager.h (3)
  • isDigital (141-142)
  • isDigital (180-180)
  • is2Pin (181-181)
wled00/bus_manager.cpp (4)
  • add (82-87)
  • add (82-82)
  • add (806-827)
  • add (806-806)
🔇 Additional comments (2)
wled00/data/index.js (2)

671-684: Graceful defaults for missing LED metadata — LGTM

Defensive checks for i.leds.count and i.leds.maxseg prevent UI crashes and use sane fallbacks. Indentation complies with tabs.


2280-2282: rptSeg: safe read of power state — LGTM

Fallback to false when the power element is absent avoids exceptions.

@willmmiles
Copy link
Member

Truncating too long buses is not OK -- it creates the weird behavior that the configuration "mutates" when saved by the user. (Why won't my settings stick??) I think what we need to do is create a placeholder bus object (BusPlaceholder?) that doesn't actually do anything, but carries forward the settings for later inspection.

(We should probably also consider removing the Bus::isOk() checks in getSettingsJS() and serializeConfig() -- that way failed buses can still be edited in the settings page as well.)

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 17, 2025

Truncating too long buses is not OK -- it creates the weird behavior that the configuration "mutates" when saved by the user. (Why won't my settings stick??) I think what we need to do is create a placeholder bus object (BusPlaceholder?) that doesn't actually do anything, but carries forward the settings for later inspection.

(We should probably also consider removing the Bus::isOk() checks in getSettingsJS() and serializeConfig() -- that way failed buses can still be edited in the settings page as well.)

very good point, I saw that and thought the same thing but did not think about a solution yet. Any suggestion in how to preserve the config?

Copy link
Contributor

@blazoncek blazoncek left a comment

Choose a reason for hiding this comment

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

I would have fixed the count issues in finalizeInit() instead.

DEBUG_PRINTF_P(PSTR("Bus %d memory usage exceeds limit, setting count to %d\n"), (int)bus.type, bus.count);
}
if (BusManager::add(bus) != -1) {
mem += bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount : 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to estimate when you have bus already created.
Use BusManager::getBus(BusManager::getNumBusses()-1)->getBusSize(); instead.

@willmmiles
Copy link
Member

very good point, I saw that and thought the same thing but did not think about a solution yet. Any suggestion in how to preserve the config?

Make up a new Bus subclass BusPlaceholder (or some better name) that ignores the IO calls (_valid is false, pure virtuals are no-ops) but retains the relevant config data. Then, in finalizeInit, if a bus will exceed the memory limit, we add a BusPlaceholder instead. Since the "type" value gets preserved, future round-trips through finalizeInit will attempt to create the "correct" bus class first.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 17, 2025

Make up a new Bus subclass BusPlaceholder (or some better name) that ignores the IO calls (_valid is false, pure virtuals are no-ops) but retains the relevant config data. Then, in finalizeInit, if a bus will exceed the memory limit, we add a BusPlaceholder instead. Since the "type" value gets preserved, future round-trips through finalizeInit will attempt to create the "correct" bus class first.

thanks, I need to process that. Working on heap/ram stuff RN so this is on backburner. If you have the time and interest, feel free to contribute directly to this PR.

In the event that a Bus fails to initialize, or the memory validation
fails, keep the configuration around so the settings contents don't
change out from under the user.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
wled00/FX_fcn.cpp (1)

1198-1204: Don’t stop initializing after the first placeholder

Breaking on the first non-OK bus prevents later valid buses from being begun, causing unexpected loss of output. Continue past placeholders instead.

-    if (!bus || !bus->isOk() || bus->getStart() + bus->getLength() > MAX_LEDS) break;
+    if (!bus) break;
+    if (!bus->isOk() || bus->getStart() + bus->getLength() > MAX_LEDS) continue;
wled00/bus_manager.cpp (1)

820-844: Channel-limit check ignores the new bus; enforce “would exceed” correctly

The current test uses existing counts only, so it never triggers. Include the pending bus in the calculation.

-  if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true;
+  const bool newIsDigital   = Bus::isDigital(bc.type) && !Bus::is2Pin(bc.type);
+  const bool newIsAnalogPWM = Bus::isPWM(bc.type);
+  const unsigned newAnalogPins = newIsAnalogPWM ? Bus::numPWMPins(bc.type) : 0;
+  if ((digital + (newIsDigital ? 1 : 0)) > WLED_MAX_DIGITAL_CHANNELS ||
+      (analog  + newAnalogPins)         > WLED_MAX_ANALOG_CHANNELS) {
+    placeholder = true;
+  }
🧹 Nitpick comments (1)
wled00/FX_fcn.cpp (1)

1749-1765: Replace early break on non-OK buses with continue (skip placeholders, don't mask later buses)

Replace:

-    if (!bus || !bus->isOk()) break;
+    if (!bus) break;
+    if (!bus->isOk()) continue;

Apply in wled00/FX_fcn.cpp at lines 986, 1751, 1761, 1898.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97f68b3 and f8ca0f9.

📒 Files selected for processing (5)
  • wled00/FX_fcn.cpp (2 hunks)
  • wled00/bus_manager.cpp (5 hunks)
  • wled00/bus_manager.h (3 hunks)
  • wled00/cfg.cpp (1 hunks)
  • wled00/xml.cpp (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
wled00/**/*.{cpp,h}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use spaces (2 per level) for C++ firmware files

Files:

  • wled00/xml.cpp
  • wled00/cfg.cpp
  • wled00/bus_manager.cpp
  • wled00/FX_fcn.cpp
  • wled00/bus_manager.h
🧠 Learnings (11)
📓 Common learnings
Learnt from: DedeHai
PR: wled/WLED#4939
File: wled00/FX_fcn.cpp:1176-1187
Timestamp: 2025-09-16T18:08:42.848Z
Learning: In WLED finalizeInit() bus creation (wled00/FX_fcn.cpp), intentionally allowing memory overruns when bus configurations exceed MAX_LED_MEMORY is a deliberate design choice. The trade-off prioritizes creating buses with reduced LED counts over completely failing to create buses, which would cause no LED output and UI failures. This approach forces users to update configurations after migrating to version 0.16 while maintaining basic functionality.
📚 Learning: 2025-09-16T18:08:42.848Z
Learnt from: DedeHai
PR: wled/WLED#4939
File: wled00/FX_fcn.cpp:1176-1187
Timestamp: 2025-09-16T18:08:42.848Z
Learning: In WLED finalizeInit() bus creation (wled00/FX_fcn.cpp), intentionally allowing memory overruns when bus configurations exceed MAX_LED_MEMORY is a deliberate design choice. The trade-off prioritizes creating buses with reduced LED counts over completely failing to create buses, which would cause no LED output and UI failures. This approach forces users to update configurations after migrating to version 0.16 while maintaining basic functionality.

Applied to files:

  • wled00/xml.cpp
  • wled00/cfg.cpp
  • wled00/bus_manager.cpp
  • wled00/FX_fcn.cpp
  • wled00/bus_manager.h
📚 Learning: 2025-08-26T11:51:21.817Z
Learnt from: DedeHai
PR: wled/WLED#4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR #4798, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Applied to files:

  • wled00/FX_fcn.cpp
📚 Learning: 2025-08-28T08:09:20.630Z
Learnt from: mval-sg
PR: wled/WLED#4876
File: wled00/xml.cpp:0-0
Timestamp: 2025-08-28T08:09:20.630Z
Learning: The WLED codebase has opportunities for refactoring hardcoded array bounds (like the "15" used for DMX channels) to use sizeof(array)/sizeof(array[0]) for more maintainable code, but such changes should be done consistently across the entire codebase in a dedicated refactoring effort.

Applied to files:

  • wled00/FX_fcn.cpp
📚 Learning: 2025-04-30T05:41:03.633Z
Learnt from: blazoncek
PR: wled/WLED#4667
File: usermods/user_fx/user_fx.cpp:27-30
Timestamp: 2025-04-30T05:41:03.633Z
Learning: In WLED, the Segment::allocateData() method already includes optimization to check if data is allocated and sufficiently sized, handling buffer reuse to reduce memory fragmentation. Adding an external check like `if (SEGENV.data == nullptr && !SEGENV.allocateData(dataSize))` is unnecessary and could be problematic, as it bypasses proper size verification.

Applied to files:

  • wled00/FX_fcn.cpp
📚 Learning: 2025-08-31T03:38:14.114Z
Learnt from: BobLoeffler68
PR: wled/WLED#4891
File: wled00/FX.cpp:3333-3349
Timestamp: 2025-08-31T03:38:14.114Z
Learning: WLED PacMan effect (wled00/FX.cpp): Keep pacmancharacters_t position fields as signed int (not int16_t). Maintainer preference (blazoncek) prioritizes avoiding potential overhead/regressions over minor RAM savings. Avoid type shrinking here unless memory pressure is demonstrated.

Applied to files:

  • wled00/FX_fcn.cpp
📚 Learning: 2025-09-02T01:56:43.841Z
Learnt from: willmmiles
PR: wled/WLED#4890
File: lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h:173-180
Timestamp: 2025-09-02T01:56:43.841Z
Learning: willmmiles prefers to maintain consistency with upstream NeoPixelBus patterns (like unchecked malloc in construct() methods) rather than diverging until improvements are made upstream first, to minimize maintenance burden and keep the codebase aligned.

Applied to files:

  • wled00/FX_fcn.cpp
📚 Learning: 2025-09-15T19:13:56.469Z
Learnt from: DedeHai
PR: wled/WLED#4791
File: wled00/util.cpp:737-743
Timestamp: 2025-09-15T19:13:56.469Z
Learning: In WLED's util.cpp, the *_realloc_malloc functions (p_realloc_malloc and d_realloc_malloc) are intentionally designed to free the original buffer on realloc failure and allocate a new buffer, implementing a "replace buffer" semantic rather than traditional realloc behavior. This is documented in the function comments and is the intended design by the author DedeHai.

Applied to files:

  • wled00/FX_fcn.cpp
📚 Learning: 2025-08-29T15:38:46.208Z
Learnt from: DedeHai
PR: wled/WLED#4791
File: wled00/FX_fcn.cpp:1187-1191
Timestamp: 2025-08-29T15:38:46.208Z
Learning: In WLED's allocate_buffer() function, BFRALLOC_ENFORCE_PSRAM already includes fallback logic to DRAM if PSRAM is not available, as documented in the comment "use PSRAM if available, otherwise fall back to DRAM". The function also uses validateFreeHeap() for additional safety checks. During setup() when finalizeInit() runs, PSRAM has vast available memory making failures unlikely.

Applied to files:

  • wled00/FX_fcn.cpp
📚 Learning: 2025-09-01T10:26:17.959Z
Learnt from: mval-sg
PR: wled/WLED#4876
File: wled00/wled_eeprom.cpp:0-0
Timestamp: 2025-09-01T10:26:17.959Z
Learning: In WLED PR #4876, the DMXStartLED EEPROM backward compatibility issue was partially addressed by keeping it at address 2550 and reading it as a 16-bit value, with DMXChannelsValue array moved to addresses 2552-2566. This maintains compatibility with pre-0.11 EEPROM layouts for DMXStartLED, though legacy "Set to 255" (code 6) configurations may still need migration logic.

Applied to files:

  • wled00/FX_fcn.cpp
📚 Learning: 2025-08-21T00:00:17.196Z
Learnt from: willmmiles
PR: wled/WLED#4859
File: usermods/udp_name_sync/udp_name_sync.cpp:62-70
Timestamp: 2025-08-21T00:00:17.196Z
Learning: In WLED codebase, UDP packet processing code must never rely on null termination being present on the wire and must always validate packet size first. The codebase already has some vulnerabilities where malformed data can crash the software, so avoiding introduction of new ones is a priority.

Applied to files:

  • wled00/FX_fcn.cpp
🧬 Code graph analysis (3)
wled00/bus_manager.cpp (1)
wled00/bus_manager.h (4)
  • BusPlaceholder (368-395)
  • getNumberOfPins (161-161)
  • isVirtual (145-146)
  • isVirtual (184-184)
wled00/FX_fcn.cpp (2)
wled00/bus_manager.h (3)
  • isDigital (141-142)
  • isDigital (180-180)
  • is2Pin (181-181)
wled00/bus_manager.cpp (4)
  • add (76-81)
  • add (76-76)
  • add (820-843)
  • add (820-820)
wled00/bus_manager.h (1)
wled00/bus_manager.cpp (15)
  • BusPlaceholder (758-768)
  • getPins (320-324)
  • getPins (320-320)
  • getPins (575-580)
  • getPins (575-575)
  • getPins (647-651)
  • getPins (647-647)
  • getPins (716-719)
  • getPins (716-716)
  • getPins (770-776)
  • getPins (770-770)
  • add (76-81)
  • add (76-76)
  • add (820-843)
  • add (820-820)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Environments (lolin_s2_mini)
🔇 Additional comments (11)
wled00/xml.cpp (1)

317-317: Good: allow placeholders to flow to UI

Switching to a null-only break lets non-OK buses (placeholders) be serialized into settings JS. This aligns with the placeholder design.

Please confirm the UI correctly displays pins for placeholders even if PinManager::isPinOk(pin) returns false; otherwise consider printing stored pins unconditionally for placeholders.

wled00/cfg.cpp (1)

973-980: Serialize non-OK buses too — matches placeholder approach

Dropping the isOk() guard here ensures placeholder buses get saved back to cfg.json, avoiding “mutating” config.

wled00/FX_fcn.cpp (1)

1810-1811: LGTM: auto-segmentation tolerates placeholders

Keeping only a null check avoids skipping segments when a placeholder appears.

wled00/bus_manager.cpp (5)

758-769: LGTM: BusPlaceholder preserves config

Constructor copies all user config (pins, order, current, etc.). Defaults keep _valid=false, as intended.


770-776: LGTM: getPins exposes stored pins for UI/serialization

Matches Bus::getNumberOfPins(type).


933-936: LGTM: avoid touching placeholders during ESP8266 LED_BUILTIN re-init

Added isOk() gate prevents placeholder interaction.


1027-1036: LGTM: ABL setup skips placeholders

Only OK digital buses get current limits.


1072-1077: LGTM: ABL apply loops only real digital buses

Prevents placeholder side effects.

wled00/bus_manager.h (3)

128-128: Changed getLength() to always return _len — verify callers

This is necessary for placeholders, but check that length aggregations (e.g., BusManager::getTotalLength(false)) aren’t surfaced in places where “physical” length is expected. Use getTotalLength(true) or isOk() where appropriate.


366-395: LGTM: Placeholder class surface is sufficient for UI and cfg

No-ops for IO, full accessors for config. Minor: matching the base default arg for getPins is optional, current override is fine.


499-499: Signature change acknowledged

add(const BusConfig&, bool) is fine; ensure all call sites use the new flag (you updated finalizeInit()).

@blazoncek
Copy link
Contributor

@willmmiles IMO there is no need to complicate things with bus placeholder. The only thing needed to be done is ignore isOk() on some places and proceed without memory allocation and underlying physical bus initialisation.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 18, 2025

I did some experimenting in my dev branch and I think I have a good overall solution for ESP8266. It goes something like this:

  • the UI as well as finalizeInit() do not take the MAX_LED_MEMORY as a hard limit since we also need buffers and those are not all obligatory
  • instead the UI takes the MAX_LED_MEMORY to throw warnings at the user and denies writing the config if double that limit is exceeded, i.e. bus+globalbuffer+segmentbuffer (with a warning message that it cannot be handled).
  • finalizeInit() uses the same approach but has a slightly lower estimate to make sure buses get added if UI does not deny it
  • for existing configs that would exceed the limit the bus length gets truncated (thx @willmmiles for taking the effort)

still need to test how that behaves on ESP32 and maybe tweak it a little. For ESP8266 the new limit is about 720 WS281x LEDs, everything still works at that limit although a bit wonky sometimes. The user is actively informed about stability issues when setting more than about 300 LEDs.

Once fully tested, I will push an update to this PR

@DedeHai DedeHai marked this pull request as draft September 18, 2025 05:58
@willmmiles
Copy link
Member

@willmmiles IMO there is no need to complicate things with bus placeholder. The only thing needed to be done is ignore isOk() on some places and proceed without memory allocation and underlying physical bus initialisation.

I considered that approach, but it would mean that all bus types would need have to have a "don't actually construct" argument, which struck me as rather silly. I found that using a clearly defined placeholder type was a cleaner implementation in the current code. I agree that there is a general problem of handling bus initialization failure that overlaps quite a bit here, but I think that needs to be approached in context of a bigger Bus API redesign. (I haven't done a full pass over it yet, but I'd like to give serious thought to elimininating IsOk() in calls like setPixelColor() in favor of replacing a failed bus with a Placeholder instead. This should yield a slight speedup as we don't need to run the check for every pixel, it gets embedded in the virtual dispatch instead.)

@willmmiles
Copy link
Member

  • for existing configs that would exceed the limit the bus length gets truncated (thx @willmmiles for taking the effort)

I'm still hard against that. Either construct the bus or don't, but don't change the user's settings.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 18, 2025

I'm still hard against that. Either construct the bus or don't, but don't change the user's settings.

doesn't your added code take care of that? i.e. truncating the created bus but keeping the config? sorry, did not yet have time to study in detail what you added.

edit: the underlaying issue I am trying to address: ESP8266 on 0.16 will have a bad time (to the point where UI access becomes impossible) if you are coming in with a config using >1000 LEDs. Not creating the bus is one option, creating it but smaller would not leave users with a "its all bricked" moment. Not sure how edge-case this is though.

@blazoncek
Copy link
Contributor

all bus types would need have to have a "don't actually construct"

I don't think so. If they fail to construct properly, they are just not ok.

However, the approach here is that you want to check if memory exceeds MAX_LED_MEMORY before bus is created and it is done in finalizeInit(). If you move the check into BusManager::add() you can summarize existing buses, add estimate of current bus and then decide what to do. This would simplify finalizeInit() function.
I've foreseen expansion of virtual buses (see usermod bus PR) and this could be of such use. Virtual bus that does nothing, but will retain every option selected.

I, personally, would just drop such bus and every bus after it.

@willmmiles
Copy link
Member

The posted code creates a fake bus that holds the settings but is inactive -- it doesn't produce any output. It seemed the best approach to me; making working busses that have different internal sizes than their settings would be complex to say the least.

Re tightened requirements on 0.16, I think we should come at this from two fronts:

  • First, add a check in 0.15.2 that'll refuse the update if your config won't be valid. Implement Metadata-Based OTA Release Compatibility Checking System #4930 puts the framework in place for that -- while the prototype there only checks the release name, we're free to add additional useful information in the metadata struct, such as a "recommended max LEDs" value. I expect this to catch 90-95% of problem cases. Users can still use the "override" flag to force an update, but at that point we're justified in saying "you were warned".
  • Second, if the config is no longer valid in the new version, then we prioritize (a) keeping the web interface up to allow the user to fix the config and/or switch firmware; then (b) making it clear the config is unsupported (for example: if you upload a config with too many LEDs with this PR; then after you reboot, the settings page tells you why it's unhappy right away when you open it). If the user wants to fix their config by lowering the number of LEDs, they're free to do so at that point; but I suspect most such users would rather downgrade than have their configuration changed. As long as the web API stays up, the device isn't "bricked" - it's just not working ;)

@willmmiles
Copy link
Member

all bus types would need have to have a "don't actually construct"

I don't think so. If they fail to construct properly, they are just not ok.

I was trying to make the point that allowing a "bus is not ok" state puts a performance cost on every bus access. If we do not allow "not ok" busses to exist at all -- replacing them with placeholders or the like -- then we don't ever have to check if the bus is ok, speeding up the core render loops.

However, the approach here is that you want to check if memory exceeds MAX_LED_MEMORY before bus is created and it is done in finalizeInit(). If you move the check into BusManager::add() you can summarize existing buses, add estimate of current bus and then decide what to do. This would simplify finalizeInit() function. I've foreseen expansion of virtual buses (see usermod bus PR) and this could be of such use. Virtual bus that does nothing, but will retain every option selected.

I agree that the division of work between finalizeInit() and BusManager isn't in the right place. As usual my predilection for the minimal patch involves extra work to avoid changes to the code structure. I'll see if I can spare another hour to do a better cleanup.

I, personally, would just drop such bus and every bus after it.

We cannot drop busses as it destroys the user configuration. IMO "my configuration didn't save" is always a bug.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 18, 2025

I have a new approach/suggestion whicht might not require to divert or drop any buses but its a bit of a stretch: accept all buses if memory permits (or even if it does not really). Let the heap checker in the main loop handle the rest.
Reasoning: this whole situation only arises if a user
a) upates from a version that is incompatible
b) manually manipulates the config
If the heap-check in the main loop is updated to free as much heap as required (for example by dropping all buses) the UI will still work (after a while) and users are able to update the config.

@willmmiles
Copy link
Member

If the heap-check in the main loop is updated to free as much heap as required (for example by dropping all buses) the UI will still work (after a while) and users are able to update the config.

Unfortunately, if it drops all busses, none of them will appear in the settings page: the bus list there is generated from the live BusManager state.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 19, 2025

If the heap-check in the main loop is updated to free as much heap as required (for example by dropping all buses) the UI will still work (after a while) and users are able to update the config.

Unfortunately, if it drops all busses, none of them will appear in the settings page: the bus list there is generated from the live BusManager state.

since the major change in this matter are the added buffers, simply dropping those should work. The heap checker currently drops all segments but if the culprit is the global _pixels buffer, resetting segments is not enough. Adding a "heap panic" extension to the checker and maybe some function to free that buffer might do the trick without messing with buses.

Edit: this works

    if (heap < MIN_HEAP_SIZE && lastHeap < MIN_HEAP_SIZE) {
      DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap);
      if(heapPanic) {
        DEBUG_PRINTLN(F("!Heap panic reset!"));
        strip.~WS2812FX();      // deallocate strip and all its memory
        new(&strip) WS2812FX(); // re-create strip object, respecting memory limits
      }
      forceReconnect = true;
      strip.resetSegments(); // remove all but one segments from memory
      heapPanic = true;      // enable last resort measures on next call if heap is still low
    } else if (heap < MIN_HEAP_SIZE) {
      DEBUG_PRINTLN(F("Heap low, purging segments."));
      strip.purgeSegments();
    } else {
      heapPanic = false;
    }

Edit2:
I finally get your point about the "dummy bus" to respect the config settings. While I do agree that messing with users setting is bad, truncating a bus wont directly do that unless a user saves them but you are correct that it may be confusing. If we do add stubs, those must be highlighted in settings to indicate so or the next question will be: "why won't LEDs light up?"
To recap:

  • I agree there should be a workaround for setups that now exceed the capabilities
  • I disagree that a bus should just be dropped/duped, leaving users literally "in the dark", capping its length is a better indicator (imagine a ESP8266 with just one output, 900 LEDs)
  • I dont have a good solution to the config issue, I imagine adding a new variable to distinguish configured length and actual length is a bad workaround.

Edit3:
with the above heap guard "fix" we could keep the old bus creation logic, thus any bus that was allowed before will still be allowed. if it breaks the UI, it will recover within a minute and not change any settings. My current code will not let a user save settings that are considered unsafe, so they are left with the choice to update the settings to safe levels or downgrade.

@willmmiles
Copy link
Member

willmmiles commented Sep 20, 2025

  • I disagree that a bus should just be dropped/duped, leaving users literally "in the dark", capping its length is a better indicator (imagine a ESP8266 with just one output, 900 LEDs)
  • I dont have a good solution to the config issue, I imagine adding a new variable to distinguish configured length and actual length is a bad workaround.

Right, I don't think you can have both. Either you seperately store "configured" vs "live" lengths, OR you have to null out buses that can't be allocated. I really don't think we should be trying to support "partially functional" buses. Also, if we override the user config, we open ourselves up to "why doesn't my configuration work??? I saved 900 LEDs but only 30 light up??" if there's ever a bug in the JS validation.

I applaud that you're pushing for graceful degredation. What would change my mind on this is a better solution for posting error messages back to the main UI. Something that would stay on screen (without covering up parts of the interface) and clearly indicate what the system is unhappy about, what recovery actions were taken, and where to go to fix it. Otherwise I think sophisticated recovery behaviour will be more confusing to users than helpful.

Edit3: with the above heap guard "fix" we could keep the old bus creation logic, thus any bus that was allowed before will still be allowed. if it breaks the UI, it will recover within a minute and not change any settings. My current code will not let a user save settings that are considered unsafe, so they are left with the choice to update the settings to safe levels or downgrade.

I'm on board with the heap guard fix, but I do think it's worth pointing out that the impact of it is the same as dropping the bus: the LEDs will stay dark if there isn't enough memory to allocate _pixels or segment buffers.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 20, 2025

The way I see it is once again a question of "how do we anger the least users" :)
Disregarding the general issue about buses that can't be allocated (to which I think your approach is good) it is mostly if not only about ESP8266 with more than 700 LEDs, which we saw might be not that uncommon.
With 0.14 and 4k of LED buffer memory the max should be around 1300 LEDs. Not sure about non WS281x types but I assume those are less common on this ESP. In my current approach (not in this PR) its like this:

  • use the same calculation limit as it was, I tested a bus with 1200 LEDs.
  • At bootup, there is enough heap to allocate this amount, including _pixels[] and segment buffer and the LEDs light up.
  • After wifi connects, heap drops below 6k, bricking the UI
  • When heap recovery kicks in, enough heap is available (almost 10k) and UI starts working
  • LEDs are still on, lit in orange but cant be controlled as there is no segment buffer anymore.
  • Settings show a big red warning that memory is exceeded more than 2 fold.
  • When trying to save, it sais that this many LEDs cant be handled and user has to reduce to about 700LEDs in order to allow saving settings.

what I did not try yet is if the heap is enough to do an OTA downgrade. IMHO this would be a good approach to solve this particular case.
If we say we only allow full functionality or non at all, the new limit for the ESP8266 is around 300 pixels, for ESP32 about 3000, which I personally think a lot of existing setups may exceed thus resulting in lots of complaints. A somewhat functional system (i.e. only one segment layer, not all effects work with transitions, sometimes segments will not get a buffer etc.) but working UI and working in general is limited to approx. 700LEDs on ESP8266 and 4000LEDs on ESP32 (tested).

@willmmiles
Copy link
Member

The way I see it is once again a question of "how do we anger the least users" :)

We're in agreement on that!

  • use the same calculation limit as it was, I tested a bus with 1200 LEDs.
  • At bootup, there is enough heap to allocate this amount, including _pixels[] and segment buffer and the LEDs light up.
  • After wifi connects, heap drops below 6k, bricking the UI
  • When heap recovery kicks in, enough heap is available (almost 10k) and UI starts working
  • LEDs are still on, lit in orange but cant be controlled as there is no segment buffer anymore.
  • Settings show a big red warning that memory is exceeded more than 2 fold.
  • When trying to save, it sais that this many LEDs cant be handled and user has to reduce to about 700LEDs in order to allow saving settings.

I'm worried that seeing it light the LEDs in orange might give the false impression that we're applying some artificial limit in the config page. After all, clearly the software is capable of lighting them up -- so why can't I use them?

Still I think we're largely in agreement in the overall thrust of the solution. Lighting the LEDs or not when under memory pressure is negotiable.

what I did not try yet is if the heap is enough to do an OTA downgrade. IMHO this would be a good approach to solve this particular case.

+1 - we should guarantee that at least.

If we say we only allow full functionality or non at all, the new limit for the ESP8266 is around 300 pixels, for ESP32 about 3000, which I personally think a lot of existing setups may exceed thus resulting in lots of complaints. A somewhat functional system (i.e. only one segment layer, not all effects work with transitions, sometimes segments will not get a buffer etc.) but working UI and working in general is limited to approx. 700LEDs on ESP8266 and 4000LEDs on ESP32 (tested).

I think we should set the "hard" limits where things will work with no layering or transitions. Those are fun features but at least some real-world setups can get by without them. In the code, we should do what we can to degrade gracefully (eg. cancel transitions first if we're out of memory for segments; have a strong and well-documented ordering for discarding segments if there's insufficient memory for all of them; etc.) and provide some guidance on the "definitely safe" limits on the web site.

@willmmiles
Copy link
Member

what I did not try yet is if the heap is enough to do an OTA downgrade. IMHO this would be a good approach to solve this particular case.
+1 - we should guarantee that at least.

Short answer seems to be that this will need work... I wasn't intending to test this, but I ran in to it with #4930 since I was just using the same 8266 I was last testing for this PR, configured for 500 LEDs. It triggered the low heap handler during the upload and reset the wifi. :(

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 21, 2025

how long did that OTA take? I just tested with my enhanced code and OTA works as long as it takes less than 45 seconds. could up that limit easily.

Let me summarize what we discussed and the options as I see them:

  • updates from <0.16 can have more LEDs configured than can safely be handled
  • if there are bugs in 0.16 onward, we want to handle failed buses more gracefully: currently, they are dropped and no longer show on the config page but still remain in the config file. Suggestion by @willmmiles is to create "dummy" buses as placeholders. These should be rare cases, I agree with this handling.
  • mostly (only?) relevant for 8266 setups with > 700 LEDs configured: after updating, we have these options to handle it:
    • let bus creation fail silently, probably leaving users puzzled as why nothing works, settings will show the bus removed
    • defer bus creation to a dummy, leaving settings page intact. Settings page does inform about excessive memory use but LEDs wont do anything and cant be controlled, also leaving users puzzled.
    • truncate the bus to 30 LEDs showing at least some effort, config will show a 30LED bus. If users hit save, it will overwrite the config and they have to set that up again after downgrading. less puzzling but probably more annoying.
    • accept the way too large bus (works after the OTA reboot) leaving no heap to load the UI, i.e. its bricked until the heap-guard kicks in and drops the buffers after 30 seconds (tested and working). This may also leave users puzzled as "it works for a few seconds" but then stops doing so. The config page will show correct bus(es) warning is present.
  • In all cases, saving LED settings is denied until reasonable values are entered (about 700 LEDs currently)

To me, any of the options are possible, personally I think the last one might be the least disruptive but would collide with the dummy approach which has other benefits in the future (no isOK() checking). I do agree here that users need more info on what is going on. We could add a slightly more sophisticated "error" log. Instead of doing the current short "toast" message, an error / symbol could be added to the UI, if clicked, it opens an error log of the last x error messages that could be a bit more descriptive. Extending the ERR_x list in const.h is possible.

@willmmiles
Copy link
Member

I've put a review on #4928. After those fixes, yes please go ahead and merge your updates here!

@DedeHai DedeHai marked this pull request as ready for review September 28, 2025 09:29
coderabbitai[bot]

This comment was marked as resolved.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 28, 2025

@willmmiles I added in my improvements to the low-heap handler in main loop.
also added disableForceReconnect which is enabled during OTA. it does not have a timout so if OTA gets interrupted due to network error, I think it wont reach the cleanup stage and stays disabled (and strip stays suspended, as it was, IMHO an edge case with no horrible consequences).
Also added a isPlacehoder() to the bus: could also use isOK() but I thought this might have some future benefit, can remove later if it turns out that its not useful.
from my side this is ready to merge, I have some more heap handling improvements I did not want to add to this PR which fix some issues on ESP8266.
I think this is a good base to refine the bus management, still needs more work to come to a conclusion on how to handle bus & buffer memory as MAX_LED_MEMORY currently serves double-duty: once for bus allocation and once for UI memory limits. In UI, required buffers are included but MAX_LED_MEMORY is doubled as buffers should respect min heap whereas busses do not (and currently can't as they use constructors and not the malloc_x() functions)

@willmmiles
Copy link
Member

also added disableForceReconnect which is enabled during OTA. it does not have a timout so if OTA gets interrupted due to network error, I think it wont reach the cleanup stage and stays disabled (and strip stays suspended, as it was, IMHO an edge case with no horrible consequences).

I fixed the update cleanup handling in #4960, it'll run no matter why the update failed. (Although deleting the segments to free RAM is, alas, currently unrecoverable.) I'd also added an "update in progress" test to the heap check -- I went the other way and had the heap checker inspect the Update state directly so it'd work with ArduinoOTA or other non-HTTP implementations. (Although such backdoors will also skip the suspend() and the #4960 validation, sadly. :( )

Merge conflicts, here we come... ;)

Also added a isPlacehoder() to the bus: could also use isOK() but I thought this might have some future benefit, can remove later if it turns out that its not useful.

Ultimately I want to go the other way around -- I don't think we should ever have any isSomeBusType() calls. The Bus abstraction ought to be actually properly abstract over the needs of Bus clients -- that needs to be a goal of the Bus API improvements.

I think this is a good base to refine the bus management, still needs more work to come to a conclusion on how to handle bus & buffer memory as MAX_LED_MEMORY currently serves double-duty: once for bus allocation and once for UI memory limits. In UI, required buffers are included but MAX_LED_MEMORY is doubled as buffers should respect min heap whereas busses do not (and currently can't as they use constructors and not the malloc_x() functions)

Agreed. Personally I'd also like to clean up the duplication of the memory estimation code in C++ and JS. Later I think we should add an HTTP API endpoint for letting the JS call the C++ code to do the memory estimation. Hopefully should be straightforward after the bus management work is done.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 28, 2025

I'd also added an "update in progress" test to the heap check
Merge conflicts, here we come... ;)

much better way of doing it. I will remove my crude "hold up with that reset" for one less conflict ;)

Also added a isPlacehoder() to the bus: could also use isOK() but I thought this might have some future benefit, can remove later if it turns out that its not useful.

Ultimately I want to go the other way around -- I don't think we should ever have any isSomeBusType() calls. The Bus abstraction ought to be actually properly abstract over the needs of Bus clients -- that needs to be a goal of the Bus API improvements.

sounds like a good goal. I will leave the check in for now.

Agreed. Personally I'd also like to clean up the duplication of the memory estimation code in C++ and JS. Later I think we should add an HTTP API endpoint for letting the JS call the C++ code to do the memory estimation. Hopefully should be straightforward after the bus management work is done.

Fully agree. I also thought about that but could not come up with a good plan on how to do it, my JS / HTTP / WS knowledge is very limited.

So is this good to merge after I remove the connection reset blocker mentioned above?

@willmmiles
Copy link
Member

So is this good to merge after I remove the connection reset blocker mentioned above?

I haven't been able to do a thorough review or test it, but the overall approach seems OK.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 28, 2025

no problem, I'll wait with merging until you approve.

coderabbitai[bot]

This comment was marked as resolved.

- reverting changes as they are related to parallel loading, I have found a better way to only use sequential loading which makes these changes obsolete
@DedeHai DedeHai changed the title Safety Checks for UI, fix for cfg exceeding LED limit Fix for cfg exceeding LED limit Oct 17, 2025
the "wiggle room" is to not reset segments if min heap is just slightly underrun: alloc functions respect min heap, but UI or other functions may grab some extra heap, leading to unnecessary resets.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
wled00/wled.cpp (4)

42-43: Use 32-bit tick for clarity and rollover safety.

16-bit timing with casts works but is nonstandard and easy to misread. Prefer a 32-bit timestamp and the usual millis-delta pattern.

Apply:

-  static uint16_t      heapTime = 0;   // timestamp for heap check
+  static uint32_t      heapTime = 0;   // ms timestamp for heap check
   static uint8_t       heapDanger = 0; // counter for consecutive low-heap readings

And in the check block (see Lines 172-212):

-  if ((uint16_t)(millis() - heapTime) > 1000) {
+  uint32_t nowMs = millis();
+  if (nowMs - heapTime >= 1000) {
     ...
-    heapTime = (uint16_t)millis();
+    heapTime = nowMs;
   }

175-183: Avoid unconditional delay(15) on ESP32-C3; gate on active LED updates.

The 15ms pause every second can add jitter and slows the main loop even when no update is in progress. Make it conditional to preserve responsiveness.

-    // calling getContiguousFreeHeap() during led update causes glitches on C3, this is a dirty workaround (tested up to 1500 LEDs)
-    // this can (probably) be removed once RMT driver for C3 is fixed
-    delay(15);
+    // calling getContiguousFreeHeap() during LED update causes glitches on C3
+    // gate the delay to active updates to minimize loop jitter
+    if (strip.isUpdating()) delay(15);

Based on learnings.


184-186: Saturate heapDanger to prevent wraparound re-triggering.

With uint8_t, continuous low-heap for >255s wraps to 0, retriggering 15s/30s actions later. Saturate to the highest tier you need.

-    if (heap < MIN_HEAP_SIZE - 1024) heapDanger++; // allow 1k of "wiggle room" for things that do not respect min heap limits
-    else heapDanger = 0;
+    if (heap < MIN_HEAP_SIZE - 1024) { // allow 1k of "wiggle room"
+      if (heapDanger < 45) ++heapDanger; // saturate at top tier
+    } else {
+      heapDanger = 0;
+    }

187-207: Optional: auto-recovery hooks when heap stabilizes.

You disable transitions and force STATIC at 15s but never restore on recovery. Consider re-enabling transitions (and optionally re-applying previous modes) after N consecutive healthy seconds to improve UX.

No diff provided; if desired, I can draft a small state machine to re-enable transitions after, e.g., 5 healthy seconds.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70015bf and 40039bf.

📒 Files selected for processing (1)
  • wled00/wled.cpp (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
wled00/**/*.cpp

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use 2-space indentation for C++ source files (.cpp)

Files:

  • wled00/wled.cpp
🧠 Learnings (3)
📓 Common learnings
Learnt from: DedeHai
PR: wled/WLED#4939
File: wled00/FX_fcn.cpp:1176-1187
Timestamp: 2025-09-16T18:08:42.848Z
Learning: In WLED finalizeInit() bus creation (wled00/FX_fcn.cpp), intentionally allowing memory overruns when bus configurations exceed MAX_LED_MEMORY is a deliberate design choice. The trade-off prioritizes creating buses with reduced LED counts over completely failing to create buses, which would cause no LED output and UI failures. This approach forces users to update configurations after migrating to version 0.16 while maintaining basic functionality.
Learnt from: willmmiles
PR: wled/WLED#4939
File: wled00/FX_fcn.cpp:1183-1194
Timestamp: 2025-09-18T03:17:30.107Z
Learning: In WLED finalizeInit() bus creation (wled00/FX_fcn.cpp), digitalCount is intentionally incremented even for placeholder buses to maintain consistent resource mapping across all bus configurations.
📚 Learning: 2025-09-18T02:59:33.666Z
Learnt from: willmmiles
PR: wled/WLED#4928
File: wled00/FX_fcn.cpp:1181-1181
Timestamp: 2025-09-18T02:59:33.666Z
Learning: In WLED, getFreeHeapSize() is a platform-agnostic function defined in fcn_declare.h that provides accurate free heap reporting across ESP32 and ESP8266 platforms. On ESP32, it uses heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) instead of ESP.getFreeHeap() because the latter can include other memory types. On ESP8266, it uses ESP.getFreeHeap().

Applied to files:

  • wled00/wled.cpp
📚 Learning: 2025-09-28T09:53:42.670Z
Learnt from: DedeHai
PR: wled/WLED#4939
File: wled00/data/index.js:2406-2413
Timestamp: 2025-09-28T09:53:42.670Z
Learning: WLED's heap panic recovery in the main loop intentionally prioritizes UI functionality over complete system reinitialization. Strip reinitialization may be deliberately skipped during heap panic as a last-resort measure to free enough memory to maintain a working UI.

Applied to files:

  • wled00/wled.cpp
🧬 Code graph analysis (1)
wled00/wled.cpp (2)
wled00/fcn_declare.h (4)
  • getFreeHeapSize (462-462)
  • getFreeHeapSize (465-465)
  • getContiguousFreeHeap (463-463)
  • getContiguousFreeHeap (466-466)
wled00/FX.h (1)
  • WS2812FX (821-884)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32c3dev)
  • GitHub Check: wled_build / Build Environments (esp32_eth)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_16MB_opi)
  • GitHub Check: wled_build / Build Environments (lolin_s2_mini)
  • GitHub Check: wled_build / Build Environments (esp32_wrover)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_compat)
  • GitHub Check: wled_build / Build Environments (esp32dev)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_compat)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_160)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_160)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_160)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full)
  • GitHub Check: wled_build / Build Environments (esp8266_2m)
  • GitHub Check: wled_build / Build Environments (nodemcuv2)
🔇 Additional comments (1)
wled00/wled.cpp (1)

201-206: The proposed guard references a non-existent variable.

The variable disableForceReconnect does not exist anywhere in the codebase—it appears only in the comment at line 205 itself. Additionally, there is no OTA progress state variable to check against. The ArduinoOTA callbacks (onStart, onError) do not set any flags to indicate an OTA operation is in progress.

The concern about forceReconnect interrupting OTA operations may be valid, but the proposed solution cannot be implemented as written. The codebase would need:

  1. A new OTA state tracking variable
  2. Logic to set/clear it in ArduinoOTA callbacks
  3. Code to guard the forceReconnect assignment

The reviewer's suggestion appears based on misunderstandings about what was implemented in this PR.

Likely an incorrect or invalid review comment.

#else
#ifdef CONFIG_IDF_TARGET_ESP32C3
// calling getContiguousFreeHeap() during led update causes glitches on C3, this is a dirty workaround (tested up to 1500 LEDs)
// this can (probably) be removed once RMT driver for C3 is fixed
Copy link
Member

@softhack007 softhack007 Nov 16, 2025

Choose a reason for hiding this comment

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

@DedeHai this reminds me of something. I've seen similar behaviour also on other esp32 boards - especially esp32 and -S3, and a few times on -S2 too.

  • When calling ESP.getFreeHeap() while strip.isUpdating(), this was almost a dead-sure combination causing LED flickering.
  • My intermediate hack in WLED-MM was to skip memory checking with ESP.getFreeHeap() when strip.isUpdating(). It removed the annoying "almost every 5 second flicker".

My educated guess here was that something inside ESP.getFreeHeap() uses some kind of "big kernel lock" that suspends everything while counting free heap bytes.

Maybe it would also help in upstream to avoid heap size checking while RMT is sending out LEDs. Its definitely not just a -C3 only problem.

Copy link
Member

Choose a reason for hiding this comment

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

Edit: a similar, unguarded call that you might want to protect is here:

root[F("freeheap")] = getFreeHeapSize();

this is running in webServer context when you refresh the main UI, so there is a possibility that this leads to flickering, too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

doh, that is the command I was looking for last week and could not remember. The C3 flickers whenever flash is accessed or other blocking processes run, like the free heap check. Its also an issue in the image FX when running animated gif's, that is where I wanted to add a check to not load a new gif frame while LEDs are being updated.
The flickering is (or should be) fixed with the new RMT driver update, but for the C3 it is still pending. I could not get any flickering on a ESP32, S2 or S3 no matter what I tried.

Copy link
Member

@softhack007 softhack007 Nov 16, 2025

Choose a reason for hiding this comment

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

maybe my famous "be nice but not too nice" code would help:

   unsigned t0 = millis();
   while (strip.isUpdating() && (millis() - t0 < 15)) delay(1);    // be nice, but not too nice. Waits up to 15ms

you could also use delay(0), its just important that the task scheduler gets a chance for context switching.

Don't use yield() on esp32, because yield() does not work like on 8266.

Copy link
Member

Choose a reason for hiding this comment

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

My educated guess here was that something inside ESP.getFreeHeap() uses some kind of "big kernel lock" that suspends everything while counting free heap bytes.

Yes, it locks the heap and has to walk the free lists. It's quite expensive. We may someday want to think about some other approach to heap usage monitoring -- the web server has the same problem trying to estimate heap availability.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Jan 11, 2026

@willmmiles did you get a chance to test this? I am working on quite a lot of changes in bus handling to allow RMT and I2S mixed buses and this PR has some overlap with that work and the bus placeholder would be really useful there so I would like to get this PR approved & merged.

Copy link
Member

@willmmiles willmmiles left a comment

Choose a reason for hiding this comment

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

Seems to be working for me. Future work might be to treat buses that cause the total to exceed MAX_LEDS to be handled the same way (placeholders) instead of being discarded at cfg parse time.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Jan 19, 2026

thanks for the review. I will fix the conflicts, add that "wait until bus is done" to the heap check. Once this is merged #5303 may need a little ironing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

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

1171-1183: Limit checks ignore the new bus (and bus type).

digital/analog are counted only for existing busses and the limit check is unconditional. This can allow a real bus to be created beyond channel limits (or force a placeholder for a virtual bus). Suggest computing projected counts based on bc.type before deciding on placeholder.

🐛 Suggested fix
   unsigned digital = 0;
   unsigned analog  = 0;
   unsigned twoPin  = 0;
   for (const auto &bus : busses) {
     if (bus->isPWM()) analog += bus->getPins(); // number of analog channels used
     if (bus->isDigital() && !bus->is2Pin()) digital++;
     if (bus->is2Pin()) twoPin++;
   }
-  if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true; // TODO: add errorFlag here
+  unsigned digitalProjected = digital + (Bus::isDigital(bc.type) && !Bus::is2Pin(bc.type));
+  unsigned analogProjected  = analog  + (Bus::isPWM(bc.type) ? Bus::numPWMPins(bc.type) : 0);
+  if (digitalProjected > WLED_MAX_DIGITAL_CHANNELS || analogProjected > WLED_MAX_ANALOG_CHANNELS) {
+    placeholder = true; // TODO: add errorFlag here
+  }
wled00/xml.cpp (1)

315-341: Guard pin emission for non‑OK, non‑placeholder buses.

With the isOk() gate removed, getPins() is now called even for failed (non‑placeholder) busses. For some bus types, pins are not initialized when construction fails, so the UI can receive garbage pin numbers and overwrite config unintentionally. Consider either initializing pins from BusConfig even on failure, or skip pin emission unless isOk() or isPlaceholder() is true.

🐛 Safer pin emission (XML-side)
-      uint8_t pins[OUTPUT_MAX_PINS];
-      int nPins = bus->getPins(pins);
+      uint8_t pins[OUTPUT_MAX_PINS] = {255};
+      int nPins = (bus->isOk() || bus->isPlaceholder()) ? bus->getPins(pins) : 0;
wled00/FX_fcn.cpp (1)

1190-1221: Address contradiction between placeholder handling and I2S memory estimation.

The code at line 1219 decrements digitalCount when a digital non-2pin bus is added as a placeholder. However, the documented design principle states that digitalCount should be intentionally incremented even for placeholder buses to maintain consistent resource mapping.

This decrement affects the usesI2S check (lines 1196, 1198) for subsequent buses in the loop. If a placeholder reduces digitalCount, it can change whether later buses are flagged as using I2S, potentially affecting their memory estimates.

The decrement is intentional (per the inline comment), but verify whether this behavioral change is correct: should placeholder buses be excluded from the digital count for I2S allocation purposes, or should they contribute to the count regardless of placeholder status?

🧹 Nitpick comments (3)
wled00/bus_manager.h (1)

399-406: Replace _pins[5] with OUTPUT_MAX_PINS.

Keeps placeholder layout aligned with BusConfig and avoids future mismatches if the pin limit changes. Based on learnings, prefer meaningful constants over magic numbers.

♻️ Proposed change
-    uint8_t _pins[5];
+    uint8_t _pins[OUTPUT_MAX_PINS];
wled00/wled.cpp (2)

177-181: The delay(15) workaround may cause timing issues in the main loop.

While this addresses the C3 glitch issue noted in past reviews, a 15ms blocking delay in the main loop (even once per second) could affect time-sensitive operations. Based on prior discussion, consider waiting for !strip.isUpdating() instead:

unsigned t0 = millis();
while (strip.isUpdating() && (millis() - t0 < 15)) delay(1);

This yields to the task scheduler rather than hard-blocking, and only waits when necessary.

♻️ Suggested improvement
    `#ifdef` CONFIG_IDF_TARGET_ESP32C3
-   // calling getContiguousFreeHeap() during led update causes glitches on C3, this is a dirty workaround (tested up to 1500 LEDs)
-   // this can (probably) be removed once RMT driver for C3 is fixed
-   delay(15);
+   // calling getContiguousFreeHeap() during led update causes glitches on C3
+   // wait for LED update to complete, but don't block indefinitely
+   {
+     unsigned t0 = millis();
+     while (strip.isUpdating() && (millis() - t0 < 15)) delay(1);
+   }
    `#endif`

184-210: Consider using named constants for the magic numbers.

The threshold values (15, 30, 45 seconds, 1024 bytes wiggle room) are meaningful configuration values that would benefit from named constants for maintainability and self-documentation.

♻️ Optional: define constants for thresholds
// At file scope or in a header:
constexpr uint8_t HEAP_DANGER_PURGE_THRESHOLD = 15;   // seconds
constexpr uint8_t HEAP_DANGER_RESET_THRESHOLD = 30;   // seconds
constexpr uint8_t HEAP_DANGER_PANIC_THRESHOLD = 45;   // seconds
constexpr uint32_t HEAP_WIGGLE_ROOM = 1024;           // bytes

// Then in the code:
if (heap < MIN_HEAP_SIZE - HEAP_WIGGLE_ROOM) heapDanger++;
// ...
switch (heapDanger) {
  case HEAP_DANGER_PURGE_THRESHOLD: // ...
  case HEAP_DANGER_RESET_THRESHOLD: // ...
  case HEAP_DANGER_PANIC_THRESHOLD: // ...
}

As per coding guidelines, replacing magic numbers with defined constants improves code maintainability.

@DedeHai DedeHai merged commit af8db57 into wled:main Jan 19, 2026
23 checks passed
@DedeHai DedeHai deleted the fix_UIandBusMemory branch January 19, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No fallback if bus fails to be added in 0.16

4 participants