Skip to content

Conversation

@limpkin
Copy link

@limpkin limpkin commented Jan 10, 2026

Summary by CodeRabbit

  • New Features
    • Added "Star Sky" visual effect: a 3D starfield with per-star brightness, fading, animated behavior, and controls for density and speed; adapts to size and control changes.
    • Per-board power sensing: voltage and current calculations now use board-specific formulas and a stored active board ID to apply the correct measurement logic.

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'review', 'context'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

StarSkyEffect, a new 3D star-field animation node, is added and registered in MoonLight effects ordering and allocation. ModuleIO gains a current_board_id field and board-aware ADC-to-voltage/current conversions for SE16V1 vs LightCrafter16, with branching logic applied where readings are computed.

Changes

Cohort / File(s) Summary
Module Integration
src/MoonLight/Modules/ModuleEffects.h
Inserts StarSkyEffect into public node ordering and allocation sequences (placed after FireEffect) so it appears in selection and instantiation flows.
New Effect Implementation
src/MoonLight/Nodes/Effects/E_MoonLight.h
Adds StarSkyEffect class: public identifiers (name, dim, tags), controls (star_speed, star_fill_ratio, usePalette), lifecycle methods (setup, onSizeChanged, onUpdate, loop), per-star buffers (nb_stars, stars_fade_dir, stars_indexes, stars_brightness, stars_colors), setup_animation() and resetStars() for allocation/cleanup, destructor, and runtime animation/brightness logic.
I/O & Board Handling
src/MoonBase/Modules/ModuleIO.h
Adds uint8_t current_board_id member; setBoardPresetDefaults stores active board ID; voltage and current calculations branch by board type (SE16V1 vs LightCrafter16) with distinct ADC-to-voltage/current formulas and gating behavior.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant ModuleEffects
  participant StarSkyEffect
  participant Allocator
  participant LEDRenderer

  User->>ModuleEffects: select "Star Sky"
  ModuleEffects->>StarSkyEffect: instantiate node
  StarSkyEffect->>Allocator: allocate per-star buffers (setup_animation)
  Allocator-->>StarSkyEffect: return pointers / status
  StarSkyEffect->>StarSkyEffect: initialize stars and state
  loop every frame
    StarSkyEffect->>StarSkyEffect: update star positions, brightness, fade directions
    StarSkyEffect->>LEDRenderer: write computed colors to LED buffer
    LEDRenderer-->>StarSkyEffect: frame rendered
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇✨ I count the bytes where starlight stays,
Buffers hum softly in semaphore bays.
I plant bright dots across the velvet sweep,
Hop, blink, buffer — awake while others sleep.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 'starsky effect' accurately matches the main changes, which introduce a new StarSkyEffect class and integrate it into the MoonLight effects system.

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

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0602d4 and d286b53.

📒 Files selected for processing (1)
  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
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: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 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.
📚 Learning: 2026-01-06T15:24:29.770Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-18T15:15:57.828Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T13:45:03.067Z
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).

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T15:24:17.118Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-28T10:28:24.157Z
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.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-18T15:59:30.556Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:59:30.556Z
Learning: In DripEffect (E_WLED.h), the bounce fails because the damped velocity is too weak to keep the drop at pos > 0 for multiple frames. When the drop returns to pos <= 0 while still in the bouncing state (colIndex == bouncing), it immediately resets to init (line 1663-1664) before any visible bounce occurs. The bounce velocity must be strong enough (divide by 1.1 or less, or use minimal damping like *= 0.9) so the drop stays airborne long enough for a visible bounce arc before the eventual second ground hit triggers the reset.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-25T13:34:40.097Z
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.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧬 Code graph analysis (1)
src/MoonLight/Nodes/Effects/E_MoonLight.h (3)
src/MoonBase/Nodes.h (2)
  • Node (67-182)
  • Node (238-238)
src/MoonBase/Module.cpp (2)
  • addControl (353-362)
  • addControl (353-353)
src/MoonLight/Nodes/Effects/E_MoonModules.h (2)
  • onSizeChanged (183-199)
  • onUpdate (120-124)
🔇 Additional comments (8)
src/MoonLight/Nodes/Effects/E_MoonLight.h (8)

41-46: LGTM: Class declaration follows framework conventions.

The static metadata methods are properly defined for effect registration.


48-51: LGTM: Control variables with sensible defaults.

The defaults provide a reasonable starting point for the effect.


53-58: LGTM: Proper deferred initialization.

Correctly defers setup_animation() to onSizeChanged() when the layer is guaranteed to be available. This addresses part of the initialization timing concerns from the crash analysis.


60-66: LGTM: Safe buffer initialization.

All pointers initialized to nullptr and nb_stars to 0, enabling the guard checks in loop() to detect uninitialized state correctly.


67-79: LGTM: Proper resource cleanup.

The destructor and resetStars() correctly free all allocated buffers and reset state. The pattern of checking before freeing and nulling after is defensive and correct.


103-106: LGTM: Appropriate triggers for re-initialization.

Correctly calls setup_animation() when layer size changes or when star_fill_ratio or usePalette controls change, as these affect buffer allocation and star count.


81-101: Allocation failure handling is correct and properly implemented.

The allocation checks at lines 88-92 correctly validate all allocMB return values and handle failures by calling resetStars() and returning early. The framework guarantees non-zero layer dimensions (initialized to prevent division-by-zero), so the divisions at line 112 in loop() are safe.


108-136: Guard is comprehensive, but division-by-zero and unmapped fixture concerns require verification.

The pointer guard at line 109 is good, but the 1D-to-3D index conversion at line 112 divides by layer->size.x and layer->size.y without validating they are non-zero. If either dimension is 0, this causes a crash. Other effects in the codebase (e.g., E_MovingHeads.h, VirtualLayer::XYZUnModified) also perform arithmetic on size dimensions without guards, suggesting the framework may guarantee size > 0, but this assumption should be verified.

Additionally, stars_indexes[i] is bounded by layer->nrOfLights (line 95, 127), but if the layer has unmapped LEDs or a non-rectangular shape, the computed position may exceed layer bounds. Per the framework design, effect developers are responsible for staying within bounds, but confirm that this effect is only used with fully-mapped rectangular fixtures, or add an out-of-bounds check after line 112 if complex geometries are intended to be supported.


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.

@limpkin
Copy link
Author

limpkin commented Jan 10, 2026

@CodeRabbit: getting the following error:

Guru Meditation Error: Core 1 panic'ed (StoreProhibited). Exception was unhandled.

Core 1 register dump:
PC : 0x40056fa1 PS : 0x00060430 A0 : 0x8206d9ec A1 : 0x3fceb360
A2 : 0x00000048 A3 : 0x3fceb3ce A4 : 0x00000003 A5 : 0x00000048
A6 : 0x00002525 A7 : 0x00000000 A8 : 0x00000000 A9 : 0x00000000
A10 : 0x3fcec218 A11 : 0x00000001 A12 : 0x803805ab A13 : 0x3fc9c640
A14 : 0x00000000 A15 : 0x0000044d SAR : 0x0000001f EXCCAUSE: 0x0000001d
EXCVADDR: 0x00000048 LBEG : 0x40056f5c LEND : 0x40056f72 LCOUNT : 0xffffffff

Backtrace: 0x40056f9e:0x3fceb360 0x4206d9e9:0x3fceb370 0x42044501:0x3fceb3c0 0x4207bf33:0x3fceb400 0x4208fe83:0x3fceb460 0x420867dc:0x3fceb490 0x4209c1c3:0x3fceb500 0x4203b1fa:0x3fceb5e0 0x4203b209:0x3fceb600 0x4203b2eb:0x3fceb620 0x4203b35a:0x3fceb640 0x4203de15:0x3fceb660 0x4203dfcb:0x3fceb8d0 0x4203e523:0x3fcebb40 0x4217b48b:0x3fcebc00 0x4203b6ae:0x3fcebc20 0x4203ee25:0x3fcebc40 0x4203eeb5:0x3fcebd40 0x42084fc1:0x3fcebda0 0x42085071:0x3fcebde0 0x4209f84d:0x3fcebe20 0x420abd46:0x3fcebe80 0x4037fd89:0x3fcebea0
#0 0x40056f9e in __ssputs_r in ROM
#1 0x4206d9e9 in VirtualLayer::setLight(unsigned long, unsigned char const*, unsigned char, unsigned char) at src/MoonLight/Layers/VirtualLayer.cpp:167
#2 0x42044501 in VirtualLayer::setRGB(unsigned long, CRGB) at src/MoonLight/Layers/VirtualLayer.h:123
#3 0x4207bf33 in StarSkyEffect::setup_animation() at src/MoonLight/Nodes/Effects/E_MoonLight.h:74 (discriminator 1)
#4 0x4208fe83 in StarSkyEffect::setup() at src/MoonLight/Nodes/Effects/E_MoonLight.h:84

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

🤖 Fix all issues with AI agents
In @src/MoonLight/Nodes/Effects/E_MoonLight.h:
- Around line 68-74: The per-star debug log in StarSkyEffect is too verbose;
replace or remove the EXT_LOGD call that logs each star's index/brightness/fade
(the log using stars_indexes[i], start_brightness, stars_fade_dir[i]) so it
doesn't flood logs — either change it to a verbose-level log (EXT_LOGV) for
per-star details or drop it entirely and keep only a single summary EXT_LOGD
after the loop that reports nb_stars and layer->nrOfLights; update the logging
around the loop that assigns stars_indexes[], stars_fade_dir[], and calls
layer->setRGB(...) accordingly.
- Around line 87-104: In loop20ms(), avoid uint8_t wraparound when changing
brightness: read current = layer->getRGB(stars_indexes[i]).r into a
wider/temporary variable or use the uint8_t but perform bounds checks before
doing +1/−1; compute new_brightness by clamping (e.g., if fading up and current
< 255 then new = current + 1 else set fade direction to 0 and new = 255;
similarly for fading down) and then call layer->setRGB(...) with new_brightness;
update references to stars_fade_dir, stars_indexes and nb_stars accordingly so
the boundary tests occur before setRGB to prevent overflow/underflow.
- Line 79: The onUpdate() method currently has the wrong signature so it never
overrides Node::onUpdate(const Char<20>& oldValue, const JsonObject& control)
and therefore changes to controls like slowness or star_fill_ratio won't call
setup_animation(); fix by replacing the no-arg onUpdate() with the correct
override signature (Node::onUpdate(const Char<20>& oldValue, const JsonObject&
control)) and call setup_animation() inside it when relevant controls changed
(check oldValue or inspect control), or if re-initialization on control change
is not desired simply remove the incorrect onUpdate() method.
- Around line 66-67: allocMB calls assigning stars_indexes and stars_fade_dir
can return null; after the allocations in the E_MoonLight initialization (where
allocMB<uint16_t>(nb_stars) and allocMB<uint8_t>(nb_stars) are called) add null
checks for both stars_indexes and stars_fade_dir and handle failure immediately
(e.g., free any previously allocated memory, log/error out, and return an error
or false from the initializer). Ensure you reference and clean up the other
member(s) if one allocation succeeds and the other fails to avoid leaks.
- Line 65: The star density calc using nb_stars = ((uint32_t)star_fill_ratio *
(uint32_t)layer->nrOfLights)/10000 + 1 produces unintuitive low densities;
update the divisor to a more intuitive scale (e.g., 100 or 1000) and revisit the
+1 offset so the slider maps sensibly to percentage-like behavior — replace the
10000 divisor with your chosen scale (100 or 1000) in the nb_stars computation
and remove or adjust the +1 if it causes an unwanted minimum of 1 star, ensuring
nb_stars scales proportionally with star_fill_ratio and layer->nrOfLights.
🧹 Nitpick comments (1)
src/MoonLight/Nodes/Effects/E_MoonLight.h (1)

69-75: Consider checking for duplicate star positions.

Multiple stars can currently occupy the same pixel index (line 70), which may cause them to interfere with each other during brightness updates. If this is unintentional, add duplicate checking during initialization.

💡 Example approach
std::set<uint16_t> used_positions;
for (uint32_t i = 0; i < nb_stars; i++) {
  uint16_t pos;
  int attempts = 0;
  do {
    pos = random16(layer->nrOfLights);
    attempts++;
  } while (used_positions.count(pos) && attempts < 100);
  
  stars_indexes[i] = pos;
  used_positions.insert(pos);
  // ... rest of initialization
}

Note: This requires including <set> and may impact performance for large star counts.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 987b5fb and afa4309.

📒 Files selected for processing (2)
  • src/MoonLight/Modules/ModuleEffects.h
  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
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.
🧬 Code graph analysis (2)
src/MoonLight/Nodes/Effects/E_MoonLight.h (3)
src/MoonBase/Nodes.h (2)
  • Node (67-182)
  • Node (238-238)
src/MoonLight/Layers/PhysicalLayer.cpp (2)
  • loop20ms (78-83)
  • loop20ms (78-78)
src/MoonLight/Layers/VirtualLayer.cpp (2)
  • loop20ms (71-75)
  • loop20ms (71-71)
src/MoonLight/Modules/ModuleEffects.h (1)
src/MoonLight/Nodes/Effects/E_MoonLight.h (1)
  • StarSkyEffect (42-105)
🔇 Additional comments (1)
src/MoonLight/Modules/ModuleEffects.h (1)

87-87: LGTM! StarSkyEffect registration is properly positioned.

The effect is correctly added to both the control list and allocation sequence, positioned after FireEffect as intended.

Also applies to: 180-180

freeMB(stars_fade_dir);
if (layer->nrOfLights == 0) { return; }
layer->fill_solid(CRGB::Black);
nb_stars = ((uint32_t)star_fill_ratio * (uint32_t)layer->nrOfLights)/10000 + 1;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file in question
head -n 100 src/MoonLight/Nodes/Effects/E_MoonLight.h | tail -n 50

Repository: MoonModules/MoonLight

Length of output: 2153


🏁 Script executed:

# Check the full context around line 65
sed -n '50,80p' src/MoonLight/Nodes/Effects/E_MoonLight.h

Repository: MoonModules/MoonLight

Length of output: 1300


🏁 Script executed:

# Search for star_fill_ratio definition and usage
rg "star_fill_ratio" -B 3 -A 3 -i

Repository: MoonModules/MoonLight

