Skip to content

FastLED 3.11#121

Open
ewowi wants to merge 4 commits intomainfrom
fastled-3.11
Open

FastLED 3.11#121
ewowi wants to merge 4 commits intomainfrom
fastled-3.11

Conversation

@ewowi
Copy link
Collaborator

@ewowi ewowi commented Feb 5, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved LED initialization and stability across hardware; fixed several memory-copy and boundary-handling issues to prevent corner-case glitches.
    • Minor effect and modifier fixes to reduce visual artifacts.
  • Chores

    • Increased stack sizes for core services and drivers to reduce runtime issues.
    • Updated LED library reference to a newer commit for consistent behavior.
    • Cleaned up configuration comments and guarded a macro to avoid conflicts.
  • UI/Layout

    • Adjusted several layout ring counts and coordinates for more accurate light arrangements.

- pio.ini: include latest FastLED
- memcpy: (void *) on CRGB
- CRGB + CRGB not defined (replaced by += workaround)
- MAX -> max
- D_FastLED.h: remove pun 19 and 20 from CONFIG_IDF_TARGET_ESP32S3
@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Walkthrough

Updated build flags and FastLED dependency; replaced several C-style struct CRGB* usages and adjusted memcpy casts; swapped macro MAX for max across many files; refactored FastLED driver initialization to a runtime channel-based flow; added a function-pointer cast for script delay() registration.

Changes

Cohort / File(s) Summary
Build & Dependency
platformio.ini, firmware/esp32-d0.ini, firmware/esp32-s3.ini
Bumped APP_DATE; increased HTTPD_STACK_SIZE and DRIVERS_STACK_SIZE; pinned FastLED to a specific commit hash; removed/comment cleanup in platform ini files; added ESP32 core/loop variables.
FastLED driver refactor
src/MoonLight/Nodes/Drivers/D_FastLED.h
Removed extensive compile-time per-pin cases and chipset fields; introduced runtime channel creation and registration (channel/timing config, engine selection, FastLED.add/channel registration) and driver diagnostics; removed several public member fields.
VirtualLayer & CRGB helpers
src/MoonLight/Layers/VirtualLayer.cpp, src/MoonLight/Layers/VirtualLayer.h
Changed function signatures to CRGB* (removed struct), cast numToFill to uint16_t for fill_rainbow, switched MAXmax, adjusted memcpy calls to use explicit void* casts, and refactored addRGB to use a temporary CRGB.
MAX→max standardization
src/MoonLight/Modules/ModuleLightsControl.h, src/MoonLight/Nodes/Drivers/D_Infrared.h, src/MoonLight/Nodes/Effects/E_MoonLight.h, src/MoonLight/Nodes/Effects/E_WLED.h, src/MoonLight/Nodes/Modifiers/M_MoonLight.h
Replaced uses of the MAX macro with max (often with explicit unsigned literal) across timing, effects, and modifier code paths for consistent type semantics.
Script binding change
src/MoonBase/Nodes.cpp
Registered delay(uint32_t) with an explicit function-pointer cast when exposing it to the script engine.
Layout adjustments
src/MoonLight/Nodes/Layouts/L_MoonLight.h
Adjusted CarLightsLayout coordinates and reduced several ring LED counts (34→32) for left/right inner/outer rings.
Defensive/misc updates
lib/framework/CoreDump.cpp, src/MoonLight/Nodes/Effects/E_WLED.h, others
Guarded MIN macro definition with include-guard; cleaned up commented lines; fixed several memcpy source casts to void* for CRGB copies.

Sequence Diagram(s)

sequenceDiagram
  participant Init as InitRoutine
  participant Layout as LayoutBuilder
  participant ChannelCfg as ChannelConfigFactory
  participant Engine as EngineSelector
  participant FastLED as FastLED_Manager
  participant Logger as DiagnosticsLogger

  Init->>Layout: compute layout & targets
  Layout->>ChannelCfg: build timing/config per target (pin, timing, span)
  ChannelCfg->>Engine: select engine (I2S / PARLIO) based on platform
  Engine-->>ChannelCfg: return engine instance
  ChannelCfg->>FastLED: create channel & FastLED.add(channel)
  FastLED->>FastLED: register driver, set exclusivity
  FastLED->>Logger: report driver count/info
  Logger-->>Init: log driver diagnostics
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Board presets p4 #74: Related changes to VirtualLayer fast-fill functions and CRGB handling (signatures/casts, fill_rainbow usage).
  • pre 71 fixes #107: Overlaps VirtualLayer sizing/mapping and safer buffer/memcpy handling.
  • v0.7.0 preparations #76: Overlapping edits to FastLED driver and VirtualLayer initialization/driver mapping.

Poem

🐰 I hopped through headers, casts in paw,

swapped macros for safer law.
Pinned a lib, adjusted stacks by night,
stitched channels up in runtime light.
Tiny hops — brighter LEDs in sight.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'FastLED 3.11' is too vague and does not clearly summarize the main changes. While the PR involves upgrading FastLED, the title fails to convey the significant configuration adjustments, stack size increases, macro replacements, and driver refactoring that are central to this changeset. Consider a more descriptive title such as 'Upgrade FastLED to 3.11 with driver refactoring and stack adjustments' or 'FastLED 3.11 upgrade: Runtime channel initialization and configuration updates' to better reflect the scope of changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fastled-3.11

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

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
src/MoonLight/Nodes/Drivers/D_FastLED.h (1)

158-166: ⚠️ Potential issue | 🔴 Critical

Remove unguarded GPIO11 cases—not RMT5-specific but a universal hardware conflict on ESP32.

GPIO11 (along with GPIO6–10) is part of the on-module SPI flash bus on standard ESP32 variants and cannot be used for general GPIO. The template instantiation of addLeds<..., 11> will fail at compile time regardless of build profile. Remove or comment out the GPIO11 cases entirely (as already done for GPIO6–10 in other sections of this file) rather than guarding by RMT5. The constraint is hardware-level, not FastLED RMT5-specific.

Suggested fix: remove or comment out GPIO11 cases
-        case 11:
-          FastLED.addLeds<ML_CHIPSET, 11 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
-          break;
+        // case 11: FastLED.addLeds<ML_CHIPSET, 11 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break; // 11-17 reserved for SPI FLASH
🤖 Fix all issues with AI agents
In `@src/MoonLight/Layers/VirtualLayer.cpp`:
- Line 293: The use of max(nrOfLights, indexV + 1) causes a template deduction
error due to mixed types (indexV+1 is int); fix by casting the second operand to
the same type as nrOfLights (nrOfLights_t) so the overload resolves, e.g.
replace with nrOfLights = max(nrOfLights, static_cast<nrOfLights_t>(indexV +
1)); ensure this change is applied where nrOfLights, indexV and nrOfLights_t are
used together (VirtualLayer.cpp).

In `@src/MoonLight/Nodes/Effects/E_MoonLight.h`:
- Line 1613: The setup() override builds the control range using layer->size but
mistakenly uses layer->size.x three times, shrinking bounds for non‑cubic
layouts; update the max expression in the addControl call (in setup) to compute
the maximum of layer->size.x, layer->size.y and layer->size.z (e.g., nested max
of x, y, then z) and subtract 1 as before so the pos control uses the true
largest dimension.

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 5, 2026

@coderabbitai, how on earth do I get Stack canary watchpoint triggered (AppDrivers) when the stack is 1820 bytes just before the exception?

