Skip to content

Add IrModule (learn-any-remote) + WLED audio sync + Scheduler::setControl#35

Merged
ewowi merged 3 commits into
mainfrom
next-iteration
Jul 4, 2026
Merged

Add IrModule (learn-any-remote) + WLED audio sync + Scheduler::setControl#35
ewowi merged 3 commits into
mainfrom
next-iteration

Conversation

@ewowi

@ewowi ewowi commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Two commits of new capability, all bench-proven on hardware:

1 — WLED audio sync + MHC-WLED P4 shield line-in (2f8387a)

  • WLED-compatible audio sync over UDP in AudioModule: an Off / Send / Receive mode. Send broadcasts the analysed audio as a WLED v2 packet (port 11988, throttled); Receive drives effects from a peer's audio and auto-blends back to the local mic ~1 s after the peer goes quiet. Interoperates with the WLED ecosystem (the format the netmindz/WLED-sync library and MoonLight's D_WLEDAudio use). New WLEDAudioSyncPacket.h holds the wire format in one place.
  • Boot-crash fix: a sync-enabled board opened its socket in setup() before NetworkModule brought an interface up → lwip asserted on a null core mutex. Socket opening is now deferred to the tick path (syncEnsureSocket()), gated on the new platform::networkReady().
  • MHC-WLED ESP32-P4 shield line-in (PCM1808 ADC via a configurable mclkPin); catalog entry + wladi credit.

2 — IrModule (learn-any-remote) + Scheduler::setControl extraction; LightCrafter Ethernet (6f51373)

  • IrModule — a new IR-receiver peripheral that learns any remote: real NEC decode over RMT (platform_esp32_ir.cpp), a learn select that binds the next received code to an action, and a bound code drives the device (brightness ±16 clamped, palette prev/next). Learned codes persist. Status feedback throughout. Live-proven on the SE16 (GPIO 5, IR/Eth switch) and LightCrafter (GPIO 4, IR + Ethernet simultaneously).
  • Scheduler::setControl — extracted the generic "set any control by name" primitive (was private to HttpServerModule) up to the Scheduler, where the module tree lives. IR, HTTP, Improv, and the WLED bridge now compose against one control-agnostic primitive — adding an input never adds plumbing. HttpServer's applySetControl became a thin result→HTTP-status mapper (net −54 lines).
  • LightCrafter Ethernet — W5500 over SPI, with the board's WIZ850IO reset pin (ethRstGpio: 3) added to the catalog; without releasing it the W5500 reads 0x00 ("version mismatched"). Builds on the earlier W5500 ISR-service / poll fixes.
  • IR → supported on both Limpkin boards (SE16, LightCrafter) now that decode works on hardware.
  • CodeRabbit findings on the audio-sync packet: parseWledAudioSync now clamps each wire float into its uint16 field (NaN/out-of-range guard); sync socket-lifecycle unit tests added.

Verification

  • Desktop build clean (-Werror); unit + scenario tests pass; all 8 ships=True ESP32 variants build clean (the platform + core changes compile across every WiFi/Eth build-guard combination).
  • New tests: unit_IrModule (9 cases: learn/bind/fire, clamping, independent bindings, robustness), unit_AudioModule_sync, unit_WledAudioSyncPacket (golden vector + clamp).
  • Reviewer (Opus, whole-branch diff): architecturally clean, merge-ready, no must-fix findings — the setControl extraction is a genuine subtraction, the ISR-safe IR decode and the audio-sync lazy-open are verified correct, platform/domain boundaries hold. One nice-to-have (consolidate two module-by-name tree-walkers) deferred to backlog.
  • Bench-proven: audio sync P4→Olimex interop; IR learn+drive on both Limpkin boards; LightCrafter IR + Ethernet at once.

🤖 Generated with Claude Code

Adds WLED-compatible audio sync over UDP (broadcast/receive on port 11988) as an AudioModule mode, so a projectMM device can send its analysed audio to the WLED ecosystem or drive its own effects from a peer's audio with automatic fallback to the local mic. Also brings up line-in on the MHC-WLED ESP32-P4 shield (PCM1808 ADC) and fixes a boot crash where a sync-enabled board opened a socket before the network stack was up.

