Skip to content

Comments

FastLED audio#130

Open
ewowi wants to merge 4 commits intomainfrom
FastLED-audio
Open

FastLED audio#130
ewowi wants to merge 4 commits intomainfrom
FastLED-audio

Conversation

@ewowi
Copy link
Collaborator

@ewowi ewowi commented Feb 22, 2026

Summary by CodeRabbit

  • New Features

    • Audio-responsive LED driver and new audio-reactive visual effect with beat, vocal and frequency signals.
    • New "Tubes" layout option for arranging LEDs in tube/grid patterns.
  • Improvements

    • Waterfall effect now renders true 3D column-based visuals.
    • Shared telemetry expanded with vocals, vocal confidence, bass, mid, treble, beat and percussion type.
  • Chores

    • FastLED tooling/build updated to latest build.

Backend
=======
- Add D_FastLEDAudio.h
Add FastLED audio variables to SharedData - WIP
- Add Tubes Layout
- Add FastLEDAudioDriver
- Add FLAudioEffect
@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Walkthrough

Adds I2S-based FastLED audio support (new FastLEDAudioDriver, shared audio fields), a reactive FLAudioEffect, a TubesLayout, driver/effect/layout registrations, FastLED driver update-handler lifecycle fixes, a WaterfallEffect 3D shift refactor, a PlatformIO build bump, and a minor startup serial change.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
platformio.ini
Increased DRIVERS_STACK_SIZE 4096→6144; bumped FASTLED_BUILD and FastLED lib ref from 2026022120260223.
Core shared data & headers
src/MoonBase/Nodes.h
Added audio-related SharedData fields (vocalsActive, vocalConfidence, bassLevel, midLevel, trebleLevel, beat, percussionType); changed LiveScriptNode destructor to ~LiveScriptNode() override;; replaced single FastLED include with D_FastLEDAudio.h and D_FastLEDDriver.h and reordered driver includes.
New audio driver
src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h
New FastLEDAudioDriver Node: I2S input lifecycle, fl::AudioProcessor usage, event callbacks that populate SharedData, runtime pin/config management, setup/loop and safe teardown.
FastLED driver updates
src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h
Added stored update handler IDs (ioUpdateHandler, controlUpdateHandler), register/remove handlers on setup/teardown, replaced FastLED.reset(ResetFlags::CHANNELS) calls with FastLED.clear(ClearFlags::CHANNELS).
IMU & Infrared driver lifecycle fixes
src/MoonLight/Nodes/Drivers/D_IMU.h, src/MoonLight/Nodes/Drivers/D_Infrared.h
Store ioUpdateHandler IDs from addUpdateHandler() and remove handlers in destructors to ensure proper cleanup.
New audio effect
src/MoonLight/Nodes/Effects/E_FastLED.h
Added FLAudioEffect Node that consumes shared audio fields (beat, bands, vocals, percussion) and renders reactive visuals with configurable fade and beat decay.
Effects refactor (3D behavior)
src/MoonLight/Nodes/Effects/E_WLED.h
WaterfallEffect vertical shift now iterates 3D coordinates and shifts along y-axis per (x,z) column; top-row/peak writes apply across the top plane.
New layout
src/MoonLight/Nodes/Layouts/L_MoonLight.h
Added TubesLayout (2D layout) composing multiple SingleColumnLayout columns with properties: nrOfTubes, ledsPerTube, tubeDistance, reversed_order.
Module registration updates
src/MoonLight/Modules/ModuleDrivers.h, src/MoonLight/Modules/ModuleEffects.h
Registered FastLEDAudioDriver, TubesLayout, and FLAudioEffect in driver/effect/layout lists and node-creation paths.
Startup change
src/main.cpp
Removed enabling Serial.setDebugOutput(true) (left Serial.flush()), altering startup debug output side-effect.

Sequence Diagram(s)

sequenceDiagram
    participant I2S as I2S Audio HW
    participant AudioDrv as FastLEDAudioDriver
    participant Proc as fl::AudioProcessor
    participant Shared as SharedData
    participant Effect as FLAudioEffect
    participant LEDs as FastLED Renderer

    I2S->>AudioDrv: deliver audio samples
    AudioDrv->>Proc: feed samples
    Proc->>Proc: analyze (bands, beat, vocals, percussion)
    Proc->>Shared: update bass/mid/treble, beat, vocalsActive, vocalConfidence, percussionType
    AudioDrv->>Shared: clear transient beat each loop
    Effect->>Shared: read audio fields
    alt vocalsActive
        Effect->>Effect: render vocal-driven visuals
    else beat
        Effect->>Effect: trigger beat visuals
    else
        Effect->>Effect: normal rendering
    end
    Effect->>LEDs: write pixel output
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Add node mutex  #103: Related to LiveScriptNode destructor signature and Node lifecycle/mutex changes.
  • pre 71 fixes #107: Modifies D_Infrared.h IO update handler lifecycle; overlaps with this PR's handler management pattern.
  • Board presets tuning #66: Earlier changes to src/MoonBase/Nodes.h (shared data / destructors) that overlap with the current Nodes.h edits.

Poem

🐇
I hear I2S and twitch my ear, lights follow every beat,
Tubes stack tall, vocals hum, colors skip on tiny feet,
A fluttered hop, a pixel cheer, rhythms paint the night aglow,
I nibble code and blink along — the LEDs and I put on a show!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'FastLED audio' is directly related to the main objective of integrating FastLED's audio library into MoonLight, accurately summarizing the primary change.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch FastLED-audio

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

🧹 Nitpick comments (4)
src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h (4)

39-42: Unused member variables currentBPM and onsetCount.

currentBPM and onsetCount are declared but never read or written anywhere in this class. If they're planned for future use, consider removing them until needed to keep the interface clean (YAGNI).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` around lines 39 - 42, The
members currentBPM (float) and onsetCount (uint32_t) in the D_FastLEDAudio class
are declared but never used; remove these two unused member declarations from
the class in D_FastLEDAudio.h to keep the interface clean (or, if they are
intentionally planned, add a clear TODO comment and minimal read/write usage to
avoid unused-variable warnings). Locate the declarations named currentBPM and
onsetCount in the D_FastLEDAudio class and delete them (or replace with a
commented TODO) and rebuild to ensure no references remain.

21-28: Hardcoded I2S pins and static globals in a header file.

Two concerns:

  1. Pin configuration: The #define macros for I2S pins (6, 4, 5) are hardcoded and will silently shadow or collide with any same-named macros elsewhere. Consider making these configurable (e.g., via onUpdate/UI controls, as done for other drivers) or at minimum wrapping them in #ifndef guards.

  2. Static globals in a header: i2sConfig, config, and audioInput are static at file scope — each translation unit including this header gets its own copy. If this header is included from multiple .cpp files, you'll have duplicated state and the audioInput shared_ptr won't actually be shared. If single-TU inclusion is guaranteed by the build, this works but is fragile.

Consider moving the static variables inside the class (as static inline members or into the .cpp file), and making pin assignments configurable or at least guarded:

-  `#define` I2S_CLK_PIN 6
-  `#define` I2S_WS_PIN 4
-  `#define` I2S_SD_PIN 5
-  `#define` I2S_CHANNEL fl::Left
-
-static fl::AudioConfigI2S i2sConfig(I2S_WS_PIN, I2S_SD_PIN, I2S_CLK_PIN, 0, I2S_CHANNEL, 44100, 16, fl::Philips);
-static fl::AudioConfig config(i2sConfig);
-static fl::shared_ptr<fl::IAudioInput> audioInput;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` around lines 21 - 28, The
header hardcodes I2S_PIN macros (I2S_CLK_PIN, I2S_WS_PIN, I2S_SD_PIN) and
declares file-scope static objects (i2sConfig, config, audioInput) which cause
macro collisions and duplicated state across TUs; change the pin defines to be
configurable or at minimum wrap them with `#ifndef` guards (so existing builds can
override them) and move i2sConfig, config, and audioInput out of the header into
the implementation (or declare them as static inline class members) so there is
a single shared instance — update the driver class to accept pin/runtime config
(e.g., via onUpdate/UI controls) and instantiate the AudioConfigI2S and
shared_ptr<fl::IAudioInput> in the .cpp or as inline static members instead of
file-scope statics in the header.

51-83: Callback capture lists are inconsistent — consider uniformity.

  • onBeat, onVocalStart, onVocalEnd capture [&] (by reference), which captures this and all locals. These callbacks only access sharedData (global) and member variables (beatCount, lastBeatTime).
  • onVocalConfidence, onBass, onTreble use [] (no capture) and only access sharedData (global).