Length of output: 1703


🏁 Script executed:

# Search for any documentation or comments about stars
rg "nb_stars|star.*ratio" -B 2 -A 2 -i

Repository: MoonModules/MoonLight

Length of output: 2248


🏁 Script executed:

# Search for addControl implementation to understand slider constraints
rg "addControl.*slider" -B 2 -A 5

Repository: MoonModules/MoonLight

Length of output: 50377


🏁 Script executed:

# Look for tests or examples of StarSkyEffect
fd -e "test\|spec\|example" -i | xargs rg "StarSky|star_fill" 2>/dev/null || echo "No test files found"

Repository: MoonModules/MoonLight

Length of output: 1040


🏁 Script executed:

# Search for any configuration files or examples that use this effect
rg "star.*fill|StarSky" -t json -t yaml -t txt -t md

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Check if there are any comments or documentation in the header file about the star effect
sed -n '1,100p' src/MoonLight/Nodes/Effects/E_MoonLight.h

Repository: MoonModules/MoonLight

Length of output: 3825


🏁 Script executed:

# Check if there's documentation or pattern for default slider ranges
rg "addControl.*slider.*\)" -A 1 src/MoonLight/Nodes/Effects/E_MoonLight.h | head -40

Repository: MoonModules/MoonLight

Length of output: 1385


🏁 Script executed:

# Look for any test or config files that show star_fill_ratio usage
find . -type f \( -name "*test*" -o -name "*config*" -o -name "*example*" \) -exec rg -l "star" {} \;

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Check git history or comments for context on the divisor choice
rg "10000|star_fill" --type-list

Repository: MoonModules/MoonLight

Length of output: 454


🏁 Script executed:

# Look for any related proportional calculations in the codebase to understand the pattern
rg "/10000|/1000|/100" src/MoonLight/Nodes/Effects/ -B 2 -A 2 | head -50

Repository: MoonModules/MoonLight

Length of output: 1347


Revisit the star fill ratio calculation for intuitive percentage-based control.

The control labeled "star fill" currently uses a divisor of 10000, producing only 1 star at the default value (ratio=1) and ~26 stars at maximum (ratio=255) for a 1000-LED setup. This results in 0.1–2.6% star density, making the slider feel unintuitive despite the "star fill" label suggesting a percentage-based control.

Consider changing the divisor to 100 or 1000 to align with user expectations: a divisor of 100 would yield 10 stars at default and 2550 at max (more reasonable coverage), while 1000 would yield 1 star at default but 255 at max, providing better granularity across the 0–255 slider range.

🤖 Prompt for AI Agents
In @src/MoonLight/Nodes/Effects/E_MoonLight.h at line 65, The star density calc
using nb_stars = ((uint32_t)star_fill_ratio * (uint32_t)layer->nrOfLights)/10000
+ 1 produces unintuitive low densities; update the divisor to a more intuitive
scale (e.g., 100 or 1000) and revisit the +1 offset so the slider maps sensibly
to percentage-like behavior — replace the 10000 divisor with your chosen scale
(100 or 1000) in the nb_stars computation and remove or adjust the +1 if it
causes an unwanted minimum of 1 star, ensuring nb_stars scales proportionally
with star_fill_ratio and layer->nrOfLights.

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Read the StarSkyEffect code from E_MoonLight.h
echo "=== StarSkyEffect in E_MoonLight.h ==="
cat src/MoonLight/Nodes/Effects/E_MoonLight.h

echo -e "\n=== VirtualLayer.cpp around line 167 ==="
cat src/MoonLight/Layers/VirtualLayer.cpp | head -n 180 | tail -n 30

echo -e "\n=== VirtualLayer.h around line 123 ==="
cat src/MoonLight/Layers/VirtualLayer.h | head -n 130 | tail -n 20

Length of output: 65631


🏁 Script executed:

#!/bin/bash

# Get more context around VirtualLayer::setLight line 167 to see the full function
echo "=== VirtualLayer::setLight function ==="
cat src/MoonLight/Layers/VirtualLayer.cpp | sed -n '130,200p'

# Check if there are any logging statements or other code that could cause the crash
echo -e "\n=== Checking for EXT_LOG statements around crash area ==="
rg -n "EXT_LOG" src/MoonLight/Layers/VirtualLayer.cpp | head -n 20

# Check the allocMB function to understand if it can return null
echo -e "\n=== Finding allocMB definition ==="
rg -n "allocMB" --type cpp -A 5 -B 2 | head -n 40

# Check VirtualLayer constructor/initialization
echo -e "\n=== VirtualLayer constructor ==="
rg -n "VirtualLayer::" src/MoonLight/Layers/VirtualLayer.cpp -A 3 | head -n 50

Length of output: 9526


🏁 Script executed:

#!/bin/bash

# Find the allocMB template definition
echo "=== Finding allocMB template definition ==="
rg -n "template.*allocMB" -A 10

# Find where layerP is defined and initialized
echo -e "\n=== Finding layerP member and initialization ==="
rg -n "layerP\s*=" src/MoonLight/Layers/VirtualLayer.cpp -B 2 -A 2

# Check VirtualLayer header for layerP declaration
echo -e "\n=== VirtualLayer.h layerP declaration ==="
rg -n "PhysicalLayer.*layerP|layerP.*Physical" src/MoonLight/Layers/VirtualLayer.h -B 1 -A 1

# Check if there's any initialization order issue
echo -e "\n=== Check Node::setup timing ==="
rg -n "void setup\(\)" src/MoonLight/Nodes/Node.h -A 5

# Check how layers are created and initialized
echo -e "\n=== Layer creation and initialization ==="
rg -n "new VirtualLayer|VirtualLayer.*layer" src/MoonLight --type cpp | head -n 30

Length of output: 1954


🏁 Script executed:

#!/bin/bash

# Find freeMB to understand the memory allocation pattern
echo "=== Finding freeMB and allocMB pattern ==="
rg -n "freeMB|allocMB" src/MoonLight --type h -B 1 -A 3 | head -n 50

# Check PhysicalLayer initialization and when it sets layerP
echo -e "\n=== PhysicalLayer constructor and layer creation ==="
cat src/MoonLight/Layers/PhysicalLayer.cpp | head -n 50

# Check how Node gets the layer pointer
echo -e "\n=== Node class definition ==="
rg -n "class Node" src/MoonLight/Nodes -A 20 --type h | head -n 60

# Look for layer assignment pattern
echo -e "\n=== Finding where layer pointer is set in Node ==="
rg -n "layer\s*=" src/MoonLight/Nodes --type h -C 2 | head -n 40

# Check if there's an addNode function that sets up the layer relationship
echo -e "\n=== Finding addNode implementation ==="
rg -n "addNode" src/MoonLight/Layers/VirtualLayer.cpp -A 10 -B 2

Length of output: 8174


🏁 Script executed:

#!/bin/bash

# Find Node base class definition
echo "=== Finding Node base class ==="
find src/MoonLight -name "*.h" -exec grep -l "class Node" {} \; | head -n 5

# Read the Node base class
echo -e "\n=== Node base class content ==="
cat src/MoonLight/MoonBase/Node.h | head -n 100