KPI: 16384lights | PC:465KB | tick:150/116/159/20/2/367/79/23/28/216/155/22/1/47us(FPS:6666/8620/6289/50000/500000/2724/12658/43478/35714/4629/6451/45454/1000000/21276) | ESP32(P4):tick:1870us(FPS:536) | src:167(32018) | test:116(16553) | lizard:113w

Core:
- AudioModule: added WLED audio sync — a `sync` control (Off / Send / Receive). Send broadcasts the analysed frame as a WLED v2 packet (~40/s, throttled); Receive binds the port and drives effects from a peer's audio, auto-blending back to the local mic ~1s after the peer goes quiet. `syncPort` is configurable and shown in Send + Receive; floor/gain stay visible (they govern the fallback mic). Added line-in support via a configurable `mclkPin` (the PCM1808 needs a master clock).
- AudioModule: fixed a boot crash-loop — setup() opened the sync socket before NetworkModule brought an interface up, so the lwip call asserted on a null core mutex and rebooted forever. Socket opening is now deferred to the tick path via syncEnsureSocket(), gated on platform::networkReady(); syncReinit() only closes + records the mode. Mirrors the existing DevicesModule lazy-open pattern.
- platform: added networkReady() — true once an interface has an IP (ESP32: eth/sta/ap connected; desktop: always). The runtime "safe to open a socket" predicate, the counterpart to the compile-time hasNetwork.
- platform: added hasNetwork = hasWiFi || hasEthernet (both configs) — "needs an IP stack" (audio sync / UDP), distinct from hasWiFi ("needs the radio": SSID / AP / TX-power / RSSI).
- NetworkModule: hide txPowerSetting unless the radio is on (Ethernet-only mode has no TX power to set).

Light domain:
- WLEDAudioSyncPacket.h: new — the netmindz/WLED-sync v2 wire format in one place (44-byte "00002" packet, port 11988), with build/parse helpers. Parse rejects v1 / short / foreign packets so a receiver ignores them rather than crashing.

Tests:
- unit_WledAudioSyncPacket: golden-vector + round-trip + rejection tests pinning the 44-byte layout, "00002" header, and exact field offsets (the WLED compatibility contract).

Docs / CI:
- README: credited wladi (myhome-control) for the MHC-WLED ESP32-P4 shield and the PCM1808 line-in pinout / FMT-pin details.
- deviceModels.json: MHC-WLED ESP32-P4 shield (renamed from ABC-WLED) — line-in AudioModule child (ws 26 / sd 33 / sck 32 / mclk 36, gain 100), planned RS-485/DMX, supported LEDs/Ethernet/Audio. Board image + pin/DIP diagram.
- Plan-20260703 - WLED audio sync.md: saved approved plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 882e24e6-8123-4d0f-b599-be190fe1cf94

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

This PR adds two independent features: a WLED-compatible v2 audio-sync UDP protocol integrated into AudioModule with a new platform networkReady() API, and an IrModule peripheral decoding NEC IR codes via a new platform::irRead() seam, routed through a new Scheduler::setControl entry point that HttpServerModule now delegates to. Supporting docs, device metadata, and unit tests are included.

Changes

WLED Audio Sync

Layer / File(s) Summary
Planning and credits
docs/history/plans/Plan-20260703 - WLED audio sync.md, README.md
Documents the design plan for the WLED v2 sync protocol and credits hardware pinout contribution.
Packet build/parse and tests
src/light/WLEDAudioSyncPacket.h, test/unit/light/unit_WledAudioSyncPacket.cpp, test/CMakeLists.txt
Defines the fixed 44-byte WLED v2 wire format, implements build/parse helpers, and adds layout/round-trip/negative/golden-byte tests.
Platform networkReady() API
src/platform/platform.h, src/platform/desktop/platform_desktop.cpp, src/platform/desktop/platform_config.h, src/platform/esp32/platform_esp32.cpp, src/platform/esp32/platform_config.h
Adds networkReady() and hasNetwork across desktop/esp32 platforms, plus W5500 SPI init tweaks.
AudioModule sync integration
src/core/AudioModule.h
Adds mclkPin/sync/syncPort controls, socket lifecycle, send/receive loop logic, and status readout.
AudioModule sync unit tests
test/unit/core/unit_AudioModule_sync.cpp
Validates send/receive socket lifecycle, throttling, freshness fallback, and bind-failure backoff.
Supporting NetworkModule, perf data, device metadata
src/core/NetworkModule.h, test/scenarios/light/scenario_perf_full.json, web-installer/deviceModels.json
Hides txPowerSetting when radio is off, refreshes perf benchmark data, and updates the MHC-WLED shield entry.