[ 4478][D][PhysicalLayer.cpp:248] onLayoutPost(): [💫] AppDrivers (1820) pass 1 mp:0 #:256 / 768 s:16,16,1
[ 4497][D][PhysicalLayer.cpp:251] onLayoutPost(): [💫] AppDrivers (1820) positions stored (1 -> 2)
[ 4507][D][PhysicalLayer.cpp:115] loopDrivers(): [💫] AppDrivers (1820) mapLayout virtual requested
[ 4517][D][PhysicalLayer.cpp:156] onLayoutPre(): [💫] AppDrivers (1820) pass 2 mp:0
[ 4526][D][VirtualLayer.cpp:240] createMappingTableAndAddOneToOne(): [💫] AppDrivers (1820) Filling mappingTable < 1
[ 4538][D][PhysicalLayer.cpp:259] onLayoutPost(): [💫] AppDrivers (1820) pass 2 indexP: 256
[ 4548][I][VirtualLayer.cpp:331] onLayoutPost(): [💫] AppDrivers (1820) irregular mapping 1 !
[ 4557][I][VirtualLayer.cpp:356] onLayoutPost(): [🌙] AppDrivers (1820) V:16 x 16 x 1 = v:256 = 1:0:0 + 1:1:256 + mti:0 (1:m:0)
[ 4570][D][D_FastLED.h:64] loop(): [💫] AppDrivers (1820) setBrightness 246
[ 4578][D][D_FastLED.h:69] loop(): [💫] AppDrivers (1820) setMaxPower 10 watt
[ 4587][D][D_FastLED.h:80] loop(): [💫] AppDrivers (1820) setColorCorrection r:255, g:255, b:255 (#:256)
Guru Meditation Error: Core 1 panic'ed (Unhandled debug exception).
Debug exception reason: Stack canary watchpoint triggered (AppDrivers)
Core 1 register dump:
PC : 0x40384f6f PS : 0x00060736 A0 : 0x80384ee4 A1 : 0x3fcc3c00
A2 : 0x3fcea9ec A3 : 0x00000041 A4 : 0x00000004 A5 : 0x00060523
A6 : 0x0000003c A7 : 0x3fce9710 A8 : 0xbaad5678 A9 : 0xabba1234
A10 : 0x3fceaa2d A11 : 0x00000006 A12 : 0x3fcc3d9c A13 : 0x00060723
A14 : 0x00000004 A15 : 0x3fcab4c8 SAR : 0x0000001e EXCCAUSE: 0x00000001
EXCVADDR: 0x00000000 LBEG : 0x400570e8 LEND : 0x400570f3 LCOUNT : 0x00000000

Backtrace: 0x40384f6c:0x3fcc3c00 0x40384ee1:0x3fcc3c30 0x40376da3:0x3fcc3c50 0x40376dbd:0x3fcc3c80 0x40376f7c:0x3fcc3ca0 0x403862dd:0x3fcc3cc0 0x42006866:0x3fcc3ce0 0x42005065:0x3fcc3d00 0x42008096:0x3fcc3d20 0x420081ca:0x3fcc3dc0 0x42016b82:0x3fcc3e30 0x42016f5b:0x3fcc3eb0 0x42018485:0x3fcc3fd0 0x420188d1:0x3fcc41a0 0x4201b0fd:0x3fcc42b0 0x4201b386:0x3fcc4750 0x4200b07a:0x3fcc4780 0x4200b135:0x3fcc4810 0x42197b9d:0x3fcc4840 0x42005535:0x3fcc4860 0x4200b2fd:0x3fcc4880 0x420038ce:0x3fcc4940 0x420039d5:0x3fcc4960 0x42089645:0x3fcc4980 0x420855f1:0x3fcc49f0 0x42088db0:0x3fcc4a40 0x40380119:0x3fcc4a80
#0 0x40384f6c in poison_allocated_region at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/multi_heap_poisoning.c:75
#1 0x40384ee1 in multi_heap_malloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/multi_heap_poisoning.c:257
(inlined by) multi_heap_malloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/multi_heap_poisoning.c:243
#2 0x40376da3 in aligned_or_unaligned_alloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps_base.c:93
(inlined by) heap_caps_aligned_alloc_base at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps_base.c:175
#3 0x40376dbd in heap_caps_malloc_base at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps_base.c:202
#4 0x40376f7c in heap_caps_malloc_default at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/heap/heap_caps.c:120
#5 0x403862dd in malloc at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/newlib/src/heap.c:24
#6 0x42006866 in fl::malloc(unsigned int) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/stl/malloc.cpp.hpp:8
(inlined by) fl::StringHolder::StringHolder(unsigned int) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/stl/detail/string_holder.cpp.hpp:22
#7 0x42005065 in fl::shared_ptrfl::StringHolder fl::make_shared<fl::StringHolder, unsigned int&>(unsigned int&) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/stl/shared_ptr.h:361 (discriminator 2)
#8 0x42008096 in fl::StrN<64u>::write(char const*, unsigned int) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/stl/string.h:558
#9 0x420081ca in fl::string::append(char const*, unsigned int) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/stl/string.h:2484
(inlined by) fl::StrStream::appendFormatted(long) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/stl/strstream.cpp.hpp:73
#10 0x42016b82 in fl::enable_if<fl::is_multi_byte_integer::value, fl::StrStream&>::type fl::StrStream::operator<< (int) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/stl/strstream.h:320
(inlined by) fl::ChannelEngineSpi::acquireSpiHost() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/platforms/esp/32/drivers/spi/channel_engine_spi.cpp.hpp:1225
#11 0x42016f5b in fl::ChannelEngineSpi::createChannel(fl::ChannelEngineSpi::SpiChannelState*, gpio_num_t, fl::SpiTimingConfig const&, unsigned int, fl::ChipsetTimingConfig const*) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/platforms/esp/32/drivers/spi/channel_engine_spi.cpp.hpp:836
#12 0x42018485 in fl::ChannelEngineSpi::acquireChannel(gpio_num_t, fl::SpiTimingConfig const&, unsigned int, fl::ChipsetTimingConfig const&) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/platforms/esp/32/drivers/spi/channel_engine_spi.cpp.hpp:656
#13 0x420188d1 in fl::ChannelEngineSpi::beginTransmission(fl::span<fl::shared_ptrfl::ChannelData const, 4294967295u>) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/platforms/esp/32/drivers/spi/channel_engine_spi.cpp.hpp:514 (discriminator 1)
#14 0x4201b0fd in fl::ChannelEngineSpi::beginBatchedTransmission(fl::span<fl::shared_ptrfl::ChannelData const, 4294967295u>) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/platforms/esp/32/drivers/spi/channel_engine_spi.cpp.hpp:419 (discriminator 1)
#15 0x4201b386 in fl::ChannelEngineSpi::show() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/platforms/esp/32/drivers/spi/channel_engine_spi.cpp.hpp:287 (discriminator 2)
#16 0x4200b07a in fl::ChannelBusManager::beginTransmission(fl::span<fl::shared_ptrfl::ChannelData const, 4294967295u>) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/channels/bus_manager.cpp.hpp:251
#17 0x4200b135 in fl::ChannelBusManager::show() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/channels/bus_manager.cpp.hpp:194 (discriminator 3)
(inlined by) fl::ChannelBusManager::show() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/channels/bus_manager.cpp.hpp:186 (discriminator 3)
#18 0x42197b9d in fl::ChannelBusManager::onEndFrame() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/channels/bus_manager.cpp.hpp:258
#19 0x42005535 in non-virtual thunk to fl::ChannelBusManager::onEndFrame() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/channels/bus_manager.h:134
#20 0x4200b2fd in fl::EngineEvents::_onEndFrame() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/engine_events.cpp.hpp:106
#21 0x420038ce in fl::EngineEvents::onEndFrame() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/fl/engine_events.h:86 (discriminator 1)
#22 0x420039d5 in CFastLED::onEndFrame() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/FastLED.cpp.hpp:176
(inlined by) CFastLED::show(unsigned char) at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/FastLED.cpp.hpp:170
#23 0x42089645 in CFastLED::show() at .pio/libdeps/esp32-s3-n16r8v/FastLED/src/FastLED.h:1135
(inlined by) FastLEDDriver::loop() at src/MoonLight/Nodes/Drivers/D_FastLED.h:87
#24 0x420855f1 in PhysicalLayer::loopDrivers() at src/MoonLight/Layers/PhysicalLayer.cpp:134
#25 0x42088db0 in driverTask(void*) at src/main.cpp:190

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the AppDrivers task creation to check stack size
echo "=== Searching for AppDrivers task creation in main.cpp ==="
rg -n -A5 -B5 "AppDrivers|driverTask" src/main.cpp | head -80

echo ""
echo "=== Checking D_FastLED.h loop method around line 87 ==="
sed -n '60,100p' src/MoonLight/Nodes/Drivers/D_FastLED.h

echo ""
echo "=== Looking for task stack size constants or defines ==="
rg -n "xTaskCreate|stack|STACK" src/main.cpp | head -20

Length of output: 3594


🏁 Script executed:

#!/bin/bash
# Find the definition of DRIVERS_STACK_SIZE and EFFECTS_STACK_SIZE
echo "=== Finding DRIVERS_STACK_SIZE definition ==="
rg -n "DRIVERS_STACK_SIZE|EFFECTS_STACK_SIZE" --type-add 'headers:*.h' --type headers src/

echo ""
echo "=== Also search in main.cpp and any config files ==="
rg -n "DRIVERS_STACK_SIZE|EFFECTS_STACK_SIZE" src/main.cpp src/*.h 2>/dev/null || echo "Not in main files"

echo ""
echo "=== Search in all source files ==="
rg -n "define.*STACK_SIZE" src/

Length of output: 543


🏁 Script executed:

#!/bin/bash
# Search more broadly for stack size definitions
echo "=== Search all C/C++ files for STACK_SIZE ==="
rg -n "STACK_SIZE" src/ -g "*.{cpp,h,hpp}" | head -30

echo ""
echo "=== Check if defined in platformio.ini as build flags ==="
rg -n "STACK_SIZE" platformio.ini

echo ""
echo "=== Check for default values or conditionals ==="
rg -n -B2 -A2 "8192|8K|16384|16K" src/main.cpp | head -40

echo ""
echo "=== Look for any `#ifndef` DRIVERS_STACK_SIZE defaults ==="
fd -e h -e hpp -e cpp -x grep -l "DRIVERS_STACK_SIZE\|EFFECTS_STACK_SIZE" src/

Length of output: 5702


@ewowi, I found the issue! The stack canary watchpoint fires when the canary is already corrupted, not when stack runs out. Here's what's happening:

The Problem

From platformio.ini (line 152):

-D DRIVERS_STACK_SIZE=4096

Your AppDrivers task has only 4KB of stack, but FastLED 3.11 needs significantly more than 3.10.3 did.

Understanding the 1820 Bytes "Remaining"

The confusing part: ESP-IDF places guard bytes (canaries) at the stack boundaries. When your code writes past the stack limit, it overwrites these canaries. The watchpoint doesn't fire immediately—it fires later when malloc/free checks the canaries during heap operations.

In your case:

  1. Deep call stack through FastLED's new ChannelEngineSpi (25 frames!) + string allocations overflowed the 4KB stack
  2. Stack overflow corrupted the canary bytes
  3. Code continued running (stack pointer showing 1820 bytes "remaining")
  4. When FastLED tried to allocate fl::StringHolder for debug messages (frame #6-7), malloc checked the canary
  5. Canary corruption detected → watchpoint triggered

Why FastLED 3.11 Breaks

The backtrace reveals the culprit:

  • Frames #10-15: New ChannelEngineSpi architecture (didn't exist in 3.10.3)
    • acquireSpiHost()createChannel()acquireChannel()beginTransmission()beginBatchedTransmission()
  • Frames #6-9: String formatting for debug/error messages
    • fl::StringHolder, fl::StrN<64u>, fl::StrStream::appendFormatted()

FastLED 3.11's SPI channel engine is much more complex than the old RMT-only driver, with deeper call stacks and heap allocations for string operations.

Solution

Increase the drivers stack size in platformio.ini:

-D DRIVERS_STACK_SIZE=8192  ; was 4096 - FastLED 3.11 needs more for ChannelEngineSpi

For comparison, you already have:

  • HTTPD_STACK_SIZE=5120
  • SVELTEKIT_STACK_SIZE=6144
  • EFFECTS_STACK_SIZE=3072

Given the complexity of FastLED 3.11's channel management and the 25-frame backtrace, 8192 bytes (8KB) is appropriate for the driver task.


🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-06T13:45:03.067Z
Learning: For MoonLight on ESP32 at extreme loads (e.g., 49K channels @ 100 fps): Task watchdog can fire when AppDrivers/AppEffects process many nodes without yielding. Even with vTaskDelay(1) at loop end, the node iteration for-loops (PhysicalLayer::loopDrivers line 108-111, PhysicalLayer::loop line 73-75) can take >5 seconds without yielding, starving IDLE task (priority 0). Solution: add taskYIELD() or vTaskDelay(1) inside the node/layer iteration loops to give IDLE task chances to run and reset watchdog. Lowering task priority won't help since IDLE is already at 0 (lowest).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-28T18:06:23.868Z
Learning: ESP32-P4 observed lower headroom for httpd task stack in MoonLight: NodeManager rename (create new node + rebuild controls + remove old) can overflow httpd stack when onUpdate runs in httpd. Mitigation: defer only NodeManager rename heavy work to SvelteKit loop via a small pendingRenames queue; keep identity short-circuit in compareRecursive and equality-guarded controls updates. Increasing httpd stack_size for P4 builds is an optional, broader fix.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-25T10:45:58.923Z
Learning: In MoonLight ModuleDevices: Variable Length Arrays (VLAs) like `char buffer[packetSize]` on ESP32 task stack are unsafe because tasks have limited stack space (~8KB). When packetSize exceeds buffer size or stack limit, causes stack overflow and crashes (including WebSocket failures). Solution: use `char buffer[sizeof(UDPMessage)]` or static/heap allocation, and always validate packetSize before reading: `if (packetSize > sizeof(UDPMessage)) { deviceUDP.flush(); continue; }`.
</learning]>

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:13:55.631Z
Learning: In MoonLight effectTask (src/main.cpp): The memcpy(channelsE, channelsD) at line 137 happens AFTER releasing swapMutex, allowing driverTask to swap the channelsD/channelsE pointers mid-copy, causing buffer corruption. This specifically breaks effects like GameOfLife that use getRGB() to read previous-frame data for blending decisions. Other effects work because they only read current-frame data they just wrote. Fix: hold swapMutex during the entire memcpy operation to prevent pointer swap during copy.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-25T13:31:32.367Z
Learning: For ESP32 LED drivers using DMA, I2S, LCD, or PARLIO: always pin driver tasks to Core 1 (APP_CPU), not Core 0. WiFi/BT runs at priority ~23 on Core 0 and would constantly preempt lower-priority driver tasks, breaking LED timing requirements. ESP-IDF explicitly recommends display/DMA processing on Core 1 to avoid WiFi interference.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:33.598Z
Learning: In MoonLight GameOfLifeEffect (src/MoonLight/Nodes/Effects/E_MoonModules.h): The effect's nested loops (especially lines 257-323) iterate through all cells (e.g., 128×128 = 16,384) with nested neighbor checks (18 per cell = 294,912 operations) without yielding. This starves effectTask (priority 3) when higher-priority tasks like HTTP server (priority 5) run, causing the effect to disappear. UI actions (save, refresh) temporarily show one frame because HTTP completes and briefly releases CPU back to effectTask. Solution: Add taskYIELD() inside outer X loop to prevent task starvation. This is the same root cause as issue `#102` (Starsky glitches with many driver nodes).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T17:07:33.344Z
Learning: FastLED library does not support direct 4-byte-per-pixel (CRGBW) buffers. It always expects CRGB (3-byte) arrays and internally generates the W channel from RGB using .setRgbw() API. Casting a 4-byte RGBW buffer to CRGB* causes misalignment (W becomes next pixel's R). For MoonLight's FastLED driver with channelsPerLight=4: must either (1) repack RGB in-place before FastLED.show() then restore after, (2) use driver-specific channelsPerLight=3 layout, or (3) allocate temporary RGB-only buffer. Option 1 (in-place repacking) is best for ESP32-D0 memory constraints.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-25T13:34:40.097Z
Learning: For the MoonLight LED system: Effects task can run on Core 0 (PRO_CPU) since it only performs computation (calculating colors, writing to channels array) without timing-critical hardware interaction. WiFi preemption is acceptable for pure computation tasks. Only the driver task (using DMA, I2S, LCD, PARLIO) requires Core 1 (APP_CPU) for uninterrupted timing-critical hardware operations.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-26T14:59:22.091Z
Learning: In MoonModules/MoonLight PR `#114`: ModuleDevices deadlock caused by: (1) StatefulService.h line 204-205 bug - after 500ms mutex timeout, continues WITHOUT acquiring mutex, causing race conditions and JSON corruption. (2) receiveUDP() while-loop processes own UDP broadcasts via loopback, creating circular call chain: sendUDP→broadcast→receiveUDP→updateDevices→update→handlers→sendUDP. (3) updateDevices() holds _accessMutex during expensive sort/JSON operations with 15 devices, can exceed 500ms with nested calls. Solution: Fix StatefulService to wait forever or reboot on timeout; skip own broadcasts by checking message.name == ownHostname; optimize updateDevices to sort outside mutex.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T12:49:07.297Z
Learning: In MoonLight channel reordering architecture: Keep rgbwBufferMapping in driver task (not effect task) because: (1) Driver has spare CPU cycles waiting for DMA/I2S/Art-Net timing; effect task is CPU-bound and starvation-prone. (2) Contiguous RGB writes in effects are cache-friendly; scattered writes at final offsets are cache-hostile for non-RGB orders (GRB, BGR, etc.). (3) forEachLightIndex already handles sparse mapping iteration; moving reordering to effects adds complexity without benefit since drivers must send full frames anyway (can't skip iteration). (4) rgbwBufferMapping does brightness/gamma LUT + white extraction + reordering—moving just reordering creates code duplication. (5) Effects should express color intent (CRGB), not hardware details (offsetRed/Green/Blue). Driver-side transformation maintains separation of concerns and task load balance for scaling to 98K+ pixels.

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 5, 2026

@coderabbitai, I am in the process of replacing the FastLED RMT drivers with the new parallel IO drivers, but still on the RMT Drivers I get dramatic low performance of 2 FPS.

And I get this in the logging:

[ 30198][D][isr_esp32_idf5.hpp:355] detach_handler(): [fl_isr_esp32] Handler detached
[ 30678][D][isr_esp32_idf5.hpp:355] detach_handler(): [fl_isr_esp32] Handler detached
[ 31147][D][isr_esp32_idf5.hpp:355] detach_handler(): [fl_isr_esp32] Handler detached
[ 31626][D][isr_esp32_idf5.hpp:355] detach_handler(): [fl_isr_esp32] Handler detached
[ 32098][D][isr_esp32_idf5.hpp:355] detach_handler(): [fl_isr_esp32] Handler detached
[ 32568][D][isr_esp32_idf5.hpp:355] detach_handler(): [fl_isr_esp32] Handler detached
[ 33039][D][isr_esp32_idf5.hpp:355] detach_handler(): [fl_isr_esp32] Handler detached
[ 33509][D][isr_esp32_idf5.hpp:355] detach_handler(): [fl_isr_esp32] Handler detached
[ 33988][D][isr_esp32_idf5.hpp:355] detach_handler(): [fl_isr_esp32] Handler detached

Can you check what is going on?

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Examining D_FastLED.h driver implementation ==="
cat src/MoonLight/Nodes/Drivers/D_FastLED.h

echo ""
echo "=== Checking FastLED ISR handler detach code ==="
# The library is at .pio/libdeps/esp32-s3-n16r8v/FastLED
if [ -d ".pio/libdeps/esp32-s3-n16r8v/FastLED" ]; then
  echo "Found FastLED library, examining ISR code..."
  find .pio/libdeps/esp32-s3-n16r8v/FastLED -name "isr_esp32_idf5.hpp" -exec cat {} \; | sed -n '340,370p'
else
  echo "FastLED library not found in expected location"
fi

echo ""
echo "=== Searching for RMT-related code in FastLED ==="
if [ -d ".pio/libdeps/esp32-s3-n16r8v/FastLED" ]; then
  rg -n "detach_handler|Handler detached" .pio/libdeps/esp32-s3-n16r8v/FastLED/ -A2 -B2 | head -50
fi

echo ""
echo "=== Checking for FastLED show() calls in driver ==="
rg -n "FastLED.show|CFastLED::show" src/MoonLight/Nodes/Drivers/D_FastLED.h -A3 -B3

echo ""
echo "=== Looking for RMT driver configuration ==="
if [ -d ".pio/libdeps/esp32-s3-n16r8v/FastLED" ]; then
  find .pio/libdeps/esp32-s3-n16r8v/FastLED/src/platforms/esp -name "*rmt*" -type f | head -10
fi

Length of output: 31743


🌐 Web query:

FastLED 3.11 RMT driver ISR handler detached issue ESP32

💡 Result:

This “ISR handler detached” / RMT-ISR weirdness on ESP32 with FastLED 3.11 is almost always caused by two different RMT stacks getting initialized in the same firmware (legacy RMT APIs vs the newer IDF RMT/led_strip stack), so one side installs/uninstalls (detaches) the interrupt handler the other side expects.

Key points behind it:

  • In ESP-IDF, calling rmt_driver_install() installs a default RMT ISR, and you must not also register a generic ISR handler with rmt_isr_register() afterward (it will fail / conflict). [1]
  • FastLED explicitly warns that RMT4 and RMT5 cannot coexist in the same sketch, and that ESP-IDF can boot-crash if both attempt to initialize. [2]

What to do (practical fixes):

  1. Ensure only one RMT path is used
  • If you’re on Arduino-ESP32 core 3.x / ESP-IDF 5+, prefer RMT5 (FastLED’s default in that environment).
  • If you force RMT4 (FASTLED_RMT5=0), make sure nothing else in your build (including other LED libs, board “onboard RGB” helpers, or components using led_strip) is using the new RMT driver.
  1. Don’t mix FastLED with another RMT-based LED library at the same time
  • E.g., NeoPixelBus (RMT backend), Adafruit NeoPixel (RMT backend on ESP32), or board packages that auto-init an onboard WS2812 via led_strip can trigger the conflict that leads to ISR detach behavior.
  1. If you just need stability under Wi‑Fi/interrupt load, consider switching away from RMT (FastLED ESP32 SPI / I2S), rather than trying to run mixed RMT stacks.

Sources: ESP-IDF RMT interrupt handler restrictions [1], FastLED’s “RMT4 and RMT5 cannot coexist” warning and how to force RMT5/RMT4 [2].

[1] (docs.espressif.com)
[2] (github.com)

Citations:


🌐 Web query:

FastLED commit 8b4fd586ba38a6c3b67ce94f78d68e77aa505fa5 changes RMT

💡 Result:

I wasn’t able to retrieve the actual diff / commit message for commit 8b4fd586ba38a6c3b67ce94f78d68e77aa505fa5 from the public web index (the commit page/patch didn’t show up in search results), so I can’t say exactly what that specific hash changed in the RMT code.

What I can confirm from FastLED’s current docs/issues is what the RMT-related change-set in this era is about:

  • FastLED now has two ESP32 RMT paths: a custom RMT4 (legacy) driver and an RMT5 driver that wraps Espressif’s led_strip component for ESP-IDF 5.x+ toolchains. FastLED generally selects RMT5 by default on IDF 5.x+, and RMT4 on older cores/IDF 4.x. [1]
  • You can force RMT4 even on IDF 5.x+ by compiling with FASTLED_RMT5=0 (e.g., PlatformIO build flag -D FASTLED_RMT5=0). [1]
  • RMT4 and RMT5 can’t coexist in the same binary on some ESP32/Arduino-core combinations: mixing “legacy” RMT and “driver_ng” (new) can trigger a boot-time abort like CONFLICT! driver_ng is not allowed to be used with the legacy driver. This is a known failure mode and is why projects often must choose one at compile time. [1][2]
  • There’s also ongoing discussion that RMT5 is more prone to Wi‑Fi/interrupt jitter flicker than the old RMT4 approach, because RMT4 used a tighter low-level ping‑pong refill style. [3]

If you paste the commit’s diff (or link the commit URL), I can tell you precisely what 8b4fd586… changed (files, symbols, and behavior) and how it affects RMT4 vs RMT5 selection/build flags.

Sources: [1] FastLED repo README/docs (ESP32 RMT driver configuration) (github.com), [2] FastLED issue #1656 (driver_ng vs legacy conflict abort) (github.com), [3] FastLED issue #2082 (RMT5 flicker under Wi‑Fi; RMT4 vs RMT5 background) (github.com)

Citations:


🏁 Script executed:

#!/bin/bash

echo "=== Checking platformio.ini for RMT-related build flags ==="
rg -n "FASTLED_RMT|RMT5|RMT4" platformio.ini

echo ""
echo "=== Checking for other RMT-related definitions ==="
rg -n "define.*RMT" src/ -g "*.h" -g "*.cpp" | head -20

echo ""
echo "=== Checking ESP-IDF version from platformio.ini ==="
rg -n "platform.*=.*espressif32" platformio.ini

echo ""
echo "=== Checking Arduino framework version ==="
rg -n "platform_packages|framework" platformio.ini | head -10

Length of output: 3685


🏁 Script executed:

#!/bin/bash

echo "=== Checking for other LED libraries or RMT usage ==="
rg -n "NeoPixel|Adafruit|led_strip|WS281|SK6812" platformio.ini

echo ""
echo "=== Checking lib_deps in platformio.ini for conflicting libraries ==="
rg -n "lib_deps" platformio.ini -A 20 | head -60

echo ""
echo "=== Checking for board RGB LED initialization ==="
rg -n "BOARD_HAS_.*LED|RGB_LED|onboard" src/ -g "*.cpp" -g "*.h" | head -20

echo ""
echo "=== Checking the active platform details ==="
grep -n "^platform = " platformio.ini | tail -1

echo ""
echo "=== Checking if D_FastLED.h shows RMT5 flag ==="
grep -n "usesRMT5\|FASTLED_RMT5" src/MoonLight/Nodes/Drivers/D_FastLED.h

Length of output: 4607


@ewowi, I found the root cause! The "Handler detached" log and 2 FPS performance are caused by FastLED 3.11's default RMT5 driver on ESP-IDF 5.x+.

The Problem

On ESP-IDF 5.x+ (your platform 55.03.35), FastLED 3.11 defaults to RMT5, which wraps Espressif's led_strip component. However, RMT5 has known issues with ISR handler lifecycle management—causing the repeated detach/reattach cycles you're seeing every ~500ms.

Your platformio.ini (line 52) uses:

platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip

This is ESP-IDF v5.5.x / Arduino v3.3.5, so FastLED automatically selects RMT5.

Why It's Breaking

RMT5 is more prone to Wi-Fi/interrupt jitter than the legacy RMT4 implementation, and the repeated ISR detachment suggests FastLED's RMT5 driver is continuously deallocating/reallocating resources—destroying performance.

Solution: Force Legacy RMT4 Driver

Add this build flag to the [moonlight] section of platformio.ini (around line 148):

[moonlight]
build_flags = 
  -D FT_MOONLIGHT=1
  -D FT_MONITOR=1
  -D EFFECTS_STACK_SIZE=3072
  -D DRIVERS_STACK_SIZE=4096
  -D FASTLED_RMT5=0  ; ← Force legacy RMT4 driver (more stable than RMT5 on IDF 5.x+)
  
  ; FastLED pre-compiled settings:
  -D ML_CHIPSET=WS2812B
  -D ML_COLOR_ORDER=GRB

This forces FastLED to use the legacy RMT4 driver instead of RMT5, avoiding the ISR handler issues.

Alternative: Switch to Parallel I/O

Since you mentioned transitioning to parallel I/O drivers, that's an even better long-term solution—parallel I/O doesn't use RMT at all, avoiding these conflicts entirely.


Note: You may also want to increase DRIVERS_STACK_SIZE from 4096 to 8192 as mentioned in my earlier response, since FastLED 3.11 has deeper call stacks than 3.10.3.


✏️ Learnings added
Learnt from: ewowi
URL: https://github.com/MoonModules/MoonLight/pull/121

Timestamp: 2026-02-05T13:20:45.988Z
Learning: In MoonLight with FastLED 3.11 on ESP-IDF 5.x+ (Arduino core 3.x): FastLED defaults to RMT5 driver (wraps Espressif's led_strip component), which has known ISR handler lifecycle issues causing repeated "Handler detached" messages every ~500ms and severe performance degradation (e.g., 2 FPS). RMT5 is more prone to Wi-Fi/interrupt jitter than legacy RMT4. Solution: force legacy RMT4 by adding `-D FASTLED_RMT5=0` to build flags in platformio.ini, or switch to FastLED's parallel I/O drivers which don't use RMT. RMT4 and RMT5 cannot coexist in the same binary.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-06T13:45:03.067Z
Learning: For MoonLight on ESP32 at extreme loads (e.g., 49K channels @ 100 fps): Task watchdog can fire when AppDrivers/AppEffects process many nodes without yielding. Even with vTaskDelay(1) at loop end, the node iteration for-loops (PhysicalLayer::loopDrivers line 108-111, PhysicalLayer::loop line 73-75) can take >5 seconds without yielding, starving IDLE task (priority 0). Solution: add taskYIELD() or vTaskDelay(1) inside the node/layer iteration loops to give IDLE task chances to run and reset watchdog. Lowering task priority won't help since IDLE is already at 0 (lowest).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:33.598Z
Learning: In MoonLight GameOfLifeEffect (src/MoonLight/Nodes/Effects/E_MoonModules.h): The effect's nested loops (especially lines 257-323) iterate through all cells (e.g., 128×128 = 16,384) with nested neighbor checks (18 per cell = 294,912 operations) without yielding. This starves effectTask (priority 3) when higher-priority tasks like HTTP server (priority 5) run, causing the effect to disappear. UI actions (save, refresh) temporarily show one frame because HTTP completes and briefly releases CPU back to effectTask. Solution: Add taskYIELD() inside outer X loop to prevent task starvation. This is the same root cause as issue `#102` (Starsky glitches with many driver nodes).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-25T13:31:32.367Z
Learning: For ESP32 LED drivers using DMA, I2S, LCD, or PARLIO: always pin driver tasks to Core 1 (APP_CPU), not Core 0. WiFi/BT runs at priority ~23 on Core 0 and would constantly preempt lower-priority driver tasks, breaking LED timing requirements. ESP-IDF explicitly recommends display/DMA processing on Core 1 to avoid WiFi interference.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-25T13:34:40.097Z
Learning: For the MoonLight LED system: Effects task can run on Core 0 (PRO_CPU) since it only performs computation (calculating colors, writing to channels array) without timing-critical hardware interaction. WiFi preemption is acceptable for pure computation tasks. Only the driver task (using DMA, I2S, LCD, PARLIO) requires Core 1 (APP_CPU) for uninterrupted timing-critical hardware operations.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T12:49:07.297Z
Learning: In MoonLight channel reordering architecture: Keep rgbwBufferMapping in driver task (not effect task) because: (1) Driver has spare CPU cycles waiting for DMA/I2S/Art-Net timing; effect task is CPU-bound and starvation-prone. (2) Contiguous RGB writes in effects are cache-friendly; scattered writes at final offsets are cache-hostile for non-RGB orders (GRB, BGR, etc.). (3) forEachLightIndex already handles sparse mapping iteration; moving reordering to effects adds complexity without benefit since drivers must send full frames anyway (can't skip iteration). (4) rgbwBufferMapping does brightness/gamma LUT + white extraction + reordering—moving just reordering creates code duplication. (5) Effects should express color intent (CRGB), not hardware details (offsetRed/Green/Blue). Driver-side transformation maintains separation of concerns and task load balance for scaling to 98K+ pixels.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-28T18:06:23.868Z
Learning: ESP32-P4 observed lower headroom for httpd task stack in MoonLight: NodeManager rename (create new node + rebuild controls + remove old) can overflow httpd stack when onUpdate runs in httpd. Mitigation: defer only NodeManager rename heavy work to SvelteKit loop via a small pendingRenames queue; keep identity short-circuit in compareRecursive and equality-guarded controls updates. Increasing httpd stack_size for P4 builds is an optional, broader fix.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-08T11:39:52.878Z
Learning: In MoonLight WebSocket monitor: When browser tabs are hidden, browsers throttle JavaScript (including WebSocket onmessage handlers) but keep TCP connections open. Monitor data (48KB frames every 20-300ms) continues arriving at TCP level but isn't processed. This fills ESP32's httpd_ws_send_frame_async() buffers, causing ESP_FAIL. When tab becomes visible, hundreds of queued frames overwhelm the JavaScript main thread, freezing the UI. Solution: use Page Visibility API (client_visibility event) to stop emitting monitor data to hidden clients, preventing buffer overflow and backlog buildup.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR `#84`: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-12T15:53:21.519Z
Learning: In MoonLight's FreeRTOS environment: `taskYIELD()` does not help with Idle Watchdog issues because it only yields to tasks with equal or higher priority. Since Effect Task and Driver Task run at priority 3 while IDLE task runs at priority 0, `taskYIELD()` will never allow IDLE to run. Only `vTaskDelay(1)` works because it blocks the calling task and allows the scheduler to run lower-priority tasks like IDLE, which can then reset the watchdog timer.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:13:55.631Z
Learning: In MoonLight effectTask (src/main.cpp): The memcpy(channelsE, channelsD) at line 137 happens AFTER releasing swapMutex, allowing driverTask to swap the channelsD/channelsE pointers mid-copy, causing buffer corruption. This specifically breaks effects like GameOfLife that use getRGB() to read previous-frame data for blending decisions. Other effects work because they only read current-frame data they just wrote. Fix: hold swapMutex during the entire memcpy operation to prevent pointer swap during copy.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T17:07:33.344Z
Learning: FastLED library does not support direct 4-byte-per-pixel (CRGBW) buffers. It always expects CRGB (3-byte) arrays and internally generates the W channel from RGB using .setRgbw() API. Casting a 4-byte RGBW buffer to CRGB* causes misalignment (W becomes next pixel's R). For MoonLight's FastLED driver with channelsPerLight=4: must either (1) repack RGB in-place before FastLED.show() then restore after, (2) use driver-specific channelsPerLight=3 layout, or (3) allocate temporary RGB-only buffer. Option 1 (in-place repacking) is best for ESP32-D0 memory constraints.

- pio.ini: increase httpd stack 5120->6144 and drivers stack from 4096 to 5120
- FastLED: remove gpio 11 for esp32-do (part of spi flash)
Copy link

@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: 2

🤖 Fix all issues with AI agents
In `@src/MoonLight/Nodes/Drivers/D_FastLED.h`:
- Around line 127-131: The FastLED path misinterprets MoonLight's
4-byte-per-light buffers (layerP.lights.channelsD) as CRGB (3-byte) causing
misaligned pixels; update the FastLED setup around fl::ChannelConfig /
fl::makeTimingConfig usage to first check layerP.lights.header.channelsPerLight
and only use CRGB/fastled when it's 3, otherwise perform RGB repacking like
parlio.cpp (convert 4-byte RGBW to contiguous 3-byte RGB for the LEDs, call
FastLED.show(), then restore or use a temporary buffer), ensuring startLed
indexing into CRGB uses the repacked buffer so the white channel no longer
overwrites the next pixel.
- Around line 133-145: The FastLED exclusive driver is being set after creating
the channel which doesn't affect the already-instantiated engine; move the
FastLED.setExclusiveDriver(...) call to before creating the channel and
instantiate the channel with the explicit engine templates (use
fl::Channel::create<fl::ChannelEngineSpi>(config) on targets that need SPI and
fl::Channel::create<fl::ChannelEnginePARLIO>(config) for PARLIO) so the correct
engine is chosen at creation time; then call FastLED.addLedChannel(channel) as
before.
🧹 Nitpick comments (2)
src/MoonLight/Nodes/Drivers/D_FastLED.h (2)

16-22: Avoid relying on transitive includes for channel APIs.
These commented includes are either dead code or a hint that explicit channel headers are needed. If you intend to use explicit engines (SPI/PARLIO), re‑enable the required headers; otherwise delete the commented block to reduce clutter.


151-156: Cache driver infos once to avoid repeated allocations.
FastLED.getDriverInfos() is called on every loop iteration; if it allocates or builds a temporary list, this is wasteful. Cache the result once outside the loop and iterate it.

Possible cleanup
-    for (size_t i = 0; i < FastLED.getDriverCount(); i++) {
-      auto drivers = FastLED.getDriverInfos();
-      auto& info = drivers[i];
+    auto drivers = FastLED.getDriverInfos();
+    for (size_t i = 0; i < drivers.size(); i++) {
+      auto& info = drivers[i];
       EXT_LOGD(ML_TAG, "Driver: %s, Priority: %d, Enabled: %s", info.name.c_str(), info.priority, info.enabled ? "yes" : "no");
     }

Comment on lines 127 to 131
//https://github.com/FastLED/FastLED/blob/master/src/fl/channels/README.md

#if CONFIG_IDF_TARGET_ESP32S2
case 0:
FastLED.addLeds<ML_CHIPSET, 0 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 1:
FastLED.addLeds<ML_CHIPSET, 1 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 2:
FastLED.addLeds<ML_CHIPSET, 2 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 3:
FastLED.addLeds<ML_CHIPSET, 3 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 4:
FastLED.addLeds<ML_CHIPSET, 4 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 5:
FastLED.addLeds<ML_CHIPSET, 5 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 6:
FastLED.addLeds<ML_CHIPSET, 6 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 7:
FastLED.addLeds<ML_CHIPSET, 7 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 8:
FastLED.addLeds<ML_CHIPSET, 8 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 9:
FastLED.addLeds<ML_CHIPSET, 9 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 10:
FastLED.addLeds<ML_CHIPSET, 10 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 11:
FastLED.addLeds<ML_CHIPSET, 11 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 12:
FastLED.addLeds<ML_CHIPSET, 12 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 13:
FastLED.addLeds<ML_CHIPSET, 13 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 14:
FastLED.addLeds<ML_CHIPSET, 14 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 15:
FastLED.addLeds<ML_CHIPSET, 15 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 16:
FastLED.addLeds<ML_CHIPSET, 16 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 17:
FastLED.addLeds<ML_CHIPSET, 17 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 18:
FastLED.addLeds<ML_CHIPSET, 18 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
#if !ARDUINO_USB_CDC_ON_BOOT
// 19 + 20 = USB HWCDC. reserved for USB port when ARDUINO_USB_CDC_ON_BOOT=1
case 19:
FastLED.addLeds<ML_CHIPSET, 19 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 20:
FastLED.addLeds<ML_CHIPSET, 20 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
#endif
case 21:
FastLED.addLeds<ML_CHIPSET, 21 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
// 22 to 32: not connected, or reserved for SPI FLASH
// case 22: FastLED.addLeds<ML_CHIPSET, 22 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
// case 23: FastLED.addLeds<ML_CHIPSET, 23 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
// case 24: FastLED.addLeds<ML_CHIPSET, 24 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
// case 25: FastLED.addLeds<ML_CHIPSET, 25 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
#if !defined(BOARD_HAS_PSRAM)
// 26-32 = reserved for PSRAM
case 26:
FastLED.addLeds<ML_CHIPSET, 26 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
// case 27: FastLED.addLeds<ML_CHIPSET, 27 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
// case 28: FastLED.addLeds<ML_CHIPSET, 28 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
// case 29: FastLED.addLeds<ML_CHIPSET, 29 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
// case 30: FastLED.addLeds<ML_CHIPSET, 30 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
// case 31: FastLED.addLeds<ML_CHIPSET, 31 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
// case 32: FastLED.addLeds<ML_CHIPSET, 32 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
#endif
case 33:
FastLED.addLeds<ML_CHIPSET, 33 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 34:
FastLED.addLeds<ML_CHIPSET, 34 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 35:
FastLED.addLeds<ML_CHIPSET, 35 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 36:
FastLED.addLeds<ML_CHIPSET, 36 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 37:
FastLED.addLeds<ML_CHIPSET, 37 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 38:
FastLED.addLeds<ML_CHIPSET, 38 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 39:
FastLED.addLeds<ML_CHIPSET, 39 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 40:
FastLED.addLeds<ML_CHIPSET, 40 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 41:
FastLED.addLeds<ML_CHIPSET, 41 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 42:
FastLED.addLeds<ML_CHIPSET, 42 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 43:
FastLED.addLeds<ML_CHIPSET, 43 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 44:
FastLED.addLeds<ML_CHIPSET, 44 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
case 45:
FastLED.addLeds<ML_CHIPSET, 45 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL;
break;
// 46 input-only
// case 46: FastLED.addLeds<ML_CHIPSET, 46 COLOR_ORDER_ARG>(leds, startLed, nrOfLights).setCorrection(TypicalLEDStrip) RGBW_CALL; break;
#endif // CONFIG_IDF_TARGET_ESP32S2
fl::ChipsetTimingConfig timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
fl::ChannelConfig config(pins[pinIndex], timing, fl::span<CRGB>(&leds[startLed], layerP.ledsPerPin[pinIndex]), GRB);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "D_FastLED.h" -type f

Repository: MoonModules/MoonLight

Length of output: 106


🏁 Script executed:

cat -n src/MoonLight/Nodes/Drivers/D_FastLED.h | head -150

Repository: MoonModules/MoonLight

Length of output: 6778


🏁 Script executed:

cat -n src/MoonLight/Nodes/Drivers/D_FastLED.h | sed -n '115,150p'

Repository: MoonModules/MoonLight

Length of output: 1722


🏁 Script executed:

rg "channelsPerLight|channelsD|RGBW" src/MoonLight/Nodes/Drivers/D_FastLED.h -A 3 -B 3

Repository: MoonModules/MoonLight

Length of output: 263


🏁 Script executed:

rg "channelsPerLight" src/MoonLight/Layers/ -l

Repository: MoonModules/MoonLight

Length of output: 214


🏁 Script executed:

grep -n "channelsPerLight" src/MoonLight/Layers/VirtualLayer.h | head -20

Repository: MoonModules/MoonLight

Length of output: 2498


🏁 Script executed:

grep -n "channelsD\|offsetRGBW" src/MoonLight/Layers/VirtualLayer.h | head -20

Repository: MoonModules/MoonLight

Length of output: 2574


🏁 Script executed:

sed -n '1,100p' src/MoonLight/Layers/VirtualLayer.h | cat -n

Repository: MoonModules/MoonLight

Length of output: 3467


🏁 Script executed:

grep -n "channelsPerLight == 3\|channelsPerLight == 4\|if.*RGB\|if.*RGBW" src/MoonLight/Nodes/Drivers/D_FastLED.h

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

grep -rn "repack\|FastLED.*RGB\|FastLED.*RGBW" src/MoonLight/Nodes/Drivers/ --include="*.h" --include="*.cpp"

Repository: MoonModules/MoonLight

Length of output: 1253


🏁 Script executed:

sed -n '119,150p' src/MoonLight/Nodes/Drivers/D_FastLED.h | cat -n

Repository: MoonModules/MoonLight

Length of output: 1596


🏁 Script executed:

grep -rn "channelsPerLight\s*=\s*4\|RGBW.*FastLED" src/ --include="*.h" --include="*.cpp"

Repository: MoonModules/MoonLight

Length of output: 253


🏁 Script executed:

grep -rn "FastLED\|D_FastLED" src/ --include="*.cpp" | grep -i "driver\|init" | head -20

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

find src -name "*.h" -exec grep -l "ML_CHANNELS_PER_LIGHT\|CHANNELS_PER_LIGHT" {} \;

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

sed -n '445,475p' src/MoonBase/Nodes.cpp

Repository: MoonModules/MoonLight

Length of output: 925


🏁 Script executed:

sed -n '450,470p' src/MoonBase/Nodes.cpp | cat -n

Repository: MoonModules/MoonLight

Length of output: 801


Gate FastLED to RGB-only devices or add buffer repacking for RGBW.

The cast (CRGB*)layerP.lights.channelsD misaligns RGBW data: MoonLight stores 4-byte RGBW buffers (R, G, B, W per light), but CRGB expects 3-byte elements. When startLed is calculated as light count and used to index CRGB pointers, the arithmetic is off—the white channel overwrites the next pixel's red.

MoonLight supports multiple RGBW presets (RGBW, GRBW, WRGB with channelsPerLight=4), so this path can be reached. Either add a runtime guard if (layerP.lights.header.channelsPerLight == 3) to skip FastLED for 4-channel devices, or repack the RGB data in-place before FastLED.show() then restore, following the pattern used in parlio.cpp.

🤖 Prompt for AI Agents
In `@src/MoonLight/Nodes/Drivers/D_FastLED.h` around lines 127 - 131, The FastLED
path misinterprets MoonLight's 4-byte-per-light buffers
(layerP.lights.channelsD) as CRGB (3-byte) causing misaligned pixels; update the
FastLED setup around fl::ChannelConfig / fl::makeTimingConfig usage to first
check layerP.lights.header.channelsPerLight and only use CRGB/fastled when it's
3, otherwise perform RGB repacking like parlio.cpp (convert 4-byte RGBW to
contiguous 3-byte RGB for the LEDs, call FastLED.show(), then restore or use a
temporary buffer), ensuring startLed indexing into CRGB uses the repacked buffer
so the white channel no longer overwrites the next pixel.

Comment on lines 133 to 145
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
// fl::ChannelPtr channel = fl::Channel::create<fl::ChannelEngineSpi>(config);
fl::ChannelPtr channel = fl::Channel::create(config);
FastLED.setExclusiveDriver("SPI");
#else
// auto channel = fl::Channel::create<fl::ChannelEnginePARLIO>(config);
fl::ChannelPtr channel = fl::Channel::create(config);
FastLED.setExclusiveDriver("PARLIO");
#endif

default:
EXT_LOGW(ML_TAG, "FastLEDPin assignment: pin not supported %d", pins[pinIndex]);
} // switch pins[pinIndex]
// Register with FastLED
FastLED.addLedChannel(channel);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the full context of the D_FastLED.h file around the flagged lines
fd D_FastLED.h

Repository: MoonModules/MoonLight

Length of output: 104


🏁 Script executed:

# Search for FastLED Channel::create calls and setExclusiveDriver usage in the codebase
rg "Channel::create|setExclusiveDriver" -t cpp -t h -B 2 -A 2

Repository: MoonModules/MoonLight

Length of output: 1042


🌐 Web query:

FastLED 3.11 Channel::create exclusive driver behavior order of operations

💡 Result:

In FastLED’s new ESP32 “channels” architecture (3.10.x and newer, including 3.11), driver choice is evaluated at channel/engine creation time and (per the FastLED docs) any driver enable/exclusive changes are applied on the next FastLED.show(). So the practical order-of-operations is: configure driver policy → ensure channels/engines get (re)created under that policy → show(). [1]

What “exclusive driver” does

  • FastLED.setExclusiveDriver("RMT") (or "SPI", "PARLIO", etc.) disables all other drivers (including future ones) and prevents the normal “try highest priority, then fall back” selection behavior. [1]
  • If the exclusive driver can’t allocate resources (channels/hosts/memory), you’ll generally see repeated warnings rather than a silent fallback to another driver (because you told FastLED not to use any other driver). This matches real-world reports when forcing drivers during channel creation. [2]

The order that matters (what to do in your sketch)

For deterministic behavior with Channel::create / channel allocation:

  1. Call FastLED.setExclusiveDriver(...) in setup() before the first FastLED.show() (best practice).
  2. Then do your FastLED.addLeds(...) registrations.
  3. Only then call FastLED.show() (first show is where the driver selection/allocation is guaranteed to be “locked in” under the exclusive policy). [1]

You can call setExclusiveDriver() after addLeds() as long as it’s still before the first show(), because the docs state the change takes effect on the next show(). [1]

If you change exclusivity after channels already exist

Because the policy applies on the “next show()” [1], switching exclusivity mid-run may or may not force a full teardown/recreate of already-created channel engines (that detail is implementation-specific and can vary by driver). If you need a hard guarantee, the reliable approach is: set exclusivity before the first show().

Sources: FastLED runtime driver control / exclusive driver semantics and “next show()” behavior [1]; community report of forcing a driver via setExclusiveDriver() and observing channel-engine allocation warnings/behavior [2].


Move FastLED.setExclusiveDriver() before channel creation and use explicit engine templates.

In FastLED 3.11, driver selection is locked at channel creation time. Setting the exclusive driver after instantiation does not retroactively change the already-created channel engine—it only affects the policy for the next show(). To guarantee SPI or PARLIO is selected from the start (avoiding the RMT5 default on ESP-IDF 5.x+), call setExclusiveDriver() first, then create the channel with an explicit engine template.

Suggested change
 `#if` CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
-        fl::ChannelPtr channel = fl::Channel::create(config);
-        FastLED.setExclusiveDriver("SPI");
+        FastLED.setExclusiveDriver("SPI");
+        fl::ChannelPtr channel = fl::Channel::create<fl::ChannelEngineSpi>(config);
 `#else`
-        fl::ChannelPtr channel = fl::Channel::create(config);
-        FastLED.setExclusiveDriver("PARLIO");
+        FastLED.setExclusiveDriver("PARLIO");
+        fl::ChannelPtr channel = fl::Channel::create<fl::ChannelEnginePARLIO>(config);
 `#endif`
📝 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 CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
// fl::ChannelPtr channel = fl::Channel::create<fl::ChannelEngineSpi>(config);
fl::ChannelPtr channel = fl::Channel::create(config);
FastLED.setExclusiveDriver("SPI");
#else
// auto channel = fl::Channel::create<fl::ChannelEnginePARLIO>(config);
fl::ChannelPtr channel = fl::Channel::create(config);
FastLED.setExclusiveDriver("PARLIO");
#endif
default:
EXT_LOGW(ML_TAG, "FastLEDPin assignment: pin not supported %d", pins[pinIndex]);
} // switch pins[pinIndex]
// Register with FastLED
FastLED.addLedChannel(channel);
`#if` CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
// fl::ChannelPtr channel = fl::Channel::create<fl::ChannelEngineSpi>(config);
FastLED.setExclusiveDriver("SPI");
fl::ChannelPtr channel = fl::Channel::create<fl::ChannelEngineSpi>(config);
`#else`
// auto channel = fl::Channel::create<fl::ChannelEnginePARLIO>(config);
FastLED.setExclusiveDriver("PARLIO");
fl::ChannelPtr channel = fl::Channel::create<fl::ChannelEnginePARLIO>(config);
`#endif`
// Register with FastLED
FastLED.addLedChannel(channel);
🤖 Prompt for AI Agents
In `@src/MoonLight/Nodes/Drivers/D_FastLED.h` around lines 133 - 145, The FastLED
exclusive driver is being set after creating the channel which doesn't affect
the already-instantiated engine; move the FastLED.setExclusiveDriver(...) call
to before creating the channel and instantiate the channel with the explicit
engine templates (use fl::Channel::create<fl::ChannelEngineSpi>(config) on
targets that need SPI and fl::Channel::create<fl::ChannelEnginePARLIO>(config)
for PARLIO) so the correct engine is chosen at creation time; then call
FastLED.addLedChannel(channel) as before.

pio.ini / esp32-S3.ini / D-FastLED.h: cleanup old FastLED directives, include latest FastLED

Layouts: correct CarLightsLayout
Copy link

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
platformio.ini (1)

148-156: ⚠️ Potential issue | 🟠 Major

Missing -D FASTLED_RMT5=0 build flag to prevent RMT5 ISR issues.

Per known issues with FastLED 3.11 on ESP-IDF 5.x, the RMT5 driver (default) causes repeated "Handler detached" messages and ~2 FPS performance. While the code now uses setExclusiveDriver("I2S") / "PARLIO", adding -D FASTLED_RMT5=0 as a belt-and-suspenders measure ensures RMT5 is compiled out entirely, preventing any accidental fallback.

Proposed addition
 [moonlight]
 build_flags = 
   -D FT_MOONLIGHT=1
   -D FT_MONITOR=1
   -D EFFECTS_STACK_SIZE=3072 ; psramFound() ? 4 * 1024 : 3 * 1024
   -D DRIVERS_STACK_SIZE=8192 ;  FastLED 3.11 needs more stack
+  -D FASTLED_RMT5=0 ; Force legacy RMT4, avoid RMT5 ISR lifecycle issues on ESP-IDF 5.x

Based on learnings: "FastLED defaults to RMT5 driver... which has known ISR handler lifecycle issues causing repeated 'Handler detached' messages every ~500ms and severe performance degradation (e.g., 2 FPS). Solution: force legacy RMT4 by adding -D FASTLED_RMT5=0 to build flags in platformio.ini."

🤖 Fix all issues with AI agents
In `@platformio.ini`:
- Line 152: The DRIVERS_STACK_SIZE value is a typo (4196) and must be increased
to 8192 to avoid stack canary crashes with deeper FastLED 3.11 call chains;
update the DRIVERS_STACK_SIZE definition (replace DRIVERS_STACK_SIZE=4196) to
DRIVERS_STACK_SIZE=8192 in the platform configuration and update the adjacent
inline comment to reflect the 8KB size, then rebuild and run the AppDrivers task
to verify the stack-related crash is resolved.

In `@src/MoonLight/Nodes/Drivers/D_FastLED.h`:
- Around line 106-110: The FastLED driver name "I2S" is invalid and will fail at
runtime; update calls to FastLED.setExclusiveDriver(...) (and any
ChannelOptions.mAffinity uses) to use a supported driver name — replace "I2S"
with "PARLIO" for the ESP32/ESP32S2/ESP32S3 branches (and optionally the other
branches for consistency) so the calls in the D_FastLED.h code use a valid
driver ("PARLIO", "SPI", or "RMT") with "PARLIO" recommended to avoid RMT ISR
issues.
🧹 Nitpick comments (4)
platformio.ini (1)

156-156: Pinning to a commit hash on master is fragile for production.

Using a floating commit on master (f413fee...) means there's no official release guaranteeing API stability. This is fine for development/testing, but consider pinning to a tagged release once FastLED 3.11 is officially released.

src/MoonLight/Nodes/Drivers/D_FastLED.h (3)

140-145: getDriverInfos() is fetched on every loop iteration instead of once.

Minor inefficiency: drivers is re-fetched inside the for body on each iteration. Pull it out before the loop.

Proposed fix
      // Check which drivers are available
+     auto drivers = FastLED.getDriverInfos();
      for (size_t i = 0; i < FastLED.getDriverCount(); i++) {
-       auto drivers = FastLED.getDriverInfos();
        auto& info = drivers[i];
        EXT_LOGD(ML_TAG, "Driver: %s, Priority: %d, Enabled: %s", info.name.c_str(), info.priority, info.enabled ? "yes" : "no");
      }

16-22: Remove commented-out includes — they add noise without value.

These commented-out #include directives for channel/engine headers are dead code. If they're kept as documentation of which headers exist, a brief comment explaining why they're not needed would be more helpful.


131-136: Debug logging inside the per-pin loop may be noisy in production.

The controller-walk logging on every pin iteration is useful during bringup but will spam logs in normal operation. Consider guarding with VERBOSE level or removing once the channel API is validated.

; -D ML_CHIPSET=SK6812 ; 🚧
-D ML_COLOR_ORDER=GRB ; define your color order here if needed (why WS2812 has GRB instead of RGB?) Only for FastLED, PD software configurable
; -D ML_RGBW=1 ; define whether to enable RGBW (1 = yes, 0 = no) ... could be derived from ML_CHIPSET???
-D DRIVERS_STACK_SIZE=4196 ; psramFound() ? 4 * 1024 : 3 * 1024
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

DRIVERS_STACK_SIZE=4196 looks like a typo — should be 8192.

The PR discussion identifies 4096 as insufficient for FastLED 3.11's deeper call chains (stack canary crash). 4196 is only 100 bytes more — almost certainly a typo rather than an intentional increase. The recommended fix from the crash analysis is 8192 (8 KB).

Proposed fix
-  -D DRIVERS_STACK_SIZE=4196 ;  psramFound() ? 4 * 1024 : 3 * 1024
+  -D DRIVERS_STACK_SIZE=8192 ;  FastLED 3.11 needs more stack (was 4096, see PR `#121` crash analysis)

Based on learnings: "FastLED 3.11 introduces deeper call chains and additional allocations (ChannelEngineSpi, channel creation/acquire, beginTransmission/batchedTransmission, logging/formatting). The project's AppDrivers task was built with DRIVERS_STACK_SIZE=4096 (4KB) in platformio.ini, which is insufficient."

📝 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
-D DRIVERS_STACK_SIZE=4196 ; psramFound() ? 4 * 1024 : 3 * 1024
-D DRIVERS_STACK_SIZE=8192 ; FastLED 3.11 needs more stack (was 4096, see PR `#121` crash analysis)
🤖 Prompt for AI Agents
In `@platformio.ini` at line 152, The DRIVERS_STACK_SIZE value is a typo (4196)
and must be increased to 8192 to avoid stack canary crashes with deeper FastLED
3.11 call chains; update the DRIVERS_STACK_SIZE definition (replace
DRIVERS_STACK_SIZE=4196) to DRIVERS_STACK_SIZE=8192 in the platform
configuration and update the adjacent inline comment to reflect the 8KB size,
then rebuild and run the AppDrivers task to verify the stack-related crash is
resolved.

Comment on lines +102 to +110
// https://github.com/FastLED/FastLED/blob/master/src/fl/channels/README.md
fl::ChipsetTimingConfig timing = fl::makeTimingConfig<fl::TIMING_WS2812_800KHZ>();
CRGB* leds = (CRGB*)layerP.lights.channelsD;
uint16_t startLed = 0;
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
FastLED.setExclusiveDriver("I2S");
#else
FastLED.setExclusiveDriver("PARLIO");
#endif
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "chipSet|colorOrder|ML_CHIPSET|ML_COLOR_ORDER" --type=h --type=cpp -g '!node_modules' -C2

Repository: MoonModules/MoonLight

Length of output: 820


🏁 Script executed:

sed -n '95,130p' src/MoonLight/Nodes/Drivers/D_FastLED.h

Repository: MoonModules/MoonLight

Length of output: 1470


Hardcoded WS2812 timing and GRB color order removes previously available configurability.

The code has hardcoded fl::TIMING_WS2812_800KHZ (line 103) and GRB (line 127). Evidence shows these were previously configurable: UI controls for colorOrder, clockPin, and latchPin exist but are commented out in src/MoonBase/Nodes.cpp lines 183-185, and the prior implementation (line 131 comment) supported configurable chipsets and color orders.

This change prevents non-WS2812 chipsets (APA102/SK9822 with clock signals, WS2811 400KHz variants) and non-GRB color orders (RGB, BRG, etc.) from being used. If this is an intentional simplification, uncomment and document the decision. Otherwise, restore configurability via the UI.

Comment on lines +106 to +110
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
FastLED.setExclusiveDriver("I2S");
#else
FastLED.setExclusiveDriver("PARLIO");
#endif
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

FastLED 3.11 setExclusiveDriver API valid driver names documentation

💡 Result:

In FastLED’s runtime driver-control API (introduced in v3.10.x and present in current FastLED), FastLED.setExclusiveDriver(name) expects the registered ESP32 driver name string. The documented driver names are: "PARLIO", "SPI", "RMT". [1]

If you want to be version/platform-safe (including FastLED 3.11), query what your build actually registered at runtime and use those names:

  • FastLED.getDriverInfo() → list of driver records including driver.name (the exact string to pass) [1]
  • FastLED.getDriverCount(), FastLED.isDriverEnabled("RMT"), etc. [1]

Source documentation: the FastLED GitHub README section “Runtime Driver Control” (shows setExclusiveDriver("RMT") and the available driver table/names). [1]

References

  1. FastLED/FastLED GitHub README (“FastLED 3.10.4: Runtime Driver Control”, driver table and examples). (github.com)

Citations:


🏁 Script executed:

# Find and examine the D_FastLED.h file to see lines 115-120
find . -name "D_FastLED.h" -type f

Repository: MoonModules/MoonLight

Length of output: 106


🏁 Script executed:

cat -n ./src/MoonLight/Nodes/Drivers/D_FastLED.h | sed -n '100,125p'

Repository: MoonModules/MoonLight

Length of output: 1334


Replace "I2S" with a valid driver name — "I2S" is not recognized by FastLED 3.11.

FastLED 3.11's setExclusiveDriver() and ChannelOptions.mAffinity only accept: "PARLIO", "SPI", or "RMT". The code uses "I2S" at lines 107 and 117, which will fail at runtime. Use "PARLIO" or "SPI" instead. "PARLIO" is the safer choice for ESP32/S2/S3 to avoid RMT driver ISR stability issues. This also applies to the else branches at lines 109 and 119 if consistency is needed across all platforms.

🤖 Prompt for AI Agents
In `@src/MoonLight/Nodes/Drivers/D_FastLED.h` around lines 106 - 110, The FastLED
driver name "I2S" is invalid and will fail at runtime; update calls to
FastLED.setExclusiveDriver(...) (and any ChannelOptions.mAffinity uses) to use a
supported driver name — replace "I2S" with "PARLIO" for the
ESP32/ESP32S2/ESP32S3 branches (and optionally the other branches for
consistency) so the calls in the D_FastLED.h code use a valid driver ("PARLIO",
"SPI", or "RMT") with "PARLIO" recommended to avoid RMT ISR issues.

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.

1 participant