# Find where allocMB and freeMB are defined
echo -e "\n=== Finding allocMB/freeMB definitions ==="
rg -n "^\s*template.*allocMB|^template.*allocMB" -A 15

# Also check for macro definitions
echo -e "\n=== Check for allocMB macros ==="
rg -n "#define.*allocMB|#define.*freeMB" -A 2

# Find the addNode implementation in VirtualLayer
echo -e "\n=== VirtualLayer addNode full implementation ==="
rg -n "void.*addNode" src/MoonLight/Layers -A 20 --type cpp

Length of output: 291


🏁 Script executed:

#!/bin/bash

# Find all header files and search for Node class
echo "=== Finding Node class definition ==="
find src -name "*.h" -type f | xargs grep -l "class Node" | head -n 10

# Search for allocMB more broadly
echo -e "\n=== Search for allocMB implementation ==="
rg -n "allocMB" src --type h | grep -v "freeMB\|allocMB<" | head -n 20

# Search in cpp files too
echo -e "\n=== Search allocMB in cpp files ==="
rg -n "allocMB" src --type cpp | grep -v "freeMB\|allocMB<" | head -n 20

# Look in utilities or common headers
echo -e "\n=== Check Utilities files ==="
find src -name "*Util*" -o -name "*util*" -o -name "*Memory*" -o -name "*memory*" | head -n 10

# Check what's in MoonBase directory
echo -e "\n=== MoonBase directory contents ==="
ls -la src/MoonLight/MoonBase/ 2>/dev/null || echo "Directory not found"

# Let's look at the entire directory structure
echo -e "\n=== Directory structure ==="
find src/MoonLight -type f -name "*.h" | head -n 30

Length of output: 2971


🏁 Script executed:

#!/bin/bash

# Get allocMB definition
echo "=== allocMB definition from Utilities.h ==="
cat src/MoonBase/Utilities.h | sed -n '185,215p'

# Get freeMB definition
echo -e "\n=== freeMB definition ==="
rg -n "void freeMB|freeMB\(" src/MoonBase/Utilities.h -A 5

# Get Node class definition
echo -e "\n=== Node class from Nodes.h ==="
cat src/MoonBase/Nodes.h | head -n 80

# Check if layer is set during node creation
echo -e "\n=== Check addNode in VirtualLayer ==="
cat src/MoonLight/Layers/VirtualLayer.cpp | sed -n '230,280p'

# Let's also see how nrOfLights is set
echo -e "\n=== Check nrOfLights usage ==="
rg -n "nrOfLights\s*=" src/MoonLight/Layers/VirtualLayer.cpp -B 2 -A 2

Length of output: 8099


🏁 Script executed:

#!/bin/bash

# Find where addNode actually sets the layer pointer for the node
echo "=== Search for addNode implementation ==="
rg -n "void.*addNode|addNode.*\(" src/MoonLight/Modules --type h -A 15 -B 2

# Also search in cpp files
echo -e "\n=== addNode in cpp files ==="
rg -n "void.*addNode|addNode.*\(" src/MoonLight --type cpp -A 15 -B 2

# Check when setup() is called on nodes
echo -e "\n=== When is node setup called ==="
rg -n "node->setup\(\)|setup\(\)" src/MoonLight/Modules --type h -B 3 -A 3 | head -n 50

# Check if there's an init order in VirtualLayer
echo -e "\n=== VirtualLayer initialization in PhysicalLayer ==="
cat src/MoonLight/Layers/PhysicalLayer.cpp | sed -n '25,70p'

Length of output: 22123


🏁 Script executed:

#!/bin/bash

# Find where node->layer is set - this is the critical missing piece
echo "=== Finding where node->layer is assigned ==="
rg -n "node->layer\s*=" src/MoonLight/Modules --type h -B 3 -A 3

# Check checkAndAlloc template
echo -e "\n=== checkAndAlloc implementation ==="
rg -n "checkAndAlloc" src/MoonBase --type h -A 10 -B 2

# Check if lights.channelsE could be null when StarSkyEffect::setup is called
echo -e "\n=== Check PhysicalLayer::setup call order ==="
rg -n "layerP.setup\(\)|layerP->setup\(\)" src --type cpp -B 3 -A 3

# Check order of initialization in main
echo -e "\n=== Check main.cpp initialization ==="
cat src/main.cpp | grep -n "layerP\|setup" | head -n 30

Length of output: 2702


🏁 Script executed:

#!/bin/bash

# Get the full context of where node is set up in ModuleEffects
echo "=== Full context of node setup in ModuleEffects ==="
cat src/MoonLight/Modules/ModuleEffects.h | sed -n '250,290p'

# Same for ModuleDrivers
echo -e "\n=== Full context of node setup in ModuleDrivers ==="
cat src/MoonLight/Modules/ModuleDrivers.h | sed -n '155,175p'

# Check if there's any layer assignment I'm missing
echo -e "\n=== Search for layer assignment patterns ==="
rg -n "\.layer\s*=|->layer\s*=" src/MoonLight/Modules --type h -C 3

# Check if the layers array in VirtualLayer has nodes vector initialization
echo -e "\n=== Check VirtualLayer nodes setup ==="
rg -n "nodes\.(push_back|insert|emplace)" src/MoonLight --type cpp -C 2 | head -n 30

Length of output: 3487


🏁 Script executed:

#!/bin/bash

# Find the constructor method in Node base class
echo "=== Finding Node constructor method ==="
rg -n "void constructor" src/MoonBase --type h -A 10 -B 2

# Check if there's initialization order that could cause issues
echo -e "\n=== Check when effects are added vs layerP.setup ==="
rg -n "addNode.*Effect|layers\[0\]" src/main.cpp -C 3

# Look for any null checks in setLight
echo -e "\n=== Check for null pointer guards in VirtualLayer ==="
rg -n "if.*layerP|if.*channelsE|if.*mappingTable" src/MoonLight/Layers/VirtualLayer.cpp | head -n 20

# Check the exact line count to understand what line 167 really is
echo -e "\n=== Line numbers around VirtualLayer::setLight ==="
cat -n src/MoonLight/Layers/VirtualLayer.cpp | sed -n '145,175p'

Length of output: 4183


@limpkin, I've identified the root cause of your crash. The issue is a null pointer dereference due to failed memory allocation.

Root Cause Analysis

The crash occurs at line 167 in VirtualLayer.cpp:

memcpy(&layerP->lights.channelsE[index], channels, length);

The EXCVADDR: 0x00000048 indicates that either:

  1. layerP->lights.channelsE is null (memory allocation failed), OR
  2. layerP itself is null (though less likely given the constructor setup)

Looking at the backtrace, StarSkyEffect::setup_animation() is called during setup(), which immediately tries to use layer->setRGB(). If the physical layer's channel buffer (layerP->lights.channelsE) wasn't successfully allocated, this will crash.

Issues Found

1. Primary Issue: No null checks after memory allocation fails

In PhysicalLayer::setup(), if allocMB fails to allocate lights.channelsE, the code logs an error but continues execution, leaving channelsE as null.

2. Secondary Issue: StarSkyEffect doesn't validate allocations

Lines 66-67 in your effect:

stars_indexes = allocMB<uint16_t>(nb_stars);
stars_fade_dir = allocMB<uint8_t>(nb_stars);

These allocations can fail and return null, but the code immediately uses them without checking.

Recommended Fix

Add null pointer checks in StarSkyEffect::setup_animation():

void setup_animation() {
  freeMB(stars_indexes); 
  freeMB(stars_fade_dir); 
  
  // Check if layer is properly initialized
  if (!layer || !layer->layerP || !layer->layerP->lights.channelsE) {
    EXT_LOGE(ML_TAG, "StarSkyEffect: layer not properly initialized");
    return;
  }
  
  if (layer->nrOfLights == 0) { return; }
  
  layer->fill_solid(CRGB::Black);
  nb_stars = ((uint32_t)star_fill_ratio * (uint32_t)layer->nrOfLights)/10000 + 1;
  
  stars_indexes = allocMB<uint16_t>(nb_stars);
  stars_fade_dir = allocMB<uint8_t>(nb_stars);
  
  // Check if allocations succeeded
  if (!stars_indexes || !stars_fade_dir) {
    EXT_LOGE(ML_TAG, "StarSkyEffect: Failed to allocate memory for %d stars", nb_stars);
    freeMB(stars_indexes);
    freeMB(stars_fade_dir);
    nb_stars = 0;
    return;
  }
  
  EXT_LOGD(ML_TAG, "StarSkyEffect: %d stars added for a total of %d pixels", nb_stars, layer->nrOfLights);
  
  for (uint32_t i = 0; i < nb_stars; i++) {
    stars_indexes[i] = random16(layer->nrOfLights);
    stars_fade_dir[i] = random8(2);
    uint8_t start_brightness = random8();
    EXT_LOGD(ML_TAG, "StarSkyEffect: using pixel #%d, start brightness %d, fade dir %d", stars_indexes[i], start_brightness, stars_fade_dir[i]);
    layer->setRGB(stars_indexes[i], CRGB(start_brightness, start_brightness, start_brightness));
  }
}

Also add a guard in loop20ms():

void loop20ms() override {
  if (!stars_indexes || !stars_fade_dir || nb_stars == 0) return;
  
  if (local_clock++ == slowness) {
    // ... rest of the code

Why This Happens

Your ESP32 is running out of memory during initialization. The error suggests you may have:

  • Too many LEDs configured
  • Insufficient PSRAM
  • Memory fragmentation
  • Other large allocations consuming available memory

Check your memory availability and LED configuration to ensure you have enough RAM/PSRAM for your setup.


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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 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.

@limpkin
Copy link
Author

limpkin commented Jan 10, 2026

@ewowi we're good for merge, AI isn't very useful here

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

🤖 Fix all issues with AI agents
In @src/MoonLight/Nodes/Effects/E_MoonLight.h:
- Around line 62-67: setup_animation() frees the star buffers but can return
early when layer->nrOfLights == 0, leaving nb_stars and star pointer variables
(stars_indexes, stars_fade_dir, stars_brightness) in a stale state and causing
potential use-after-free in loop20ms(); after calling freeMB(...) ensure you set
nb_stars = 0 and explicitly set stars_indexes, stars_fade_dir, and
stars_brightness to nullptr before any early return, and apply the same
nulling/nb_stars reset to the other similar cleanup block covering the 100-117
region so no freed pointers or stale counts remain referenced later.
- Around line 91-93: The onUpdate method in E_MoonLight is missing the override
specifier, which can allow silent signature drift; locate the onUpdate method
that takes a Char<20> and a JsonObject and add the override specifier to its
declaration (making it consistent with onSizeChanged) while keeping the body
that calls setup_animation().
- Around line 100-117: In loop20ms(), the increment/decrement of
stars_brightness (used in stars_brightness[i] = stars_brightness[i] ± 1) can
wrap uint8_t and break the stars_fade_dir checks; fix by using saturating
arithmetic or explicit clamps when updating brightness in the loop (e.g.,
replace direct +/- with qadd8/qsub8 or check bounds before modifying) and ensure
after updating you set stars_fade_dir[i] when brightness hits 0 or UINT8_MAX
(and still allow the random flip). Update references to stars_brightness,
stars_fade_dir, and loop20ms to use the saturating operation so no
overflow/underflow occurs.
🧹 Nitpick comments (1)
src/MoonLight/Nodes/Effects/E_MoonLight.h (1)

68-71: “star fill” control range vs nb_stars math is unclear

nb_stars = star_fill_ratio * nrOfLights / 10000 + 1 makes star fill effectively basis-points (and forces at least 1 star even if set to 0). If the UI intends percent, this should likely be / 100 and slider max 100; if it intends “density”, consider renaming and/or setting explicit min/max.

Also applies to: 94-98

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afa4309 and 73b852d.

📒 Files selected for processing (1)
  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR #84: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-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: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 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.
📚 Learning: 2026-01-06T15:24:29.770Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T13:45:03.067Z
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).

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-18T15:15:57.828Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T15:24:17.118Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-28T10:28:24.157Z
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.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-18T15:59:30.556Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:59:30.556Z
Learning: In DripEffect (E_WLED.h), the bounce fails because the damped velocity is too weak to keep the drop at pos > 0 for multiple frames. When the drop returns to pos <= 0 while still in the bouncing state (colIndex == bouncing), it immediately resets to init (line 1663-1664) before any visible bounce occurs. The bounce velocity must be strong enough (divide by 1.1 or less, or use minimal damping like *= 0.9) so the drop stays airborne long enough for a visible bounce arc before the eventual second ground hit triggers the reset.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧬 Code graph analysis (1)
src/MoonLight/Nodes/Effects/E_MoonLight.h (4)
src/MoonBase/Nodes.h (2)
  • Node (67-182)
  • Node (238-238)
src/MoonLight/Nodes/Effects/E_MoonModules.h (2)
  • onSizeChanged (183-199)
  • onUpdate (120-124)
src/MoonBase/Nodes.cpp (2)
  • onUpdate (401-565)
  • onUpdate (401-401)
src/MoonLight/Layers/VirtualLayer.cpp (2)
  • loop20ms (71-75)
  • loop20ms (71-71)
🔇 Additional comments (1)
src/MoonLight/Nodes/Effects/E_MoonLight.h (1)

62-68: [Rewritten review comment]
[Classification tag]

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

🤖 Fix all issues with AI agents
In @src/MoonLight/Nodes/Effects/E_MoonLight.h:
- Around line 78-100: setup_animation() takes swapMutex via xSemaphoreTake but
returns early when layer is null or empty without releasing it; before the "if
(!layer || layer->nrOfLights == 0) return;" add xSemaphoreGive(swapMutex) (or
restructure to only take the mutex after the null/empty check) so the semaphore
is always released on every return path; also verify other early-return paths
(allocation failures) already call xSemaphoreGive(swapMutex) as shown.
- Around line 111-112: loop20ms currently checks nb_stars, stars_indexes,
stars_fade_dir and stars_brightness but does not verify that layer is non-null;
add a guard at the start of E_MoonLight::loop20ms to return early if layer is
null (in addition to the existing pointer checks) so subsequent accesses to
layer (used later in loop20ms and set up by setup_animation()) cannot
dereference a null pointer.
- Around line 119-134: The loop handling star fades overflows/underflows because
it increments/decrements stars_brightness[i] before checking bounds; fix by
checking brightness limits before arithmetic in the loop in E_MoonLight (use the
stars_brightness[], stars_fade_dir[], stars_indexes[], layer->setRGB(),
random8(), random16(), layer->nrOfLights symbols). Specifically: when
stars_fade_dir[i] == 1, if stars_brightness[i] == UINT8_MAX then set fade_dir to
0 (and write max brightness) else increment and write; when stars_fade_dir[i] ==
0, if stars_brightness[i] == 0 then pick a new stars_indexes[i], set fade_dir to
1 (and write zero brightness) else decrement and write; preserve the random8()
chance to flip but apply it after safely updating without causing wraparound.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2cddf6 and 9c2e859.

