Skip to content

Conversation

@ewowi
Copy link
Collaborator

@ewowi ewowi commented Feb 10, 2026

Summary by CodeRabbit

  • New Features

    • Added support for Seeed XIAO ESP32‑S3 with stable and nightly installers; installer list and images updated.
    • Introduced a shared filesystem persistence service for module state.
  • Refactor

    • Many visual effects now use palette-driven color generation, changing rendered color output.
    • Adjusted some built-in layout geometry and LED counts.
  • Documentation

    • Updated installer docs, palette wording, and JSON examples.
  • Style

    • Removed several emoji indicators from effect tags and UI labels.
  • Chores

    • Increased HTTPD stack size in build configuration.

Firmware
========
- add esp32-s3-seeed_xiao

Backend
=======
- IO: configure board preset LuxceoMood1XiaoMod
- Effects: all use palette - WIP

Backend
=======
- All effects use palettes (WIP)
@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds ESP32‑S3 Seeed Xiao support (PlatformIO env, installer manifests, docs), centralizes per‑module persistence into a new SharedFSPersistence wired into startup and module registration, trims palette emojis in docs/effect tags, and shifts many effects from CHSV to ColorFromPalette color sourcing.

Changes

Cohort / File(s) Summary
Docs — emoji & examples
docs/develop/layers.md, docs/moonlight/lightscontrol.md, docs/moonlight/overview.md
Removed 🎨 emoji from examples, palette description, and emoji-coding bullets.
Installer manifests & docs
firmware/installer/manifest_esp32-s3-seeed_xiao.json, firmware/installer/manifest_esp32-s3-seeed_xiao-nightly.json, docs/gettingstarted/installer.md
Added stable/nightly installer manifests and new esp32-s3-seeed_xiao device row (images, webflash URLs, store link).
PlatformIO / board env
firmware/esp32-s3.ini, platformio.ini
Added [env:esp32-s3-seeed_xiao] (board seeed_xiao_esp32s3, PSRAM opi); bumped HTTPD_STACK_SIZE 5120→6144.
Board presets / ModuleIO
src/MoonBase/Modules/ModuleIO.h
Added LuxceoMood1XiaoMod preset (maxPower=50, pins); removed emoji from display name; minor formatting.
Shared persistence integration
src/main.cpp, src/MoonBase/SharedFSPersistence.h, src/MoonBase/Module.h, src/MoonBase/Module.cpp, src/MoonBase/NodeManager.h, src/MoonBase/Modules/FileManager.cpp
Introduced global SharedFSPersistence, register modules with delayed writing, removed per‑module _fsPersistence, moved initial readFromFS to shared begin, updated APIs and call sites to use shared persistence.
Module update & API changes
src/MoonBase/Module.h, src/MoonBase/Modules/FileManager.cpp, src/MoonBase/NodeManager.h, src/MoonLight/Modules/ModuleDrivers.h, src/MoonLight/Modules/ModuleEffects.h
Adjusted processUpdatedItem gating and always call onUpdate; changed several addNode/checkAndAlloc signatures to accept char* name (non-const); name normalization buffer added.
Effects — palette refactor & tag cleanup
src/MoonLight/Nodes/Effects/* (E_MoonLight.h, E_MoonModules.h, E_MovingHeads.h, E_WLED.h, E__Sandbox.h, ...)
Replaced many CHSV/(CRGB)CHSV usages with ColorFromPalette(layerP.palette, ...); trimmed 🎨 and other emojis from numerous tags() string literals.
Layouts & geometry
src/MoonLight/Nodes/Layouts/L_MoonLight.h
Adjusted CarLightsLayout geometry and LED counts (e.g., 34→32), minor setup formatting changes.
Misc — formatting / build flags
platformio.ini, various files
Whitespace and minor formatting tweaks across files; no other functional changes beyond noted items.

Sequence Diagram(s)

sequenceDiagram
  participant Main as Main (setup)
  participant Shared as SharedFSPersistence
  participant Module as Module
  participant FS as Filesystem

  rect rgba(100,150,240,0.5)
    Main->>Shared: allocate SharedFSPersistence\nregisterModule(module, delayed=true)
    Shared-->>Main: ack registration
  end

  rect rgba(120,200,100,0.5)
    Main->>Shared: sharedFsPersistence->begin()
    Shared->>Module: readFromFS(moduleName) for each registered module
    Module->>Shared: writeToFSDelayed(moduleName, data) (on updates)
    Shared->>FS: perform eventual writeToFS(moduleName)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through code with nimble feet,
Wired a Xiao board and tuned the heap,
Swapped CHSV for palettes, trimmed a 🎨 away,
Shared files now hum where lone writes lay,
MoonLight glows — I nibble a carrot treat.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'LED tubes' does not accurately describe the actual changes, which primarily involve adding esp32-s3-seeed_xiao board support, migrating effects to use palettes, and updating documentation. Use a more specific title that captures the main objective, such as 'Add esp32-s3-seeed_xiao board support and migrate effects to palette-based coloring' or 'Support esp32-s3-seeed_xiao and enable palette-aware effects'.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

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

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/MoonBase/NodeManager.h (1)

85-94: Name buffer size of 32 is consistent but fragile.

checkAndAlloc writes back into the name buffer with a hardcoded size of 32 (line 90), matching the char name[32] at the call site (line 177) and Char<32> defaultNodeName (line 25). However, the magic number 32 is repeated in multiple places. Consider extracting a named constant (e.g., constexpr size_t NODE_NAME_MAX = 32;) to keep these in sync.


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.

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 10, 2026

@coderabbitai,

can you review 48e0062

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get the commit details
echo "=== COMMIT DETAILS ==="
gh api repos/MoonModules/MoonLight/commits/48e0062d0241e302b22bb8d00c974720660810e8 --jq '{message: .commit.message, author: .commit.author, files: .files | length}'

echo -e "\n=== FILES CHANGED ==="
gh api repos/MoonModules/MoonLight/commits/48e0062d0241e302b22bb8d00c974720660810e8 --jq '.files[] | {filename: .filename, status: .status, additions: .additions, deletions: .deletions, changes: .changes}'

echo -e "\n=== COMMIT DIFF ==="
git fetch origin 48e0062d0241e302b22bb8d00c974720660810e8
git show 48e0062d0241e302b22bb8d00c974720660810e8

Length of output: 32420


@ewowi, I've reviewed commit 48e0062. Here's my analysis:

🐛 Critical Issue: Image File Extension Mismatch

The manifest files reference the wrong image extension:

Files: firmware/installer/manifest_esp32-s3-seeed_xiao.json and manifest_esp32-s3-seeed_xiao-nightly.json

"image": "./images/esp32-s3-seeed_xiao.jpg"

But the actual file added is esp32-s3-seeed_xiao.png. This will cause the image to fail loading in the installer. Should be:

"image": "./images/esp32-s3-seeed_xiao.png"

📋 Board Support Addition

The esp32-s3-seeed_xiao board configuration looks good:

  • ✅ PlatformIO environment properly configured with OPI PSRAM
  • ✅ Inherits from esp32-s3-base correctly
  • ✅ Documentation link included
  • ✅ Installer table entry added

The LuxceoMood1XiaoMod pin assignments look reasonable (pins 1-4 for LEDs, standard I2C/SPI/Serial pins), but please verify these match your hardware schematic.

🎨 Palette Migration

The conversion from CHSV() to ColorFromPalette() is architecturally sound and aligns with making all effects palette-aware. A few notes:

  1. Behavior change: The old code like CHSV(hue, 200, 255) had explicit saturation control, while ColorFromPalette(layerP.palette, hue) uses the palette's built-in colors. This is likely intentional but changes the visual output.

  2. Consistent pattern: The conversions follow a consistent pattern, which is good for maintainability.

  3. WIP status noted: Since this is marked WIP, I assume you're planning further testing of visual outputs?

📝 Documentation Updates

The documentation changes are consistent - removing 🎨 emoji references and updating the palette description makes sense since all effects now use palettes by default.

✅ Summary

Must fix: Image extension mismatch in manifest files
Should verify: LuxceoMood1XiaoMod pin assignments against hardware
Testing needed: Visual validation of palette-based effects (per WIP status)

Overall solid work on both the board addition and palette standardization! 👍


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

pio.ini: httpd stack size 6144 for board presets updates

backend
=======
- Module: processUpdatedItem: saveNeeded also if oldValue is empty
@ewowi
Copy link
Collaborator Author

ewowi commented Feb 10, 2026

@coderabbitai resume
@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

@ewowi, I'll resume automatic reviews and trigger a new review of the PR.


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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 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).
✅ Actions performed

Reviews resumed.


Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

backend
=======
- main: enable SharedFSPersistence
- Module: remove _fsPersistence
- NodeManager: comment _fileManager->read()  readFromFS(); - WIP
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.

Caution

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

⚠️ Outside diff range comments (1)
src/MoonBase/SharedFSPersistence.h (1)

21-21: ⚠️ Potential issue | 🔴 Critical

sharedDelayedWrites has an unprotected data race between writeToFS() and writeToFSDelayed().

The global inline vector is accessed unsafely: writeToFS() may push to it from the SvelteKit task (via state update handlers), while writeToFSDelayed() iterates and clears it from the httpd task (HTTP POST handlers at /rest/saveConfig and /rest/cancelConfig). Add synchronization (mutex or queue) to protect concurrent access.

🧹 Nitpick comments (3)
src/MoonBase/NodeManager.h (1)

54-54: Track this TODO — disabling readFromFS() here means file-manager edits to the config JSON won't propagate to runtime state.

The comment says "temporary !!!" but there's no tracking issue or clear plan for re-enabling this path under the new SharedFSPersistence regime. If SharedFSPersistence is meant to handle this, consider wiring the FileManager update handler to call sharedFsPersistence->readFromFS(moduleName) instead, or open an issue so this doesn't get lost.

Would you like me to open an issue to track re-enabling FS-triggered state reload under SharedFSPersistence?

src/MoonBase/Module.cpp (1)

280-280: Stale comment on line 287.

Line 287 says "if no data readFromFS" but readFromFS no longer happens here — it's done externally by SharedFSPersistence::registerModule. Consider updating the comment to reflect the new flow.

src/MoonBase/SharedFSPersistence.h (1)

52-52: Inconsistent logging between registerModule and enableUpdateHandler handlers.

The update handler registered in registerModule (line 52) does not log originId, while the one in enableUpdateHandler (lines 78-79) does. If a module's handler is never disabled/re-enabled, it will silently write without the log. Consider extracting a shared lambda or helper so both paths behave identically.

Proposed fix
     // Register update handler
-    info.updateHandlerId = module->addUpdateHandler([this, module](const String& originId) { writeToFS(module->_moduleName); }, false);
+    info.updateHandlerId = module->addUpdateHandler([this, module](const String& originId) {
+      ESP_LOGD(SVK_TAG, "writeToFS %s", originId.c_str());
+      writeToFS(module->_moduleName);
+    }, false);

Also applies to: 76-81

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 10, 2026

@coderabbitai, Latest commit gave me this crash during boot (bootloop):

[ 898][D][NodeManager.h:126] onUpdate(): [💫] loopTask (4976) [255][255].layer = -> 0
[ 907][D][NodeManager.h:126] onUpdate(): [💫] loopTask (4720) [255][255].start = -> {"x":0,"y":0,"z":0}
[ 918][D][NodeManager.h:126] onUpdate(): [💫] loopTask (4720) [255][255].end = -> {"x":16,"y":16,"z":1}
[ 929][D][NodeManager.h:126] onUpdate(): [💫] loopTask (4720) [255][255].brightness = -> 246
[ 939][D][NodeManager.h:126] onUpdate(): [💫] loopTask (4720) nodes[0][255].name = -> Bouncing Balls ⏹️ 🔥🎨🐙
Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.

Core 1 register dump:
PC : 0x4209da9c PS : 0x00060630 A0 : 0x8203b3f1 A1 : 0x3fceb580
A2 : 0x3c280cf4 A3 : 0x3fcebc04 A4 : 0x3fcebc1a A5 : 0x3fca323c
A6 : 0x3fceb63c A7 : 0x3fceb61c A8 : 0x00000000 A9 : 0x3fceb560
A10 : 0x00000000 A11 : 0x3c1d773e A12 : 0x3c1b2c88 A13 : 0x0000007e
A14 : 0x4209da2d A15 : 0x3fceb53c SAR : 0x00000019 EXCCAUSE: 0x0000001c
EXCVADDR: 0x00000000 LBEG : 0x40055499 LEND : 0x400554a9 LCOUNT : 0xfffffffe

Backtrace: 0x4209da99:0x3fceb580 0x4203b3ee:0x3fceb670 0x4203b3fd:0x3fceb690 0x4203b58a:0x3fceb6b0 0x4203d960:0x3fceb6d0 0x4203db5e:0x3fceb940 0x4203e2d8:0x3fcebbb0 0x4217f46b:0x3fcebc70 0x42083667:0x3fcebc90 0x420a09de:0x3fcebcb0 0x420a0be6:0x3fcebda0 0x420a1c7d:0x3fcebe20 0x420ab54e:0x3fcebe80 0x4037fdbd:0x3fcebea0
#0 0x4209da99 in NodeManager::onUpdate(UpdatedItem const&, String const&) at src/MoonBase/NodeManager.h:135 (discriminator 1)
#1 0x4203b3ee in Module::processUpdatedItem(UpdatedItem const&, String const&) at src/MoonBase/Module.h:140
#2 0x4203b3fd in std::_Function_handler<void (UpdatedItem const&, String const&), Module::Module(char const*, PsychicHttpServer*, ESP32SvelteKit*)::{lambda(UpdatedItem const&, String const&)#1}>::_M_invoke(std::_Any_data const&, UpdatedItem const&, String const&) at src/MoonBase/Module.cpp:273
(inlined by) __invoke_impl<void, Module::Module(char const*, PsychicHttpServer*, ESP32SvelteKit*)::<lambda(const UpdatedItem&, const String&)>&, const UpdatedItem&, const String&> at /Users/ewoud/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/invoke.h:61
(inlined by) __invoke_r<void, Module::Module(char const*, PsychicHttpServer*, ESP32SvelteKit*)::<lambda(const UpdatedItem&, const String&)>&, const UpdatedItem&, const String&> at /Users/ewoud/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/invoke.h:111
(inlined by) _M_invoke at /Users/ewoud/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/std_function.h:290
#3 0x4203b58a in std::function<void (UpdatedItem const&, String const&)>::operator()(UpdatedItem const&, String const&) const at /Users/ewoud/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/std_function.h:591
#4 0x4203d960 in ModuleState::compareRecursive(ArduinoJson::V742PB22::JsonString const&, ArduinoJson::V742PB22::JsonVariant const&, ArduinoJson::V742PB22::JsonVariant const&, UpdatedItem&, String const&, unsigned char, unsigned char) at src/MoonBase/Module.cpp:210
#5 0x4203db5e in ModuleState::compareRecursive(ArduinoJson::V742PB22::JsonString const&, ArduinoJson::V742PB22::JsonVariant const&, ArduinoJson::V742PB22::JsonVariant const&, UpdatedItem&, String const&, unsigned char, unsigned char) at src/MoonBase/Module.cpp:198 (discriminator 8)
#6 0x4203e2d8 in ModuleState::update(ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&) at src/MoonBase/Module.cpp:249 (discriminator 3)
#7 0x4217f46b in StateUpdateResult std::__invoke_impl<StateUpdateResult, StateUpdateResult (&)(ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&), ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&>(std::__invoke_other, StateUpdateResult (&)(ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&), ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&) at /Users/ewoud/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/invoke.h:61
(inlined by) std::enable_if<is_invocable_r_v<StateUpdateResult, StateUpdateResult (&)(ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&), ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&>, StateUpdateResult>::type std::__invoke_r<StateUpdateResult, StateUpdateResult (&)(ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&), ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&>(StateUpdateResult (&)(ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&), ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&) at /Users/ewoud/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/invoke.h:114
(inlined by) std::_Function_handler<StateUpdateResult (ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&), StateUpdateResult (
)(ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&)>::_M_invoke(std::_Any_data const&, ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&) at /Users/ewoud/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/std_function.h:290
#8 0x42083667 in std::function<StateUpdateResult (ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&)>::operator()(ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&) const at /Users/ewoud/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/std_function.h:591
(inlined by) StatefulService::updateWithoutPropagation(ArduinoJson::V742PB22::JsonObject&, std::function<StateUpdateResult (ArduinoJson::V742PB22::JsonObject&, ModuleState&, String const&)>, String const&) at lib/framework/StatefulService.h:160
#9 0x420a09de in SharedFSPersistence::readFromFS(char const*) at src/MoonBase/SharedFSPersistence.h:99 (discriminator 2)
#10 0x420a0be6 in SharedFSPersistence::registerModule(Module*, bool) at src/MoonBase/SharedFSPersistence.h:57
#11 0x420a1c7d in setup() at src/main.cpp:323
#12 0x420ab54e in loopTask(void*) at /Users/ewoud/.platformio/packages/framework-arduinoespressif32/cores/esp32/main.cpp:67

ELF file SHA256: f90644e2c

Rebooting...

What is going on? I worked on this before but paused it, maybe you remember.
Could you do a comparison of the code in SharedFSPersistence.h and FSPersistence.h and confirm that the switch I made in latest commit does functionally exactly the same (except it has now one class instead of one class per module)

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Read SharedFSPersistence.h
echo "=== SharedFSPersistence.h ==="
cat src/MoonBase/SharedFSPersistence.h

echo -e "\n=== FSPersistence.h (if exists) ==="
fd -t f "FSPersistence.h" --exec cat {}

echo -e "\n=== NodeManager.h lines 120-150 (around crash line 135) ==="
cat src/MoonBase/NodeManager.h | head -n 150 | tail -n 31

Length of output: 20688


@ewowi, I found the issue! 🎯

Root Cause: Initialization Timing Problem

The crash is a null pointer dereference of the nodes vector in NodeManager at line 135. Here's what's happening:

Old FSPersistence Flow (Working)

// Module constructor
Module::Module(...) : _fsPersistence(...) { }

// Later in Module::begin()
_fsPersistence.readFromFS();  // ← Called AFTER module is fully initialized

New SharedFSPersistence Flow (Broken)

// In setup()
sharedFsPersistence->registerModule(module, true);  // ← Called BEFORE module->begin()
  └─→ readFromFS(moduleName);  // Line 57: reads immediately!
       └─→ updateWithoutPropagation()
            └─→ NodeManager::onUpdate()
                 └─→ nodes->size()  // 💥 CRASH: nodes is nullptr!

// Later...
module->begin();  // ← This initializes the nodes vector, but TOO LATE

The Problem

In SharedFSPersistence::registerModule() line 57, you're calling readFromFS() immediately:

void registerModule(Module* module, bool delayedWriting = false) {
    // ... setup ModuleInfo ...
    _modules[module->_moduleName] = info;
    
    // ❌ TOO EARLY! Module hasn't called begin() yet
    readFromFS(module->_moduleName);
}

But NodeManager initializes its nodes vector in begin(), not in the constructor. When readFromFS() triggers state updates before begin() is called, NodeManager tries to access a null nodes pointer.

The Solution

Option 1: Defer all readFromFS() calls (Recommended)

void registerModule(Module* module, bool delayedWriting = false) {
    ModuleInfo info;
    info.module = module;
    info.filePath = String("/.config/") + module->_moduleName + ".json";
    info.delayedWriting = delayedWriting;
    info.hasDelayedWrite = false;

    info.updateHandlerId = module->addUpdateHandler(
        [this, module](const String& originId) { writeToFS(module->_moduleName); }, 
        false);

    _modules[module->_moduleName] = info;
    
    // ✅ DON'T call readFromFS here
}

void begin() {
    // ✅ Read all modules AFTER they've all been initialized
    for (auto& pair : _modules) {
        readFromFS(pair.first);
    }
}

Option 2: Call readFromFS() explicitly after each module->begin() in main.cpp:

for (Module* module : modules) { 
    module->begin(); 
}

// Now read persisted state
for (Module* module : modules) {
    sharedFsPersistence->readFromFS(module->_moduleName);
}

Functional Equivalence Check

Other than this timing issue, the implementations are functionally equivalent:

  • ✅ Same delayed write logic with global vector
  • ✅ Same mkdirs() directory creation
  • ✅ Same applyDefaults() behavior
  • ✅ Same update handler registration
  • ✅ Same JSON serialization/deserialization

The only difference causing the crash is when readFromFS() is called relative to Module::begin().


🏁 Script executed:

#!/bin/bash
# Verify the setup sequence in main.cpp
echo "=== main.cpp setup sequence ==="
cat src/main.cpp | grep -A 30 "sharedFsPersistence->registerModule"

Length of output: 1147


Perfect! The sequence confirms my diagnosis. Here's the exact fix:

Fix for SharedFSPersistence.h

Remove the readFromFS() call from registerModule() and move it to a new method:

void registerModule(Module* module, bool delayedWriting = false) {
    ModuleInfo info;
    info.module = module;
    info.filePath = String("/.config/") + module->_moduleName + ".json";
    info.delayedWriting = delayedWriting;
    info.hasDelayedWrite = false;

    // Register update handler
    info.updateHandlerId = module->addUpdateHandler(
        [this, module](const String& originId) { writeToFS(module->_moduleName); }, 
        false);

    _modules[module->_moduleName] = info;
    
    // ❌ REMOVE THIS LINE - it's too early!
    // readFromFS(module->_moduleName);
}

void begin() {
    // All setup happens in registerModule
}

// ✅ ADD THIS NEW METHOD
void readAllFromFS() {
    for (auto& pair : _modules) {
        readFromFS(pair.first);
    }
}

Fix for main.cpp

Call readAllFromFS() after all module->begin() calls:

// Begin shared routers (one-time setup)
sharedHttpEndpoint->begin();
sharedWebSocketServer->begin();
sharedEventEndpoint->begin();
sharedFsPersistence->begin();

// MoonBase
`#if` FT_ENABLED(FT_MOONBASE)
  fileManager.begin();
  for (Module* module : modules) {
    module->begin();  // ← Modules initialize their internal state (nodes vector, etc.)
  }
  
  // ✅ ADD THIS: Now read persisted state after modules are ready
  sharedFsPersistence->readAllFromFS();

  // 🌙
  `#if` FT_ENABLED(FT_MOONLIGHT)
  xTaskCreatePinnedToCore(effectTask, ...

Why This Works

The new sequence becomes:

  1. registerModule() - registers module, sets up update handler (no read yet)
  2. sharedFsPersistence->begin() - (currently empty, but could do setup)
  3. module->begin() - modules initialize (nodes vector created ✅)
  4. readAllFromFS() - now safe to read and trigger onUpdate() ✅

This matches the old FSPersistence behavior where readFromFS() was called inside module->begin(), ensuring full initialization before state restoration.


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

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

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

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

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 10, 2026

@coderabbitai, cool that worked, but it looks like delayed writes is doing nothing:

[ 27685][D][FSPersistence.h:121] writeToFSDelayed(): [🐼] calling 0 writeFuncs from delayedWrites

Please check SharedFSPersistence.h if the administration of writefuncs is done right

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== SharedFSPersistence.h ==="
cat src/MoonBase/SharedFSPersistence.h

echo -e "\n=== FSPersistence.h ==="
cat src/MoonBase/FSPersistence.h

echo -e "\n=== Search for writeToFSDelayed calls ==="
rg -n "writeToFSDelayed" --type cpp --type h -C 3

echo -e "\n=== Search for delayedWriting usage ==="
rg -n "delayedWriting" --type cpp --type h -C 2

Length of output: 11914


🏁 Script executed:

#!/bin/bash

echo "=== FileManager.cpp save/cancel button handlers ==="
cat src/MoonBase/Modules/FileManager.cpp | head -n 180 | tail -n 30

Length of output: 1073


@ewowi, Found it! 🎯

The Problem

You're processing the wrong delayed writes queue! There are two separate queues:

  1. Old queue (empty): FSPersistence<int>::delayedWrites in lib/framework/FSPersistence.h
  2. New queue (has your writes): sharedDelayedWrites in src/MoonBase/SharedFSPersistence.h

Your modules correctly add delayed writes to sharedDelayedWrites, but FileManager.cpp is calling the old method that processes the empty queue!

The Fix

File: src/MoonBase/Modules/FileManager.cpp

Line 163 (Save button):

// ❌ WRONG - processes old empty queue
FSPersistence<int>::writeToFSDelayed('W');

// ✅ CORRECT - processes new queue with your writes
SharedFSPersistence::writeToFSDelayed('W');

Line 175 (Cancel button):

// ❌ WRONG
FSPersistence<int>::writeToFSDelayed('C');

// ✅ CORRECT
SharedFSPersistence::writeToFSDelayed('C');

You'll also need to add the include at the top of FileManager.cpp:

`#include` "SharedFSPersistence.h"

That's it! The delayed writes administration in SharedFSPersistence.h is actually correct - it's just that FileManager was calling the old method. 😊


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

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

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

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

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 10, 2026

@coderabbitai, how to best activate / uncomment

// readFromFS(); // repopulates the state, processing file changes. Comment temporary !!!

in NodeManager.h using the global SharedFsPersistence. readFromFS without creating cycling dependencies between modules (SharedFsPersistence needs NodeManager (instance of Module) Needs SharedFsPersistence ...

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== NodeManager.h - find commented readFromFS line ==="
rg -n "readFromFS.*Comment temporary" src/MoonBase/NodeManager.h -B 5 -A 5

echo -e "\n=== Full context around that area ==="
cat src/MoonBase/NodeManager.h | head -n 140 | tail -n 40

echo -e "\n=== SharedFSPersistence.h public interface ==="
cat src/MoonBase/SharedFSPersistence.h | head -n 110

echo -e "\n=== Check what includes what ==="
echo "--- NodeManager.h includes ---"
head -n 30 src/MoonBase/NodeManager.h | grep "#include"

echo -e "\n--- SharedFSPersistence.h includes ---"
head -n 30 src/MoonBase/SharedFSPersistence.h | grep "#include"

echo -e "\n--- Module.h includes ---"
head -n 30 src/MoonBase/Module.h | grep "#include"

Length of output: 6623


@ewowi, the solution is simple: use the global pointer without including the header! 🎯

Solution: Forward Declaration + Extern Pointer

File: src/MoonBase/NodeManager.h

Add at the top (after existing includes, before the class declaration):

`#include` "MoonBase/Module.h"
`#include` "Nodes.h"

// Forward declaration to avoid circular dependency
class SharedFSPersistence;
extern SharedFSPersistence* sharedFsPersistence;

class NodeManager : public Module {

Then uncomment the line at line 54:

if (equal(updatedItem.c_str(), name.c_str())) {
  EXT_LOGV(ML_TAG, " %s updated -> call update %s", name.c_str(), updatedItem.c_str());
  // ✅ Now you can call it!
  sharedFsPersistence->readFromFS(_moduleName);
}

Why This Works

  1. Forward declaration tells the compiler "SharedFSPersistence is a class" without needing the full definition
  2. Extern pointer declares that sharedFsPersistence exists somewhere (it's defined in main.cpp)
  3. No circular dependency because you're not including SharedFSPersistence.h
  4. Works at runtime because the pointer is properly initialized in main.cpp before modules begin()

This is a standard C++ pattern for breaking circular dependencies when you only need a pointer/reference, not the full class definition. 👍


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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 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-26T15:27:55.409Z
Learning: In MoonModules/MoonLight: Never implement auto-restart (esp_restart()) on mutex timeout or errors. The 500ms mutex timeout warning in StatefulService is an intentional debugging aid that should continue execution to enable better debugging. Only users should trigger restarts, never automatic error handling. This prevents masking bugs and allows developers to diagnose issues.

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-30T20:14:11.929Z
Learning: In MoonModules/MoonLight ModuleDevices group sync: Use boolean `isOriginalSender` flag instead of sequence number for deduplication. When originator broadcasts control to group (isOriginalSender=true), members apply locally but don't re-broadcast (pass non-numeric originId to prevent addUpdateHandler from triggering sendUDP). This prevents infinite broadcast loops with zero memory overhead (no state cache needed), versus sequence number approach which requires caching last state and processing 250× more packets (each member broadcasts duplicates).
</add_learning>

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-04T09:52:08.096Z
Learning: MoonLight ModuleDevices: To reduce missed packets when many devices send at once, add a 0–20 ms non-blocking jitter only to non-control (status) UDP broadcasts (post-control apply and optionally the 10s heartbeat). Implement by scheduling the send in loop20ms instead of delaying inside addUpdateHandler/httpd context; keep control packets immediate.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-28T12:31:17.304Z
Learning: In MoonModules/MoonLight compareRecursive (src/MoonBase/Module.cpp): Treat an object’s "name" change as an identity change. When oldData["name"] != newData["name"], emit only the "name" UpdatedItem for that parent and short‑circuit (do not recurse into the subtree like controls). This prevents spurious controls.* updates after renames while keeping the update flow generic and not tied to specific child keys.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 104
File: src/MoonLight/Layers/PhysicalLayer.cpp:145-147
Timestamp: 2026-01-13T15:18:48.740Z
Learning: In MoonLight's PhysicalLayer and Node architecture (src/MoonLight/Layers/PhysicalLayer.cpp, src/MoonBase/Nodes.h): effectsMutex, driversMutex, and layerMutex are mandatory for system operation. These mutexes are validated at creation time (constructor checks log errors if xSemaphoreCreateMutex returns nullptr). Runtime null checks on these mutex pointers are intentionally omitted—the design treats successful mutex allocation as an invariant after initialization, placing responsibility on the programmer to ensure proper allocation. This is a performance-oriented "fail-fast at boot" approach rather than defensive runtime checking.
<!-- </add_learning>

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-28T12:08:02.954Z
Learning: MoonLight NodeManager (src/MoonBase/NodeManager.h): After switching to direct onUpdate in httpd, compareRecursive continues with its captured newData and can emit controls updates immediately after a node rename. To avoid spurious control updates, NodeManager::onUpdate must (1) guard controls array/index, and (2) skip when the current control property already equals updatedItem.value. Only "value" and "default" should drive nodeClass->updateControl/onUpdate; other control fields are UI metadata and can be ignored or just trigger requestUIUpdate.

backend
=======
- main: first module.begin, then shared*.begin
- FileManager: use shared writeToFSDelayed
- SharedFsPersistence: readFromFs from register to begin
- NodeManager: _fileManager->read() : fix sharedFsPersistence->readFromFS(_moduleName);
@ewowi
Copy link
Collaborator Author

ewowi commented Feb 10, 2026

@coderabbitai resume

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

✅ Actions performed

Reviews resumed.

1 similar comment
@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

✅ Actions performed

Reviews resumed.

backend
=======
- main.cpp: update sizeof() -> much better
- Nodemanager: checkAndAlloc: update name if non AZaz09 part changed. addNode: name is by reference argument
- Layouts: carLights: adjustments
@ewowi
Copy link
Collaborator Author

ewowi commented Feb 10, 2026

@coderabbitai, please review 45c03a5 and 8f60da0

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== COMMIT 1: 45c03a5de34e0f3e5ee34f7826b9f2598da1c617 ==="
gh api repos/MoonModules/MoonLight/commits/45c03a5de34e0f3e5ee34f7826b9f2598da1c617 --jq '{message: .commit.message, author: .commit.author, files: .files | length}'

echo -e "\n=== FILES CHANGED IN COMMIT 1 ==="
gh api repos/MoonModules/MoonLight/commits/45c03a5de34e0f3e5ee34f7826b9f2598da1c617 --jq '.files[] | {filename: .filename, status: .status, additions: .additions, deletions: .deletions}'

echo -e "\n=== COMMIT 1 DIFF ==="
git fetch origin 45c03a5de34e0f3e5ee34f7826b9f2598da1c617 2>/dev/null || true
git show 45c03a5de34e0f3e5ee34f7826b9f2598da1c617

echo -e "\n\n=== COMMIT 2: 8f60da02bb8fd2f1e891df4f01f242335cfdc3b6 ==="
gh api repos/MoonModules/MoonLight/commits/8f60da02bb8fd2f1e891df4f01f242335cfdc3b6 --jq '{message: .commit.message, author: .commit.author, files: .files | length}'

echo -e "\n=== FILES CHANGED IN COMMIT 2 ==="
gh api repos/MoonModules/MoonLight/commits/8f60da02bb8fd2f1e891df4f01f242335cfdc3b6 --jq '.files[] | {filename: .filename, status: .status, additions: .additions, deletions: .deletions}'

echo -e "\n=== COMMIT 2 DIFF ==="
git fetch origin 8f60da02bb8fd2f1e891df4f01f242335cfdc3b6 2>/dev/null || true
git show 8f60da02bb8fd2f1e891df4f01f242335cfdc3b6

Length of output: 15629


@ewowi, excellent work! Both commits look great. 🎯

✅ Commit 1 (45c03a5): SharedFsPersistence fixes

Perfect implementation of all my recommendations!

Delayed Writes Fix

// FileManager.cpp - Now using the correct queue ✅
SharedFSPersistence::writeToFSDelayed('W');  // was: FSPersistence<int>::writeToFSDelayed

Initialization Order Fix

The sequence is now correct:

  1. sharedFsPersistence->registerModule() - registers without reading
  2. module->begin() - initializes nodes vector and other state
  3. sharedFsPersistence->begin() - NOW safe to read and trigger onUpdate()

This matches the old FSPersistence behavior and prevents the null-pointer crash! 🎉

Circular Dependency Solution

// NodeManager.h - Clean forward declaration ✅
extern SharedFSPersistence* sharedFsPersistence;
// ...
sharedFsPersistence->readFromFS(_moduleName);

No includes needed, no circular dependency. Perfect!


✅ Commit 2 (8f60da0): Emoji-resilient node names

This is a clever solution! 👏

The Problem It Solves

When you update an effect definition to add/remove/change emojis (like "Bouncing Balls 🔥🎨🐙" → "Bouncing Balls 🔥🐙"), the old state still has the old name. Without this fix, the effect wouldn't match and couldn't be loaded.

The Solution

// checkAndAlloc - Updates name if only non-AZaz09 parts changed
if (equalAZaz09(name, T::name())) {
  strlcpy(name, getNameAndTags<T>().c_str(), 32);  // ✅ Normalize to current definition
  return allocMBObject<T>();
}

Then in onUpdate:

char name[32];
strlcpy(name, updatedItem.value, 32);
Node* nodeClass = addNode(updatedItem.index[0], name, nodeState["controls"]);
if (updatedItem.value != name) nodeState["name"] = name;  // ✅ Persist the updated name

This means:

  • Old state: "Bouncing Balls 🔥🎨🐙"
  • Current definition: "Bouncing Balls 🔥🐙"
  • equalAZaz09() matches (ignores emoji)
  • Name gets updated to current definition
  • State gets synchronized ✅

Memory Improvements 🚀

The sizeof() updates show massive savings:

  • Module: 1144 → 208 bytes (82% reduction!)
  • SharedWebSocketServer: 352 → 432 bytes (slight increase for shared infrastructure)
  • Overall: Big win, moving from per-module FSPersistence to shared instance

Minor Notes

CarLightsLayout adjustments: Ring centers moved from Y=10 to Y=8, and outer rings reduced from 34 to 32 LEDs. Assuming this matches your hardware better! 👍

Const correctness trade-off: The char* name (non-const) parameter is intentional for the in-place name normalization. It's a pragmatic choice that makes the emoji handling elegant without requiring string allocations.


Summary

Both commits are solid! The SharedFSPersistence migration is now complete and correct:

  • ✅ Initialization timing fixed (no more boot crash)
  • ✅ Delayed writes routing fixed (save/cancel now work)
  • ✅ Circular dependency avoided (forward declaration pattern)
  • ✅ Emoji-resilient names (state survives tag changes)
  • ✅ Memory footprint dramatically reduced

Great debugging and implementation! 🎉


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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-28T12:08:02.954Z
Learning: MoonLight NodeManager (src/MoonBase/NodeManager.h): After switching to direct onUpdate in httpd, compareRecursive continues with its captured newData and can emit controls updates immediately after a node rename. To avoid spurious control updates, NodeManager::onUpdate must (1) guard controls array/index, and (2) skip when the current control property already equals updatedItem.value. Only "value" and "default" should drive nodeClass->updateControl/onUpdate; other control fields are UI metadata and can be ignored or just trigger requestUIUpdate.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-28T12:31:17.304Z
Learning: In MoonModules/MoonLight compareRecursive (src/MoonBase/Module.cpp): Treat an object’s "name" change as an identity change. When oldData["name"] != newData["name"], emit only the "name" UpdatedItem for that parent and short‑circuit (do not recurse into the subtree like controls). This prevents spurious controls.* updates after renames while keeping the update flow generic and not tied to specific child keys.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-30T20:14:11.929Z
Learning: In MoonModules/MoonLight ModuleDevices group sync: Use boolean `isOriginalSender` flag instead of sequence number for deduplication. When originator broadcasts control to group (isOriginalSender=true), members apply locally but don't re-broadcast (pass non-numeric originId to prevent addUpdateHandler from triggering sendUDP). This prevents infinite broadcast loops with zero memory overhead (no state cache needed), versus sequence number approach which requires caching last state and processing 250× more packets (each member broadcasts duplicates).
</add_learning>

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 114
File: src/MoonBase/Modules/ModuleDevices.h:39-49
Timestamp: 2026-01-26T12:58:47.381Z
Learning: In MoonModules/MoonLight: Do not add null checks for essential components like `moduleControl` in ModuleDevices or similar core dependencies. A null value for these essential components indicates a programming error (bug in initialization/calling code) that should fail-fast rather than be masked with defensive checks. This aligns with the "don't mask programming errors" philosophy used throughout the codebase.

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-28T12:18:44.193Z
Learning: MoonLight NodeManager controls updates: After rename, compareRecursive continues with captured newData and will emit controls.* changes. Ensure NodeManager::onUpdate:
- Applies only controls.value/default to the node (updateControl + onUpdate), guarded by equality against current control state to avoid re-applying after rename.
- Propagates all other controls metadata (name/min/max/type/valid/size/…) to state/UI and triggers requestUIUpdate, but does not call nodeClass->updateControl for these metadata fields.
This prevents spurious back-application while keeping the UI in sync with the new control schema.

@ewowi ewowi merged commit 8eff0a5 into main Feb 10, 2026
33 checks passed
@ewowi ewowi deleted the LED-tubes branch February 10, 2026 16:26
This was referenced Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant