Skip to content

Sensors#128

Open
ewowi wants to merge 4 commits intomainfrom
sensors
Open

Sensors#128
ewowi wants to merge 4 commits intomainfrom
sensors

Conversation

@ewowi
Copy link
Collaborator

@ewowi ewowi commented Feb 16, 2026

Summary by CodeRabbit

  • New Features
    • PIR sensor support for motion-based light control on compatible boards
    • Per-source debounce for push/toggle/PIR inputs for more reliable controls
  • Refactor
    • Unified periodic hooks and centralized module lifecycle for consistent timing and initialization
    • Pin reading moved to an event-driven flow for cleaner startup
  • UI
    • I2C pin labels show connector icon; PIR exposed in public pin controls
    • Update dialog now consumes full release data for firmware installs
  • Chores
    • Build metadata and dependency reference updates; system status exposes lps value

pio.ini: Latest FastLED

Backend
=======
- IO: add pin_PIR and use in board_LuxceoMood1XiaoMod (pin 4)
- LightsControl: support PIR
@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Walkthrough

Adds PIR (motion) sensor support, per-source debounce for inputs, and centralizes periodic module loops: new pin_PIR, PIR pin assignment/UI, PIR handling in lights control, new Module hooks (loop20ms, loop1s, loop10s) with centralized module registration and per-interval invocation; plus minor build metadata and FastLED dependency updates.

Changes

Cohort / File(s) Summary
Build config
platformio.ini
Bumped APP_DATE and FASTLED_BUILD; updated FastLED lib_deps commit/hash/date.
IO / Board presets
src/MoonBase/Modules/ModuleIO.h
Added pin_PIR to IO_PinUsageEnum; adjusted LuxceoMood1XiaoMod LED pin list and assigned pin 4 to PIR; added PIR ♨️ control UI entry; updated I2C labels to include emoji.
Module base / lifecycle
src/MoonBase/Module.h, src/MoonBase/Modules/ModuleDevices.h, src/MoonBase/Modules/ModuleTasks.h
Introduced virtual hooks loop20ms(), loop1s(), loop10s(); moved per-tick logic to loop20ms() and added override specifiers.
Drivers / Effects / LiveScripts
src/MoonLight/Modules/ModuleDrivers.h, src/MoonLight/Modules/ModuleEffects.h, src/MoonLight/Modules/ModuleLiveScripts.h
Replaced loop() overrides with loop20ms(); Effects added loop1s(); drivers moved initial pin read toward event-driven readPins() (removed loop-based init).
Lights control
src/MoonLight/Modules/ModuleLightsControl.h
Added public uint8_t pinPIR; extended readPins() to parse/configure PIR pin (INPUT/INPUT_PULLUP) and log; introduced per-source debounce timestamps and per-source last-state variables; added PIR-driven lightsOn handling; migrated per-tick work to loop20ms().
ModuleIO runtime tweaks
src/MoonBase/Modules/ModuleIO.h
Renamed loop()loop20ms(); added one-time post-boot update flag to trigger initial read/pin updates and I2CReady handling.
Main app / module registration
src/main.cpp
Centralized creation of shared routers (HTTP/WebSocket/Event/FS), built modules vector, registered modules with routers, unified periodic invocation of loop20ms()/loop1s()/loop10s() across modules; removed scattered per-module loop calls and diagnostics.
UI / Update flow
interface/src/routes/system/update/GithubFirmwareManager.svelte
confirmGithubUpdate now accepts release and iterates release.assets; adjusted checks and messaging to reference release fields.
System status
lib/framework/SystemStatus.cpp
Added "lps": 100 field to system status JSON.
Minor comments / constants
src/MoonLight/Nodes/Drivers/D_Infrared.h, lib/framework/UploadFirmwareService.h, src/MoonLight/Nodes/Drivers/D_IMU.h
Infrared comment clarified; ESP32P4 chip ID constant changed (19→18); IMU: added i2cActuallyReady flag and IO update subscription, adjusted init gating.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Main as "main.cpp"
    participant Router as "Shared Routers\n(HTTP/WebSocket/Event/FS)"
    participant Module as "ModuleLightsControl"
    participant HW as "Hardware GPIO / PIR"

    Main->>Router: create shared routers
    Main->>Module: create & registerWithRouters(Router)
    Main->>Module: init()

    loop every 20ms
        Main->>Module: loop20ms()
        Module->>HW: digitalRead(pinPIR)
        HW-->>Module: level (HIGH/LOW)
        Module->>Module: debounce & update lastPIRPinState
        Module->>Module: set lightsOn based on PIR
        Module->>Router: emit state/event if changed
    end

    loop every 1s
        Main->>Module: loop1s()
        Module->>Router: update shared status (fps/clients/connection)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I sniffed the night and found a spark,