IR Remote Module and Scheduler Refactor

Layer / File(s) Summary
Planning and UI docs
docs/history/plans/Plan-20260704 - IrModule brightness.md, docs/moonmodules/core/ui/ui.md
Documents the IR module plan/outcome and adds the user-facing "IR" UI documentation section.
Scheduler::setControl and HttpServerModule delegation
src/core/Scheduler.h, src/core/Scheduler.cpp, src/core/HttpServerModule.cpp, src/core/FilesystemModule.cpp
Introduces scheduler singleton, setControl(), a persistence dirty-hook, and rewrites HttpServerModule::applySetControl to delegate.
IrModule peripheral logic
src/core/IrModule.h, src/main.cpp
Adds learn/bind/action processing and registers IrModule with the module factory.
Platform irRead() implementation
src/platform/platform.h, src/platform/desktop/platform_desktop.cpp, src/platform/esp32/platform_esp32_ir.cpp, esp32/main/CMakeLists.txt
Adds NEC RMT decoding on ESP32 and a desktop stub.
Device catalog wiring and tests
scripts/check/check_devices.py, web-installer/deviceModels.json, test/unit/core/unit_IrModule.cpp
Adds IR to the validator vocabulary, wires IrModule into device models, and adds IrModule unit tests.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AudioModule
  participant Platform
  participant UDPSocket
  participant PeerDevice

  AudioModule->>Platform: networkReady()
  Platform-->>AudioModule: true
  AudioModule->>UDPSocket: syncEnsureSocket()
  alt Send mode
    AudioModule->>AudioModule: buildWledAudioSync(frame_)
    AudioModule->>UDPSocket: send(packet)
    UDPSocket->>PeerDevice: broadcast packet
  else Receive mode
    PeerDevice->>UDPSocket: incoming packet
    AudioModule->>UDPSocket: syncReceive()
    UDPSocket->>AudioModule: parseWledAudioSync(packet)
    AudioModule->>AudioModule: overwrite frame_
  end
Loading
sequenceDiagram
  participant IrHardware
  participant IrModule
  participant Scheduler
  participant TargetModule

  IrHardware->>IrModule: loop() polls platform::irRead
  IrModule->>IrModule: processCode(code)
  alt Learn mode
    IrModule->>IrModule: bind code to action, persist hex
  else Match binding
    IrModule->>TargetModule: read current control value
    IrModule->>Scheduler: setControl(module, control, delta value)
    Scheduler-->>IrModule: SetControlResult
  end
Loading

Possibly related PRs

  • MoonModules/projectMM#12: Introduces ModuleRole::Peripheral child lifecycle wiring in SystemModule that the new mm::IrModule (also ModuleRole::Peripheral) depends on.
  • MoonModules/projectMM#23: Overlaps with the HttpServerModule::applySetControl/Scheduler::setControl apply-core plumbing changed in this PR.

Suggested labels: enhancement, audio, ir, scheduler

Suggested reviewers: MoonModules maintainers familiar with AudioModule and Scheduler internals

🐰 A hop through UDP, a beam through the air,
Sync frames now travel, codes learn with care,
The Scheduler hums a single control tune,
While IR whispers "brighten" beneath the moon.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main changes: adding IrModule, WLED audio sync, and Scheduler::setControl.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch next-iteration

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
test/scenarios/light/scenario_perf_full.json (1)

87-104: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the benchmark timestamps realistic.

The updated at values now point to 2026-07-04, which is after the review date for this change set. That makes the capture history impossible to reproduce and weakens any freshness checks that trust this metadata.