The [&] captures are safe as long as the FastLEDAudioDriver instance outlives audioProcessor (which it does since it's a member). However, mixing capture styles is slightly inconsistent. The onVocalStart/onVocalEnd lambdas don't need [&] since they only touch the global sharedData.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` around lines 51 - 83, The
callbacks use mixed capture styles; make them explicit and consistent: change
audioProcessor.onBeat's lambda to capture [this] (it modifies member variables
beatCount and lastBeatTime) and keep audioProcessor.onVocalStart / onVocalEnd as
[this] if they will ever touch members (or change them to [] if they only touch
the global sharedData), and leave audioProcessor.onVocalConfidence, onBass,
onTreble as [] (no capture) since they only use the global sharedData; update
the capture lists for the lambdas referenced by onBeat, onVocalStart,
onVocalEnd, onVocalConfidence, onBass, and onTreble accordingly so each lambda
explicitly captures only what it needs.

94-94: Stop audio input in destructor to release hardware resources.

The audioInput static shared_ptr is started in setup() but never stopped. When the driver is destroyed, the I2S hardware resource remains active. Call audioInput->stop() (designed per FastLED API for this purpose) to properly release the resource:

♻️ Proposed fix
- ~FastLEDAudioDriver() override {}
+ ~FastLEDAudioDriver() override {
+   if (audioInput) {
+     audioInput->stop();
+   }
+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` at line 94, The destructor
~FastLEDAudioDriver() currently does nothing but must stop the shared audioInput
resource started in setup(); update the destructor for FastLEDAudioDriver to
check the static std::shared_ptr audioInput and call audioInput->stop() (and
optionally reset the pointer) to release the I2S hardware resource; locate the
static member audioInput and the setup() method for context and ensure the
destructor calls audioInput->stop() only if the pointer is non-null.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@platformio.ini`:
- Around line 207-209: Add the compiler define to force the legacy RMT4 driver:
in the platformio.ini's [moonlight] build_flags (the same place where -D
FASTLED_BUILD is set), append -D FASTLED_RMT5=0 so FastLED uses RMT4 instead of
RMT5; update the build_flags string that currently references FASTLED_BUILD so
it includes this new define to prevent the RMT5 ISR "Handler detached" issues
described.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h`:
- Around line 47-49: Check the result of fl::IAudioInput::create by verifying
audioInput and errorMsg before calling audioInput->start(); if create fails, log
the descriptive errorMsg (or a fallback message) and avoid calling start so you
don't dereference a null pointer. Add a null guard in the class's loop20ms()
(e.g., if (!audioInput) return;) to skip audio processing when audioInput wasn't
created. Ensure all locations that use audioInput (construction/start and
loop20ms) handle the null case consistently and do not assume successful
creation.
- Around line 86-92: The beat flag is being cleared at the end of loop20ms(), so
any onBeat-set sharedData.beat is erased before FLAudioEffect::loop() can
observe it; move the sharedData.beat = false reset to the start of loop20ms()
(before the while loop that calls audioInput->read() and
audioProcessor.update(sample)) so the flag can be set by the onBeat callback
during processing and persist until the next cycle.

---

Nitpick comments:
In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h`:
- Around line 39-42: The members currentBPM (float) and onsetCount (uint32_t) in
the D_FastLEDAudio class are declared but never used; remove these two unused
member declarations from the class in D_FastLEDAudio.h to keep the interface
clean (or, if they are intentionally planned, add a clear TODO comment and
minimal read/write usage to avoid unused-variable warnings). Locate the
declarations named currentBPM and onsetCount in the D_FastLEDAudio class and
delete them (or replace with a commented TODO) and rebuild to ensure no
references remain.
- Around line 21-28: The header hardcodes I2S_PIN macros (I2S_CLK_PIN,
I2S_WS_PIN, I2S_SD_PIN) and declares file-scope static objects (i2sConfig,
config, audioInput) which cause macro collisions and duplicated state across
TUs; change the pin defines to be configurable or at minimum wrap them with
`#ifndef` guards (so existing builds can override them) and move i2sConfig,
config, and audioInput out of the header into the implementation (or declare
them as static inline class members) so there is a single shared instance —
update the driver class to accept pin/runtime config (e.g., via onUpdate/UI
controls) and instantiate the AudioConfigI2S and shared_ptr<fl::IAudioInput> in
the .cpp or as inline static members instead of file-scope statics in the
header.
- Around line 51-83: The callbacks use mixed capture styles; make them explicit
and consistent: change audioProcessor.onBeat's lambda to capture [this] (it
modifies member variables beatCount and lastBeatTime) and keep
audioProcessor.onVocalStart / onVocalEnd as [this] if they will ever touch
members (or change them to [] if they only touch the global sharedData), and
leave audioProcessor.onVocalConfidence, onBass, onTreble as [] (no capture)
since they only use the global sharedData; update the capture lists for the
lambdas referenced by onBeat, onVocalStart, onVocalEnd, onVocalConfidence,
onBass, and onTreble accordingly so each lambda explicitly captures only what it
needs.
- Line 94: The destructor ~FastLEDAudioDriver() currently does nothing but must
stop the shared audioInput resource started in setup(); update the destructor
for FastLEDAudioDriver to check the static std::shared_ptr audioInput and call
audioInput->stop() (and optionally reset the pointer) to release the I2S
hardware resource; locate the static member audioInput and the setup() method
for context and ensure the destructor calls audioInput->stop() only if the
pointer is non-null.

platformio.ini Outdated
Comment on lines 207 to 209
-D FASTLED_BUILD=\"20260222\"
lib_deps =
https://github.com/FastLED/FastLED#9d0b0eb9b5e59e4093982e0c2bdcfdff72ca80cb ; master 20260221
https://github.com/FastLED/FastLED#27c99130c83ab666c4c6dbf389425fec5f27db05 ; master 20260222
Copy link

@coderabbitai coderabbitai bot Feb 22, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine the platformio.ini file around the specified lines and check for build flags
cat -n platformio.ini | head -230 | tail -50

Repository: MoonModules/MoonLight

Length of output: 3265


🏁 Script executed:

#!/bin/bash
# Search for all FastLED-related configuration flags in the repo
rg -n "FASTLED_RMT5|FASTLED_RMT4|FASTLED_BUILD|FASTLED_" --type ini --type txt --type h --type cpp

Repository: MoonModules/MoonLight

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Check the exact commit details and verify build string alignment
# Find the git repository containing the pinned FastLED commit
rg -n "27c99130c83ab666c4c6dbf389425fec5f27db05" --type ini

Repository: MoonModules/MoonLight

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Search for FASTLED_RMT5, FASTLED_RMT4, and related driver flags without file type restrictions
rg -n "FASTLED_RMT5|FASTLED_RMT4|FASTLED_SPI" .

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Check the complete [moonlight] section and build_flags to see all configuration
sed -n '199,211p' platformio.ini

Repository: MoonModules/MoonLight

Length of output: 811


🏁 Script executed:

#!/bin/bash
# Check all build_flags sections across the entire platformio.ini
rg -B2 -A5 "build_flags|FASTLED" platformio.ini

Repository: MoonModules/MoonLight

Length of output: 2738


🏁 Script executed:

#!/bin/bash
# Verify the commit date by examining git if available, or search related documentation
# Check if there are any comments/docs about the FastLED version and RMT configuration
rg -n "27c99130c83ab666c4c6dbf389425fec5f27db05|RMT5|RMT4|Handler detached" .

Repository: MoonModules/MoonLight

Length of output: 181


🏁 Script executed:

#!/bin/bash
# Check platform-espressif32 version 55.03.37 for ESP-IDF details
# Look for package.json or version info in typical locations
rg -n "esp-idf|ESP-IDF|5\.[0-9]" platformio.ini | head -20

Repository: MoonModules/MoonLight

Length of output: 2966


🏁 Script executed:

#!/bin/bash
# Search for any Arduino core version or framework configuration
rg -n "framework|arduino|framework-esp32idf|framework-espidf" platformio.ini

Repository: MoonModules/MoonLight

Length of output: 3009


Add -D FASTLED_RMT5=0 to force legacy RMT4 driver and prevent ISR handler degradation.

The FASTLED_BUILD string aligns correctly with the pinned commit. However, platform 55.03.37 uses ESP-IDF 5.5+ with Arduino core 3.x, which causes FastLED to default to the RMT5 driver. RMT5 has documented ISR handler lifecycle issues (see FastLED issue #1903) causing repeated "Handler detached" messages and severe performance drops (~2 FPS). Add -D FASTLED_RMT5=0 to the [moonlight] build_flags to force the stable legacy RMT4 driver.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platformio.ini` around lines 207 - 209, Add the compiler define to force the
legacy RMT4 driver: in the platformio.ini's [moonlight] build_flags (the same
place where -D FASTLED_BUILD is set), append -D FASTLED_RMT5=0 so FastLED uses
RMT4 instead of RMT5; update the build_flags string that currently references
FASTLED_BUILD so it includes this new define to prevent the RMT5 ISR "Handler
detached" issues described.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not true on newest FastLED

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

FastLED RMT5 driver fixes February 2026 handler detached

💡 Result:

I couldn’t find any FastLED change/issue (through the public repo + recent searches) specifically tied to a “handler detached” message for the ESP32 RMT5 driver in February 2026—so there doesn’t appear to be a known “Feb 2026” targeted fix labeled that way. (If you have the exact log line + platform/core version, I can search more precisely.)

What is documented/fixed recently around ESP32 RMT5 stability:

  • FastLED 3.10.1 fixed an ESP32‑S3 RMT5 break where an internal improvement accidentally set DMA mode true, breaking RMT5. Updating beyond 3.10.1 is recommended if you hit DMA-related failures. [1]
  • Ongoing RMT5 reliability discussions (especially Wi‑Fi flicker/jitter) note RMT5’s limits and practical mitigations (e.g., raising RMT interrupt priority to the max the driver allows and increasing buffering), but these are about underruns/flicker rather than a “handler detached” teardown error. [2]
  • If you need a robust workaround today, FastLED explicitly recommends switching away from RMT5 in some cases:
    • Force legacy RMT4 with -D FASTLED_RMT5=0 (typically requires older ESP32 Arduino core / IDF4-era setups; also note RMT4 and RMT5 can’t coexist). [3]
    • Or use alternative output paths (I2S/SPI/Adafruit bridge), which users/reporting/dev notes often suggest when RMT5 is unstable under load. [3]

Most actionable next step: confirm whether your “handler detached” happens only with DMA enabled. If yes, try updating FastLED (>= 3.10.1) first. [1] If it persists, try FASTLED_RMT5=0 (RMT4) or switch to a non-RMT driver as above. [3]

Sources:
[1] FastLED releases (3.10.1 notes: DMA mode accidentally set true, breaking RMT5) https://github.com/fastled/fastled/releases
[2] FastLED issue #2082 (RMT5 + Wi‑Fi flicker; buffering/priority limits/knobs discussion) FastLED/FastLED#2082
[3] FastLED repository documentation (RMT4 vs RMT5 selection; FASTLED_RMT5=0; alternative drivers) https://github.com/FastLED/FastLED


@ewowi, thank you for the correction! I appreciate you letting me know that the RMT5 "Handler detached" issues have been resolved in the newest FastLED version.

Since you originally provided the learning about RMT5 issues (learning ID 9bb17d59-f1a1-4505-b40f-39a68c64be29) and you're now confirming it's no longer applicable with the latest FastLED, would you like me to remove that outdated learning from the knowledge base? This will prevent it from being referenced in future reviews.


✏️ Learnings added
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 130
File: platformio.ini:207-209
Timestamp: 2026-02-22T12:37:03.751Z
Learning: FastLED RMT5 driver issues (ISR handler "Handler detached" messages and performance degradation) have been resolved in FastLED versions as of commit 27c99130c83ab666c4c6dbf389425fec5f27db05 (master 20260222). The `-D FASTLED_RMT5=0` workaround is no longer needed.
<!-- </add_learning>

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-02-05T13:20:46.001Z
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.

Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/**/platformio.ini : Use PlatformIO to build and upload the ESP32 backend C++ code

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-16T15:53:11.236Z
Learning: For ESP32-PICO-MINI-02 (and similar ESP32 boards with PSRAM), the critical configuration to enable PSRAM is `board_build.arduino.memory_type = qio_qspi` in platformio.ini. The board JSON may already include `-DBOARD_HAS_PSRAM` and `flash_mode: qio`, but without the memory_type setting, PSRAM won't be properly initialized, causing LoadStoreError crashes when code tries to access PSRAM addresses.

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: 121
File: src/MoonLight/Nodes/Drivers/D_FastLED.h:176-176
Timestamp: 2026-02-08T20:43:12.857Z
Learning: In MoonLight FastLED 3.11 driver (src/MoonLight/Nodes/Drivers/D_FastLED.h): Affinity is applied per-channel via fl::ChannelOptions.mAffinity passed to fl::ChannelConfig (not via FastLED.setExclusiveDriver). Keeping setExclusiveDriver commented out leaves all drivers available; FastLED then selects the appropriate driver for each channel based on the mAffinity preference ("RMT", "I2S", "SPI", "PARLIO", or "" for auto). This allows per-channel driver preferences rather than a global restriction.

Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/**/factory_settings.ini : Configure default or factory-set ESP32 parameters in `factory_settings.ini`

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: 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-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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes remove it please