📒 Files selected for processing (1)
  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR #84: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-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: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.
📚 Learning: 2025-12-28T10:28:24.157Z
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.

Applied to files:

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

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T13:45:03.067Z
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).

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-18T15:15:57.828Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T15:24:17.118Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧬 Code graph analysis (1)
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
src/MoonBase/Nodes.h (2)
  • Node (67-182)
  • Node (238-238)
src/MoonLight/Nodes/Effects/E_MoonModules.h (2)
  • onSizeChanged (183-199)
  • onUpdate (120-124)
🔇 Additional comments (3)
src/MoonLight/Nodes/Effects/E_MoonLight.h (3)

58-65: Constructor and destructor properly initialize and clean up resources.

The mutex initialization and cleanup pattern is correct. Note that xSemaphoreCreateMutex() creates a mutex in the "given" (unlocked) state by default, so line 60's xSemaphoreGive() is redundant but harmless.


67-76: LGTM! Proper cleanup pattern.

The cleanup correctly frees all allocated buffers, nullifies pointers, and resets state. This prevents use-after-free and ensures loop20ms() will skip rendering when nb_stars is 0.


102-109: LGTM! Proper lifecycle management.

The effect correctly re-initializes on size changes and when the "star fill" parameter changes. The filtering in onUpdate prevents unnecessary re-initialization.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @src/MoonLight/Nodes/Effects/E_MoonLight.h:
- Around line 58-61: StarSkyEffect's constructor calls xSemaphoreCreateMutex()
without checking for NULL and then unconditionally calls
xSemaphoreGive(swapMutex); update the constructor to check the return of
xSemaphoreCreateMutex(), assign swapMutex to NULL on failure, and only call
xSemaphoreGive(swapMutex) when swapMutex is non-NULL; also add an appropriate
failure handling path (e.g., log an error via your logger, set an error flag, or
assert) so the code never dereferences a NULL swapMutex.
- Around line 78-100: The function setup_animation acquires swapMutex with
xSemaphoreTake but can return early when (!layer || layer->nrOfLights == 0)
without releasing it, causing a deadlock; fix by ensuring the mutex is released
before any early return (e.g., call xSemaphoreGive(swapMutex) immediately before
the return in setup_animation) or alternatively move the (!layer ||
layer->nrOfLights == 0) check to before xSemaphoreTake to avoid acquiring the
mutex unnecessarily; update setup_animation to always pair
xSemaphoreTake(swapMutex) with a matching xSemaphoreGive(swapMutex) on all
control paths.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c2e859 and 11f6d9f.

📒 Files selected for processing (1)
  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
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: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 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.
📚 Learning: 2025-12-28T10:28:24.157Z
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.

Applied to files:

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

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-18T15:15:57.828Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T13:45:03.067Z
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).

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T15:24:17.118Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-18T15:59:30.556Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:59:30.556Z
Learning: In DripEffect (E_WLED.h), the bounce fails because the damped velocity is too weak to keep the drop at pos > 0 for multiple frames. When the drop returns to pos <= 0 while still in the bouncing state (colIndex == bouncing), it immediately resets to init (line 1663-1664) before any visible bounce occurs. The bounce velocity must be strong enough (divide by 1.1 or less, or use minimal damping like *= 0.9) so the drop stays airborne long enough for a visible bounce arc before the eventual second ground hit triggers the reset.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🔇 Additional comments (5)
src/MoonLight/Nodes/Effects/E_MoonLight.h (5)

67-76: LGTM!

The cleanup logic correctly frees all allocated buffers, nullifies pointers, and resets state variables.


102-103: LGTM!

Both methods correctly trigger animation restart on relevant changes. The conditional check in onUpdate ensures setup_animation is only called when the "star fill" control changes.


105-109: LGTM!

Standard setup pattern: registers UI controls and initializes the animation state.


86-91: Good allocation error handling.

The memory allocation checks on line 86, combined with proper cleanup via resetStars() and mutex release, correctly handle allocation failures. This prevents the effect from running with partially-allocated buffers.

Based on PR objectives' recommendation to check allocMB return values.


111-138: Validate layer and physical buffers before calling setRGB.

Based on the PR objectives, the reported crash occurs when layer->setRGB() is invoked while layer->layerP->lights.channelsE is null. The guard on line 112 checks effect-local state but doesn't verify that the underlying physical layer buffers are allocated and ready.

According to the crash analysis in the PR discussion, memory allocation failures can leave channelsE null while the code continues execution, and setup ordering may cause this method to run before physical buffers are allocated.

🛡️ Proposed defensive checks
 void loop20ms() override {
   if (nb_stars == 0 || !stars_indexes || !stars_fade_dir || !stars_brightness) return;
+  if (!layer || !layer->layerP || !layer->layerP->lights.channelsE) {
+    EXT_LOGE(ML_TAG, "StarSkyEffect: layer infrastructure not ready");
+    return;
+  }
   if (local_clock++ == slowness) {
     xSemaphoreTake(swapMutex, portMAX_DELAY);

Based on PR objectives and crash analysis.

⛔ Skipped due to learnings
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: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.
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: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.
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

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

🤖 Fix all issues with AI agents
In @src/MoonBase/Modules/ModuleIO.h:
- Around line 809-817: current_board_id is accessed from multiple tasks (written
in setBoardPresetDefaults() called from loop() and read in loop1s()), causing a
data race; change current_board_id to an atomic type (e.g.,
std::atomic<uint8_t>) or protect all accesses with a mutex/semaphore: update the
declaration of current_board_id to std::atomic<uint8_t> (include <atomic>),
replace direct reads/writes in setBoardPresetDefaults() and loop1s() with atomic
load/store operations (or wrap reads/writes in a mutex lock/unlock if you prefer
locking), and ensure any other places that read or write current_board_id use
the same atomic/mutex pattern so visibility and atomicity are guaranteed across
tasks.
- Around line 798-801: The code currently sets volts=0 unless current_board_id
matches board_SE16V1 or board_LightCrafter16, which causes a silent 0V update
via batteryService->updateVoltage(volts); add a default branch that either logs
a warning (including current_board_id and raw adc_mv_vinput) or skips calling
batteryService->updateVoltage when the board is unsupported, and update the
comment next to the LightCrafter16 calculation to reflect the correct divider
ratio (10k/1.43k) and the formula using adc_mv_vinput to avoid the misleading
"1k43/10k" text; reference variables: current_board_id, board_SE16V1,
board_LightCrafter16, adc_mv_vinput, volts, batteryService->updateVoltage.

In @src/MoonLight/Nodes/Effects/E_MoonLight.h:
- Around line 78-104: In setup_animation(), handle the case where swapMutex may
be nullptr by only calling xSemaphoreTake(swapMutex, ...) and
xSemaphoreGive(swapMutex) when swapMutex != nullptr; if swapMutex is nullptr,
run the existing logic unlocked (still call resetStars(), perform allocMB for
stars_indexes/stars_fade_dir/stars_brightness, check for nulls and call
resetStars() before returning). Ensure you do not call xSemaphoreGive unless you
successfully called xSemaphoreTake, and keep the early returns consistent (i.e.,
if you return due to layer==nullptr or allocation failure, only give the
semaphore when it was taken).
- Around line 48-61: The constructor for StarSkyEffect incorrectly calls
xSemaphoreGive() after xSemaphoreCreateMutex() and doesn't check for allocation
failure, and the mutex is never freed; remove the immediate xSemaphoreGive()
call, check the result of xSemaphoreCreateMutex() and handle nullptr (e.g., set
swapMutex to nullptr and avoid using it or assert/log), and add a destructor
that calls vSemaphoreDelete(swapMutex) only if swapMutex is non-null to avoid
leaks and crashes when OOM; update any code paths that call
xSemaphoreTake/xSemaphoreGive to handle a nullptr swapMutex safely.
🧹 Nitpick comments (2)
src/MoonBase/Modules/ModuleIO.h (1)

810-813: Consider extracting magic numbers to named constants.

The ADC conversion formulas contain several magic numbers (330, 250, 50.00, 37.75) that would benefit from descriptive names for maintainability.

Suggested refactor

At class or file scope:

// SE16V1 current sensing constants
static constexpr uint32_t SE16V1_CURRENT_QUIESCENT_MV = 250;
static constexpr float SE16V1_CURRENT_MV_PER_MA = 0.02f;  // 50 mA/mV = 1/0.02 mV/mA

// LightCrafter16 current sensing constants  
static constexpr uint32_t LC16_CURRENT_QUIESCENT_MV = 330;
static constexpr float LC16_CURRENT_MV_PER_MA = 0.0265f;  // 37.75 mA/mV

// Threshold for valid current reading
static constexpr uint32_t CURRENT_SENSE_THRESHOLD_MV = 330;

Then in the calculation:

-if (adc_mv_cinput > 330)
+if (adc_mv_cinput > CURRENT_SENSE_THRESHOLD_MV)
 {
-  if (current_board_id == board_SE16V1) { batteryService->updateCurrent((((float)(adc_mv_cinput)-250) * 50.00) / 1000); }
-  else if (current_board_id == board_LightCrafter16) { batteryService->updateCurrent((((float)(adc_mv_cinput)-330) * 37.75) / 1000); }
+  if (current_board_id == board_SE16V1) { 
+    batteryService->updateCurrent(((float)(adc_mv_cinput - SE16V1_CURRENT_QUIESCENT_MV)) / SE16V1_CURRENT_MV_PER_MA / 1000); 
+  }
+  else if (current_board_id == board_LightCrafter16) { 
+    batteryService->updateCurrent(((float)(adc_mv_cinput - LC16_CURRENT_QUIESCENT_MV)) / LC16_CURRENT_MV_PER_MA / 1000); 
+  }
 }

This makes the hardware parameters explicit and easier to update if board revisions change.

src/MoonLight/Nodes/Effects/E_MoonLight.h (1)

106-113: Set explicit bounds for star fill slider and clarify density scaling logic.

star_fill_ratio is a uint8_t slider with no explicit bounds in addControl(), which is inconsistent with other slider controls in the codebase (e.g., addControl(width, "width", "slider", 1, 20)). The formula (star_fill_ratio * nrOfLights) / 10000 implements an undocumented density scale. Consider:

  • Adding bounds to the addControl() call to match the codebase convention and limit the range (e.g., addControl(star_fill_ratio, "star fill", "slider", 0, 255))
  • Documenting the intended scale (appears to be "stars as percentage density: 0-255 represents ~0-2.55% of pixels")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11f6d9f and f0602d4.

📒 Files selected for processing (2)
  • src/MoonBase/Modules/ModuleIO.h
  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR #84: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-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: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 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.
📚 Learning: 2025-12-28T10:28:24.157Z
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.

Applied to files:

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

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-18T15:15:57.828Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T13:45:03.067Z
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).

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2026-01-06T15:24:17.118Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-18T15:59:30.556Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:59:30.556Z
Learning: In DripEffect (E_WLED.h), the bounce fails because the damped velocity is too weak to keep the drop at pos > 0 for multiple frames. When the drop returns to pos <= 0 while still in the bouncing state (colIndex == bouncing), it immediately resets to init (line 1663-1664) before any visible bounce occurs. The bounce velocity must be strong enough (divide by 1.1 or less, or use minimal damping like *= 0.9) so the drop stays airborne long enough for a visible bounce arc before the eventual second ground hit triggers the reset.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-12-25T13:34:40.097Z
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.

Applied to files:

  • src/MoonLight/Nodes/Effects/E_MoonLight.h
🧬 Code graph analysis (1)
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
src/MoonBase/Nodes.h (2)
  • Node (67-182)
  • Node (238-238)
src/MoonLight/Layers/VirtualLayer.cpp (2)
  • loop20ms (71-75)
  • loop20ms (71-71)
🔇 Additional comments (3)
src/MoonBase/Modules/ModuleIO.h (2)

241-241: LGTM: Board ID tracking initialized correctly.

The board ID is properly stored when presets are applied, enabling board-specific ADC conversions downstream.


824-824: Field declaration looks correct.

The default value UINT8_MAX appropriately indicates an uninitialized state. However, ensure thread-safe access as flagged in the earlier comment regarding lines 809-817.

src/MoonLight/Nodes/Effects/E_MoonLight.h (1)

115-142: The proposed channelsE guard is unnecessary and conflicts with the codebase design.

channelsE allocation is guaranteed before effects run (PhysicalLayer::setup() completes during global setup() before the effect loop starts). More importantly, the intentional omission of null checks in VirtualLayer::setLight() is a performance requirement for 100K+ LED counts—bounds checking is disabled by design (not a safety gap). Adding this guard contradicts that documented tradeoff.

The swapMutex null-check has some merit as defensive coding (xSemaphoreCreateMutex() can fail), but the effect's local swapMutex is a separate concern from the PR objectives. If mutex creation fails, the effect should be disabled upstream, not silently skipped at runtime.

Likely an incorrect or invalid review comment.

Comment on lines +798 to 801
float volts = 0;
if (current_board_id == board_SE16V1) { volts = ((float)adc_mv_vinput) * 2 / 1000; } // /2 resistor divider
else if (current_board_id == board_LightCrafter16) { volts = ((float)adc_mv_vinput) * 11.43 / (1.43 * 1000); } // 1k43/10k resistor divider
batteryService->updateVoltage(volts);
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

Add default case or log warning for unsupported boards.

If current_board_id is neither board_SE16V1 nor board_LightCrafter16, volts remains 0 and updateVoltage(0) is called. This silently reports 0V for boards with voltage sensing pins configured but no conversion formula implemented, which could mask configuration issues.

Additionally, the comment on Line 800 states "1k43/10k resistor divider" but the calculation 11.43 / (1.43 * 1000) suggests a 10k/1.43k divider ratio.

Suggested improvements

Consider one of these approaches:

Option 1: Log a warning for unsupported boards

 float volts = 0;
 if (current_board_id == board_SE16V1) { volts = ((float)adc_mv_vinput) * 2 / 1000; } // /2 resistor divider
-else if (current_board_id == board_LightCrafter16) { volts = ((float)adc_mv_vinput) * 11.43 / (1.43 * 1000); } // 1k43/10k resistor divider 
+else if (current_board_id == board_LightCrafter16) { volts = ((float)adc_mv_vinput) * 11.43 / (1.43 * 1000); } // 10k/1.43k resistor divider
+else {
+  EXT_LOGW(MB_TAG, "No voltage conversion for board_id %d", current_board_id);
+}
 batteryService->updateVoltage(volts);

Option 2: Skip the update entirely if unsupported

 float volts = 0;
+bool voltage_calculated = false;
-if (current_board_id == board_SE16V1) { volts = ((float)adc_mv_vinput) * 2 / 1000; } // /2 resistor divider
-else if (current_board_id == board_LightCrafter16) { volts = ((float)adc_mv_vinput) * 11.43 / (1.43 * 1000); } // 1k43/10k resistor divider 
+if (current_board_id == board_SE16V1) { 
+  volts = ((float)adc_mv_vinput) * 2 / 1000; 
+  voltage_calculated = true;
+} else if (current_board_id == board_LightCrafter16) { 
+  volts = ((float)adc_mv_vinput) * 11.43 / (1.43 * 1000); // 10k/1.43k resistor divider
+  voltage_calculated = true;
+}
+if (voltage_calculated) {
-batteryService->updateVoltage(volts);
+  batteryService->updateVoltage(volts);
+}
📝 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
float volts = 0;
if (current_board_id == board_SE16V1) { volts = ((float)adc_mv_vinput) * 2 / 1000; } // /2 resistor divider
else if (current_board_id == board_LightCrafter16) { volts = ((float)adc_mv_vinput) * 11.43 / (1.43 * 1000); } // 1k43/10k resistor divider
batteryService->updateVoltage(volts);
float volts = 0;
if (current_board_id == board_SE16V1) { volts = ((float)adc_mv_vinput) * 2 / 1000; } // /2 resistor divider
else if (current_board_id == board_LightCrafter16) { volts = ((float)adc_mv_vinput) * 11.43 / (1.43 * 1000); } // 10k/1.43k resistor divider
else {
EXT_LOGW(MB_TAG, "No voltage conversion for board_id %d", current_board_id);
}
batteryService->updateVoltage(volts);
Suggested change
float volts = 0;
if (current_board_id == board_SE16V1) { volts = ((float)adc_mv_vinput) * 2 / 1000; } // /2 resistor divider
else if (current_board_id == board_LightCrafter16) { volts = ((float)adc_mv_vinput) * 11.43 / (1.43 * 1000); } // 1k43/10k resistor divider
batteryService->updateVoltage(volts);
float volts = 0;
bool voltage_calculated = false;
if (current_board_id == board_SE16V1) {
volts = ((float)adc_mv_vinput) * 2 / 1000;
voltage_calculated = true;
} else if (current_board_id == board_LightCrafter16) {
volts = ((float)adc_mv_vinput) * 11.43 / (1.43 * 1000); // 10k/1.43k resistor divider
voltage_calculated = true;
}
if (voltage_calculated) {
batteryService->updateVoltage(volts);
}
🤖 Prompt for AI Agents
In @src/MoonBase/Modules/ModuleIO.h around lines 798 - 801, The code currently
sets volts=0 unless current_board_id matches board_SE16V1 or
board_LightCrafter16, which causes a silent 0V update via
batteryService->updateVoltage(volts); add a default branch that either logs a
warning (including current_board_id and raw adc_mv_vinput) or skips calling
batteryService->updateVoltage when the board is unsupported, and update the
comment next to the LightCrafter16 calculation to reflect the correct divider
ratio (10k/1.43k) and the formula using adc_mv_vinput to avoid the misleading
"1k43/10k" text; reference variables: current_board_id, board_SE16V1,
board_LightCrafter16, adc_mv_vinput, volts, batteryService->updateVoltage.

Comment on lines +809 to 817
if ((current_board_id == board_SE16V1) || (current_board_id == board_LightCrafter16)) {
if (adc_mv_cinput > 330) // datasheet quiescent output voltage of 0.5V, which is ~330mV after the 10k/5k1 voltage divider. Ideally, this value should be measured at boot when nothing is displayed on the LEDs
{
if (current_board_id == board_SE16V1) { batteryService->updateCurrent((((float)(adc_mv_cinput)-250) * 50.00) / 1000); } // 40mV / A with a /2 resistor divider, so a 50mA/mV
else if (current_board_id == board_LightCrafter16) { batteryService->updateCurrent((((float)(adc_mv_cinput)-330) * 37.75) / 1000); } // 40mV / A with a 10k/5k1 resistor divider, so a 37.75mA/mV
} else {
batteryService->updateCurrent(0);
}
}
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

Potential race condition: unsynchronized access to current_board_id.

current_board_id is written in setBoardPresetDefaults() (called from loop() running in the sveltekit task, line 602) and read here in loop1s() (likely a different task). Without synchronization, this creates a data race where:

  1. loop1s() might read current_board_id while it's being updated
  2. loop1s() might read stale values due to cache coherency
  3. If loop1s() runs before setBoardPresetDefaults() is called, current_board_id will be UINT8_MAX and current updates will be skipped silently
Recommended fix: Add volatile qualifier or mutex protection

Option 1: Use volatile for simple visibility (minimal fix)

-uint8_t current_board_id = UINT8_MAX;
+volatile uint8_t current_board_id = UINT8_MAX;

Option 2: Use atomic access (preferred for ESP-IDF)

+#include <atomic>
 private:
   ESP32SvelteKit* _sveltekit;
-  uint8_t current_board_id = UINT8_MAX;
+  std::atomic<uint8_t> current_board_id{UINT8_MAX};

Note: volatile provides visibility but not atomicity. For uint8_t on ESP32, reads/writes are atomic, so volatile should suffice. However, std::atomic is clearer about intent.

Based on learnings, MoonLight tasks can run on different cores with preemption, making thread-safe access patterns critical.

🤖 Prompt for AI Agents
In @src/MoonBase/Modules/ModuleIO.h around lines 809 - 817, current_board_id is
accessed from multiple tasks (written in setBoardPresetDefaults() called from
loop() and read in loop1s()), causing a data race; change current_board_id to an
atomic type (e.g., std::atomic<uint8_t>) or protect all accesses with a
mutex/semaphore: update the declaration of current_board_id to
std::atomic<uint8_t> (include <atomic>), replace direct reads/writes in
setBoardPresetDefaults() and loop1s() with atomic load/store operations (or wrap
reads/writes in a mutex lock/unlock if you prefer locking), and ensure any other
places that read or write current_board_id use the same atomic/mutex pattern so
visibility and atomicity are guaranteed across tasks.

@ewowi ewowi merged commit cc8c4da into MoonModules:main Jan 11, 2026
1 check passed
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