Also applies to: 176-193, 265-282, 352-369, 1217-1234, 1318-1335, 1411-1428, 1597-1614, 1902-1919, 1995-2012

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/scenarios/light/scenario_perf_full.json` around lines 87 - 104, The
benchmark metadata in the scenario JSON has unrealistic future timestamps, which
makes the capture history non-reproducible and breaks freshness checks. Update
the `at` arrays in the affected benchmark entries (including `pc-macos` and the
other listed sections) to dates that are not after the review date and are
consistent with realistic capture history. Keep the values in the same
structure, and make sure all referenced benchmark blocks use valid past
timestamps.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/AudioModule.h`:
- Around line 528-595: Add module-level tests for the sync socket lifecycle in
AudioModule, since only buildWledAudioSync and parseWledAudioSync are currently
covered. Verify syncEnsureSocket opens once per mode, retries until
platform::networkReady() is true, and updates syncOpen_/syncStr_ for send and
receive paths. Add coverage for syncSend throttling and packet emission via
syncSock_.sendTo, and for syncReceive draining, frame_ replacement,
lastSyncRecv_ stamping, and the fresh-vs-stale fallback behavior. Use the
syncEnsureSocket, syncSend, syncReceive, and syncSock_ symbols to locate the
logic.
- Around line 528-559: syncEnsureSocket() in AudioModule is retrying a failed
open/connect-or-bind on every loop tick with no cooldown, so add a
backoff/timing gate for repeated bring-up failures. Use the existing sync state
in AudioModule (and the related syncReinit()/syncSend retry patterns) to track
the next allowed retry time, and return false without reopening until the
backoff expires after a failed syncSock_.open(), syncSock_.connect(), or
syncSock_.bind() attempt.

In `@src/light/WLEDAudioSyncPacket.h`:
- Around line 72-82: In parseWledAudioSync, the values read with wledGetFloatLE
are cast directly to uint16_t, so clamp or otherwise validate each parsed float
before conversion to prevent NaN or out-of-range input from producing undefined
results. Update the assignments for out.level, out.levelSmoothed, out.peakMag,
and out.peakHz to use a safe bounded conversion, and revise the nearby comment
to avoid saying “rounded back” since static_cast truncates.

In `@web-installer/deviceModels.json`:
- Around line 850-855: The board entry for MHC-WLED ESP32-P4 shield still points
its url field to the old ABC-WLED product page, so update that url to the new
product listing to match the renamed shield and the new image. Locate the
deviceModels.json entry for MHC-WLED ESP32-P4 shield and change only the url
value so the catalog stays consistent with the renamed product.

---

Outside diff comments:
In `@test/scenarios/light/scenario_perf_full.json`:
- Around line 87-104: The benchmark metadata in the scenario JSON has
unrealistic future timestamps, which makes the capture history non-reproducible
and breaks freshness checks. Update the `at` arrays in the affected benchmark
entries (including `pc-macos` and the other listed sections) to dates that are
not after the review date and are consistent with realistic capture history.
Keep the values in the same structure, and make sure all referenced benchmark
blocks use valid past timestamps.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8cc2aa0a-cf1e-46d3-a6c9-b1fdc0dd555a

📥 Commits

Reviewing files that changed from the base of the PR and between d5ee07c and 2f8387a.

⛔ Files ignored due to path filters (2)
  • docs/assets/boards/abc-wled-esp32-p4-shield.jpg is excluded by !**/*.jpg
  • docs/assets/boards/mhc-wled-esp32-p4-shield.jpg is excluded by !**/*.jpg
📒 Files selected for processing (14)
  • README.md
  • docs/history/plans/Plan-20260703 - WLED audio sync.md
  • src/core/AudioModule.h
  • src/core/NetworkModule.h
  • src/light/WLEDAudioSyncPacket.h
  • src/platform/desktop/platform_config.h
  • src/platform/desktop/platform_desktop.cpp
  • src/platform/esp32/platform_config.h
  • src/platform/esp32/platform_esp32.cpp
  • src/platform/platform.h
  • test/CMakeLists.txt
  • test/scenarios/light/scenario_perf_full.json
  • test/unit/light/unit_WledAudioSyncPacket.cpp
  • web-installer/deviceModels.json

Comment thread src/core/AudioModule.h
Comment thread src/core/AudioModule.h
Comment thread src/light/WLEDAudioSyncPacket.h
Comment on lines +850 to +855
"name": "MHC-WLED ESP32-P4 shield",
"chip": "ESP32-P4",
"firmwares": [
"esp32p4-eth"
],
"image": "assets/boards/abc-wled-esp32-p4-shield.jpg",
"image": "assets/boards/mhc-wled-esp32-p4-shield.jpg",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the product URL to match the renamed shield.