Copy link

Choose a reason for hiding this comment

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

@ewowi, I've removed the outdated learning about the FastLED RMT5 driver issues. The knowledge base now reflects the current state: that RMT5 issues have been resolved in FastLED as of commit 27c99130c83ab666c4c6dbf389425fec5f27db05 (master 20260222), and the -D FASTLED_RMT5=0 workaround is no longer needed. Thank you for keeping the information up to date!

🐰💭
(Learning tidied up!)
/) /)
( •.• )
/>📝✨


⛔ Learnings removed
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-05T13:20:46.001Z
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.

🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-05T13:20:46.001Z
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.

Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/**/platformio.ini : Use PlatformIO to build and upload the ESP32 backend C++ code

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-16T15:53:11.236Z
Learning: For ESP32-PICO-MINI-02 (and similar ESP32 boards with PSRAM), the critical configuration to enable PSRAM is `board_build.arduino.memory_type = qio_qspi` in platformio.ini. The board JSON may already include `-DBOARD_HAS_PSRAM` and `flash_mode: qio`, but without the memory_type setting, PSRAM won't be properly initialized, causing LoadStoreError crashes when code tries to access PSRAM addresses.

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: 130
File: platformio.ini:207-209
Timestamp: 2026-02-22T12:37:03.751Z
Learning: FastLED RMT5 driver issues (ISR handler "Handler detached" messages and performance degradation) have been resolved in FastLED versions as of commit 27c99130c83ab666c4c6dbf389425fec5f27db05 (master 20260222). The `-D FASTLED_RMT5=0` workaround is no longer needed.
<!-- </add_learning>

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 121
File: src/MoonLight/Nodes/Drivers/D_FastLED.h:176-176
Timestamp: 2026-02-08T20:43:12.857Z
Learning: In MoonLight FastLED 3.11 driver (src/MoonLight/Nodes/Drivers/D_FastLED.h): Affinity is applied per-channel via fl::ChannelOptions.mAffinity passed to fl::ChannelConfig (not via FastLED.setExclusiveDriver). Keeping setExclusiveDriver commented out leaves all drivers available; FastLED then selects the appropriate driver for each channel based on the mAffinity preference ("RMT", "I2S", "SPI", "PARLIO", or "" for auto). This allows per-channel driver preferences rather than a global restriction.

Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/**/factory_settings.ini : Configure default or factory-set ESP32 parameters in `factory_settings.ini`

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-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: 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: 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: 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.

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

🧩 Analysis chain
🏁 Script executed:

#!/bin/bash
# First, let's examine the platformio.ini file around the specified lines and check for build flags
cat -n platformio.ini | head -230 | tail -50

Repository: MoonModules/MoonLight

Length of output: 3265

🏁 Script executed:

#!/bin/bash
# Search for all FastLED-related configuration flags in the repo
rg -n "FASTLED_RMT5|FASTLED_RMT4|FASTLED_BUILD|FASTLED_" --type ini --type txt --type h --type cpp

Repository: MoonModules/MoonLight

Length of output: 92

🏁 Script executed:

#!/bin/bash
# Check the exact commit details and verify build string alignment
# Find the git repository containing the pinned FastLED commit
rg -n "27c99130c83ab666c4c6dbf389425fec5f27db05" --type ini

Repository: MoonModules/MoonLight

Length of output: 92

🏁 Script executed:

#!/bin/bash
# Search for FASTLED_RMT5, FASTLED_RMT4, and related driver flags without file type restrictions
rg -n "FASTLED_RMT5|FASTLED_RMT4|FASTLED_SPI" .

Repository: MoonModules/MoonLight

Length of output: 47

🏁 Script executed:

#!/bin/bash
# Check the complete [moonlight] section and build_flags to see all configuration
sed -n '199,211p' platformio.ini

Repository: MoonModules/MoonLight

Length of output: 811

🏁 Script executed:

#!/bin/bash
# Check all build_flags sections across the entire platformio.ini
rg -B2 -A5 "build_flags|FASTLED" platformio.ini

Repository: MoonModules/MoonLight

Length of output: 2738

🏁 Script executed:

#!/bin/bash
# Verify the commit date by examining git if available, or search related documentation
# Check if there are any comments/docs about the FastLED version and RMT configuration
rg -n "27c99130c83ab666c4c6dbf389425fec5f27db05|RMT5|RMT4|Handler detached" .

Repository: MoonModules/MoonLight

Length of output: 181

🏁 Script executed:

#!/bin/bash
# Check platform-espressif32 version 55.03.37 for ESP-IDF details
# Look for package.json or version info in typical locations
rg -n "esp-idf|ESP-IDF|5\.[0-9]" platformio.ini | head -20

Repository: MoonModules/MoonLight

Length of output: 2966

🏁 Script executed:

#!/bin/bash
# Search for any Arduino core version or framework configuration
rg -n "framework|arduino|framework-esp32idf|framework-espidf" platformio.ini

Repository: MoonModules/MoonLight

Length of output: 3009

Add -D FASTLED_RMT5=0 to force legacy RMT4 driver and prevent ISR handler degradation.

