Skip to content

Sensors#128

Open
ewowi wants to merge 2 commits intomainfrom
sensors
Open

Sensors#128
ewowi wants to merge 2 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 and improved input handling for more reliable controls
  • Refactor
    • Unified periodic loop hooks and centralized module lifecycle for consistent timing and initialization
  • Chores
    • Build metadata and dependency reference updates

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 and centralizes periodic module loops: new pin_PIR, PIR pin assignment for LuxceoMood1XiaoMod, PIR debounce & handling in lights control, new Module periodic hooks (loop20ms/loop1s/loop10s), and centralized module registration; plus minor build metadata and FastLED dependency update.

Changes

Cohort / File(s) Summary
Build config
platformio.ini
Updated build metadata strings (APP_DATE, FASTLED_BUILD) and FastLED git dependency commit/hash and comment date.
IO / Board presets
src/MoonBase/Modules/ModuleIO.h
Added pin_PIR to IO_PinUsageEnum; adjusted LuxceoMood1XiaoMod preset: LED pins reduced to {1,2,3} and pin 4 assigned to PIR; added PIR control entry in UI.
Module base / lifecycle
src/MoonBase/Module.h, src/MoonBase/Modules/ModuleDevices.h, src/MoonBase/Modules/ModuleTasks.h
Introduced loop20ms(), loop1s(), loop10s() virtual hooks; moved loop logic to loop20ms() and added override specifiers on several module implementations.
Drivers / Effects / LiveScripts
src/MoonLight/Modules/ModuleDrivers.h, src/MoonLight/Modules/ModuleEffects.h, src/MoonLight/Modules/ModuleLiveScripts.h
Renamed per-tick handlers from loop() to loop20ms(); added loop1s() in Effects; updated internal base-class calls to loop20ms()/loop1s() as appropriate.
Lights control
src/MoonLight/Modules/ModuleLightsControl.h
Added public uint8_t pinPIR; extended readPins() to parse/configure pin_PIR and set INPUT_PULLUP; introduced per-source debounce timestamps and per-source pin state tracking; implemented PIR-driven lightsOn updates.
ModuleIO / loop naming
src/MoonBase/Modules/ModuleIO.h (module changes)
Renamed loop()loop20ms() in ModuleIO and updated internal calls to Module::loop20ms().
Main app / module registration
src/main.cpp
Centralized creation of shared routers (HTTP/WebSocket/Event/FS persistence), assembled modules vector, registered modules with shared routers, and unified periodic invocation of loop20ms()/loop1s()/loop10s() across modules; removed scattered per-module loop calls and diagnostic blocks.

Sequence Diagram(s)

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

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

    loop every 20ms
        Main->>Module: loop20ms()
        Module->>HW: read PIR pin
        HW-->>Module: PIR level (HIGH/LOW)
        Module->>Module: debounce & update lightsOn
        Module->>Router: emit events if state 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

Suggested labels

enhancement

Poem

🐰 A little PIR hops in to play,
It wakes the lights when shadows sway,
Loops now hum at twenty and one,
Modules register, work is done,
MoonLight twinkles—hooray! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Sensors' is vague and does not clearly convey the main changes in the changeset, which involves refactoring loop timing architecture and adding PIR sensor support. Consider a more specific title such as 'Add PIR sensor support and refactor loop timing architecture' to better communicate the primary changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ 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.

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