The entry now says MHC-WLED and uses the new board image, but the url immediately below still points to the ABC-WLED product page. That leaves the catalog inconsistent and sends users to the wrong listing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-installer/deviceModels.json` around lines 850 - 855, The board entry for
MHC-WLED ESP32-P4 shield still points its url field to the old ABC-WLED product
page, so update that url to the new product listing to match the renamed shield
and the new image. Locate the deviceModels.json entry for MHC-WLED ESP32-P4
shield and change only the url value so the catalog stays consistent with the
renamed product.

…tCrafter Ethernet

Adds an IR-receiver peripheral that learns any remote: decode a button's NEC code, bind it to an action, and thereafter that button drives the device (brightness, palette) — the physical-remote counterpart of the WLED-app bridge. To do it cleanly, the generic "set any control by name" logic moves out of HttpServer into a Scheduler primitive that IR, HTTP, Improv, and the WLED bridge all share. Also brings up W5500 Ethernet on the LightCrafter (it needs the WIZ850IO reset pin released) and folds in the earlier CodeRabbit findings on the audio-sync packet.

KPI: 16384lights | PC:465KB | tick:121/94/120/15/2/299/61/17/20/172/120/17/1/36us(FPS:8264/10638/8333/66666/500000/3344/16393/58823/50000/5813/8333/58823/1000000/27777) | ESP32-S3:tick:1073us(FPS:931) | src:169(32480) | test:118(16934) | lizard:114w

Core:
- Scheduler: extracted setControl(module, control, value) — the generic control-set primitive (find module, validate + apply, rebuild controls, onUpdate, persist, conditional buildState). It was private to HttpServerModule; now it lives where the module tree does, so any module can drive another's control by composing against one primitive. HttpServer's applySetControl became a thin result→HTTP-status mapper (−54 lines net). Added Scheduler::instance() (the FilesystemModule singleton pattern) so a factory-created module reaches it, and a noteDirty hook so a control set from anywhere schedules the debounced save.
- IrModule: new IR-receiver peripheral (factory-registered, catalog-wired, not auto-wired). Decodes an IR remote on `pin` and learns it: a `learn` select arms an action, the next received code binds to it (persisted per action in a readonly Text control, rebuilt into a fast lookup on load), and a bound code runs its action — brightness ±16 (clamped) / palette prev-next — through Scheduler::setControl. Status reports setup state, learning, a binding, a fired action, or an unassigned code. latestCode() is the seam a future LightsControl consumes.
- AudioModule: 🐇 CodeRabbit — no functional change here beyond the packet-parse hardening below (kept for the branch).
- platform: added irRead(pin, code) — real NEC-over-RMT decode on ESP32 (platform_esp32_ir.cpp: a persistent RX channel, an ISR that only signals a queue, decode + re-arm on the render task — never in interrupt context), a false stub on desktop. NetworkModule W5500 path unchanged from the earlier ISR/poll fix.

Light domain:
- WLEDAudioSyncPacket: 🐇 CodeRabbit — parse now clamps each wire float into its uint16 field (wledFloatToU16) so a foreign packet carrying NaN or an out-of-range value can't produce an undefined cast; comment corrected from "rounded" to "truncated".

UI:
- ui.md: IR entry rewritten for the learn workflow (learn select + per-action code read-outs; no action buttons, no last-code control — the status line carries the received code). Embeds the remote photo effects.md-style.

Scripts / MoonDeck:
- check_devices.py: added "IR" to the supported-capability vocabulary now that IrModule backs it.

Tests:
- unit_IrModule: learn→bind→fire, clamping, independent multi-action bindings, unassigned-code and missing-target robustness (9 cases, injectCodeForTest seam since irRead stubs on desktop).
- unit_AudioModule_sync, unit_WledAudioSyncPacket: 🐇 CodeRabbit — added the sync socket-lifecycle tests and the float-clamp cases.

Docs / catalog:
- deviceModels.json: SE 16 V1 + LightCrafter 16 — IR moved planned→supported (decode works on hardware); IrModule child (SE16 pin 5, LightCrafter pin 4, parent System); LightCrafter Ethernet gains ethRstGpio 3 (the WIZ850IO nRST — without it the W5500 reads 0x00 "version mismatched").
- IRDriver.jpeg moved from the MoonLight image pool to assets/core/IrModule.jpeg; the stale MoonLight controls screenshot deleted.
- Plan-20260704 - IrModule brightness + palette: saved plan.

Reviews:
- 🐇 WLEDAudioSyncPacket: parse clamps NaN/out-of-range floats before the uint16 cast — fixed.
- 🐇 AudioModule: sync socket-lifecycle unit tests added — fixed.
- 🐇 deviceModels.json MHC-WLED url still ABC-WLED — accepted as-is: the shop's product path is literally .../ABC-WLED-ESP32-P4-shield/HW10027 (verified live); the rename is internal only, changing it would break the link.
- 🐇 scenario_perf_full.json "future timestamps" — accepted: the `at` dates are ≤ the capture date; the finding reflects the reviewer's training-cutoff clock, not real future dates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ewowi ewowi changed the title Add WLED audio sync + MHC-WLED P4 shield line-in Add IrModule (learn-any-remote) + WLED audio sync + Scheduler::setControl Jul 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/core/AudioModule.h (1)

600-617: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"level RMS" read-out freezes while receiving peer audio.

syncReceive() replaces frame_ with the peer's frame and stamps lastSyncRecv_, but never updates levelPeak_. Since loop() returns early on a fresh receive (line 268), the mic-path statement that normally feeds levelPeak_ (if (frame_.level > levelPeak_) levelPeak_ = frame_.level;) is skipped entirely. loop1s() still publishes levelStr_ from levelPeak_, so the "level RMS" control stays frozen at its pre-Receive value even while frame_.peakHz/peakStr_ correctly track the peer.

🔧 Proposed fix
             AudioFrame rf;
             if (parseWledAudioSync(pkt, static_cast<size_t>(n), rf)) {
                 frame_ = rf;                       // received audio drives the effects
                 lastSyncRecv_ = platform::millis();
+                if (frame_.level > levelPeak_) levelPeak_ = static_cast<uint8_t>(frame_.level);
             }

Also applies to: 261-271, 387-406

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/AudioModule.h` around lines 600 - 617, The “level RMS” value is not
being refreshed when peer audio is received because `syncReceive()` updates
`frame_` and `lastSyncRecv_` but leaves `levelPeak_` unchanged, and `loop()`
returns early on fresh sync input. Update `syncReceive()` (or the early-return
path in `loop()`) so the received `AudioFrame` also drives `levelPeak_` the same
way the local mic path does, keeping `loop1s()` and `levelStr_` in sync with
peer audio.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/IrModule.h`:
- Around line 128-150: The learn path in processCode() updates learnedCode_ and
codeStr_ but only marks the module dirty, so the new binding may not be
persisted immediately. Update the learn branch to also trigger the debounced
save via FilesystemModule::noteDirty() alongside markDirty(), using the
processCode() learn handling and existing setStatus()/learn_ flow as the place
to hook it in.

In `@src/main.cpp`:
- Line 214: The IrModule help-link target is pointing at the wrong docs
location, so the registered page path needs to be updated. In main.cpp, adjust
the mm::ModuleFactory::registerType<mm::IrModule> call for "IrModule" to use the
generated core/moxygen/IrModule.md path instead of core/IrModule.md so the UI
help link resolves correctly.

In `@src/platform/esp32/platform_esp32_ir.cpp`:
- Around line 117-129: The irRead flow in platform_esp32_ir.cpp decodes a frame
and then blindly calls arm(), which can leave the receiver unusable if re-arming
fails. Update irRead so it checks the result of arm() after decodeNec(), and if
re-arming fails, clear or reset the channel state used by ensureChannel() (such
as rxChan_ and currentPin_) and return false so the pin can be reinitialized
cleanly on the next read.

In `@test/unit/core/unit_IrModule.cpp`:
- Around line 165-174: The IrModule test only covers the unassigned-code no-op
path and never exercises the missing-target behavior described in the comment.
Update the unit test in the IrModule test case to explicitly trigger a binding
that resolves to a missing target and assert the typed not-found result from the
control path, while still verifying no crash and unchanged driver state. Use the
existing Rig, r.fire, latestCode, and setControl/missing-target flow referenced
by the current test to locate where to add the additional assertion.

---

Outside diff comments:
In `@src/core/AudioModule.h`:
- Around line 600-617: The “level RMS” value is not being refreshed when peer
audio is received because `syncReceive()` updates `frame_` and `lastSyncRecv_`
but leaves `levelPeak_` unchanged, and `loop()` returns early on fresh sync
input. Update `syncReceive()` (or the early-return path in `loop()`) so the
received `AudioFrame` also drives `levelPeak_` the same way the local mic path
does, keeping `loop1s()` and `levelStr_` in sync with peer audio.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 13ac43dc-e450-44a6-a366-144d893e062e

📥 Commits

Reviewing files that changed from the base of the PR and between 2f8387a and 6f51373.

⛔ Files ignored due to path filters (2)
  • docs/assets/core/IrModule.jpeg is excluded by !**/*.jpeg
  • docs/backlog/moonlight_images/moonlight/drivers/irdrivercontrols.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • docs/history/plans/Plan-20260704 - IrModule brightness.md
  • docs/moonmodules/core/ui/ui.md
  • esp32/main/CMakeLists.txt
  • scripts/check/check_devices.py
  • src/core/AudioModule.h
  • src/core/FilesystemModule.cpp
  • src/core/HttpServerModule.cpp
  • src/core/IrModule.h
  • src/core/Scheduler.cpp
  • src/core/Scheduler.h
  • src/light/WLEDAudioSyncPacket.h
  • src/main.cpp
  • src/platform/desktop/platform_desktop.cpp
  • src/platform/esp32/platform_esp32.cpp
  • src/platform/esp32/platform_esp32_ir.cpp
  • src/platform/platform.h
  • test/CMakeLists.txt
  • test/scenarios/light/scenario_perf_full.json
  • test/unit/core/unit_AudioModule_sync.cpp
  • test/unit/core/unit_IrModule.cpp
  • test/unit/light/unit_WledAudioSyncPacket.cpp
  • web-installer/deviceModels.json

Comment thread src/core/IrModule.h
Comment thread src/main.cpp
mm::ModuleFactory::registerType<mm::SystemModule>("SystemModule", "core/SystemModule.md");
mm::ModuleFactory::registerType<mm::AudioModule>("AudioModule", "core/AudioModule.md");
mm::ModuleFactory::registerType<mm::I2cScanModule>("I2cScanModule", "core/I2cScanModule.md");
mm::ModuleFactory::registerType<mm::IrModule>("IrModule", "core/IrModule.md");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -HI 'IrModule.md' docs

Repository: MoonModules/projectMM

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== repo files matching IrModule.md =='
git ls-files | rg '(^|/)IrModule\.md$' || true

echo
echo '== text references to IrModule.md or core/IrModule.md =='
rg -n --hidden --glob '!.git' 'IrModule\.md|core/IrModule\.md' .

echo
echo '== registration context =='
rg -n --hidden --glob '!.git' 'registerType<mm::IrModule>|IrModule' src docs .

Repository: MoonModules/projectMM

Length of output: 8380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== ModuleFactory doc-path handling =='
rg -n --hidden --glob '!.git' 'registerType<|doc|help|technical|moxygen|moonmodules' src test docs

echo
echo '== likely factory implementation =='
fd -HI 'ModuleFactory*' src test

Repository: MoonModules/projectMM

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== ModuleFactory definitions =='
fd -HI 'ModuleFactory*' src | sed -n '1,20p'

echo
echo '== registerType call sites with doc paths =='
rg -n 'registerType<[^>]+>\("([^"]+)", "([^"]+)"\)' src -r '$0' --only-matching

echo
echo '== docPath field / API types plumbing =='
rg -n --hidden --glob '!.git' 'docPath|/api/types|types' src/core src/ui src/platform test

Repository: MoonModules/projectMM

Length of output: 22190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== docs/moonmodules/core tree =='
fd -HI . docs/moonmodules/core | sed -n '1,200p'

echo
echo '== docs/coding-standards module-doc sections =='
sed -n '111,133p' docs/coding-standards.md

echo
echo '== src/core/ModuleFactory.h docPath comments =='
sed -n '1,90p' src/core/ModuleFactory.h

Repository: MoonModules/projectMM

Length of output: 10167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== representative core archive docs =='
for f in docs/moonmodules/core/archive/HttpServerModule.md docs/moonmodules/core/archive/AudioModule.md docs/moonmodules/core/ui/ui.md; do
  echo
  echo "-- $f --"
  sed -n '1,120p' "$f"
done

echo
echo '== links to archive or moxygen paths under docs/moonmodules/core =='
rg -n 'archive/|moxygen/' docs/moonmodules/core

Repository: MoonModules/projectMM

Length of output: 44673


Point IrModule at the generated docs page src/main.cpp:214core/IrModule.md doesn’t line up with the core docs layout here, so the UI help link lands on a dead page. Use the generated core/moxygen/IrModule.md path instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.cpp` at line 214, The IrModule help-link target is pointing at the
wrong docs location, so the registered page path needs to be updated. In
main.cpp, adjust the mm::ModuleFactory::registerType<mm::IrModule> call for
"IrModule" to use the generated core/moxygen/IrModule.md path instead of
core/IrModule.md so the UI help link resolves correctly.