The FASTLED_BUILD string aligns correctly with the pinned commit. However, platform 55.03.37 uses ESP-IDF 5.5+ with Arduino core 3.x, which causes FastLED to default to the RMT5 driver. RMT5 has documented ISR handler lifecycle issues (see FastLED issue #1903) causing repeated "Handler detached" messages and severe performance drops (~2 FPS). Add -D FASTLED_RMT5=0 to the [moonlight] build_flags to force the stable legacy RMT4 driver.

🤖 Prompt for AI Agents

Disabling RMT5 should not be necessary now? The RMT5 driver now allocates more memory during WIFI active mode. Please let me know if this isn't the case. RMT5 will have stress testing either this week if ai credits sustain. Otherwise post friday on refresh.

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

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.

🧹 Nitpick comments (1)
src/MoonBase/Nodes.h (1)

350-356: Optional: prefer 0.0f literals for float member initializers.

float vocalConfidence = 0, float bassLevel = 0, and float trebleLevel = 0 are valid C++ but using integer literals for float members is less explicit than 0.0f.

✨ Proposed style fix
-  float vocalConfidence = 0;
-  float bassLevel = 0;
-  float trebleLevel = 0;
+  float vocalConfidence = 0.0f;
+  float bassLevel = 0.0f;
+  float trebleLevel = 0.0f;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonBase/Nodes.h` around lines 350 - 356, Update the float member
initializers to use explicit float literals: change the initializers for
vocalConfidence, bassLevel, and trebleLevel from integer zero (e.g. = 0) to
0.0f; keep the boolean members vocalsActive and beat unchanged. This ensures
types are explicit for members vocalConfidence, bassLevel, and trebleLevel in
the class/struct where they are declared.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/MoonBase/Nodes.h`:
- Around line 350-356: Update the float member initializers to use explicit
float literals: change the initializers for vocalConfidence, bassLevel, and
trebleLevel from integer zero (e.g. = 0) to 0.0f; keep the boolean members
vocalsActive and beat unchanged. This ensures types are explicit for members
vocalConfidence, bassLevel, and trebleLevel in the class/struct where they are
declared.

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 22, 2026

@coderabbitai, can you verify this issue FastLED/FastLED#2170 and compare it with the commits in this PR and come with a review summary and propose follow up actions to do in this PR

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Tip

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

🧩 Analysis chain

🌐 Web query:

FastLED GitHub issue 2170

💡 Result:

FastLED/FastLED Issue #2170 is titled “Audio library issues and opportunities” and is Open. It was opened by @4wheeljive on February 11, 2026. The issue provides feedback on FastLED’s newest pre-release audio library, noting that while the overall architecture/API is good, the DSP/implementations have significant gaps; it specifically calls out the need for a signal conditioning / preprocessing stage (e.g., DC removal, spike filtering, noise gating, auto-gain) before detectors run, because otherwise users end up re-implementing that work themselves. [1]

Source: [1]


🏁 Script executed:

#!/bin/bash
# Get detailed information about the audio-related changes in this PR
echo "=== Examining FastLEDAudioDriver implementation ==="
cat src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h

echo -e "\n=== Examining FLAudioEffect implementation ==="
cat src/MoonLight/Nodes/Effects/E_FastLED.h

echo -e "\n=== Checking for audio processor configuration ==="
rg -n "AudioProcessor|audioProcessor" --type=cpp --type=h -C3

echo -e "\n=== Checking platformio.ini changes ==="
cat platformio.ini | grep -A5 -B5 "FASTLED\|DRIVERS_STACK"

Length of output: 10286


@ewowi, I've analyzed FastLED issue #2170 and compared it with your PR changes. Here's my findings:

FastLED Issue #2170 Summary

FastLED issue #2170 titled "Audio library issues and opportunities" is open and was created by @4wheeljive on February 11, 2026. The issue notes that while the FastLED audio library's "overall architecture/API is good", the "DSP/implementations have significant gaps", specifically calling out the need for a "signal conditioning / preprocessing stage" (e.g., DC removal, spike filtering, noise gating, auto-gain) before detectors run.

PR #130 Implementation Analysis

Your PR integrates FastLED's audio library through:

  1. FastLEDAudioDriver (D_FastLEDAudio.h):

    • Creates I2S audio input with hardcoded pins and configuration
    • Uses fl::AudioProcessor directly
    • Registers callbacks for beat, vocals, bass, and treble
    • Passes raw audio samples to audioProcessor.update() with no preprocessing
    • Only applies minimal 0.01f threshold filtering in bass/treble callbacks
  2. FLAudioEffect (E_FastLED.h):

    • Visualizes audio data with 4 vertical bars (bass, treble, vocal, beat)
    • Implements adaptive scaling using max values with slow decay
    • Basic visual effect to demonstrate audio integration
  3. Infrastructure:

    • Stack size increased from 4096 to 6144 bytes (50% increase)
    • Note in platformio.ini suggests considering moving audio to separate task
    • FastLED updated to latest commit (20260222)

Gap Analysis

The implementation does NOT address the preprocessing concerns raised in issue #2170:

  • ❌ No DC removal
  • ❌ No spike filtering
  • ❌ No noise gating
  • ❌ No auto-gain control
  • ⚠️ Only basic threshold filtering (0.01f)

Your implementation relies entirely on FastLED's AudioProcessor as-is, which according to the issue has the DSP gaps mentioned above.

Proposed Follow-up Actions

Immediate (This PR)

  1. Document known limitations - Add comments in D_FastLEDAudio.h noting reliance on FastLED's audio processing without additional preprocessing
  2. Make audio config more flexible - Consider exposing I2S pin configuration as UI controls instead of hardcoded defines
  3. Add error handling - The current implementation continues if audioInput creation fails; consider more robust handling
  4. ⚠️ Stack size consideration - Monitor if 6144 bytes is sufficient; your own note suggests potentially moving to separate task

Short-term Testing

  1. 📊 Real-world testing - Test with various audio sources (music, speech, ambient) to identify if preprocessing gaps cause issues
  2. 📊 Monitor for symptoms - Watch for: false beat triggers, poor vocal detection, level instability, DC offset issues
  3. 📊 Benchmark performance - Verify the stack increase is adequate and monitor CPU usage

Medium-term Improvements

  1. 🔧 Consider preprocessing layer - If issues surface, implement signal conditioning before AudioProcessor:
    • High-pass filter for DC removal
    • Moving average or median filter for spike suppression
    • Threshold-based noise gate
    • AGC (automatic gain control) for level normalization
  2. 🔧 Expose tuning parameters - Add UI controls for thresholds, sensitivity, etc.
  3. 🤝 Contribute upstream - If you implement preprocessing, consider contributing back to FastLED to address issue #2170

Documentation

  1. 📝 Update PR description - Note the dependency on FastLED's audio library and any observed limitations
  2. 📝 User guidance - Document recommended audio input levels and hardware setup for best results

Recommendation

Proceed with merging, but treat this as an initial integration that may require refinement. The FastLED audio library is evolving (issue #2170 is still open), and your implementation provides a good foundation for testing. Monitor real-world performance and be prepared to add a preprocessing layer if the gaps mentioned in #2170 manifest as actual issues in your use case.

Would you like me to help implement any of these follow-up actions, such as adding configuration flexibility or documenting the known limitations?


🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 130
File: platformio.ini:207-209
Timestamp: 2026-02-22T12:37:03.751Z
Learning: FastLED RMT5 driver issues (ISR handler "Handler detached" messages and performance degradation) have been resolved in FastLED versions as of commit 27c99130c83ab666c4c6dbf389425fec5f27db05 (master 20260222). The `-D FASTLED_RMT5=0` workaround is no longer needed.
<!-- </add_learning>

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 22, 2026

@coderabbitai, regarding

static fl::AudioConfigI2S i2sConfig(I2S_WS_PIN, I2S_SD_PIN, I2S_CLK_PIN, 0, I2S_CHANNEL, 44100, 16, fl::Philips);
static fl::AudioConfig config(i2sConfig);
static fl::shared_ptrfl::IAudioInput audioInput;

How can I bring these in the FastLEDAudioDriver class: create them in setup() and destroy in the destructor

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Examine the current FastLEDAudioDriver implementation to see the static variables and class structure
cat src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h

Length of output: 2830


@ewowi, here's how to move those static variables into the FastLEDAudioDriver class as member variables:

Recommended Approach (using pointers)

class FastLEDAudioDriver : public Node {
 private:
  // Member variables for audio configuration
  fl::unique_ptr<fl::AudioConfigI2S> i2sConfig;
  fl::unique_ptr<fl::AudioConfig> config;
  fl::shared_ptr<fl::IAudioInput> audioInput;

 public:
  static const char* name() { return "FastLED Audio"; }
  static uint8_t dim() { return _NoD; }
  static const char* tags() { return "☸️"; }

  fl::AudioProcessor audioProcessor;

  void setup() override {
    Node::setup();  // !!

    // Create configuration objects
    i2sConfig = fl::make_unique<fl::AudioConfigI2S>(
        I2S_WS_PIN, I2S_SD_PIN, I2S_CLK_PIN, 0, 
        I2S_CHANNEL, 44100, 16, fl::Philips);
    
    config = fl::make_unique<fl::AudioConfig>(*i2sConfig);

    // Create audio input using the config
    fl::string errorMsg;
    audioInput = fl::IAudioInput::create(*config, &errorMsg);
    if (!audioInput) {
      EXT_LOGE(ML_TAG, "Failed to create audio input: %s", errorMsg.c_str());
      return;
    }
    audioInput->start();

    // ... rest of your setup code (callbacks, etc.)
  }

  void loop20ms() override {
    if (!audioInput) return;
    sharedData.beat = false;
    while (fl::AudioSample sample = audioInput->read()) {
      audioProcessor.update(sample);
    }
  }

  ~FastLEDAudioDriver() override {
    if (audioInput) {
      audioInput->stop();
    }
    // unique_ptr will automatically clean up i2sConfig and config
  }
};

Alternative: If fl::unique_ptr is not available

If FastLED doesn't provide unique_ptr, use raw pointers with manual cleanup:

class FastLEDAudioDriver : public Node {
 private:
  fl::AudioConfigI2S* i2sConfig = nullptr;
  fl::AudioConfig* config = nullptr;
  fl::shared_ptr<fl::IAudioInput> audioInput;

 public:
  // ... static methods ...
  
  fl::AudioProcessor audioProcessor;

  void setup() override {
    Node::setup();

    // Create configuration objects
    i2sConfig = new fl::AudioConfigI2S(
        I2S_WS_PIN, I2S_SD_PIN, I2S_CLK_PIN, 0, 
        I2S_CHANNEL, 44100, 16, fl::Philips);
    
    config = new fl::AudioConfig(*i2sConfig);

    // Create audio input
    fl::string errorMsg;
    audioInput = fl::IAudioInput::create(*config, &errorMsg);
    // ... rest of setup ...
  }

  ~FastLEDAudioDriver() override {
    if (audioInput) {
      audioInput->stop();
      audioInput.reset();  // Explicitly release shared_ptr
    }
    
    // Clean up raw pointers
    delete config;
    config = nullptr;
    
    delete i2sConfig;
    i2sConfig = nullptr;
  }
};

Key Points

  1. Initialization order matters: Create i2sConfig first, then config (which references i2sConfig), then audioInput
  2. Destruction order: The destructor automatically handles the reverse order - audioInput is stopped first, then configs are cleaned up
  3. Memory management:
    • unique_ptr provides automatic cleanup (preferred)
    • Raw pointers require manual delete in destructor
    • shared_ptr for audioInput is already reference-counted
  4. Remove the static variables: Delete the three static lines at file scope once you move them into the class

This approach ensures proper object lifetime management and allows multiple instances of FastLEDAudioDriver if needed in the future.

FastLED Audio driver
- Move static variables inside class
- add controls (gain etc)
- add onUpdate to set controls
- add readPins to get I2S pins from IO module
- add initAudio and stopAudio
- destructor

FLAudio effect
- add Mid
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: 6

🧹 Nitpick comments (2)
src/MoonLight/Nodes/Effects/E_FastLED.h (1)

48-48: hue is declared but never used.

It looks like a copy from RainbowEffect. Remove it or add a future use to avoid dead state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Effects/E_FastLED.h` at line 48, The field uint16_t hue
in E_FastLED.h is unused (leftover from RainbowEffect); either delete the hue
declaration to remove dead state or implement its intended use where FastLED
effect color cycling is computed (e.g., inside the class/method that updates LED
colors in this file) — locate the unused symbol "hue" and either remove it or
wire it into the effect update routine so it affects color/hue calculations.
src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h (1)

26-27: Optional: replace raw owning pointers with unique_ptr.

i2sConfig and config are manual-new/manual-delete owning pointers. The current startAudio()/stopAudio()/destructor pattern is correct, but std::unique_ptr would make ownership self-documenting and eliminate the need for the explicit null-checks and delete calls in stopAudio().

♻️ Proposed refactor
-  fl::AudioConfigI2S* i2sConfig = nullptr;
-  fl::AudioConfig* config = nullptr;
+  std::unique_ptr<fl::AudioConfigI2S> i2sConfig;
+  std::unique_ptr<fl::AudioConfig> config;
   void startAudio() {
-    i2sConfig = new fl::AudioConfigI2S(pinI2SWS, pinI2SSD, pinI2SSCK, 0,
-        channel == 1 ? fl::Right : channel == 2 ? fl::Both : fl::Left, 44100, 16, fl::Philips);
-    config = new fl::AudioConfig(*i2sConfig);
+    i2sConfig = std::make_unique<fl::AudioConfigI2S>(pinI2SWS, pinI2SSD, pinI2SSCK, 0,
+        channel == 1 ? fl::Right : channel == 2 ? fl::Both : fl::Left, 44100, 16, fl::Philips);
+    config = std::make_unique<fl::AudioConfig>(*i2sConfig);
     ...
   }

   void stopAudio() {
     if (audioInput) { audioInput->stop(); audioInput.reset(); }
-    if (config)    { delete config;    config    = nullptr; }
-    if (i2sConfig) { delete i2sConfig; i2sConfig = nullptr; }
+    config.reset();
+    i2sConfig.reset();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` around lines 26 - 27, Replace
the raw owning pointers i2sConfig and config with std::unique_ptr to make
ownership explicit: change their declarations to
std::unique_ptr<fl::AudioConfigI2S> i2sConfig and
std::unique_ptr<fl::AudioConfig> config, allocate with std::make_unique (or
reset) inside startAudio(), and remove explicit delete/null-check logic from
stopAudio() and the destructor (use reset() if explicit teardown is needed).
Update any pointer usages to dereference the unique_ptr (->) and ensure
stopAudio() sets the unique_ptrs to nullptr via reset() if you need to release
before scope exit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h`:
- Line 153: The log string passed to EXT_LOGI in the D_FastLEDAudio driver
contains an extra closing parenthesis inside the message "(re)creating
audioInput)"; remove the stray ')' so the call to EXT_LOGI(ML_TAG, "(re)creating
audioInput") prints the correct message. Locate the EXT_LOGI usage in
D_FastLEDAudio (the log call shown) and edit the string literal to drop the
unmatched parenthesis.
- Around line 81-86: Fix the two incorrect log formats in the audio callbacks:
in the audioProcessor.onMid lambda change the log label from "onBass" to "onMid"
so it matches the callback (reference: audioProcessor.onMid and
sharedData.midLevel, ML_TAG), and in the onVocalConfidence log change the printf
format specifier from %d to %f to match the float argument (reference:
audioProcessor.onVocalConfidence). Update those commented log lines accordingly
so they are correct when uncommented.
- Line 51: The lambda passed to moduleIO->addUpdateHandler in FastLEDAudioDriver
captures this and is registered with allowRemove=false causing a permanent
callback to readPins(), risking use-after-free; change the registration to store
the returned handler ID (from addUpdateHandler) as a member (e.g.,
updateHandlerId_), and in the FastLEDAudioDriver destructor call
moduleIO->removeUpdateHandler(updateHandlerId_) (or re-register with
allowRemove=true) to ensure the callback is removed before the object is
destroyed; update the class to hold the handler ID and perform removal in the
destructor (or clearly document framework guarantees if you choose not to
remove).

In `@src/MoonLight/Nodes/Effects/E_FastLED.h`:
- Line 82: Guard against division-by-zero when computing the decrement for
beatLevel: ensure layer->size.y is not zero before using it in 255 /
layer->size.y (for example compute an integer divisor like int divisor =
layer->size.y ? layer->size.y : 1; or use a conditional that substitutes a safe
value) and then use that safe divisor in the MIN expression that updates
beatLevel (referencing beatLevel and layer->size.y in the existing line). This
prevents a hardware exception on Xtensa while preserving the original behavior
when size.y > 0.
- Around line 68-73: The drawLine calls in E_FastLED.h pass columnNr and
columnNr++ in the same expression which causes undefined/unspecified behavior;
fix each call by separating the increment from the function call so x1 and x2
are deterministic (e.g., call layer->drawLine(columnNr, ..., columnNr, ...) and
then increment columnNr on the next line), updating the four drawLine
invocations that use columnNr/columnNr++ to ensure columnNr is only read or
written once per expression.
- Around line 59-79: The drawLine calculations can divide by zero because
maxBass/maxMid/maxTreble/maxVocal may be 0 and the decay can undershoot below 0;
fix by clamping the maxima and decays: before each drawLine that uses e.g.
maxBass, ensure a safeDenom = (maxBass > 0.0001f) ? maxBass : 1.0f (or skip
drawing when both sharedData.bassLevel and maxBass are 0), and replace uses of
"maxX -= 0.01" with "maxX = max(0.0f, maxX - 0.01f)"; apply the same pattern for
maxMid, maxTreble, maxVocal and use those safeDenom variables in the
layer->drawLine calls referenced in this block.

---

Nitpick comments:
In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h`:
- Around line 26-27: Replace the raw owning pointers i2sConfig and config with
std::unique_ptr to make ownership explicit: change their declarations to
std::unique_ptr<fl::AudioConfigI2S> i2sConfig and
std::unique_ptr<fl::AudioConfig> config, allocate with std::make_unique (or
reset) inside startAudio(), and remove explicit delete/null-check logic from
stopAudio() and the destructor (use reset() if explicit teardown is needed).
Update any pointer usages to dereference the unique_ptr (->) and ensure
stopAudio() sets the unique_ptrs to nullptr via reset() if you need to release
before scope exit.

In `@src/MoonLight/Nodes/Effects/E_FastLED.h`:
- Line 48: The field uint16_t hue in E_FastLED.h is unused (leftover from
RainbowEffect); either delete the hue declaration to remove dead state or
implement its intended use where FastLED effect color cycling is computed (e.g.,
inside the class/method that updates LED colors in this file) — locate the
unused symbol "hue" and either remove it or wire it into the effect update
routine so it affects color/hue calculations.

Comment on lines 81 to 86
audioProcessor.onMid([](float level) {
if (level > 0.01f) {
sharedData.midLevel = level;
// EXT_LOGD(ML_TAG, "onBass: %f", level);
}
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: copy-paste log label in onMid callback says "onBass".

Line 84 reads "onBass: %f" inside the onMid lambda — a copy-paste leftover. Also, the onVocalConfidence log (line 71) uses %d for a float argument; should be %f. Both are currently commented out, but worth fixing before uncomment.

🔧 Proposed fix
     audioProcessor.onMid([](float level) {
       if (level > 0.01f) {
         sharedData.midLevel = level;
-        // EXT_LOGD(ML_TAG, "onBass: %f", level);
+        // EXT_LOGD(ML_TAG, "onMid: %f", level);
       }
     });
     audioProcessor.onVocalConfidence([](float confidence) {
       sharedData.vocalConfidence = sharedData.vocalsActive ? confidence : 0.0;
-      // EXT_LOGD(ML_TAG, "onVocalConfidence %d", confidence);
+      // EXT_LOGD(ML_TAG, "onVocalConfidence %f", confidence);
     });
📝 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
audioProcessor.onMid([](float level) {
if (level > 0.01f) {
sharedData.midLevel = level;
// EXT_LOGD(ML_TAG, "onBass: %f", level);
}
});
audioProcessor.onMid([](float level) {
if (level > 0.01f) {
sharedData.midLevel = level;
// EXT_LOGD(ML_TAG, "onMid: %f", level);
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` around lines 81 - 86, Fix the
two incorrect log formats in the audio callbacks: in the audioProcessor.onMid
lambda change the log label from "onBass" to "onMid" so it matches the callback
(reference: audioProcessor.onMid and sharedData.midLevel, ML_TAG), and in the
onVocalConfidence log change the printf format specifier from %d to %f to match
the float argument (reference: audioProcessor.onVocalConfidence). Update those
commented log lines accordingly so they are correct when uncommented.

Comment on lines 68 to 73
uint8_t columnNr = 0;
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.bassLevel / maxBass, CRGB::Red);
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.midLevel / maxMid, CRGB::Orange);
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.trebleLevel / maxTreble, CRGB::Green);
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.vocalConfidence / maxVocal, CRGB::Blue);
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * beatLevel / 255, CRGB::Purple);
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

Critical: columnNr++ as a function argument alongside columnNr is undefined/unspecified behaviour.

Each drawLine call passes columnNr (arg 1, x1) and columnNr++ (arg 3, x2) in the same expression. In C++14 and earlier this is undefined behaviour (unsequenced read and modification). In C++17 it is indeterminately sequenced — the compiler may evaluate arg 3 before arg 1, giving drawLine(1, …, 0, …) instead of the intended drawLine(0, …, 0, …). Since x1 ≠ x2 in that ordering, drawLine renders a diagonal segment rather than a vertical bar, and columnNr itself is left at an unexpected value for subsequent calls.

Separate the increment from the call:

🐛 Proposed fix
-    uint8_t columnNr = 0;
-    layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.bassLevel / maxBass, CRGB::Red);
-    layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.midLevel / maxMid, CRGB::Orange);
-    layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.trebleLevel / maxTreble, CRGB::Green);
-    layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.vocalConfidence / maxVocal, CRGB::Blue);
-    layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * beatLevel / 255, CRGB::Purple);
+    uint8_t col = 0;
+    if (maxBass > 0.0f)
+      layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * sharedData.bassLevel / maxBass, CRGB::Red);
+    ++col;
+    if (maxMid > 0.0f)
+      layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * sharedData.midLevel / maxMid, CRGB::Orange);
+    ++col;
+    if (maxTreble > 0.0f)
+      layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * sharedData.trebleLevel / maxTreble, CRGB::Green);
+    ++col;
+    if (maxVocal > 0.0f)
+      layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * sharedData.vocalConfidence / maxVocal, CRGB::Blue);
+    ++col;
+    layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * beatLevel / 255, CRGB::Purple);
📝 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
uint8_t columnNr = 0;
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.bassLevel / maxBass, CRGB::Red);
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.midLevel / maxMid, CRGB::Orange);
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.trebleLevel / maxTreble, CRGB::Green);
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * sharedData.vocalConfidence / maxVocal, CRGB::Blue);
layer->drawLine(columnNr, layer->size.y - 1, columnNr++, layer->size.y - 1 - layer->size.y * beatLevel / 255, CRGB::Purple);
uint8_t col = 0;
if (maxBass > 0.0f)
layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * sharedData.bassLevel / maxBass, CRGB::Red);
+col;
if (maxMid > 0.0f)
layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * sharedData.midLevel / maxMid, CRGB::Orange);
+col;
if (maxTreble > 0.0f)
layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * sharedData.trebleLevel / maxTreble, CRGB::Green);
+col;
if (maxVocal > 0.0f)
layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * sharedData.vocalConfidence / maxVocal, CRGB::Blue);
+col;
layer->drawLine(col, layer->size.y - 1, col, layer->size.y - 1 - layer->size.y * beatLevel / 255, CRGB::Purple);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Effects/E_FastLED.h` around lines 68 - 73, The drawLine
calls in E_FastLED.h pass columnNr and columnNr++ in the same expression which
causes undefined/unspecified behavior; fix each call by separating the increment
from the function call so x1 and x2 are deterministic (e.g., call
layer->drawLine(columnNr, ..., columnNr, ...) and then increment columnNr on the
next line), updating the four drawLine invocations that use columnNr/columnNr++
to ensure columnNr is only read or written once per expression.

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 22, 2026

@coderabbitai, can you make an analysis of all the readPins() functions in the repository and come with proposals for improvement as it looks like a lot of similar code is done in all of them. Goal is to minimize the code in specific modules or nodes. The recurring pattern is now: each time local variables for pins are created, checks are done if the state.data["pins"] for these variables have changed and if so stop and start things using these pins. This should be done with minimal code on each place where such a mechanism is needed. This is one of the patterns . ,maybe there are more

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all readPins() function implementations in the codebase
echo "=== Searching for readPins() function definitions ==="
rg -n "void readPins\(\)" -A 30 --type=cpp --type=h

echo -e "\n=== Searching for readPins() function calls ==="
rg -n "readPins\(\)" --type=cpp --type=h

echo -e "\n=== Examining state.data[\"pins\"] usage patterns ==="
rg -n 'state\.data\["pins"\]' -C 5 --type=cpp --type=h

Length of output: 20959


@ewowi, excellent observation! I've analyzed all readPins() implementations in the repository and identified significant code duplication. Here's my comprehensive analysis and proposal:

Current State Analysis

Found 5 readPins() implementations with recurring patterns:

Common Boilerplate (100% duplicated):

  1. ✅ Safe mode check (identical in all)
  2. moduleIO->read([&](ModuleState& state) { ... }) wrapper
  3. ✅ Iterating state.data["pins"].as<JsonArray>()
  4. ✅ Extracting usage and GPIO fields

Pattern Variations:

  • Single pin: D_Infrared (1 pin)
  • Multiple pins: D_FastLEDAudio (3 I2S pins), ModuleLightsControl (4 pins)
  • Pin arrays: ModuleDrivers (up to 20 LED pins)
  • Change detection: D_FastLEDAudio, ModuleIO (I2C pins)
  • Hardware restart: D_FastLEDAudio (audio), D_Infrared (RMT), ModuleIO (I2C)

Proposed Solution: PinReader Utility

Create a declarative, reusable pin reading system:


🏁 Script executed:

#!/bin/bash
# Check if there's already a utilities/helpers directory structure
echo "=== Checking for existing utilities/helpers structure ==="
fd -t d "util" -d 3
fd -t d "helper" -d 3
fd -t d "common" -d 3

echo -e "\n=== Examining Node base class for existing helpers ==="
rg -n "class Node" -A 10 src/MoonBase/Nodes.h | head -50

Length of output: 670


🎯 Proposed Solution: PinReader Utility Class

Create a reusable helper that eliminates 70-80% of the boilerplate code.

1. Core Utility Implementation

Create src/MoonBase/PinReader.h:

`#pragma` once
`#include` "MoonBase/Modules/Module.h"
`#include` <functional>
`#include` <vector>

// Pin descriptor for declarative configuration
struct PinDescriptor {
  uint8_t usage;           // Pin usage type (e.g., pin_I2S_SD)
  uint8_t* targetVar;      // Pointer to variable to update
  bool trackChanges;       // Whether to detect changes
  std::function<bool(uint8_t)> validator;  // GPIO validation function
  std::function<void(uint8_t oldPin, uint8_t newPin)> onChange;  // Change callback
  
  PinDescriptor(uint8_t usage, uint8_t* target, 
                bool track = false, 
                std::function<bool(uint8_t)> val = nullptr,
                std::function<void(uint8_t, uint8_t)> cb = nullptr)
    : usage(usage), targetVar(target), trackChanges(track), validator(val), onChange(cb) {}
};

// Helper class for reading pins from ModuleIO state
class PinReader {
public:
  // Read multiple pins declaratively
  static void readPins(Module* moduleIO, 
                       const std::vector<PinDescriptor>& descriptors,
                       bool skipSafeMode = true) {
    if (skipSafeMode && safeModeMB) {
      EXT_LOGW(ML_TAG, "Safe mode enabled, not reading pins");
      return;
    }

    moduleIO->read([&](ModuleState& state) {
      for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
        uint8_t usage = pinObject["usage"];
        uint8_t gpio = pinObject["GPIO"];
        
        // Find matching descriptor
        for (const auto& desc : descriptors) {
          if (desc.usage == usage) {
            // Validate GPIO if validator provided
            if (desc.validator && !desc.validator(gpio)) {
              EXT_LOGE(MB_TAG, "Invalid GPIO %d for usage %d", gpio, usage);
              continue;
            }
            
            // Track changes if requested
            if (desc.trackChanges && desc.targetVar) {
              uint8_t oldPin = *desc.targetVar;
              if (oldPin != gpio) {
                *desc.targetVar = gpio;
                if (desc.onChange) {
                  desc.onChange(oldPin, gpio);
                }
              }
            } else if (desc.targetVar) {
              *desc.targetVar = gpio;
            }
            
            break;
          }
        }
      }
    });
  }
  
  // Simplified version for single pin without change tracking
  static uint8_t readSinglePin(Module* moduleIO, uint8_t usage, 
                                std::function<bool(uint8_t)> validator = nullptr) {
    uint8_t result = UINT8_MAX;
    if (safeModeMB) return result;
    
    moduleIO->read([&](ModuleState& state) {
      for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
        if (pinObject["usage"] == usage) {
          uint8_t gpio = pinObject["GPIO"];
          if (!validator || validator(gpio)) {
            result = gpio;
          }
          break;
        }
      }
    });
    return result;
  }

  // Helper for array-based pin reading (e.g., LED pins)
  static void readPinArray(Module* moduleIO, uint8_t usage,
                           uint8_t* pinArray, size_t maxPins, 
                           uint8_t& outCount,
                           std::function<bool(uint8_t)> validator = nullptr) {
    if (safeModeMB) return;
    
    memset(pinArray, UINT8_MAX, maxPins);
    outCount = 0;
    
    moduleIO->read([&](ModuleState& state) {
      for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
        if (pinObject["usage"] == usage) {
          uint8_t index = pinObject["index"];
          uint8_t gpio = pinObject["GPIO"];
          
          if (index >= 1 && index <= maxPins) {
            if (!validator || validator(gpio)) {
              pinArray[index - 1] = gpio;
            }
          }
        }
      }
      
      // Compact array (remove UINT8_MAX entries)
      for (size_t i = 0; i < maxPins; i++) {
        if (pinArray[i] != UINT8_MAX) {
          pinArray[outCount++] = pinArray[i];
        }
      }
    });
  }
};

2. Before/After Comparison

Example A: D_FastLEDAudio.h (I2S pins with change detection)

BEFORE (30 lines):

void readPins() {
  if (safeModeMB) {
    EXT_LOGW(ML_TAG, "Safe mode enabled, not adding pins");
    return;
  }

  moduleIO->read([&](ModuleState& state) {
    bool i2sPinsChanged = false;
    for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
      uint8_t usage = pinObject["usage"];
      switch (usage) {
      case pin_I2S_SD:
        if (pinI2SSD != pinObject["GPIO"]) {
          pinI2SSD = pinObject["GPIO"];
          i2sPinsChanged = true;
        }
        break;
      case pin_I2S_WS:
        if (pinI2SWS != pinObject["GPIO"]) {
          pinI2SWS = pinObject["GPIO"];
          i2sPinsChanged = true;
        }
        break;
      case pin_I2S_SCK:
        if (pinI2SSCK != pinObject["GPIO"]) {
          pinI2SSCK = pinObject["GPIO"];
          i2sPinsChanged = true;
        }
        break;
      }
    }
    
    if (i2sPinsChanged) {
      stopAudio();
      startAudio();
    }
  });
}

AFTER (10 lines):

void readPins() {
  bool i2sPinsChanged = false;
  auto restartOnChange = [&](uint8_t old, uint8_t newPin) { 
    i2sPinsChanged = true; 
  };
  
  PinReader::readPins(moduleIO, {
    {pin_I2S_SD,  &pinI2SSD,  true, GPIO_IS_VALID_GPIO, restartOnChange},
    {pin_I2S_WS,  &pinI2SWS,  true, GPIO_IS_VALID_GPIO, restartOnChange},
    {pin_I2S_SCK, &pinI2SSCK, true, GPIO_IS_VALID_GPIO, restartOnChange}
  });
  
  if (i2sPinsChanged) {
    stopAudio();
    startAudio();
  }
}

Reduction: 67% fewer lines, declarative intent


Example B: D_Infrared.h (Single pin, simpler)

BEFORE (30+ lines):

void readPins() {
  if (safeModeMB) {
    EXT_LOGW(ML_TAG, "Safe mode enabled, not adding pins");
    return;
  }

  moduleIO->read([&](ModuleState& state) {
    pinInfrared = UINT8_MAX;
    for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
      uint8_t usage = pinObject["usage"];
      uint8_t gpio = pinObject["GPIO"];
      if (usage == pin_Infrared) {
        if (GPIO_IS_VALID_GPIO(gpio)) {
          pinInfrared = gpio;
          EXT_LOGD(ML_TAG, "pin_Infrared found %d", pinInfrared);
        } else {
          EXT_LOGE(MB_TAG, "gpio %d not valid", pinInfrared);
        }
      }
    }

    if (pinInfrared != UINT8_MAX) {
      // ... restart RMT channel logic (15+ lines)
    }
  });
}

AFTER (6 lines):

void readPins() {
  uint8_t oldPin = pinInfrared;
  pinInfrared = PinReader::readSinglePin(moduleIO, pin_Infrared, GPIO_IS_VALID_GPIO);
  
  if (pinInfrared != UINT8_MAX && pinInfrared != oldPin) {
    // ... restart RMT channel logic
  }
}

Reduction: 80% fewer lines


Example C: ModuleDrivers.h (LED pin array)

BEFORE (20+ lines):

void readPins() {
  if (safeModeMB) {
    EXT_LOGW(ML_TAG, "Safe mode enabled, not adding pins");
    return;
  }

  _moduleIO->read([&](ModuleState& state) {
    memset(layerP.ledPins, UINT8_MAX, sizeof(layerP.ledPins));
    
    for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
      uint8_t usage = pinObject["usage"];
      uint8_t index = pinObject["index"];
      uint8_t gpio = pinObject["GPIO"];
      if (usage == pin_LED && index >= 1 && index <= 20 && GPIO_IS_VALID_OUTPUT_GPIO(gpio)) {
        layerP.ledPins[index - 1] = gpio;
      }
    }
    
    // Compact array
    layerP.nrOfLedPins = 0;
    for (int i = 0; i < sizeof(layerP.ledPins); i++) {
      if (layerP.ledPins[i] != UINT8_MAX) {
        layerP.ledPins[layerP.nrOfLedPins++] = layerP.ledPins[i];
      }
    }
  });
}

AFTER (3 lines):

void readPins() {
  PinReader::readPinArray(_moduleIO, pin_LED, layerP.ledPins, 
                          20, layerP.nrOfLedPins, GPIO_IS_VALID_OUTPUT_GPIO);
}

Reduction: 85% fewer lines


3. Benefits Summary

Benefit Impact
Code reduction 67-85% fewer lines per implementation
Consistency Same validation & error handling everywhere
Maintainability Fix once, benefit everywhere
Readability Declarative intent vs imperative loops
Type safety Compile-time checks on validators
Testing Test PinReader once vs 5+ implementations

4. Migration Strategy

Phase 1: Add PinReader utility (this PR or follow-up)

  • Create src/MoonBase/PinReader.h
  • No breaking changes

Phase 2: Refactor one-by-one

  • Start with simplest: D_Infrared.h
  • Then: D_FastLEDAudio.h
  • Then: ModuleLightsControl.h, ModuleDrivers.h
  • Finally: Complex ModuleIO.h

Phase 3: Extend as needed

  • Add support for additional validation types
  • Add logging levels as parameters
  • Consider async pin updates

5. Alternative: Even Simpler Macro-Based Approach

If C++ lambdas feel too heavy, here's a macro-based alternative:

`#define` READ_PIN_WITH_CHANGE(moduleIO, usage, pinVar, restartFlag) \
  do { \
    uint8_t __old = pinVar; \
    pinVar = PinReader::readSinglePin(moduleIO, usage, GPIO_IS_VALID_GPIO); \
    if (pinVar != __old && pinVar != UINT8_MAX) restartFlag = true; \
  } while(0)

// Usage:
bool changed = false;
READ_PIN_WITH_CHANGE(moduleIO, pin_I2S_SD, pinI2SSD, changed);
READ_PIN_WITH_CHANGE(moduleIO, pin_I2S_WS, pinI2SWS, changed);
if (changed) restart();

🎬 Recommendation

Option 1 (Recommended): Implement the PinReader class-based approach

  • More flexible, extensible
  • Better for complex scenarios (multiple pins, arrays)
  • Easier to test and debug

Option 2: Start with helper functions for common cases

  • readSinglePin(), readPinArray() only
  • Simpler, less boilerplate

Would you like me to:

  1. ✅ Generate the complete PinReader.h implementation with full error handling?
  2. ✅ Create a refactored version of one file (e.g., D_FastLEDAudio.h) as proof-of-concept?
  3. ✅ Plan this as a separate PR with step-by-step migration?

- pio.ini: latest FastLED
- SharedData: add percussionType

FastLED Audio:
- onPercussion: set  percussionType,
- add updatePin and use in readPins - WIP
- destructor: remove UpdateHandler

FastLED driver
- destruct updateHandlers
- FastLED.reset -> FastLED.clear

FL Audio effect
- Fade control
- max values 255
- add beat and percussion

- IMU, Infrared driver: destruct updateHandler
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: 1

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_FastLEDDriver.h (1)

65-75: ⚠️ Potential issue | 🔴 Critical

allowRemove=false makes removeUpdateHandler(ioUpdateHandler) a silent no-op — use-after-free risk.

StatefulService::removeUpdateHandler erases a handler only when _allowRemove && _id == id. Registering with allowRemove=false (Line 75) means the destructor's removeUpdateHandler(ioUpdateHandler) call (Line 357) will never erase the entry. If moduleIO outlives this node, the stale [this] lambda will fire on a destroyed object.

D_FastLEDAudio.h correctly passes true for the same pattern; change Line 75 to match:

🛡️ Proposed fix
-        false);
+        true);