A PIR blinked and lit the dark,
Loops now tick at twenty and one,
Modules gather — work begun,
MoonLight hops, the lights say "hark!" ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Sensors' is vague and does not clearly describe the main changes in the pull request. Use a more specific title that captures the primary objective, such as 'Fix sensor initialization race and centralize pin reading' or 'Add PIR sensor support and improve I2C initialization timing'.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.91% 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 sensors

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/MoonBase/Modules/ModuleIO.h`:
- Around line 72-73: The enum now includes pin_PIR but setupDefinition() never
registers a matching UI entry, so update setupDefinition() to call
addControlValue(control, "PIR") (or "PIR ♨️") for the usage select after the
existing "Reserved" entry; locate the usage select registration in
setupDefinition and add addControlValue(control, "PIR") for the pin_PIR enum
value so pins mapped to pin_PIR show correctly in the web UI.

In `@src/MoonLight/Modules/ModuleLightsControl.h`:
- Around line 406-408: The debounce state is shared across independent inputs
(lastDebounceTime, lastButtonPinState), causing interference between the push
button, toggle button and PIR handlers; add per-input debounce variables (for
example lastDebounceTimePush, lastDebounceTimeToggle, lastDebounceTimePIR and
lastButtonPinStatePush, lastButtonPinStateToggle, lastButtonPinStatePIR) and
keep debounceDelay as-is or make a per-input constant if needed, then update
each handler (push button handler, toggle button handler, PIR handler) to
reference its corresponding lastDebounceTime* and lastButtonPinState* variables
instead of the shared ones so each input debounces independently.
- Around line 488-500: The PIR handler in ModuleLightsControl.h currently forces
_state.data["lightsOn"] to match pinPIR on every debounce interval, preventing
manual overrides; change the logic in the block that reads pinPIR to act on edge
transitions or respect a PIR-enable/timeout mode instead of absolute level:
introduce and maintain a persistent previous PIR state (e.g., lastPirState) and
only call update(...) when the PIR input transitions (rising edge -> optionally
set lightsOn true; falling edge -> start/complete a configurable auto-off
timeout before setting lightsOn false), or add a boolean state flag (pirEnabled)
that the UI can toggle and guard the PIR handling with it; keep use of millis(),
lastDebounceTime, debounceDelay and still call update(newState,
ModuleState::update, _moduleName) only when you actually want to change
lightsOn.

backend
=======
main: use loopxx() virtual functions
- modules: use loop20ms instead of loopms, add loop1s and loop10s
- Module effects: loop1s: set sharedData
- Lights control: PIR ffixes
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/MoonBase/Modules/ModuleDevices.h (1)

119-125: Note: Module::loop20ms() is not called here.

Unlike ModuleIO and ModuleLightsControl which call Module::loop20ms(), this override skips it. This means requestUIUpdate flag processing is bypassed. This appears intentional since ModuleDevices uses update() directly, but it diverges from the documented contract on Line 113 of Module.h: "any Module that overrides loop20ms() must continue to call Module::loop20ms()".

If requestUIUpdate is never set on this module, there's no functional issue — just a consistency concern.

src/main.cpp (1)

389-423: Consider removing commented-out sizeof diagnostics instead of keeping them as dead code.

This block of commented-out sizeof(...) expressions adds visual clutter without providing runtime value. If these are useful for future debugging, consider moving them to a separate header/doc or behind a #if 0 / dedicated build flag so the intent is explicit.

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 16, 2026

@coderabbitai, I am using the updated firmware manager (see d5a1806) but firmware installs are not working. See logging below. Can you explain what is happening? Look at E (200994) boot_comm: Image requires efuse blk rev >= v16.0, but chip is v1.3 especially.

Logging:

[133358][I][DownloadFirmwareService.cpp:220] downloadUpdate(): [🐼] Starting OTA from: https://github.com/MoonModules/MoonLight/releases/download/v0.8.0/MoonLight_esp32-s3-n16r8v_0-8-0_webflash.bin
[133378][W][DownloadFirmwareService.cpp:126] updateTask(): [🐼] Skipping SSL certificate verification for OTA update!
[133382][D][EventSocket.cpp:293] handleClientInfo(): [🐼] Client 53 visible: Yes
[133398][D][HTTPClient.cpp:290] beginInternal(): protocol: https, host: github.com port: 443 url: /MoonModules/MoonLight/releases/download/v0.8.0/MoonLight_esp32-s3-n16r8v_0-8-0_webflash.bin
[134417][D][HTTPClient.cpp:574] sendRequest(): request type: 'GET' redirCount: 0

[134426][D][NetworkManager.cpp:83] hostByName(): Clearing DNS cache
[134440][D][NetworkManager.cpp:127] hostByName(): DNS found IPv4 140.82.121.4
[134467][D][ssl_client.cpp:196] start_ssl_client(): WARNING: Skipping SSL Verification. INSECURE!
[134961][D][HTTPClient.cpp:1117] connect(): connected to github.com:443
[135296][D][HTTPClient.cpp:1259] handleHeaderResponse(): code: 302
[135303][D][HTTPClient.cpp:618] sendRequest(): sendRequest code=302

[135310][D][HTTPClient.cpp:657] sendRequest(): following redirect (dropped to GET/HEAD): 'https://release-assets.githubusercontent.com/github-production-release-asset/944519235/aed33b2b-6f64-4d40-90aa-f540e83fc79c?sp=r&sv=2018-11-09&sr=b&spr=https&se=2026-02-16T14%3A30%3A33Z&rscd=attachment%3B+filename%3DMoonLight_esp32-s3-n16r8v_0-8-0_webflash.bin&rsct=application%2Foctet-stream&skoid=96c2d410-5711-43a1-aedd-ab1947aa7ab0&sktid=398a6654-997b-47e9-b12b-9515b896b4de&skt=2026-02-16T13%3A29%3A42Z&ske=2026-02-16T14%3A30%3A33Z&sks=b&skv=2018-11-09&sig=%2BG1bdJR5VmeppVTKv7tsMplb5SvvjUpeARV56RR%2FWFs%3D&jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmVsZWFzZS1hc3NldHMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwia2V5Ijoia2V5MSIsImV4cCI6MTc3MTI0OTMzNiwibmJmIjoxNzcxMjQ5MDM2LCJwYXRoIjoicmVsZWFzZWFzc2V0cHJvZHVjdGlvbi5ibG9iLmNvcmUud2luZG93cy5uZXQifQ.KiOWJig_81pbOs6TZf_oEthlb5ptraAbWjOK7SWfJ2I&response-content-disposition=attachment%3B%20filename%3DMoonLight_esp32-s3-n16r8v_0-8-0_webflash.bin&response-content-type=application%2Foctet-stream' redirCount: 1

[135407][D][HTTPClient.cpp:390] disconnect(): tcp is closed

[135414][D][HTTPClient.cpp:290] beginInternal(): protocol: https, host: release-assets.githubusercontent.com port: 443 url: /github-production-release-asset/944519235/aed33b2b-6f64-4d40-90aa-f540e83fc79c?sp=r&sv=2018-11-09&sr=b&spr=https&se=2026-02-16T14%3A30%3A33Z&rscd=attachment%3B+filename%3DMoonLight_esp32-s3-n16r8v_0-8-0_webflash.bin&rsct=application%2Foctet-stream&skoid=96c2d410-5711-43a1-aedd-ab1947aa7ab0&sktid=398a6654-997b-47e9-b12b-9515b896b4de&skt=2026-02-16T13%3A29%3A42Z&ske=2026-02-16T14%3A30%3A33Z&sks=b&skv=2018-11-09&sig=%2BG1bdJR5VmeppVTKv7tsMplb5SvvjUpeARV56RR%2FWFs%3D&jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmVsZWFzZS1hc3NldHMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwia2V5Ijoia2V5MSIsImV4cCI6MTc3MTI0OTMzNiwibmJmIjoxNzcxMjQ5MDM2LCJwYXRoIjoicmVsZWFzZWFzc2V0cHJvZHVjdGlvbi5ibG9iLmNvcmUud2luZG93cy5uZXQifQ.KiOWJig_81pbOs6TZf_oEthlb5ptraAbWjOK7SWfJ2I&response-content-disposition=attachment%3B%20filename%3DMoonLight_esp32-s3-n16r8v_0-8-0_webflash.bin&response-content-type=application%2Foctet-stream
[135506][D][HTTPClient.cpp:574] sendRequest(): request type: 'GET' redirCount: 1

[135530][D][NetworkManager.cpp:127] hostByName(): DNS found IPv4 185.199.110.133
[135551][D][ssl_client.cpp:196] start_ssl_client(): WARNING: Skipping SSL Verification. INSECURE!
[135625][D][PsychicHttpServer.cpp:252] closeCallback(): [🔮] Client disconnected 53
[135634][I][EventSocket.cpp:58] onWSClose(): [🐼] ws[192.168.1.168][53] disconnect
[135811][D][HTTPClient.cpp:1117] connect(): connected to release-assets.githubusercontent.com:443
[135951][D][HTTPClient.cpp:1259] handleHeaderResponse(): code: 200
[135958][D][HTTPClient.cpp:1262] handleHeaderResponse(): size: 2663872
[135965][D][HTTPClient.cpp:618] sendRequest(): sendRequest code=200

[135973][D][HTTPUpdate.cpp:265] handleUpdate(): Header read fin.

[135980][D][HTTPUpdate.cpp:266] handleUpdate(): Server header:

[135986][D][HTTPUpdate.cpp:267] handleUpdate(): - code: 200

[135993][D][HTTPUpdate.cpp:268] handleUpdate(): - len: 2663872

[136000][D][HTTPUpdate.cpp:280] handleUpdate(): ESP32 info:

[136007][D][HTTPUpdate.cpp:281] handleUpdate(): - free Space: 3145728

[136014][D][HTTPUpdate.cpp:282] handleUpdate(): - current Sketch Size: 2899392

[136253][I][DownloadFirmwareService.cpp:72] update_started(): [🐼] HTTP Update started
[136363][D][HTTPUpdate.cpp:351] handleUpdate(): runUpdate flash...

[136376][D][Updater.cpp:239] begin(): OTA Partition: app1
[136952][D][SharedFSPersistence.h:121] writeToFS(): [🐼] delayedWrites: Add /.config/devices.json
[138403][D][PsychicHttpServer.cpp:226] openCallback(): [🔮] New client connected 53
[138423][I][EventSocket.cpp:46] onWSOpen(): [🐼] ws[192.168.1.168][53] connect
[138455][D][EventSocket.cpp:293] handleClientInfo(): [🐼] Client 53 visible: Yes
[139204][D][SharedFSPersistence.h:121] writeToFS(): [🐼] delayedWrites: Add /.config/tasks.json
[162528][D][PsychicHttpServer.cpp:252] closeCallback(): [🔮] Client disconnected 54
E (200994) boot_comm: Image requires efuse blk rev >= v16.0, but chip is v1.3
[201121][E][HTTPUpdate.cpp:491] runUpdate(): Update.end failed! (Could Not Activate The Firmware)

[201137][E][HTTPUpdate.cpp:402] handleUpdate(): Update failed

[201145][D][HTTPClient.cpp:390] disconnect(): tcp is closed

[201160][E][DownloadFirmwareService.cpp:156] updateTask(): [🐼] HTTP Update failed with error (9): Update error: Could Not Activate The Firmware

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the commit mentioned by the user
git log --oneline -1 d5a1806a30ff18ff087b7ae245c7aa0c210646d3

# Look at platformio.ini to understand build configuration
cat platformio.ini | grep -A 20 "esp32-s3-n16r8v" || echo "No specific esp32-s3-n16r8v configuration found"

# Search for chip revision or efuse related configuration
rg -n "CONFIG_ESP32S3_REV_MIN|chip_revision|efuse|REV_MIN" platformio.ini || echo "No revision config found in platformio.ini"

# Check for board configuration files
fd -t f "esp32-s3" | head -20

Length of output: 2109


🏁 Script executed:

#!/bin/bash
# Check the ESP32-S3 specific configuration file
cat firmware/esp32-s3.ini

# Look for the board definition and any chip revision settings
rg -n "esp32-s3-n16r8v" firmware/esp32-s3.ini -A 30

# Check if there are any chip revision or minimum version settings in build flags
rg -n "CONFIG_ESP32S3_REV_MIN|DCONFIG_ESP32S3|chip.*rev|min.*rev" firmware/esp32-s3.ini -i

Length of output: 6403


🏁 Script executed:

#!/bin/bash
# Check for sdkconfig files that might set chip revision requirements
fd -t f "sdkconfig" | xargs cat 2>/dev/null | grep -i "rev_min\|chip.*rev" | head -20 || echo "No sdkconfig files found with revision settings"

# Check the board definition file if it exists
cat boards/ESP32_16MB_9MB_FS.csv 2>/dev/null || echo "Partition file found, checking..."

# Look for any ESP-IDF configuration or menuconfig settings
rg -n "CONFIG_ESP32S3_REV_MIN" . --type-add 'config:*.{conf,config,sdkconfig*}' -t config || echo "No REV_MIN config found"

# Check if there's a custom board definition
ls -la boards/ 2>/dev/null || echo "No boards directory"

# Search for any recent changes to build configuration that might affect chip requirements
git log --oneline --since="2 weeks ago" -- "firmware/esp32-s3.ini" "platformio.ini" | head -10

Length of output: 1548


🌐 Web query:

ESP32-S3 chip revision efuse blk rev v16.0 vs v1.3 OTA update error

💡 Result:

That message usually means ESP-IDF’s image compatibility check is rejecting the new OTA image because the chip’s eFuse block revision is higher than what the image says it supports.

What “eFuse blk rev v16.0 vs v1.3” is

  • ESP32-S3 has eFuse block version fields (major/minor), and ESP-IDF represents them as major * 100 + minor and prints them like vMAJOR.MINOR.
  • During OTA, the running app checks the new image’s declared min/max chip rev and min/max eFuse block rev against the chip’s eFuses, using the same logic as the bootloader. If the chip’s eFuse block rev is outside the image’s allowed range, OTA is rejected (or the image won’t boot after reboot). [1]

So if your chip reports eFuse block rev = v16.0, but the OTA image was built with a max supported eFuse block revision of v1.3, the update will fail.

Why it happens

Most commonly: you built the firmware (or the OTA binary) with an older ESP-IDF / older project config whose CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL corresponds to ~v1.x, but you’re flashing/updating a newer ESP32-S3 whose factory eFuses report v16.0. ESP-IDF treats that as “this SDK/image may not understand this eFuse block format (calibration data, etc.)”, so it blocks it. [1]

What to do (recommended order)

  1. Confirm the chip’s eFuse block revision on a failing device:
    • espefuse.py summary (read-only) [2]
  2. Rebuild the OTA image with a newer ESP-IDF that supports your chip’s eFuse block revision, then re-OTA using that new binary. (This updates the image’s embedded max supported eFuse block rev.) [1]
  3. If you must bypass the “max revision” check, ESP-IDF documents an “ignore maximum eFuse block revision” mechanism via an eFuse bit—but burning eFuses is one-time and risky, so only do this if you fully understand the consequences. [1][2]

Key point: don’t try to “fix” the chip from v16.0 to v1.3—you can’t; the fix is to build firmware that declares compatibility with (or supports) the chip’s eFuse block revision. [1]

Sources:
[1] ESP-IDF Programming Guide — Chip & eFuse block revision checks (including OTA-stage checks and related Kconfig symbols) https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/system/chip_revision.html
[2] Espressif esptool docs — espefuse (reading/burning eFuses, one-time programmable warning) https://docs.espressif.com/projects/esptool/en/latest/esp32/espefuse/index.html


🏁 Script executed:

#!/bin/bash
# Check if there were recent ESP-IDF or platform version changes
rg -n "platform.*espressif32" platformio.ini

# Check the platform version and any ESP-IDF specific settings
rg -n "platform_packages|platform.*=" platformio.ini | head -20

# Look for any SDK config or ESP-IDF version constraints
rg -n "framework-espidf|CONFIG_IDF_TARGET|IDF.*VERSION" platformio.ini firmware/esp32-s3.ini

Length of output: 5749


@ewowi, the firmware manager is actually working correctly! The issue is a chip revision compatibility mismatch between the pre-built binary and your hardware.

What's Happening

The error E (200994) boot_comm: Image requires efuse blk rev >= v16.0, but chip is v1.3 means:

  1. Your ESP32-S3 chip has eFuse block revision v1.3 (older hardware revision)
  2. The pre-built binary MoonLight_esp32-s3-n16r8v_0-8-0_webflash.bin from the v0.8.0 release was compiled to require eFuse block revision >= v16.0 (newer hardware)
  3. The ESP32 bootloader refuses to activate firmware that declares it needs a newer chip revision than what's physically present

The firmware downloads successfully and writes to flash, but activation is blocked by the bootloader's hardware compatibility check.

Why This Happened

The v0.8.0 release binaries were likely built with the newer ESP-IDF 5.5.x platform (you're currently using pioarduino/platform-espressif32 55.03.37), which may have defaulted to requiring newer chip revisions for optimal performance/features.

Solutions

Option 1: Build firmware locally (Recommended for your device)

# Your local build will automatically match your chip revision
pio run -e esp32-s3-n16r8v

The locally built firmware will be compatible with your v1.3 chip revision.

Option 2: Adjust release build configuration
To make future release binaries compatible with older ESP32-S3 chips, you can add to firmware/esp32-s3.ini:

[esp32-s3-base]
build_flags = 
  ${env.build_flags}
  ${moonlight.build_flags}
  ${livescripts.build_flags}
  ${HP_ALL_DRIVERS.build_flags}
  -D CONFIG_IDF_TARGET_ESP32S3=1
  -D CONFIG_ESP32S3_REV_MIN_0=y
  ; Add this to support older chip revisions in releases:
  -D CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL=199

This would make release binaries compatible with a wider range of ESP32-S3 hardware revisions, though it may disable some newer hardware features.

The commit d5a1806 you mentioned didn't cause this issue - the firmware manager is functioning properly. This is purely a hardware compatibility issue with the pre-built binary.


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

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 16, 2026

@coderabbitai, regarding

Option 2: Adjust release build configuration

When compiling I get:

Users/ewoud/.platformio/packages/framework-arduinoespressif32-libs/esp32s3/qio_opi/include/sdkconfig.h:1010:9: warning: "CONFIG_ESP32S3_REV_MIN_0" redefined
1010 | #define CONFIG_ESP32S3_REV_MIN_0 1
| ^~~~~~~~~~~~~~~~~~~~~~~~

  So it looks we need to do something else? (we set it to Y while sdkconfig.h sets it to 1...)
  
  My board has ESP32-S3 Rev 2

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Tip

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

@ewowi, you're absolutely right - my previous suggestion was incorrect! The warning is telling you that CONFIG_ESP32S3_REV_MIN_0 is already defined in the precompiled Arduino framework's sdkconfig.h, and you can't override sdkconfig settings via -D flags when using the precompiled Arduino ESP-IDF framework.

The Real Issue

The problem isn't about chip revision settings - it's about max_efuse_blk_rev_full, which is determined by CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL and reflects whether the current IDF version supports this efuse block format or not, and should not be changed by the user.

Important distinction:

  • Your chip revision is ESP32-S3 Rev 2 (wafer revision)
  • Your eFuse block revision is v1.3 (format of calibration data in eFuses)
  • The pre-built binary requires eFuse block rev >= v16.0

These are two separate versioning systems.

Why You Can't Fix This with Build Flags

CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL cannot be changed by user - only Espressif can change it when a new version will be supported in ESP-IDF. This value is baked into the precompiled Arduino framework libraries you're using.

Solutions

Option 1: Build Locally ✅ (Already works for you)
Your local builds automatically match your chip's eFuse block revision because they're compiled on-demand with your exact hardware's capabilities.

Option 2: Fix Release Binaries
To make GitHub release binaries compatible with older ESP32-S3 chips, you'd need to use an older ESP-IDF version that has a lower CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL default. However, this means losing any new features/fixes from ESP-IDF 5.5.x.

Option 3: Document Hardware Requirements
Add a note in your releases specifying which ESP32-S3 eFuse block revisions are supported by the pre-built binaries. Users with older chips (like yours with v1.3) would need to build locally.

The root cause is that ESP-IDF 5.5.x (used in pioarduino/platform-espressif32 55.03.37) declares support for newer eFuse block formats that your older ESP32-S3 Rev 2 chip doesn't have. Unfortunately, there's no easy way to make release binaries universally compatible when using the precompiled Arduino framework.

Recommendation: Stick with local builds for your hardware, or consider providing multiple release variants built with different ESP-IDF versions if you need to support a wide range of ESP32-S3 chip ages.


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

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

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

Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Modify C++ backend code in the `src/` and `lib/` directories for ESP32 development

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 16, 2026

@coderabbitai, are the DebounceTime's for the buttons/pir still needed in ModuleLightsControl now the loop runs every 20ms instead of always? Would save a few bytes if not

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the ModuleLightsControl.h file to see the debounce implementation details
cat src/MoonLight/Modules/ModuleLightsControl.h

Length of output: 22713


@ewowi, No, the debounce delays are still absolutely necessary and should not be removed. Here's why:

Why 20ms loop interval doesn't eliminate debounce needs

Mechanical switch bounce physics haven't changed:

  • Buttons still bounce for 10-50ms (sometimes up to 157ms)
  • PIR sensors can have similar signal instability
  • The 20ms loop just means you're sampling faster, not that the hardware behaves differently

What would happen without debounce:

With your current 50ms debounce, at a single button press:

T=0ms:   Read LOW → toggle lights ✓
T=20ms:  Skip (not >50ms since last check)
T=40ms:  Skip
T=60ms:  Read state again (likely stable)

Without debounce (reading every 20ms during bounce):

T=0ms:   Read LOW → toggle lights (ON)
T=20ms:  Read HIGH (bounce) → toggle lights (OFF!)  
T=40ms:  Read LOW (bounce) → toggle lights (ON!)
T=60ms:  Read HIGH (bounce) → toggle lights (OFF!)
T=80ms:  Read LOW (stable) → toggle lights (ON!)

Result: Lights flicker 5 times for a single button press! 🎇

Current implementation

Your current 50ms debounce acts as rate-limiting - it samples the button every ~50-60ms instead of every 20ms. This gives mechanical contacts time to settle between samples, drastically reducing false triggers.

Memory consideration

The debounce variables use:

  • 3 × unsigned long (12 bytes) for timestamps
  • 3 × int (12 bytes) for last states
  • 1 × constexpr (0 bytes at runtime - it's compile-time)

Total: 24 bytes - negligible for ESP32 with hundreds of KB of RAM.

Verdict

Keep the debounce! It's preventing a significant user experience bug. Those 24 bytes are buying you reliable button behavior. 👍


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

Learnt from: 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-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-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-16T15:53:11.236Z
Learning: For ESP32-PICO-MINI-02 (and similar ESP32 boards with PSRAM), the critical configuration to enable PSRAM is `board_build.arduino.memory_type = qio_qspi` in platformio.ini. The board JSON may already include `-DBOARD_HAS_PSRAM` and `flash_mode: qio`, but without the memory_type setting, PSRAM won't be properly initialized, causing LoadStoreError crashes when code tries to access PSRAM addresses.

Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Modify C++ backend code in the `src/` and `lib/` directories for ESP32 development

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-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-02-14T19:58:39.117Z
Learning: ESP32-P4/httpd: Heavy onUpdate cascades (preset apply, rename, controls rebuild) can exhaust 6 KB httpd stack; 8192 bytes is the safe default. Defer heavy work from onUpdate (httpd) to the SvelteKit loop and avoid large StaticJsonDocument locals in onUpdate to preserve stack headroom.

firmware
========
- pio.ini: latest FastLED

Frontend
========
- Firmware manager: confirmGithubUpdate clearer message if no matching firmware found

backend
=======
- IO: callUpdateHandlers once in loop
- Module Driver: addUpdateHandler in constructor, no initial readPins (see IO)
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

🧹 Nitpick comments (2)
interface/src/routes/system/update/GithubFirmwareManager.svelte (1)

156-156: Case-sensitive "Nightly" check.

release.name.includes("Nightly") won't match "nightly" or "NIGHTLY BUILD". Consider a case-insensitive check if naming conventions aren't strictly enforced:

-|| release.name.includes("Nightly")}
+|| release.name.toLowerCase().includes("nightly")}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/system/update/GithubFirmwareManager.svelte` at line 156,
The conditional that checks for Nightly builds is case-sensitive: update the
check around release.name.includes("Nightly") used with
compareVersions(page.data.features.firmware_version, release.tag_name) so it
performs a case-insensitive match (e.g., normalize release.name to lower/upper
or use a case-insensitive regex) to catch "nightly", "NIGHTLY BUILD", etc.; keep
the overall logic (install if versions differ OR it's a nightly) but replace the
includes("Nightly") part with a case-insensitive test on release.name.
src/main.cpp (1)

389-457: Dead sizeof diagnostics block is duplicated and unreachable.

This #if 0 block contains two near-identical copies of the same sizeof listing (lines 391–424 and 425–456). It's also placed after vTaskDelete(NULL) on most builds, making it doubly unreachable. Consider removing the duplicate or the entire block.

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

In `@src/main.cpp` around lines 389 - 457, The duplicated, unreachable diagnostic
block guarded by "#if 0" (containing repeated sizeof(...) lines) should be
removed or collapsed; locate the "#if 0" / "#endif" block that lists
sizeof(esp32sveltekit), sizeof(Module), sizeof(moduleDevices), etc. (it's also
placed after vTaskDelete(NULL)), and delete the duplicate copy (or remove the
entire block) so only one diagnostic listing remains if needed or none at all;
ensure you remove both repeated series between the matching "#if 0" and "#endif"
and keep surrounding code intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/routes/system/update/GithubFirmwareManager.svelte`:
- Around line 59-70: In confirmGithubUpdate(release: any) guard against missing
or empty release.assets by checking if release.assets is truthy and has length
and return early (or show an error) if not; then replace the loose substring
check that uses
release.assets[i].name.includes(page.data.features.firmware_built_target) with a
stricter match (for example use a regex with word boundaries anchored around
page.data.features.firmware_built_target or split the asset name into segments
and compare the exact segment) so you only pick the correct .bin asset, and keep
the existing .bin check and browser_download_url assignment.

In `@lib/framework/SystemStatus.cpp`:
- Line 200: The hardcoded root["lps"] = 100 is misleading; either measure real
loops-per-second or make it a named constant/target. Fix by replacing the magic
100: if you want real LPS, add a counter (e.g., lpsCounter) incremented in the
main loop hook (e.g., loop20ms or mainLoop) and compute a rate over a short
interval (resetting the counter or using a rolling window) then assign that
computed value to root["lps"]; alternatively, if it is a target, introduce a
descriptive constant (e.g., constexpr int TARGET_LPS) and set root["target_lps"]
= TARGET_LPS and remove the misleading root["lps"] assignment. Ensure you modify
SystemStatus.cpp where root["lps"] is set and update any callers/consumers
accordingly.

---

Nitpick comments:
In `@interface/src/routes/system/update/GithubFirmwareManager.svelte`:
- Line 156: The conditional that checks for Nightly builds is case-sensitive:
update the check around release.name.includes("Nightly") used with
compareVersions(page.data.features.firmware_version, release.tag_name) so it
performs a case-insensitive match (e.g., normalize release.name to lower/upper
or use a case-insensitive regex) to catch "nightly", "NIGHTLY BUILD", etc.; keep
the overall logic (install if versions differ OR it's a nightly) but replace the
includes("Nightly") part with a case-insensitive test on release.name.

In `@src/main.cpp`:
- Around line 389-457: The duplicated, unreachable diagnostic block guarded by
"#if 0" (containing repeated sizeof(...) lines) should be removed or collapsed;
locate the "#if 0" / "#endif" block that lists sizeof(esp32sveltekit),
sizeof(Module), sizeof(moduleDevices), etc. (it's also placed after
vTaskDelete(NULL)), and delete the duplicate copy (or remove the entire block)
so only one diagnostic listing remains if needed or none at all; ensure you
remove both repeated series between the matching "#if 0" and "#endif" and keep
surrounding code intact.

Repository owner deleted a comment from coderabbitai bot Feb 17, 2026
Repository owner deleted a comment from coderabbitai bot Feb 17, 2026
@ewowi
Copy link
Collaborator Author

ewowi commented Feb 17, 2026

@coderabbitai,
can you review the readPin fixes in this commit: 27e4260

Using callUpdateHandlers in Module IO and not using readPins() // initially on modules which subscribe to the IO update handler

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the specific commit diff to review the readPin fixes
git fetch origin pull/128/head:pr-128 2>/dev/null || true
git show 27e4260d702856f367c9305a509bd89823a47656 --stat

echo "=== Detailed diff for ModuleIO.h ==="
git show 27e4260d702856f367c9305a509bd89823a47656 -- src/MoonBase/Modules/ModuleIO.h | head -200

echo "=== Detailed diff for ModuleDrivers.h ==="
git show 27e4260d702856f367c9305a509bd89823a47656 -- src/MoonLight/Modules/ModuleDrivers.h

echo "=== Detailed diff for ModuleLightsControl.h ==="
git show 27e4260d702856f367c9305a509bd89823a47656 -- src/MoonLight/Modules/ModuleLightsControl.h | head -150

echo "=== Check Module.h changes ==="
git show 27e4260d702856f367c9305a509bd89823a47656 -- src/MoonBase/Module.h

Length of output: 11867


@ewowi, excellent architectural improvement! The readPin fixes look solid. Here's my review:

✅ What's Good

1. Centralized Initialization Pattern

// ModuleIO.h - Single source of truth for initial pin reads
void loop20ms() override {
  if (!called) {
    callUpdateHandlers(_moduleName);  // triggers readPins() for all subscribers
    called = true;
  }
}

This is much cleaner than having each module manage its own initPins flag.

2. Event-Driven Consistency

// ModuleDrivers constructor - subscribe early, react when notified
_moduleIO->addUpdateHandler([this](const String& originId) { readPins(); }, false);

Modules now purely react to IO updates rather than mixing reactive + polling approaches.

3. Code Reduction

  • Removed initPins flag from ModuleDrivers
  • Removed manual readPins() calls after handler registration
  • Removed entire loop20ms() override from ModuleDrivers

💡 Minor Suggestions

1. Flag naming clarity

bool called = false;  // Generic - could be clearer

Consider renaming to initialUpdateHandlersCalled or bootPinReadTriggered for better self-documentation.

2. Comment precision

// during boot, the IO module is unchanged , not triggering updates, so need to do it manually

Excellent comment! Minor typo: remove space before comma.

3. Handler registration timing
Moving from begin() to constructor is smart - ensures handlers are registered before ModuleIO's loop20ms() runs. ✅

🎯 Architecture Analysis

Aspect Before After
Initialization Each module calls readPins() ModuleIO triggers all handlers once
Pattern Mixed (reactive + polling) Pure event-driven
Code duplication Per-module flags Single flag in ModuleIO
Responsibility Distributed Centralized

🔍 Edge Cases Handled

✅ Boot scenario where IO state doesn't change → manual trigger in loop20ms()
✅ Normal runtime updates → existing handler mechanism works
✅ Module registration timing → constructor ensures early subscription

Unrelated Change

pinMode(pinPIR, INPUT);  // was INPUT_PULLUP

This is a functional change for PIR sensors - correct if your PIR modules have external pull-ups or active output drivers.

Verdict

Approved! 🎉 This is a textbook example of refactoring toward better separation of concerns. The pattern is now consistent, maintainable, and eliminates redundant initialization logic.


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

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

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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-14T19:58:39.117Z
Learning: ESP32-P4/httpd: Heavy onUpdate cascades (preset apply, rename, controls rebuild) can exhaust 6 KB httpd stack; 8192 bytes is the safe default. Defer heavy work from onUpdate (httpd) to the SvelteKit loop and avoid large StaticJsonDocument locals in onUpdate to preserve stack headroom.

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 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: wled-install
Repo: MoonModules/MoonLight PR: 125
File: src/MoonBase/Modules/ModuleIO.h:519-522
Timestamp: 2026-02-15T13:40:05.390Z
Learning: On MyHome-Control P4 Nano Shield V1.0 and V2.0 (board_MHCP4NanoV1, board_MHCP4NanoV2) in src/MoonBase/Modules/ModuleIO.h, all RS485 pins (3, 4, 6, 53 for V2; 3, 4, 22, 24 for V1) are intentionally assigned as pin_RS485_TX to send LED data over RS485-capable pins, not for traditional RS485 UART communication. This prevents RS485 UART initialization in readPins() as designed.

Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Modify C++ backend code in the `src/` and `lib/` directories for ESP32 development

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-13T19:05:38.712Z
Learning: In MoonModules/MoonLight SharedFSPersistence (src/MoonBase/SharedFSPersistence.h): To prevent stale default values from being written to filesystem during initialization, update handlers must be enabled AFTER files are read. Implementation: (1) registerModule() stores module info without enabling handler, (2) begin() first reads all files via readFromFS(), then enables handlers only for modules with delayedWriting=true. This matches FSPersistence behavior where files are loaded before handlers become active, preventing defaults from overwriting persisted state.

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

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the specific commit diff
git show 27e4260d702856f367c9305a509bd89823a47656

Length of output: 266234


🏁 Script executed:

#!/bin/bash
# Look for the files likely touched by the readPins/init change
fd -t f 'ModuleIO.h' src | xargs -I{} sh -c 'echo "----- {} -----"; nl -ba "{}" | sed -n "1,260p"'
fd -t f 'ModuleDrivers.h' src | xargs -I{} sh -c 'echo "----- {} -----"; nl -ba "{}" | sed -n "1,260p"'
# Also check LightsControl for readPins changes
fd -t f 'ModuleLightsControl.h' src | xargs -I{} sh -c 'echo "----- {} -----"; nl -ba "{}" | sed -n "1,260p"'
# Grep for callUpdateHandlers and addUpdateHandler usage in IO/drivers
rg -n 'callUpdateHandlers\(|addUpdateHandler\(' src -S

Length of output: 2287


@ewowi thanks — I walked through the “readPins fixes” and the new IO event pattern. From the code I could locate:

  • ModuleIO subscribes itself to updates to run readPins: src/MoonBase/Modules/ModuleIO.h Line 112.
  • ModuleIO fans out once by calling callUpdateHandlers(_moduleName): src/MoonBase/Modules/ModuleIO.h Line 732.
  • Drivers now subscribe to ModuleIO and no longer do an “initial” readPins: src/MoonLight/Modules/ModuleDrivers.h Line 31.
  • LightsControl also subscribes to IO: src/MoonLight/Modules/ModuleLightsControl.h Line 71.
  • Infrared driver likewise: src/MoonLight/Nodes/Drivers/D_Infrared.h Line 106.

Verdict

  • Direction is good. Moving the “first read” to a single ModuleIO-triggered fan-out removes duplicate inits, synchronizes timing across modules, and prevents edge cases where one module reads before IO is finalized.

Two small nits to lock it down

  1. Ordering on the first tick
  • Make sure ModuleIO reads its own pins before notifying subscribers, so subscribers always see finalized IO state on that first pass.
  1. One-shot gating (boot-only)
  • Ensure the fan-out runs exactly once at boot (or when IO config changes intentionally), and avoid any recursion (readPins should not call callUpdateHandlers itself).

A tiny tweak that covers both
If you haven’t already, gate the first publish in ModuleIO::loop20ms(), read locally, then fan out:

// ModuleIO.h/.cpp
class ModuleIO : public Module {
  // ...
  bool _bootPinsPublished = false;

  void loop20ms() override {
    Module::loop20ms();

    if (!_bootPinsPublished) {
      // 1) Update IO’s own pin state first
      readPins();

      // 2) Now notify subscribers (Drivers, LightsControl, etc.)
      callUpdateHandlers(_moduleName);

      _bootPinsPublished = true;
    }
  }
}

Why this helps

  • Guarantees subscribers never race the initial IO read.
  • Prevents double work (drivers removed their own “initial read”; IO now emits once).
  • Avoids any subtle ordering dependence on the internal registration order of update handlers.

Also advisable

  • Keep readPins() side‑effect free w.r.t. updates: avoid calling callUpdateHandlers() from inside readPins() to prevent reentrancy/recursion on boot.
  • If you ever need to re-drive the fan‑out on config changes (e.g., pins remapped by user), trigger the same two‑step: readPins(); callUpdateHandlers(_moduleName); at that explicit change point rather than inside readPins().

Boot sequence (expected)

sequenceDiagram
  participant Main as main()
  participant IO as ModuleIO
  participant Drivers as ModuleDrivers
  participant Lights as ModuleLightsControl

  Main->>IO: construct + addUpdateHandler(readPins)
  Main->>Drivers: construct + IO.addUpdateHandler(Drivers::readPins)
  Main->>Lights: construct + IO.addUpdateHandler(Lights::readPins)
  Main->>Main: start loop20ms tick
  Main->>IO: loop20ms()
  IO->>IO: readPins()   // IO internal state ready
  IO->>Drivers: callUpdateHandlers("io") -> Drivers::readPins()
  IO->>Lights: callUpdateHandlers("io") -> Lights::readPins()
Loading

Overall, the change looks solid. With the one-shot gating + “IO reads first” ordering, the event-driven init should be robust, consistent, and free from double-initialization across all subscribers.

If you want, I can follow up with a minimal patch once you confirm where you prefer to place the _bootPinsPublished flag (header vs. TU) and whether loop20ms is your standardized 20 ms hook across modules.


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

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

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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-14T19:58:39.117Z
Learning: ESP32-P4/httpd: Heavy onUpdate cascades (preset apply, rename, controls rebuild) can exhaust 6 KB httpd stack; 8192 bytes is the safe default. Defer heavy work from onUpdate (httpd) to the SvelteKit loop and avoid large StaticJsonDocument locals in onUpdate to preserve stack headroom.

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 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: wled-install
Repo: MoonModules/MoonLight PR: 125
File: src/MoonBase/Modules/ModuleIO.h:519-522
Timestamp: 2026-02-15T13:40:05.390Z
Learning: On MyHome-Control P4 Nano Shield V1.0 and V2.0 (board_MHCP4NanoV1, board_MHCP4NanoV2) in src/MoonBase/Modules/ModuleIO.h, all RS485 pins (3, 4, 6, 53 for V2; 3, 4, 22, 24 for V1) are intentionally assigned as pin_RS485_TX to send LED data over RS485-capable pins, not for traditional RS485 UART communication. This prevents RS485 UART initialization in readPins() as designed.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-13T19:05:38.712Z
Learning: In MoonModules/MoonLight SharedFSPersistence (src/MoonBase/SharedFSPersistence.h): To prevent stale default values from being written to filesystem during initialization, update handlers must be enabled AFTER files are read. Implementation: (1) registerModule() stores module info without enabling handler, (2) begin() first reads all files via readFromFS(), then enables handlers only for modules with delayedWriting=true. This matches FSPersistence behavior where files are loaded before handlers become active, preventing defaults from overwriting persisted state.

Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Modify C++ backend code in the `src/` and `lib/` directories for ESP32 development

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: 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.
✅ Actions performed

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.

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 17, 2026

@coderabbitai, I am testing I2S bus and IMU driver (MPU6050) on esp32-D0 and it is not working, see logging. Can you investigate what is going on?

I would expect that it would not say: Found 0 device(s) as I connected the MPU6050 the right way on pin 21 and 22

[ 2587][I][D_IMU.h:40] initBoard(): [💫] AppDrivers (2288) Starting board 0
[ 2596][E][Wire.cpp:424] beginTransmission(): could not acquire lock
[ 2603][E][Wire.cpp:544] write(): NULL TX buffer pointer
[ 2609][E][Wire.cpp:453] endTransmission(): NULL TX buffer pointer
[ 2616][E][Wire.cpp:490] requestFrom(): NULL buffer pointer
[ 2622][E][Wire.cpp:424] beginTransmission(): could not acquire lock
[ 2630][E][Wire.cpp:544] write(): NULL TX buffer pointer
[ 2636][E][Wire.cpp:453] endTransmission(): NULL TX buffer pointer
[ 2643][E][Wire.cpp:490] requestFrom(): NULL buffer pointer
[ 2649][E][Wire.cpp:424] beginTransmission(): could not acquire lock
[ 2656][E][Wire.cpp:544] write(): NULL TX buffer pointer
[ 2662][E][Wire.cpp:453] endTransmission(): NULL TX buffer pointer
[ 2669][E][Wire.cpp:490] requestFrom(): NULL buffer pointer
[ 2676][E][Wire.cpp:424] beginTransmission(): could not acquire lock
[ 2683][E][Wire.cpp:544] write(): NULL TX buffer pointer
[ 2689][E][Wire.cpp:453] endTransmission(): NULL TX buffer pointer
[ 2696][E][Wire.cpp:490] requestFrom(): NULL buffer pointer
[ 2702][E][Wire.cpp:424] beginTransmission(): could not acquire lock
[ 2710][E][Wire.cpp:544] write(): NULL TX buffer pointer
[ 2716][E][Wire.cpp:544] write(): NULL TX buffer pointer
[ 2722][E][Wire.cpp:453] endTransmission(): NULL TX buffer pointer
[ 2729][E][Wire.cpp:424] beginTransmission(): could not acquire lock
[ 2736][E][Wire.cpp:544] write(): NULL TX buffer pointer
[ 2742][E][Wire.cpp:453] endTransmission(): NULL TX buffer pointer
[ 2749][E][Wire.cpp:490] requestFrom(): NULL buffer pointer
[ 2755][W][D_IMU.h:74] operator()(): [💫] AppDrivers (1872) Testing device connections MPU6050 connection failed
[ 3853][I][WiFiSettingsService.cpp:343] connectToWiFi(): [🐼] 17 networks found.
[ 3862][I][WiFiSettingsService.cpp:402] connectToWiFi(): [🐼] Connecting to strongest network: MoonModules, BSSID: b6:fe:4c:d9:fe:b4
[ 3875][D][WiFiSettingsService.cpp:439] configureNetwork(): [🐼] Connecting to SSID: MoonModules, Channel: 1, BSSID: b6:fe:4c:d9:fe:b4, Hostname: ML-sticks1
[ 3894][I][WiFiSettingsService.cpp:470] configureNetwork(): [🐼] WiFi setTxPower to: 34
[ 4176][D][ModuleIO.h:898] readPins(): [💫] ESP32 SvelteKit (4068) I2CSDA changed 21
[ 4185][D][ModuleIO.h:905] readPins(): [💫] ESP32 SvelteKit (4068) I2CSCL changed 22
[ 4195][I][esp32-hal-i2c-ng.c:112] i2cInit(): Initializing I2C Master: num=0 sda=21 scl=22 freq=102000
[ 4206][I][ModuleIO.h:914] readPins(): [💫] ESP32 SvelteKit (4068) initI2C Wire sda:21 scl:22 freq:102 kHz (102000)
[ 4219][D][ModuleDrivers.h:47] operator()(): [💫] ESP32 SvelteKit (4068) maxPower 10
[ 4229][D][ModuleDrivers.h:69] operator()(): [💫] ESP32 SvelteKit (4068) ledPins[0-1] = 16 (#54)
[ 4250][D][SharedFSPersistence.h:121] writeToFS(): [🐼] delayedWrites: Add /.config/inputoutput.json
[ 4261][D][Module.h:116] loop20ms(): [💫] ESP32 SvelteKit (3952) requestUIUpdate effects
[ 4274][D][SharedFSPersistence.h:121] writeToFS(): [🐼] delayedWrites: Add /.config/effects.json
[ 4284][D][Module.h:116] loop20ms(): [💫] ESP32 SvelteKit (3392) requestUIUpdate drivers
[ 4300][D][SharedFSPersistence.h:121] writeToFS(): [🐼] delayedWrites: Add /.config/drivers.json
[ 4311][D][ModuleLightsControl.h:527] loop20ms(): [💫] ESP32 SvelteKit (3296) positions sent to monitor (2 -> 3)
[ 4322][D][main.cpp:178] driverTask(): [💫] AppDrivers (1872) positions done (3 -> 0)
[ 4332][I][ModuleIO.h:1026] _updateI2CDevices(): [💫] ESP32 SvelteKit (3296) Scanning I2C bus...
[ 4331][D][VirtualLayer.cpp:61] loop(): [💫] AppEffects (1348) onSizeChanged V 16,16,1 -> 1,54,1
[ 4353][D][PhysicalLayer.cpp:106] loopDrivers(): [💫] AppDrivers (1872) mapLayout physical requested
[ 4364][D][PhysicalLayer.cpp:168] onLayoutPre(): [💫] AppDrivers (1872) pass 1 mp:0
[ 4371][I][WiFiStatus.cpp:39] onStationModeConnected(): [🐼] WiFi Connected.
[ 4380][D][PhysicalLayer.cpp:176] onLayoutPre(): [💫] AppDrivers (1872) positions in progress (0 -> 1)
[ 4414][I][ModuleIO.h:1038] _updateI2CDevices(): [💫] ESP32 SvelteKit (3296) Found 0 device(s)

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the IMU driver implementation
echo "=== D_IMU.h implementation ==="
cat src/MoonLight/Nodes/Drivers/D_IMU.h

echo -e "\n=== ModuleIO I2C initialization ==="
# Check the I2C initialization code in ModuleIO
sed -n '890,920p' src/MoonBase/Modules/ModuleIO.h

echo -e "\n=== Check when IMU drivers are created ==="
# Look for where D_IMU nodes are instantiated
rg -n "D_IMU|IMU" src/MoonLight/Modules/ModuleDrivers.h -A 3 -B 3

echo -e "\n=== Check initBoard calls ==="
rg -n "initBoard" src/MoonLight/Nodes/Drivers/ -A 2 -B 2

Length of output: 9784


🏁 Script executed:

#!/bin/bash
# Find where I2CReady is set
echo "=== Where I2CReady is set ==="
rg -n "I2CReady" src/MoonBase/Modules/ModuleIO.h -B 3 -A 3

echo -e "\n=== Check the _updateI2CDevices implementation ==="
sed -n '1020,1050p' src/MoonBase/Modules/ModuleIO.h

echo -e "\n=== Check when nodes are created and restored ==="
rg -n "node->on|restoreState|loadState" src/MoonLight/Modules/ModuleDrivers.h -B 2 -A 2

echo -e "\n=== Check the boot sequence in main.cpp ==="
sed -n '140,200p' src/main.cpp

Length of output: 3364


@ewowi, I found the root cause! It's an initialization timing race introduced by the new readPins architecture. Here's what's happening:

Timeline of Events

T=2587ms:  IMU initBoard() starts
           ├─ Reads ModuleIO state
           └─ Calls mpu.initialize() → Wire not ready yet!
           
T=2596ms:  Wire.cpp errors: "could not acquire lock", "NULL TX buffer"
           └─ Wire object hasn't been initialized with Wire.begin()

T=4176ms:  ModuleIO.readPins() runs
           ├─ Detects I2C pins 21/22
           ├─ Calls Wire.begin(21, 22, 102000)  ✓
           └─ Sets _triggerUpdateI2C = 1

T=4332ms:  ModuleIO._updateI2CDevices() runs
           └─ Sets I2CReady = true in state

T=4414ms:  I2C scan finds 0 devices (IMU may be damaged from early errors)

Root Cause

The new event-driven architecture has a race condition:

  1. IMU node is restored from state with on: true
  2. IMU.onUpdate() sets requestInitBoard = true
  3. IMU.loop20ms() runs and calls initBoard() immediately (~2.5 seconds after boot)
  4. ModuleIO.loop20ms() runs LATER and initializes I2C (~4.1 seconds after boot)

The IMU tries to use Wire 1.6 seconds before it's initialized!

Why the I2CReady Check Fails

Your code at D_IMU.h line 43 does check I2CReady, but:

  • At 2587ms, ModuleIO state might have stale I2CReady: true from a previous session's persisted state
  • Or the state simply doesn't have I2CReady yet, so .as<bool>() returns false by default, but the code structure allows the MPU calls anyway

Looking at your code more carefully:

moduleIO->read([&](ModuleState& state) {
  if (state.data["I2CReady"].as<bool>()) {
    mpu.initialize();  // This still executes!

The Fix

IMU should subscribe to ModuleIO updates and only initialize when I2C becomes ready, not poll state directly. Similar to how ModuleDrivers now works:

In D_IMU.h, modify the setup() or constructor:

void setup() override {
  addControl(gyro, "gyro", "coord3D");
  addControl(accell, "accell", "coord3D");
  addControl(board, "board", "select");
  addControlValue("MPU6050");
  addControlValue("BMI160");
  
  // Subscribe to IO updates to detect when I2C becomes ready
  moduleIO->addUpdateHandler([this](const String& originId) {
    moduleIO->read([&](ModuleState& state) {
      // Only try to init if we want to AND I2C is actually ready now
      if (on && requestInitBoard && state.data["I2CReady"].as<bool>()) {
        initBoard();
        requestInitBoard = false;
      }
    }, name());
  }, false);
}

And modify loop20ms() to remove the immediate init:

void loop20ms() override {
  // Remove this block - let the update handler do it:
  // if (requestInitBoard) {
  //   initBoard();
  //   requestInitBoard = false;
  // }

  // Rest of the loop code...
  if (!motionTrackingReady) return;
  // ... existing FIFO reading code ...
}

Alternative Quick Fix

If you don't want to restructure, you can add a more robust check in loop20ms():

void loop20ms() override {
  if (requestInitBoard) {
    // Only init if I2C is ACTUALLY ready RIGHT NOW
    bool i2cActuallyReady = false;
    moduleIO->read([&](ModuleState& state) {
      i2cActuallyReady = state.data["I2CReady"].as<bool>();
    }, name());
    
    if (i2cActuallyReady) {
      initBoard();
      requestInitBoard = false;
    }
    // else: keep requestInitBoard true and try again next loop
  }
  // ... rest of loop ...
}

This ensures IMU waits until I2C is actually initialized before trying to use Wire.

Would you like me to prepare a complete patch for the event-driven approach?


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

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

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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-14T19:58:39.117Z
Learning: ESP32-P4/httpd: Heavy onUpdate cascades (preset apply, rename, controls rebuild) can exhaust 6 KB httpd stack; 8192 bytes is the safe default. Defer heavy work from onUpdate (httpd) to the SvelteKit loop and avoid large StaticJsonDocument locals in onUpdate to preserve stack headroom.

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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-13T19:05:38.712Z
Learning: In MoonModules/MoonLight SharedFSPersistence (src/MoonBase/SharedFSPersistence.h): To prevent stale default values from being written to filesystem during initialization, update handlers must be enabled AFTER files are read. Implementation: (1) registerModule() stores module info without enabling handler, (2) begin() first reads all files via readFromFS(), then enables handlers only for modules with delayedWriting=true. This matches FSPersistence behavior where files are loaded before handlers become active, preventing defaults from overwriting persisted state.

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: wled-install
Repo: MoonModules/MoonLight PR: 125
File: src/MoonBase/Modules/ModuleIO.h:519-522
Timestamp: 2026-02-15T13:40:05.390Z
Learning: On MyHome-Control P4 Nano Shield V1.0 and V2.0 (board_MHCP4NanoV1, board_MHCP4NanoV2) in src/MoonBase/Modules/ModuleIO.h, all RS485 pins (3, 4, 6, 53 for V2; 3, 4, 22, 24 for V1) are intentionally assigned as pin_RS485_TX to send LED data over RS485-capable pins, not for traditional RS485 UART communication. This prevents RS485 UART initialization in readPins() as designed.

- IMU driver: add i2cActuallyReady and set in handler
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

🧹 Nitpick comments (1)
src/MoonLight/Nodes/Drivers/D_IMU.h (1)

120-124: No retry on failed initialization — intentional?

If initBoard() fails (e.g., MPU6050 not physically present or DMP init fails), requestInitBoard is set to false on line 123 regardless of outcome. The driver won't retry unless the user toggles on or changes board. This is reasonable to avoid infinite retries on bad hardware, but if transient I2C errors are expected, a bounded retry mechanism could improve robustness.

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

In `@src/MoonLight/Nodes/Drivers/D_IMU.h` around lines 120 - 124, The code clears
requestInitBoard unconditionally after calling initBoard(), so failed/partial
inits (e.g., MPU6050/DMP or transient I2C errors) will suppress further retries;
change init flow so requestInitBoard is only cleared when initBoard() reports
success (e.g., return bool or throw on failure) and implement a bounded
retry/backoff: add a retry counter (e.g., imuInitRetries) and/or timestamp for
next attempt, increment retries on failure and retry while retries <
MAX_INIT_RETRIES, reset counter on success, and ensure i2cActuallyReady is still
checked before attempts; update references to initBoard(), requestInitBoard,
i2cActuallyReady and add imuInitRetries/MAX_INIT_RETRIES symbols to locate and
implement this logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/MoonBase/Modules/ModuleIO.h`:
- Around line 720-735: The one-shot boot handler is firing twice because
loop20ms() calls setBoardPresetDefaults(newBoardID) which itself triggers
callUpdateHandlers(), then your subsequent guarded
callUpdateHandlers(_moduleName) runs again; to fix, rename the flag from called
to a clearer _initialPinReadDone and either (A) set _initialPinReadDone = true
before invoking setBoardPresetDefaults(newBoardID) so the explicit
callUpdateHandlers(_moduleName) is skipped when a preset change occurs, or (B)
move the one-shot callUpdateHandlers(_moduleName) into the branch guarded by
newBoardID == UINT8_MAX so it only runs when no preset change is pending; update
all references to the renamed flag accordingly.

In `@src/MoonLight/Nodes/Drivers/D_IMU.h`:
- Around line 37-39: Register the update handler with moduleIO->addUpdateHandler
as you currently do, but immediately perform an eager read to capture the
initial I2C state (call moduleIO->read(...) once after registering) so
i2cActuallyReady is set if the event already fired during ModuleIO boot; also
reformat the nested lambdas across multiple lines and change the inner lambda
capture from [&] to [this] for clarity and safety (refer to moduleIO,
addUpdateHandler, read, i2cActuallyReady and name() in D_IMU.h).

---

Nitpick comments:
In `@src/MoonLight/Nodes/Drivers/D_IMU.h`:
- Around line 120-124: The code clears requestInitBoard unconditionally after
calling initBoard(), so failed/partial inits (e.g., MPU6050/DMP or transient I2C
errors) will suppress further retries; change init flow so requestInitBoard is
only cleared when initBoard() reports success (e.g., return bool or throw on
failure) and implement a bounded retry/backoff: add a retry counter (e.g.,
imuInitRetries) and/or timestamp for next attempt, increment retries on failure
and retry while retries < MAX_INIT_RETRIES, reset counter on success, and ensure
i2cActuallyReady is still checked before attempts; update references to
initBoard(), requestInitBoard, i2cActuallyReady and add
imuInitRetries/MAX_INIT_RETRIES symbols to locate and implement this logic.

Comment on lines +720 to +735
bool called = false;

void loop20ms() override {
// run in sveltekit task
Module::loop();
Module::loop20ms();

if (newBoardID != UINT8_MAX) {
setBoardPresetDefaults(newBoardID); // run from sveltekit task
newBoardID = UINT8_MAX;
}

// during boot, the IO module is unchanged , not triggering updates, so need to do it manually
if (!called) {
callUpdateHandlers(_moduleName); // calls readPins for all subscribed handlers
called = true;
}
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

Double handler fan-out on boot when a board preset is active.

When newBoardID != UINT8_MAX on the first loop20ms() call (normal path — the saved board preset triggers onUpdate → sets newBoardID), setBoardPresetDefaultsupdate() already fires callUpdateHandlers internally. Then the explicit callUpdateHandlers on line 733 fires them all a second time.

Consider either:

  • Moving the one-shot callUpdateHandlers inside the newBoardID == UINT8_MAX branch (i.e., only when no preset change is pending), or
  • Setting called = true before setBoardPresetDefaults so the explicit call is skipped when the preset path already triggered handlers.

Also, called is a very generic name for a class-level flag — something like _initialPinReadDone would be self-documenting.

Proposed fix
-  bool called = false;
+  bool _initialPinReadDone = false;

   void loop20ms() override {
     // run in sveltekit task
     Module::loop20ms();

     if (newBoardID != UINT8_MAX) {
       setBoardPresetDefaults(newBoardID);  // run from sveltekit task
       newBoardID = UINT8_MAX;
+      _initialPinReadDone = true;          // setBoardPresetDefaults→update already notified handlers
     }

     // during boot, the IO module is unchanged , not triggering updates, so need to do it manually
-    if (!called) {
+    if (!_initialPinReadDone) {
       callUpdateHandlers(_moduleName);  // calls readPins for all subscribed handlers
-      called = true;
+      _initialPinReadDone = true;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonBase/Modules/ModuleIO.h` around lines 720 - 735, The one-shot boot
handler is firing twice because loop20ms() calls
setBoardPresetDefaults(newBoardID) which itself triggers callUpdateHandlers(),
then your subsequent guarded callUpdateHandlers(_moduleName) runs again; to fix,
rename the flag from called to a clearer _initialPinReadDone and either (A) set
_initialPinReadDone = true before invoking setBoardPresetDefaults(newBoardID) so
the explicit callUpdateHandlers(_moduleName) is skipped when a preset change
occurs, or (B) move the one-shot callUpdateHandlers(_moduleName) into the branch
guarded by newBoardID == UINT8_MAX so it only runs when no preset change is
pending; update all references to the renamed flag accordingly.

Comment on lines +37 to +39

// Subscribe to IO updates to detect when I2C becomes ready
moduleIO->addUpdateHandler([this](const String& originId) { moduleIO->read([&](ModuleState& state) { i2cActuallyReady = state.data["I2CReady"]; }, name()); }, false);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find ModuleIO class definition and addUpdateHandler implementation
find . -type f \( -name "*.h" -o -name "*.cpp" \) | xargs grep -l "class ModuleIO" 2>/dev/null | head -5

Repository: MoonModules/MoonLight

Length of output: 98


🏁 Script executed:

# Search for addUpdateHandler definition
rg -n "addUpdateHandler" --type=cpp --type=hpp -B 2 -A 5 | head -80

Repository: MoonModules/MoonLight

Length of output: 92


🏁 Script executed:

# Check when setup() is called and initialization order
rg -n "void setup\(\)" --type=cpp --type=hpp -A 3 | head -60

Repository: MoonModules/MoonLight

Length of output: 92


🏁 Script executed:

# Look for ModuleIO begin() or initialization
rg -n "begin\(\)" --type=cpp --type=hpp | grep -i "moduleio\|module" | head -20

Repository: MoonModules/MoonLight

Length of output: 92


🏁 Script executed:

# Read ModuleIO.h to find addUpdateHandler signature and implementation
cat -n ./src/MoonBase/Modules/ModuleIO.h | head -200

Repository: MoonModules/MoonLight

Length of output: 7913


🏁 Script executed:

# Search for D_IMU.h and read it to see the full context
cat -n ./src/MoonLight/Nodes/Drivers/D_IMU.h | head -150

Repository: MoonModules/MoonLight

Length of output: 6395


🏁 Script executed:

# Search for when setup() is called on drivers
rg -n "setup\(\)" -t c -B 2 -A 2 | grep -A 5 -B 5 "IMU\|Driver" | head -40

Repository: MoonModules/MoonLight

Length of output: 2421


🏁 Script executed:

# Find Module class definition
rg -n "class Module" -t c | head -10

Repository: MoonModules/MoonLight

Length of output: 878


🏁 Script executed:

# Search for addUpdateHandler implementation in Module or base classes
rg -n "addUpdateHandler" -t c -A 10 | head -100

Repository: MoonModules/MoonLight

Length of output: 8417


🏁 Script executed:

# Look for initialization order - when begin() and setup() are called
rg -n "\.begin\(\)" -t c | grep -i "module\|driver" | head -20

Repository: MoonModules/MoonLight

Length of output: 263


🏁 Script executed:

# Find Module base class to understand addUpdateHandler
fd -t f "Module.h" | head -5

Repository: MoonModules/MoonLight

Length of output: 125


🏁 Script executed:

# Read Module.h to understand addUpdateHandler signature
cat "./src/MoonBase/Module.h" | grep -A 15 "addUpdateHandler"

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Look for all drivers that call handlers manually after registration
rg -n "addUpdateHandler.*false" -A 1 -t c | grep -B 1 "readPins\|read\(\["

Repository: MoonModules/MoonLight

Length of output: 88


🏁 Script executed:

# Read Module.h to find addUpdateHandler
cat -n "./src/MoonBase/Module.h" | head -300

Repository: MoonModules/MoonLight

Length of output: 7471


🏁 Script executed:

# Search for drivers calling readPins manually after addUpdateHandler with simpler grep
rg "addUpdateHandler.*false" -A 2 -t c | head -50

Repository: MoonModules/MoonLight

Length of output: 2337


🏁 Script executed:

# Check ModuleIO to see if it calls initial read/updates
rg -n "readPins" "./src/MoonBase/Modules/ModuleIO.h" -B 3 -A 3

Repository: MoonModules/MoonLight

Length of output: 801


🏁 Script executed:

# Check how drivers are registered/initialized
rg -n "IMUDriver\|IMU" --type=c | grep -E "new|register|add" | head -20

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Check ModuleDrivers to see when drivers are added
cat -n "./src/MoonLight/Modules/ModuleDrivers.h" | head -100

Repository: MoonModules/MoonLight

Length of output: 4879


Add eager I2C-ready read to prevent missing initial state if module loads after ModuleIO's boot-time handler dispatch.

D_Infrared.h already handles this pattern (line 107): after registering an IO handler with false, it manually calls readPins() to capture the initial state. D_IMU should follow the same approach—register the handler, then eagerly read the current I2C state in case that event already fired during boot.

Also improve readability: the nested lambdas on a single line are difficult to parse. Break them across lines and use explicit [this] capture for the inner lambda rather than [&].

Suggested fix (matches D_Infrared pattern)
    // Subscribe to IO updates to detect when I2C becomes ready
-    moduleIO->addUpdateHandler([this](const String& originId) { moduleIO->read([&](ModuleState& state) { i2cActuallyReady = state.data["I2CReady"]; }, name()); }, false);
+    moduleIO->addUpdateHandler([this](const String& originId) {
+      moduleIO->read([this](ModuleState& state) {
+        i2cActuallyReady = state.data["I2CReady"];
+      }, name());
+    }, false);
+
+    // Read current I2C state in case boot-time handler dispatch already occurred
+    moduleIO->read([this](ModuleState& state) {
+      i2cActuallyReady = state.data["I2CReady"];
+    }, name());
📝 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
// Subscribe to IO updates to detect when I2C becomes ready
moduleIO->addUpdateHandler([this](const String& originId) { moduleIO->read([&](ModuleState& state) { i2cActuallyReady = state.data["I2CReady"]; }, name()); }, false);
// Subscribe to IO updates to detect when I2C becomes ready
moduleIO->addUpdateHandler([this](const String& originId) {
moduleIO->read([this](ModuleState& state) {
i2cActuallyReady = state.data["I2CReady"];
}, name());
}, false);
// Read current I2C state in case boot-time handler dispatch already occurred
moduleIO->read([this](ModuleState& state) {
i2cActuallyReady = state.data["I2CReady"];
}, name());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_IMU.h` around lines 37 - 39, Register the
update handler with moduleIO->addUpdateHandler as you currently do, but
immediately perform an eager read to capture the initial I2C state (call
moduleIO->read(...) once after registering) so i2cActuallyReady is set if the
event already fired during ModuleIO boot; also reformat the nested lambdas
across multiple lines and change the inner lambda capture from [&] to [this] for
clarity and safety (refer to moduleIO, addUpdateHandler, read, i2cActuallyReady
and name() in D_IMU.h).

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 17, 2026

@coderabbitai, applied your fixes above, not committed yet but still no MPU6050 connected. It is still strange I found 0 devices, see the log:

[ 4184][D][ModuleIO.h:900] readPins(): [💫] ESP32 SvelteKit (4084) I2CSDA changed 21
[ 4194][D][ModuleIO.h:907] readPins(): [💫] ESP32 SvelteKit (4048) I2CSCL changed 22
[ 4203][I][esp32-hal-i2c-ng.c:112] i2cInit(): Initializing I2C Master: num=0 sda=21 scl=22 freq=100000
[ 4215][I][ModuleIO.h:916] readPins(): [💫] ESP32 SvelteKit (4048) initI2C Wire sda:21 scl:22 freq:100 kHz (100000)
[ 4227][D][ModuleDrivers.h:47] operator()(): [💫] ESP32 SvelteKit (4048) maxPower 10
[ 4237][D][ModuleDrivers.h:69] operator()(): [💫] ESP32 SvelteKit (4048) ledPins[0-1] = 16 (#54)
[ 4258][D][SharedFSPersistence.h:121] writeToFS(): [🐼] delayedWrites: Add /.config/inputoutput.json
[ 4269][D][Module.h:116] loop20ms(): [💫] ESP32 SvelteKit (3920) requestUIUpdate effects
[ 4282][D][SharedFSPersistence.h:121] writeToFS(): [🐼] delayedWrites: Add /.config/effects.json
[ 4292][D][Module.h:116] loop20ms(): [💫] ESP32 SvelteKit (3392) requestUIUpdate drivers
[ 4308][D][SharedFSPersistence.h:121] writeToFS(): [🐼] delayedWrites: Add /.config/drivers.json
[ 4318][D][ModuleLightsControl.h:527] loop20ms(): [💫] ESP32 SvelteKit (3296) positions sent to monitor (2 -> 3)
[ 4330][I][ModuleIO.h:1028] _updateI2CDevices(): [💫] ESP32 SvelteKit (3296) Scanning I2C bus...
[ 4331][D][main.cpp:178] driverTask(): [💫] AppDrivers (2268) positions done (3 -> 0)
[ 4352][I][WiFiStatus.cpp:39] onStationModeConnected(): [🐼] WiFi Connected.
[ 4353][D][VirtualLayer.cpp:61] loop(): [💫] AppEffects (1352) onSizeChanged V 16,16,1 -> 1,54,1
[ 4371][D][PhysicalLayer.cpp:106] loopDrivers(): [💫] AppDrivers (2268) mapLayout physical requested
[ 4382][D][PhysicalLayer.cpp:168] onLayoutPre(): [💫] AppDrivers (2268) pass 1 mp:0
[ 4390][D][PhysicalLayer.cpp:176] onLayoutPre(): [💫] AppDrivers (2268) positions in progress (0 -> 1)
[ 4418][I][ModuleIO.h:1040] _updateI2CDevices(): [💫] ESP32 SvelteKit (3296) Found 0 device(s)
[ 4501][D][PhysicalLayer.cpp:251] nextPin(): [💫] AppDrivers (2268) nextPin #0 ledsPerPin:54 of 20 assigned:0
[ 4513][D][D_ParallelLEDDriver.h:89] onLayout(): [💫] AppDrivers (2268) nrOfLedPins 1 1 1
[ 4522][D][D_ParallelLEDDriver.h:100] onLayout(): [💫] AppDrivers (2268) onLayout pin#0 of 1: assigned:0 16->16 #54
[ 4534][D][D_ParallelLEDDriver.h:106] onLayout(): [💫] AppDrivers (2268) status: #1: 16#54
[ 4544][D][D_ParallelLEDDriver.h:122] onLayout(): [💫] AppDrivers (2268) init Parallel LED Driver 3 1 0 2 255
[ 4555][D][I2SClocklessLedDriver.h:1106] initled(): [🐸] maximum leds 54
[ 4566][D][PhysicalLayer.cpp:260] onLayoutPost(): [💫] AppDrivers (2268) pass 1 mp:0 #:54 / 162 s:1,54,1
[ 4577][D][PhysicalLayer.cpp:263] onLayoutPost(): [💫] AppDrivers (2268) positions stored (1 -> 2)
[ 4587][D][PhysicalLayer.cpp:115] loopDrivers(): [💫] AppDrivers (2268) mapLayout virtual requested
[ 4597][D][PhysicalLayer.cpp:168] onLayoutPre(): [💫] AppDrivers (2268) pass 2 mp:0
[ 4606][D][PhysicalLayer.cpp:271] onLayoutPost(): [💫] AppDrivers (2268) pass 2 indexP: 54
[ 4616][I][VirtualLayer.cpp:356] onLayoutPost(): [🌙] AppDrivers (2268) V:1 x 54 x 1 = v:54 = 1:0:0 + 1:1:54 + mti:0 (1:m:0)
[ 4632][I][D_IMU.h:47] initBoard(): [💫] AppDrivers (2268) Starting board 0
[ 4641][E][esp32-hal-i2c-ng.c:369] i2cWriteReadNonStop(): i2c_master_transmit_receive failed: [259] ESP_ERR_INVALID_STATE
[ 4653][E][Wire.cpp:520] requestFrom(): i2cWriteReadNonStop returned Error 259
[ 4662][E][esp32-hal-i2c-ng.c:369] i2cWriteReadNonStop(): i2c_master_transmit_receive failed: [259] ESP_ERR_INVALID_STATE
[ 4673][E][Wire.cpp:520] requestFrom(): i2cWriteReadNonStop returned Error 259
[ 4682][E][esp32-hal-i2c-ng.c:369] i2cWriteReadNonStop(): i2c_master_transmit_receive failed: [259] ESP_ERR_INVALID_STATE
[ 4694][E][Wire.cpp:520] requestFrom(): i2cWriteReadNonStop returned Error 259
[ 4702][E][esp32-hal-i2c-ng.c:369] i2cWriteReadNonStop(): i2c_master_transmit_receive failed: [259] ESP_ERR_INVALID_STATE
[ 4714][E][Wire.cpp:520] requestFrom(): i2cWriteReadNonStop returned Error 259
[ 4722][E][esp32-hal-i2c-ng.c:275] i2cWrite(): i2c_master_transmit failed: [259] ESP_ERR_INVALID_STATE
[ 4732][E][esp32-hal-i2c-ng.c:369] i2cWriteReadNonStop(): i2c_master_transmit_receive failed: [259] ESP_ERR_INVALID_STATE
[ 4744][E][Wire.cpp:520] requestFrom(): i2cWriteReadNonStop returned Error 259
[ 4752][W][D_IMU.h:77] initBoard(): [💫] AppDrivers (1980) Testing device connections MPU6050 connection failed

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the current D_IMU.h implementation to see what was actually applied
echo "=== Current D_IMU.h implementation ==="
cat src/MoonLight/Nodes/Drivers/D_IMU.h

echo -e "\n=== ModuleIO _updateI2CDevices to check I2CReady handling ==="
sed -n '1020,1060p' src/MoonBase/Modules/ModuleIO.h

echo -e "\n=== Check when callUpdateHandlers is called in ModuleIO ==="
rg -n "callUpdateHandlers" src/MoonBase/Modules/ModuleIO.h -B 2 -A 2

Length of output: 8359


@ewowi, good progress - the timing improved, but there's still a critical sequence bug! The error changed from "NULL buffer" to "ESP_ERR_INVALID_STATE" which means Wire is initialized, but the I2C peripheral thinks it's in an invalid state.

The Remaining Issue

Looking at your code and logs, I found the root cause:

I2CReady is being set in the wrong place!

Current (Broken) Sequence:

T=4215ms: readPins() → Wire.begin(21, 22) ✓
T=4215ms: readPins() → sets _triggerUpdateI2C = 1
T=~4230ms: loop20ms() → callUpdateHandlers() → IMU handler runs
          └─ IMU handler reads state["I2CReady"] → NOT SET YET! → i2cActuallyReady = false

T=4330ms: loop1s() → _updateI2CDevices() starts
          └─ Sets I2CReady = true (line 1023)
          └─ Scans bus (finds 0 devices)
          └─ Calls update() → triggers handlers again
          
T=~4400ms: IMU handler runs AGAIN from update()
          └─ Now reads I2CReady = true → i2cActuallyReady = true

T=4632ms: IMU loop20ms() → sees i2cActuallyReady = true → initBoard()
          └─ ESP_ERR_INVALID_STATE errors

The Problem

Line 1023 in ModuleIO.h sets I2CReady = true inside _updateI2CDevices(), which runs in loop1s() - long after Wire.begin() succeeded!

This means:

  1. Your boot-time callUpdateHandlers() runs when I2C is physically ready but state still says "not ready"
  2. The IMU waits unnecessarily
  3. Only gets notified 100-200ms later when _updateI2CDevices() finally updates state

The Fix

Set I2CReady = true immediately after Wire.begin() succeeds:

In ModuleIO.h readPins(), around line 916:

// Current code:
Wire.begin(pinSDA, pinSCL, I2CFreq);
EXT_LOGI(ML_TAG, "initI2C Wire sda:%d scl:%d freq:%d kHz (%d)", 
         pinSDA, pinSCL, I2CFreq / 1000, I2CFreq);
_triggerUpdateI2C = 1;

// Add this:
update([&](ModuleState& state) {
  state.data["I2CReady"] = true;  // Set immediately!
}, ModuleState::update, _moduleName);

Then in _updateI2CDevices(), change line 1023:

// Remove this line (redundant now):
// newState["I2CReady"] = _triggerUpdateI2C == 1;

// Keep the scan logic:
if (_triggerUpdateI2C == 1) {
  JsonArray i2cDevices = newState["i2cBus"].to<JsonArray>();
  // ... scan loop ...
}

This ensures:

  • I2CReady = true is set immediately when Wire.begin() succeeds
  • Boot-time callUpdateHandlers() will see the correct state
  • IMU can initialize on first loop20ms tick after Wire is ready

Hardware Check

The scan found 0 devices - this could still be a hardware issue:

  1. Verify wiring:

    • MPU6050 VCC → 3.3V (NOT 5V!)
    • MPU6050 GND → GND
    • MPU6050 SCL → GPIO 22
    • MPU6050 SDA → GPIO 21
  2. Check I2C address:

    • MPU6050 default is 0x68 (AD0=LOW) or 0x69 (AD0=HIGH)
    • Your scan checks 1-127, so 0x68 should be found
  3. Pull-up resistors:

    • MPU6050 modules usually have onboard 4.7kΩ pull-ups
    • ESP32 internal pull-ups (enabled by Wire.begin) should work
    • Try external 2.2kΩ pull-ups if needed
  4. Power:

    • MPU6050 needs clean 3.3V power
    • If powered from ESP32 3.3V pin, ensure sufficient current

Test

After the code fix:

  • You should see I2CReady=true in the state immediately after Wire.begin()
  • IMU should initialize ~20-40ms after boot (first loop20ms tick)
  • If still no device found, it's definitely hardware

Let me know if you want me to provide a complete patch!


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

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

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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-14T19:58:39.117Z
Learning: ESP32-P4/httpd: Heavy onUpdate cascades (preset apply, rename, controls rebuild) can exhaust 6 KB httpd stack; 8192 bytes is the safe default. Defer heavy work from onUpdate (httpd) to the SvelteKit loop and avoid large StaticJsonDocument locals in onUpdate to preserve stack headroom.

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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-13T19:05:38.712Z
Learning: In MoonModules/MoonLight SharedFSPersistence (src/MoonBase/SharedFSPersistence.h): To prevent stale default values from being written to filesystem during initialization, update handlers must be enabled AFTER files are read. Implementation: (1) registerModule() stores module info without enabling handler, (2) begin() first reads all files via readFromFS(), then enables handlers only for modules with delayedWriting=true. This matches FSPersistence behavior where files are loaded before handlers become active, preventing defaults from overwriting persisted state.

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.

@ewowi
Copy link
Collaborator Author

ewowi commented Feb 17, 2026

@coderabbitai, regarding

I2CReady is being set in the wrong place!

it's set not immediately after Wire.begin but a bit later. Why is that a problem?

I think it is only important

4190][I][esp32-hal-i2c-ng.c:112] i2cInit(): Initializing I2C Master: num=0 sda=21 scl=22 freq=100000
[ 4202][I][ModuleIO.h:924] readPins(): [💫] ESP32 SvelteKit (4048) initI2C Wire sda:21 scl:22 freq:100 kHz (100000)
[ 4256][I][ModuleIO.h:1031] _updateI2CDevices(): [💫] ESP32 SvelteKit (3952) Scanning I2C bus...
[ 4286][I][ModuleIO.h:1043] _updateI2CDevices(): [💫] ESP32 SvelteKit (3952) Found 0 device(s)

runs before the IMU driver does something, also keep in mind the IMU driver can also be added much later as it is a Node which can be added and deleted anytime in runtime, so even if there is no IMU driver, scanning I2C bus should find a device isn't it ?

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the I2C scan implementation in ModuleIO
echo "=== I2C scan implementation in _updateI2CDevices() ==="
sed -n '1020,1080p' src/MoonBase/Modules/ModuleIO.h

echo -e "\n=== Check current IMU driver code ==="
sed -n '30,90p' src/MoonLight/Nodes/Drivers/D_IMU.h

echo -e "\n=== Look for any I2C-related comments or known issues ==="
rg -n "I2C|Wire\.begin|esp32-hal-i2c" src/MoonBase/Modules/ModuleIO.h -C 2 | head -50

Length of output: 5062


🏁 Script executed:

#!/bin/bash
# Check if there's I2S or parallel driver pin conflict with I2C
echo "=== Check I2S/parallel driver pin assignments ==="
rg -n "pin.*21|pin.*22" src/MoonBase/Modules/ModuleIO.h | grep -v "I2C" | head -20

echo -e "\n=== Check board preset for conflicts ==="
# Look for the esp32-D0 board configuration
rg -n "board_ESP32D0|esp32.*d0" src/MoonBase/Modules/ModuleIO.h -i -A 10

echo -e "\n=== Check if ledPins overlaps with I2C ==="
rg -n "ledPins\[" src/MoonBase/Modules/ModuleIO.h -A 2 | head -30

Length of output: 3762


@ewowi, you're absolutely right! I was overcomplicating it. The timing of I2CReady isn't the problem here at all.

Your Analysis is Correct ✓

The sequence is fine:

  1. 4202ms: Wire.begin() completes → I2C peripheral initialized ✓
  2. 4256ms: Scan runs → finds 0 devicesThis is the real problem
  3. 4632ms: IMU tries to init → ESP_ERR_INVALID_STATE

The scan happens after Wire.begin() succeeds, so the I2C peripheral is ready. And you're right that the IMU driver being a runtime-addable Node means the scan should work independently.

The Real Issue

The scan finding 0 devices means the MPU6050 isn't responding at all. This is a hardware issue, not software timing.

The ESP_ERR_INVALID_STATE (259) that happens later is actually a symptom, not the root cause. The new esp32-hal-i2c-ng.c driver in Arduino 3.x enters an invalid state when devices NACK (don't respond). So:

MPU6050 not responding → scan gets NACK → bus enters invalid state → later operations fail with 259

Hardware Troubleshooting

Since your board (esp32-D0) has no pin conflicts (ledPins are 16,3,1,4 - not 21/22), please check:

1. Wiring verification:

MPU6050 → ESP32-D0
-----------------
VCC → 3.3V (NOT 5V!)
GND → GND
SCL → GPIO 22
SDA → GPIO 21

2. Module quality:

  • Some cheap MPU6050 clones are DOA or unreliable
  • Try a different MPU6050 module if available

3. Pull-up resistors:

  • MPU6050 modules usually have onboard 4.7kΩ pull-ups
  • ESP32's internal pull-ups should work, but you can try external 2.2kΩ pull-ups on SDA/SCL

4. Power quality:

  • MPU6050 needs clean 3.3V
  • If powered from ESP32 3.3V pin, try external regulated 3.3V supply
  • Check voltage with multimeter: should be 3.2-3.4V

5. Test with i2c_scanner sketch:
Try a standalone I2C scanner to isolate the problem:

Wire.begin(21, 22);
for(byte addr = 1; addr < 127; addr++) {
  Wire.beginTransmission(addr);
  byte error = Wire.endTransmission();
  if(error == 0) Serial.printf("Device at 0x%02X\n", addr);
}

If this also finds 0 devices, it confirms hardware issue, not MoonLight code.

My apologies for the red herring about I2CReady timing - you correctly identified that as non-issue! 🎯


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

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

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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-14T19:58:39.117Z
Learning: ESP32-P4/httpd: Heavy onUpdate cascades (preset apply, rename, controls rebuild) can exhaust 6 KB httpd stack; 8192 bytes is the safe default. Defer heavy work from onUpdate (httpd) to the SvelteKit loop and avoid large StaticJsonDocument locals in onUpdate to preserve stack headroom.

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

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

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

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-02-13T19:05:38.712Z
Learning: In MoonModules/MoonLight SharedFSPersistence (src/MoonBase/SharedFSPersistence.h): To prevent stale default values from being written to filesystem during initialization, update handlers must be enabled AFTER files are read. Implementation: (1) registerModule() stores module info without enabling handler, (2) begin() first reads all files via readFromFS(), then enables handlers only for modules with delayedWriting=true. This matches FSPersistence behavior where files are loaded before handlers become active, preventing defaults from overwriting persisted state.

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.

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