Comment thread src/platform/esp32/platform_esp32_ir.cpp
Comment thread test/unit/core/unit_IrModule.cpp Outdated
Fixes four CodeRabbit findings on the IR / audio-sync branch (a lost learned-IR binding, a silently-dead IR receiver on re-arm failure, a frozen level read-out under audio sync, and an untested code path) and carries the branch's hard-won lessons into decisions.md before the merge.

Core:
- IrModule: 🐇 a learned IR binding now schedules the debounced save (FilesystemModule::noteDirty alongside markDirty) — a bound code is written straight to the persistent Text control, not via setControl, so it must trigger persistence itself or the binding could be lost before an unrelated save runs. Dropped a stray markDirty on the transient unassigned-code status path (nothing persistent changes there).
- AudioModule: 🐇 syncReceive now feeds the received peer level into the same 1 s peak window the local mic uses, so the "level RMS" read-out reflects peer audio instead of freezing at the last local value while a peer drives the effects.
- platform_esp32_ir: 🐇 irRead checks the re-arm result after a decode; on failure it tears the RX channel down (closeChannel) and returns false, so ensureChannel reopens a fresh channel next call rather than leaving the receiver enabled-but-not-armed (silently dead) forever.

Tests:
- unit_IrModule: 🐇 added a missing-target case — learn a code, rename the target module away, fire the code, and assert the "no Drivers module" status + no crash + unchanged state (the runAction not-found branch was untested; the prior case only covered an unassigned code).