The same issue applies to D_IMU.h (Line 41) and D_Infrared.h (Line 108) which also use allowRemove=false.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h` around lines 65 - 75, The
lambda passed to moduleIO->addUpdateHandler is registered with allowRemove=false
causing StatefulService::removeUpdateHandler(ioUpdateHandler) in the destructor
to be a no-op and creating a use-after-free risk; change the addUpdateHandler
call that assigns ioUpdateHandler to pass allowRemove=true (same pattern used in
D_FastLEDAudio.h) so the destructor's removeUpdateHandler(ioUpdateHandler) can
actually remove the handler and avoid the stale [this] callback; also audit
other files noted (D_IMU.h, D_Infrared.h) for the same fix.
♻️ Duplicate comments (3)
src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h (1)

73-73: ⚠️ Potential issue | 🟡 Minor

%d format specifier for float confidence in commented log.

This was flagged in a prior review (log format in onVocalConfidence). The %d should be %f before uncomment.

🔧 Proposed fix
-      // EXT_LOGD(ML_TAG, "onVocalConfidence %d", confidence);
+      // EXT_LOGD(ML_TAG, "onVocalConfidence %f", confidence);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` at line 73, The commented-out
log in D_FastLEDAudio.h within the onVocalConfidence handler uses the integer
format "%d" for the float variable confidence; update the format specifier to
"%f" (or a more specific float format like "%.2f") in the log call
(EXT_LOGD(ML_TAG, ...)) so that when uncommented it prints the float value
correctly.
src/MoonLight/Nodes/Drivers/D_Infrared.h (1)