Docs / CI:
- decisions.md: carried forward this branch's lessons — W5500 SPI bring-up (the ISR service is required for interrupt mode, IDF v6's poll-period pairing, a failed eth init masking the bug behind a WiFi fallback, and a 0x00 "version mismatched" read meaning the chip is held in reset → a per-board reset-release pin); the Scheduler::setControl extraction (put the generic control-set primitive where the tree owner is, not per-input seams); and the RMT/IR ISR-safety rule (signal only in the ISR, decode + re-arm on the task).
- backlog-core.md: noted the two module-by-name tree-walkers (HttpServer::findModuleByName vs Scheduler::firstByName) as a consolidation candidate now that firstByName is public (Reviewer finding, deferred).
- ui.md: AudioModule entry updated with the sync (Off/Send/Receive), syncPort, mclkPin (line-in), floor/gain controls that the audio-sync feature added (doc-sync gate).

Reviews:
- 🐇 IrModule learn-path persistence — fixed (noteDirty added).
- 🐇 IrModule irRead re-arm failure — fixed (closeChannel + return false on arm() failure).
- 🐇 AudioModule level read-out under sync — fixed (levelPeak_ fed from the peer frame).
- 🐇 IrModule missing-target test coverage — fixed (new test case).
- 🐇 main.cpp IrModule doc path → core/moxygen/IrModule.md — accepted as-is: every core module registers "core/<Name>.md" (not the moxygen subpath), and the UI resolves that to the built .html; changing only IrModule would make it the odd one out and break the help link.
- 👾 Reviewer (whole-branch, pre-merge): architecturally clean, merge-ready, no must-fix; the one nice-to-have (tree-walker consolidation) recorded in backlog-core.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ewowi ewowi merged commit 2844e1d into main Jul 4, 2026
3 checks passed
@MoonModules MoonModules deleted the next-iteration branch July 4, 2026 11:15
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