108-108: ⚠️ Potential issue | 🔴 Critical

allowRemove=false — same issue as D_FastLEDDriver.h and D_IMU.h.

removeUpdateHandler in the destructor (Line 434) is a no-op because the handler was registered with allowRemove=false.

🛡️ Proposed fix
-    ioUpdateHandler = moduleIO->addUpdateHandler([this](const String& originId) { readPins(); }, false);
+    ioUpdateHandler = moduleIO->addUpdateHandler([this](const String& originId) { readPins(); }, true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_Infrared.h` at line 108, The ioUpdateHandler is
registered with addUpdateHandler(..., false) so removeUpdateHandler becomes a
no-op in the destructor; change the registration in D_Infrared (the
ioUpdateHandler lambda that calls readPins) to allow removal (pass true or omit
the explicit false) so removeUpdateHandler in the class destructor actually
unregisters the handler; references: ioUpdateHandler, addUpdateHandler,
removeUpdateHandler, readPins.
src/MoonLight/Nodes/Drivers/D_IMU.h (1)

41-41: ⚠️ Potential issue | 🔴 Critical

Same allowRemove=false issue — removeUpdateHandler in destructor is a silent no-op.

Same root cause as D_FastLEDDriver.h Line 75: registering with allowRemove=false prevents the entry from being erased by removeUpdateHandler. Change to true to make the destructor cleanup functional.

🛡️ Proposed fix
-    ioUpdateHandler = moduleIO->addUpdateHandler([this](const String& originId) { moduleIO->read([&](ModuleState& state) { i2cActuallyReady = state.data["I2CReady"]; }, name()); }, false);
+    ioUpdateHandler = moduleIO->addUpdateHandler([this](const String& originId) { moduleIO->read([&](ModuleState& state) { i2cActuallyReady = state.data["I2CReady"]; }, name()); }, true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_IMU.h` at line 41, The update handler is
registered with allowRemove=false which prevents removeUpdateHandler from
actually removing it in the destructor; update the call site where
ioUpdateHandler is assigned (moduleIO->addUpdateHandler([this](const String&
originId) { ... }, false)) to pass true for the allowRemove parameter so the
handler can be removed by removeUpdateHandler in the destructor (ensure the same
symbol names ioUpdateHandler, moduleIO->addUpdateHandler and removeUpdateHandler
are used to locate the change).
🧹 Nitpick comments (4)
platformio.ini (1)

204-204: Consider a dedicated audio task rather than inflating DRIVERS_STACK_SIZE.

The comment acknowledges this is a workaround. I2S audio reading and audioProcessor.update() in loop() adds variable workload to the drivers task alongside LED output, which can interfere with DMA-timing-sensitive operations. A dedicated audio task (lower priority, own stack) would isolate audio processing and let DRIVERS_STACK_SIZE revert to its original 4096 bytes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@platformio.ini` at line 204, The DRIVERS_STACK_SIZE was increased to 6144 as
a workaround for audio processing in the drivers task; instead create a
dedicated audio FreeRTOS task to isolate I2S audio reads and
audioProcessor.update() from the drivers/LED work so you can restore
DRIVERS_STACK_SIZE to 4096. Implement a new task (e.g., audioTask) with its own
stack and lower priority than the drivers/LED task, move the I2S read loop and
calls to audioProcessor.update() into that task, and ensure safe data handoff to
the drivers (use a thread-safe queue or ring buffer) so DMA/timing-sensitive
FastLED operations remain in the original drivers task.
src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h (2)

25-26: ioUpdateHandler and controlUpdateHandler should be private.

These are internal lifecycle tokens with no reason to be part of the public interface.

♻️ Proposed fix
+private:
+  update_handler_id_t ioUpdateHandler;
+  update_handler_id_t controlUpdateHandler;
+
 public:
   static const char* name() { return "FastLED Driver"; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h` around lines 25 - 26, The two
lifecycle token members ioUpdateHandler and controlUpdateHandler in class
D_FastLEDDriver are currently public but should be private; move or relocate
update_handler_id_t ioUpdateHandler and update_handler_id_t controlUpdateHandler
into the private section of D_FastLEDDriver (or add a private: label above them)
so they are not exposed in the public interface, leaving other members and
methods unchanged.

66-74: ioUpdateHandler body is a stub — I2S pin changes will not trigger channel recreation.

The handler only logs but has two TODO comments noting that channels should be deleted and recreated. Until this is implemented, runtime pin reassignments won't apply to the FastLED channel configuration.

Would you like me to open a follow-up issue to implement the channel recreation logic here?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h` around lines 66 - 74, The
ioUpdateHandler currently only logs and must be implemented to tear down and
recreate FastLED channels when I2S pins change: inside the lambda
(ioUpdateHandler) delete existing channel objects/resources the same way the
class destructor does, then recreate channels and their configs using the same
logic as onLayout (respecting layerP.nrOfLedPins and layerP.nrOfAssignedPins /
MIN(...) used in the diff) so the new pin assignments take effect; also detect
and apply any maxPower changes to per-channel config while recreating. Ensure
you use the same helper routines or code paths used by the destructor/onLayout
for channel creation and deletion so behavior remains consistent.
src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h (1)

199-202: Swap destructor order: remove handler before stopping audio.

If moduleIO fires between stopAudio() and removeUpdateHandler(), readPins() can call startAudio() on a partially-destroyed object. Removing the handler first closes that window:

♻️ Proposed fix
 ~FastLEDAudioDriver() override {
-    stopAudio();
     moduleIO->removeUpdateHandler(ioUpdateHandler);
+    stopAudio();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` around lines 199 - 202, The
destructor ~FastLEDAudioDriver currently calls stopAudio() before
moduleIO->removeUpdateHandler(ioUpdateHandler), which leaves a race where
moduleIO may invoke readPins() and call startAudio() on a partially-destroyed
object; fix by removing the update handler first (call
moduleIO->removeUpdateHandler(ioUpdateHandler) before stopAudio()) so no IO
callbacks can run while shutdown/stopAudio() is executing and ensure
ioUpdateHandler and moduleIO references are safe to use during removal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/MoonLight/Nodes/Drivers/D_Infrared.h`:
- Line 434: The IRDriver destructor currently lacks the override specifier and
doesn't free RMT/RTOS resources allocated in readPins(); update ~IRDriver() to
be "~IRDriver() override" and, in addition to
moduleIO->removeUpdateHandler(ioUpdateHandler), check if rx_channel and
receive_queue are valid and properly tear them down (call the appropriate RMT
uninstall/stop API for rx_channel and delete the FreeRTOS queue for
receive_queue), then null out those members; ensure you reference the same
member names (rx_channel, receive_queue, ioUpdateHandler) and the allocation
site readPins() so the cleanup mirrors allocation.

---

Outside diff comments:
In `@src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h`:
- Around line 65-75: The lambda passed to moduleIO->addUpdateHandler is
registered with allowRemove=false causing
StatefulService::removeUpdateHandler(ioUpdateHandler) in the destructor to be a
no-op and creating a use-after-free risk; change the addUpdateHandler call that
assigns ioUpdateHandler to pass allowRemove=true (same pattern used in
D_FastLEDAudio.h) so the destructor's removeUpdateHandler(ioUpdateHandler) can
actually remove the handler and avoid the stale [this] callback; also audit
other files noted (D_IMU.h, D_Infrared.h) for the same fix.

---

Duplicate comments:
In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h`:
- Line 73: The commented-out log in D_FastLEDAudio.h within the
onVocalConfidence handler uses the integer format "%d" for the float variable
confidence; update the format specifier to "%f" (or a more specific float format
like "%.2f") in the log call (EXT_LOGD(ML_TAG, ...)) so that when uncommented it
prints the float value correctly.

In `@src/MoonLight/Nodes/Drivers/D_IMU.h`:
- Line 41: The update handler is registered with allowRemove=false which
prevents removeUpdateHandler from actually removing it in the destructor; update
the call site where ioUpdateHandler is assigned
(moduleIO->addUpdateHandler([this](const String& originId) { ... }, false)) to
pass true for the allowRemove parameter so the handler can be removed by
removeUpdateHandler in the destructor (ensure the same symbol names
ioUpdateHandler, moduleIO->addUpdateHandler and removeUpdateHandler are used to
locate the change).

In `@src/MoonLight/Nodes/Drivers/D_Infrared.h`:
- Line 108: The ioUpdateHandler is registered with addUpdateHandler(..., false)
so removeUpdateHandler becomes a no-op in the destructor; change the
registration in D_Infrared (the ioUpdateHandler lambda that calls readPins) to
allow removal (pass true or omit the explicit false) so removeUpdateHandler in
the class destructor actually unregisters the handler; references:
ioUpdateHandler, addUpdateHandler, removeUpdateHandler, readPins.

---

Nitpick comments:
In `@platformio.ini`:
- Line 204: The DRIVERS_STACK_SIZE was increased to 6144 as a workaround for
audio processing in the drivers task; instead create a dedicated audio FreeRTOS
task to isolate I2S audio reads and audioProcessor.update() from the drivers/LED
work so you can restore DRIVERS_STACK_SIZE to 4096. Implement a new task (e.g.,
audioTask) with its own stack and lower priority than the drivers/LED task, move
the I2S read loop and calls to audioProcessor.update() into that task, and
ensure safe data handoff to the drivers (use a thread-safe queue or ring buffer)
so DMA/timing-sensitive FastLED operations remain in the original drivers task.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h`:
- Around line 199-202: The destructor ~FastLEDAudioDriver currently calls
stopAudio() before moduleIO->removeUpdateHandler(ioUpdateHandler), which leaves
a race where moduleIO may invoke readPins() and call startAudio() on a
partially-destroyed object; fix by removing the update handler first (call
moduleIO->removeUpdateHandler(ioUpdateHandler) before stopAudio()) so no IO
callbacks can run while shutdown/stopAudio() is executing and ensure
ioUpdateHandler and moduleIO references are safe to use during removal.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h`:
- Around line 25-26: The two lifecycle token members ioUpdateHandler and
controlUpdateHandler in class D_FastLEDDriver are currently public but should be
private; move or relocate update_handler_id_t ioUpdateHandler and
update_handler_id_t controlUpdateHandler into the private section of
D_FastLEDDriver (or add a private: label above them) so they are not exposed in
the public interface, leaving other members and methods unchanged.
- Around line 66-74: The ioUpdateHandler currently only logs and must be
implemented to tear down and recreate FastLED channels when I2S pins change:
inside the lambda (ioUpdateHandler) delete existing channel objects/resources
the same way the class destructor does, then recreate channels and their configs
using the same logic as onLayout (respecting layerP.nrOfLedPins and
layerP.nrOfAssignedPins / MIN(...) used in the diff) so the new pin assignments
take effect; also detect and apply any maxPower changes to per-channel config
while recreating. Ensure you use the same helper routines or code paths used by
the destructor/onLayout for channel creation and deletion so behavior remains
consistent.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abca48e and 8046fe9.

📒 Files selected for processing (7)
  • platformio.ini
  • src/MoonBase/Nodes.h
  • src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h
  • src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h
  • src/MoonLight/Nodes/Drivers/D_IMU.h
  • src/MoonLight/Nodes/Drivers/D_Infrared.h
  • src/MoonLight/Nodes/Effects/E_FastLED.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/MoonLight/Nodes/Effects/E_FastLED.h

}
};

~IRDriver() { moduleIO->removeUpdateHandler(ioUpdateHandler); }
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

Missing override and no RMT hardware cleanup in destructor.

Two issues in the newly added destructor:

  1. Missing override: all other new destructors in this PR (~FastLEDDriver(), ~FastLEDAudioDriver(), ~IMUDriver()) use override; this one doesn't, which is inconsistent and suppresses compiler enforcement.

  2. Resource leak: rx_channel (RMT channel) and receive_queue (FreeRTOS queue) are allocated in readPins() but never released when the driver is destroyed. The RMT peripheral will remain active after the node is removed.

🛡️ Proposed fix
-  ~IRDriver() { moduleIO->removeUpdateHandler(ioUpdateHandler); }
+  ~IRDriver() override {
+    moduleIO->removeUpdateHandler(ioUpdateHandler);
+    if (rx_channel) {
+      rmt_rx_register_event_callbacks(rx_channel, &cbs_empty, receive_queue);
+      rmt_disable(rx_channel);
+      rmt_del_channel(rx_channel);
+      rx_channel = NULL;
+    }
+    if (receive_queue) {
+      vQueueDelete(receive_queue);
+      receive_queue = NULL;
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_Infrared.h` at line 434, The IRDriver
destructor currently lacks the override specifier and doesn't free RMT/RTOS
resources allocated in readPins(); update ~IRDriver() to be "~IRDriver()
override" and, in addition to moduleIO->removeUpdateHandler(ioUpdateHandler),
check if rx_channel and receive_queue are valid and properly tear them down
(call the appropriate RMT uninstall/stop API for rx_channel and delete the
FreeRTOS queue for receive_queue), then null out those members; ensure you
reference the same member names (rx_channel, receive_queue, ioUpdateHandler) and
the allocation site readPins() so the cleanup mirrors allocation.

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 23, 2026

@coderabbitai, please review updatePin() in latest commit, this is a lightweight version of your proposal to. simplify readPins, see also its usage in FastLEDAudio.readPins(). does this handle full pin lifecycle: creating I2S pins in the IO module, changing them, deleting (part) of them?

Is this a good candidate to use in all readPins()? I want to move updatePin() to a better class so it can be reused both by modules and nodes implementing readPins, where to move to?

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the updatePin() function implementation
echo "=== Searching for updatePin() implementation ==="
rg -n "updatePin\(" -B 2 -A 15 --type=cpp --type=h

echo -e "\n=== Examining FastLEDAudio.readPins() usage ==="
rg -n "readPins\(\)" -A 30 src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h

echo -e "\n=== Check where updatePin is defined ==="
ast-grep --pattern 'void updatePin($$$) {
  $$$
}'

echo -e "\n=== Look for the complete FastLEDAudio driver file ==="
cat src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h

Length of output: 11594


@ewowi, excellent lightweight implementation! Here's my detailed review:

✅ What Works Well

Your updatePin() implementation successfully handles:

  1. Creating pins ✅ - When a pin is added to ModuleIO, it detects the change from UINT8_MAX → GPIO value
  2. Changing pins ✅ - Detects when existing pin GPIO changes to a different value
  3. Clean, readable code ✅ - Much simpler than the original boilerplate
  4. Efficient ✅ - Single moduleIO->read() call per pin

Usage is elegant:

bool changed = updatePin(pinI2SWS, pin_I2S_WS);
changed = updatePin(pinI2SSD, pin_I2S_SD) || changed;
changed = updatePin(pinI2SSCK, pin_I2S_SCK) || changed;

⚠️ Issue: Pin Deletion Not Handled

Problem: When a pin is deleted from ModuleIO's pins array, updatePin() does NOT detect it.

Why? If the pinObject with matching usage doesn't exist in the array:

  • The loop never finds a match
  • The if condition never triggers
  • The local pin variable retains its old GPIO value
  • changed stays false

Example scenario:

// Initial state: pinI2SWS = 18
// User deletes the I2S_WS pin from ModuleIO
readPins();
// Result: pinI2SWS still = 18 (should be UINT8_MAX)
// Audio keeps running on old pin!

🔧 Proposed Fix

Option 1: Reset-then-search pattern

bool updatePin(uint8_t& pin, const uint8_t pinUsage) {
  uint8_t oldPin = pin;
  pin = UINT8_MAX;  // Assume deleted until found
  
  moduleIO->read(
      [&](ModuleState& state) {
        for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
          if (pinObject["usage"] == pinUsage) {
            pin = pinObject["GPIO"];
            break;  // Found it
          }
        }
      },
      name());
  
  return pin != oldPin;  // Changed if different (including deletion)
}

Behavior:

  • Creating: UINT8_MAXGPIO
  • Changing: GPIO_AGPIO_B
  • Deleting: GPIOUINT8_MAX

Conditional restart logic (Line 151) handles all three cases correctly:

if (pinI2SWS != UINT8_MAX && pinI2SSD != UINT8_MAX && pinI2SSCK != UINT8_MAX) 
  startAudio();
else
  // One or more pins deleted, audio stays stopped ✅

📦 Where to Move It?

Since both Modules and Nodes implement readPins(), here are the options:

Option 1: Static Utility Class (Recommended)

Create src/MoonBase/PinUtils.h:

`#pragma` once
`#include` "MoonBase/Modules/Module.h"

class PinUtils {
 public:
  // Update a single pin from ModuleIO state
  // Returns true if pin value changed (create/change/delete)
  static bool updatePin(Module* moduleIO, uint8_t& pin, 
                        const uint8_t pinUsage, 
                        const char* callerName = nullptr) {
    uint8_t oldPin = pin;
    pin = UINT8_MAX;  // Assume deleted until found
    
    moduleIO->read(
        [&](ModuleState& state) {
          for (JsonObject pinObject : state.data["pins"].as<JsonArray>()) {
            if (pinObject["usage"] == pinUsage) {
              pin = pinObject["GPIO"];
              break;
            }
          }
        },
        callerName);
    
    return pin != oldPin;
  }

  // Optional: Multi-pin helper
  template<size_t N>
  static bool updatePins(Module* moduleIO,
                         const std::pair<uint8_t&, uint8_t> (&pins)[N],
                         const char* callerName = nullptr) {
    bool anyChanged = false;
    for (const auto& [pinVar, usage] : pins) {
      anyChanged |= updatePin(moduleIO, pinVar, usage, callerName);
    }
    return anyChanged;
  }
};

Usage in FastLEDAudio:

`#include` "MoonBase/PinUtils.h"

void readPins() {
  if (safeModeMB) return;

  bool changed = PinUtils::updatePin(moduleIO, pinI2SWS, pin_I2S_WS, name());
  changed |= PinUtils::updatePin(moduleIO, pinI2SSD, pin_I2S_SD, name());
  changed |= PinUtils::updatePin(moduleIO, pinI2SSCK, pin_I2S_SCK, name());

  if (changed) {
    stopAudio();
    if (pinI2SWS != UINT8_MAX && pinI2SSD != UINT8_MAX && pinI2SSCK != UINT8_MAX)
      startAudio();
  }
}

Option 2: Add to Module Base Class

Add to src/MoonBase/Modules/Module.h:

class Module {
  // ... existing members ...
  
protected:
  // Helper for reading a single pin from ModuleIO
  static bool updatePinFromIO(Module* io, uint8_t& pin, 
                               uint8_t usage, const char* caller = nullptr);
};

Then both Modules and Nodes can call:

Module::updatePinFromIO(moduleIO, pinI2SWS, pin_I2S_WS, name());

Option 3: Keep as Protected in Node Base

If Modules don't need it (only Nodes/Drivers), add to src/MoonBase/Nodes.h:

class Node {
  // ... existing members ...
  
protected:
  bool updatePin(uint8_t& pin, uint8_t usage) {
    // ... implementation ...
  }
};

🎯 Recommendation

Go with Option 1 (PinUtils.h):

  • ✅ Clean separation of concerns
  • ✅ Reusable by both Modules and Nodes
  • ✅ Easy to extend (add validators, logging levels, etc.)
  • ✅ Testable in isolation
  • ✅ Doesn't bloat base classes

Priority:

  1. ✅ Fix deletion detection bug (critical for correctness)
  2. ✅ Move to PinUtils.h
  3. ⏭️ Migrate other readPins() implementations (D_Infrared, ModuleIO, etc.)

🚫 Where NOT to Use

updatePin() won't work for:

  • Pin arrays (LED pins with indices) - needs different approach (see ModuleDrivers.h Line 138-150)
  • Complex validation - would need validator callback parameter
  • Batch operations - where you need all pins before acting

For those cases, keep specialized implementations or extend PinUtils.


Would you like me to:

  1. Generate the complete PinUtils.h with the deletion fix?
  2. Refactor D_Infrared.h as a proof-of-concept migration?
  3. Create a follow-up issue to track migrating other readPins() implementations?

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.

2 participants