DevicesModule + mDNS, 4 light modules + dynamic modifiers, pin controls, per-chip drivers, installer auto-detect#21
Conversation
The web installer now reads a device's IP straight off its USB serial log right after flashing, so a board on Ethernet (or with saved WiFi) lands on a "Device is online!" success screen and is auto-added to Your devices — no "type the IP" prompt. Each board card gains a details popup showing its full boards.json entry, and IP addresses now flow through the codebase as raw octets (uint8_t[4]) end to end, matching the existing storage convention.
KPI: 16384lights | PC:340KB | tick:117/87/115/115/54/9/1/36/19/115/331/9us(FPS:8547/11494/8695/8695/18518/111111/1000000/27777/52631/8695/3021/111111) | ESP32:1192KB | tick:799us(FPS:1251) | heap:33252KB | src:88(17091) | test:56(8938) | lizard:67w
Core:
- NetworkModule: added currentIp(uint8_t[4]) returning the live LAN IP as octets (no IP string held as state — the netif already owns it); main.cpp appends it to the per-second tick line as a stable MM_IP=<ip> token, gated to the first 60s of uptime (the installer reads at ~3-15s; afterwards the REST API is the IP source, so a permanent token would only be noise). Removed the prior dedicated printMmIp helper + loop1s re-print.
- platform: replaced the string IP getters ethGetIP/wifiStaGetIP(char*) with octet getters ethGetIPv4/wifiStaGetIPv4(uint8_t[4]) across the platform seam (ESP32 + desktop), backed by a shared netifIPv4 helper. Consumers format with formatDottedQuad at their own boundary; Improv formats inline (platform stays off core/Control.h).
Light domain:
- NetworkReceiveEffect: ArtPollReply now takes the IP as octets directly, deleting the old string -> parseDottedQuad -> octets round-trip (net subtraction); the hostIp() string fallback (desktop) still parses, as before.
UI:
- install-orchestrator.js: added readBootIp() — reads the boot serial after the post-flash port reopen and matches the MM_IP= token (with a legacy "Eth/WiFi: <ip>" fallback); a device that announces its IP routes to the already-online success path and skips Improv provisioning entirely.
- index.html: board cards gain a "details" link opening a native <dialog> popup with the full boards.json entry — a readable summary (chip, firmwares, capabilities, modules + their controls) plus a collapsible raw-JSON block, product-page URL clickable. Rewrote Step 2 as three tight bullets that lead with the already-configured (Ethernet / saved-WiFi) auto-add case.
- install-picker.js: gated the Install button on a USB port being picked (was clickable with no port); release-tag default selection unchanged.
Scripts / MoonDeck:
- check_boards.py: dropped the default_firmware required-field + membership checks; firmwares is now validated as a non-empty list of non-empty strings (entry[0] is the default).
- app.js: default firmware now reads firmwares[0] instead of the removed default_firmware key.
Docs / CI:
- boards.json: removed every default_firmware key (the file describes the as-is data; firmwares[0] is the default by convention) and reordered firmwares arrays so entry[0] is the intended default; eth clock-direction values are JSON booleans.
- coding-standards.md: extended the IPv4 rule to cover the whole seam — getters return octets, not just storage — and named the string->octet round-trip anti-pattern this change removed.
- NetworkModule.md / BoardModule.md / README.md / architecture.md / backlog.md / CLAUDE.md: documented the MM_IP-on-tick-line contract and currentIp octets; swept the remaining default_firmware references to the present-tense as-is description.
KPI Details:
PC:
tick: 117us, 87us, 115us, 115us, 54us, 9us, 1us, 36us, 19us, 115us, 331us, 9us (FPS: 8547, 11494, 8695, 8695, 18518, 111111, 1000000, 27777, 52631, 8695, 3021, 111111) (per scenario)
ESP32 (P4 reading from esp32/monitor.log):
tick: 799us (FPS: 1251) heap free: 34050667
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/platform/esp32/platform_esp32_improv.cpp (1)
239-257:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard provision success on non-zero IPv4 before emitting the URL.
Line 253 formats and sends a success URL even though this path never verifies
ipis non-zero. IfwifiStaConnected()is true before a usable address is bound, this can emithttp://0.0.0.0/as a successful provisioning result.Suggested fix
uint8_t ip[4] = {}; for (int i = 0; i < 300; i++) { // 30 s @ 100 ms vTaskDelay(pdMS_TO_TICKS(100)); - if (wifiStaConnected()) { - wifiStaGetIPv4(ip); - break; - } + if (!wifiStaConnected()) continue; + wifiStaGetIPv4(ip); + if (ip[0] || ip[1] || ip[2] || ip[3]) break; } - if (!wifiStaConnected()) { + if (!wifiStaConnected() || (!ip[0] && !ip[1] && !ip[2] && !ip[3])) { improvSetStatus("error: no IP after 30s"); improvSendError(improv::ERROR_UNABLE_TO_CONNECT); return; }As per coding guidelines, callers must treat
0.0.0.0as “no IP yet” and avoid emitting/formatting it.🤖 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/platform/esp32/platform_esp32_improv.cpp` around lines 239 - 257, The code checks if wifiStaConnected() is true and retrieves the IP address via wifiStaGetIPv4(ip), but does not verify that the retrieved IP address is non-zero before formatting and sending it as a success URL. This can result in emitting http://0.0.0.0/ as a successful provisioning result if the device connects but hasn't yet been assigned a usable address. After the wifiStaConnected() check at line 248, add validation to ensure the IP address in the ip array is non-zero (for example, check that at least one octet is non-zero, or that the full address is not 0.0.0.0). Only proceed with the snprintf call that formats the URL and the subsequent improvSetStatus call if this IP validation passes; otherwise, treat it as a failure case similar to the timeout scenario.Source: Coding guidelines
🤖 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 `@scripts/check/check_boards.py`:
- Around line 88-100: The firmware validation in the check does not reject
whitespace-only or padded string values like " " or "esp32 ", which can cause
silent failures in exact firmware-key matching downstream. After the existing
check for non-empty strings (the condition `any(not f for f in fws)`), add
another validation to reject firmware entries that are whitespace-only or
contain leading/trailing whitespace. Use string methods like strip() to detect
such cases and append an appropriate error message to the errors list when such
values are found in the fws list.
In `@src/main.cpp`:
- Around line 340-348: The condition at line 340 uses a simple time comparison
(now < 60000) that re-activates after millis() wraps around on long uptimes,
causing MM_IP logging to restart. Replace the direct comparison with a wrap-safe
elapsed-time calculation by tracking a start time and comparing the elapsed time
(now minus start time) against 60000 milliseconds instead of comparing the
absolute now value against 60000. This ensures the MM_IP logging window only
occurs during the first 60 seconds regardless of timer wraparound.
In `@src/ui/install-picker.js`:
- Around line 611-619: The install button enablement is being unconditionally
re-enabled in a finally block after install attempts, which can bypass the port
requirement check. Instead of directly enabling the button, call the
applyInstallEnabled() gate function to properly recompute the button's
enablement state based on current state conditions including the hasPort()
check. This change needs to be applied at the finally block in the install
button's click handler (lines 611-619) and also at the additional location
(lines 647-648) where the same unconditional enablement occurs.
---
Outside diff comments:
In `@src/platform/esp32/platform_esp32_improv.cpp`:
- Around line 239-257: The code checks if wifiStaConnected() is true and
retrieves the IP address via wifiStaGetIPv4(ip), but does not verify that the
retrieved IP address is non-zero before formatting and sending it as a success
URL. This can result in emitting http://0.0.0.0/ as a successful provisioning
result if the device connects but hasn't yet been assigned a usable address.
After the wifiStaConnected() check at line 248, add validation to ensure the IP
address in the ip array is non-zero (for example, check that at least one octet
is non-zero, or that the full address is not 0.0.0.0). Only proceed with the
snprintf call that formats the URL and the subsequent improvSetStatus call if
this IP validation passes; otherwise, treat it as a failure case similar to the
timeout scenario.
🪄 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: e4e180cd-8143-496f-a759-8904ee1b2b20
📒 Files selected for processing (23)
CLAUDE.mddocs/architecture.mddocs/backlog/backlog.mddocs/coding-standards.mddocs/install/README.mddocs/install/boards.jsondocs/install/index.htmldocs/install/install-orchestrator.jsdocs/moonmodules/core/BoardModule.mddocs/moonmodules/core/NetworkModule.mdscripts/check/check_boards.pyscripts/moondeck_ui/app.jssrc/core/NetworkModule.hsrc/light/effects/NetworkReceiveEffect.hsrc/main.cppsrc/platform/desktop/platform_desktop.cppsrc/platform/esp32/platform_esp32.cppsrc/platform/esp32/platform_esp32_improv.cppsrc/platform/platform.hsrc/ui/install-picker.jstest/scenarios/light/scenario_AllEffects_grid_sizes.jsontest/scenarios/light/scenario_Audio_mutation.jsontest/scenarios/light/scenario_Driver_mutation.json
…o-select
Devices now announce their name (deviceName, default MM-XXXX) to the router's DHCP server, so they show up by name instead of "Unknown" in the client list — verified over WiFi (S3) and RMII Ethernet (Olimex). The installer's board cards gain a third capability color and auto-pick a generic board when a chip is detected, and a wrong LED-pin default that lit only one strand on the P4 testbench is corrected.
KPI: 16384lights | PC:340KB | tick:118/88/116/117/56/9/1/38/19/116/350/11us(FPS:8474/11363/8620/8547/17857/111111/1000000/26315/52631/8620/2857/90909) | tick:2601us(FPS:384) | heap:33248KB | src:88(17170) | test:56(8938) | lizard:68w
Core:
- platform (esp32): added setHostname() seam storing the DHCP hostname (option 12); applied in the ETHERNET_EVENT_CONNECTED handler for eth (IDF starts the eth DHCP client on link-up, so a name set at init time is clobbered — setting it on link-up, with a dhcpc stop/set/start bounce, makes the DISCOVER carry it) and after esp_wifi_start for STA. Verified: S3 wifi shows MM-70BC, Olimex RMII shows MM-BD3C. The P4 over Ethernet still shows blank in one router despite set_hostname succeeding — same code path as the two working boards, so backlogged as a router-sticky-lease / P4-IDF-netif recheck, not a code bug.
- NetworkModule: pushes the hostname (deviceName, default MM-XXXX) via setHostname() in setup() before bring-up; added hostName() helper shared with syncMdns so the DHCP name and the mDNS .local name are always identical.
- platform (desktop): setHostname() no-op stub.
- main.cpp: the MM_IP tick-line token's 60 s window now latches off once (bootMillis + flag) instead of comparing millis() directly, so it can't re-open when millis() wraps at ~49.7 days.
UI:
- index.html: capability chips now have three colors — green = active (supported AND a backing module configured in boards.json), yellow = supported (not pre-configured), orange = planned. The capability→module link is read from modules[] via a small CAP_MODULE map (no duplicated data field).
- install-picker.js: on a chip Detect with multiple matching boards and no prior in-family pick, auto-select the catalog's generic board for that chip if one exists (else keep the generic pass-through). The install-button finally block now recomputes via applyInstallEnabled() instead of unconditionally enabling, so a no-port state stays disabled after an install attempt.
Scripts / MoonDeck:
- check_boards.py: firmware-key validation now also rejects whitespace-only or padded entries (" ", "esp32 ") that would silently miss the exact-match downstream.
Docs / CI:
- boards.json: projectMM testbench P4 ParlioLedDriver pins corrected to "20" (single strand, matching the bench wiring — the 8-pin default lit only 8 of 64 LEDs); Waveshare ESP32-P4-NANO given the explicit 8-pin "20,21,22,23,24,25,26,27".
- backlog.md: added the ESP32-P4 DHCP-hostname recheck item with the full evidence trail.
Reviews:
- 🐇 check_boards.py firmware whitespace/padded keys — fixed (guard added after the non-empty check).
- 🐇 main.cpp MM_IP window millis() wraparound — fixed (latch flag instead of absolute compare).
- 🐇 install-picker.js finally re-enables install button unconditionally — fixed (calls applyInstallEnabled()); the "second location" CodeRabbit flagged is the pre-install disable, which is correct as-is.
- 🐇 improv 0.0.0.0 success URL — fixed (the IP wait loop now polls until a non-zero address and rejects all-zero as a failure, so http://0.0.0.0/ is never emitted).
KPI Details:
PC:
tick: 118us, 88us, 116us, 117us, 56us, 9us, 1us, 38us, 19us, 116us, 350us, 11us (FPS: 8474, 11363, 8620, 8547, 17857, 111111, 1000000, 26315, 52631, 8620, 2857, 90909) (per scenario)
ESP32 (P4 reading from esp32/monitor.log):
tick: 2601us (FPS: 384) heap free: 34046603
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@coderabbitai, resume and review |
|
✅ Action performedReviews resumed. Review finished.
|
|
@coderabbitai, I did not saw reviews, so review 04413b0 and deliver a prompt for agents on your findings |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Fetch details of the specific commit
gh api repos/MoonModules/projectMM/commits/04413b0be7cb286fefbf06b4fe8971bf8128fbef --jq '{sha: .sha, message: .commit.message, files: [.files[] | {filename: .filename, status: .status, additions: .additions, deletions: .deletions, patch: .patch}]}'Length of output: 28759
Review findings✅ Good fixes in this commit
|
A new core module discovers other devices on the network (other projectMM instances, WLED, generic HTTP devices), identifies each from its REST API, and lists them in a browsable card — including this device, marked as self. It rides two new generic core controls (a List and a Button) and a new outbound HTTP probe in the platform layer. Light modules will consume the device list later (Art-Net sync, fleet OTA); the discovery machinery itself is domain-neutral. Also fixes a desktop connect-hang that froze the PC build during a scan, and processes a CodeRabbit batch on the prior commit.
KPI: 16384lights | PC:363KB | tick:120/100/118/120/57/9/1/37/19/118/330/12us(FPS:8333/10000/8474/8333/17543/111111/1000000/27027/52631/8474/3030/83333) | tick:2615us(FPS:382) | heap:33250KB | src:91(17956) | test:58(9119) | lizard:71w
Core:
- Control: added ControlType::List (a variable list of rows backed by a ListSource the owning module implements — zero per-row storage in the control system, data stays in the module's contiguous array) and ControlType::Button (a momentary action that runs the module's onUpdate, distinct from a Bool toggle). addProgress gained a `bytes` flag so a Progress bar can label a plain count ("37 / 254") instead of KB.
- DevicesModule: new core module, submodule of Network. Boot-time subnet sweep (one IP per tick, off-stack 1 KB probe buffer) HTTP-probes :80 then :8080, identifies projectMM (/api/state has "modules"), WLED (/json/info has WLED), else generic; marks self; ages out devices gone for two sweeps; a `scan` Button re-runs on demand and a `progress` bar shows sweep position. No periodic scan — the blocking probe must not run continuously on the render task (it would flicker LEDs); boot-only + manual is the v1 limit. Reports state via MoonModule::setStatus, not a bespoke control. Carries a `speaks` protocol bitmask per device so the record is ready for non-HTTP discovery strategies.
- DeviceIdentify.h: the pure classify + name-extract logic, factored out of DevicesModule so it's host-unit-testable without network I/O. Reads a foreign device's reply defensively (any garbage body yields generic + empty name).
- platform: added httpGet(url, timeoutMs, body, len) — a short-timeout outbound GET for discovery. ESP32 uses esp_http_client; desktop uses a plain socket with a NON-BLOCKING connect + select() timeout. The desktop connect was previously blocking (SO_*TIMEO does not bound connect), so one dead IP in a sweep hung the single-threaded PC build ~75 s — this is the "unresponsive PC build" fix.
UI:
- app.js: render ControlType::List as clickable rows that expand to a read-only detail panel (self row marked); ControlType::Button as an action button; a Progress control with bytes:false as a plain "value / total" count.
- index.html: a "?" help link in the installer header opening the install docs in a new tab.
- style.css: list-control + help-link styling.
Tests:
- unit_DeviceIdentify (9 cases): classify projectMM/WLED/generic incl. a truncated body; extract deviceName from the control object (not the "text" type field); garbage/null tolerance; buffer-size respect.
- unit_Control_list (4 cases): List value-array + detail-array serialization, empty -> [], read-only/non-persistable.
- scenario_DevicesModule_scan: live-only robustness — repeated scan presses must not wedge the render loop.
Docs / CI:
- DevicesModule.md spec (controls, discovery, per-protocol direction, self, wire shape, prior art).
- backlog: async-scan-task (the enabler for a FAST + safe-periodic scan — v1 is slow by necessity because the probe blocks the render task), UDP presence beacon, and a note that the P4 being missed by the sweep correlates with its router "Unknown" (P4 network responsiveness, not our logic).
Reviews:
- 🐇 applyHostname dhcpc_start failure — now logs ESP_LOGW instead of silently leaving the interface with no DHCP client.
- 🐇 CAP_MODULE Ethernet/WiFi — split: a board's NetworkModule counts as Ethernet-active only when its controls carry a real ethType (not None/0); WiFi-active whenever NetworkModule exists. Predicate now receives the module object to inspect controls.
- 🐇 hostname_ memory ordering — documented the single-writer-before-readers contract (setHostname runs in setup() before bring-up; applyHostname reads later from the event task; no lock needed, don't call concurrently).
- 🐇 hostName() fallback — replaced the colliding "mm" literal with a unique MAC-derived MM-XXXX (same scheme as deviceName), cached in a member so the const accessor returns stable storage.
KPI Details:
Desktop:
Lights: 16,384 Binary: 363 KB
tests: 377 passed / 0 failed; 16 scenarios passed / 0 failed; platform boundary PASS
ESP32 (P4 reading from esp32/monitor.log):
tick: 2615us (FPS: 382)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@coderabbitai, I did not saw reviews, so review 2ef57f3 and deliver a prompt for agents on your findings |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
gh api repos/MoonModules/projectMM/commits/2ef57f3f9ff6f8f8f3540eccee465ec260427c4e \
--jq '{
sha: .sha,
message: .commit.message,
files: [.files[] | {
filename: .filename,
status: .status,
additions: .additions,
deletions: .deletions,
patch: .patch
}]
}'Length of output: 95304
Review findings✅ Strong additions in this commit
|
The discovered-devices list now survives a reboot: a device shows its last-known peers instantly on boot instead of waiting minutes for a fresh scan. This needed a real JSON reader, so the core JSON helper gained a bounded recursive parser (arrays + nested objects) and the List control became persistable through the standard pipeline. Two project principles are restated to match how the system now grows: minimalism is elegance (reach for the recognizable complete construct), and complexity belongs in core so domain modules stay simple. Also folds in a CodeRabbit batch from the prior commit.
KPI: 16384lights | PC:363KB | tick:118/100/119/120/57/9/1/36/19/116/328/11us(FPS:8474/10000/8403/8333/17543/111111/1000000/27777/52631/8620/3048/90909) | tick:2612us(FPS:382) | heap:33243KB | src:91(18389) | test:59(9305) | lizard:74w
Core:
- JsonUtil: added a recursive-descent reader (parse → bounded JsonDoc node arena; member/element/arraySize/readString/readInt/readBool) alongside the existing flat first-match helpers (unchanged). Bounded by design — fixed node pool + depth guard, no heap — so it is safe on the ESP32 task stack and against any malformed/truncated/oversized input (parse returns false, accessors return safe defaults). forEachListElement(json,key,fn) wraps the parse/navigate/iterate boilerplate every persisted list shares; its JsonDoc (~8 KB) is a function-local static (one resident copy, parsing is serial), NOT a stack local — an on-stack JsonDoc overflowed the ESP32 task stack and boot-looped (caught on the P4).
- Control: ControlType::List is now persistable. ListSource gains a restoreList(json,key) hook; applyControlValue routes a List write to it (the persistence-overlay load path) and returns Ok. The List value round-trips as a real JSON array through the standard FilesystemModule save/load — the model owns its (de)serialization, the control system stays generic.
- DevicesModule: persists its list (sweep-end marks dirty) and restores it on boot via restoreList — the UI shows the last-known devices immediately ("N devices (cached)") rather than waiting for the minutes-long sweep, the win for slow-to-find devices (a PC instance, a generic host) that mDNS can't see. restoreList is a thin "fill one device from this element" body; core's forEachListElement does the parse/iterate/safety.
UI:
- app.js: list-control rendering reads the same JSON; no change needed beyond the existing renderer.
Tests:
- unit_JsonUtil_parse (9 cases): flat object, array-of-objects (the device-list shape), nested object, escaped strings, negative/fractional numbers, and clean-failure on malformed/overflow/depth inputs.
- unit_Control_list: updated to the new contract — List is persistable; applyControlValue drives restoreList and the round-trip rebuilds the rows.
Docs / CI:
- CLAUDE.md: "Minimalism means elegance, not fewest features" (prefer the recognizable proven construct that maximises consistency/reuse/memory/hot-path, over a crippled subset that forces hacks outward) + a new "Complexity lives in core; domain modules stay simple" principle.
- architecture.md, JsonUtil.h, FilesystemModule.h: reworded the stale flat-parser-only descriptions — the persistence FILE stays flat by choice, but a structured control value (a List's array) round-trips with the recursive reader.
- DevicesModule.md: documented the persistence / instant-boot-list behaviour.
- backlog: recorded the decided discovery/messaging split (mDNS = discovery, REST = reliable messaging, UDP = lossy streaming only) for the deferred mDNS-browse work, and that the async-scan-task is the fix for the transient FPS dip during a sweep.
Reviews:
- 🐇 probePort: gate /api/state classification on HTTP 200 — a 404/500 body containing "modules" no longer misreads as projectMM; a non-200 still treats the host as alive and falls through to WLED/generic.
- 🐇 app.js: extracted the duplicated list-row DOM construction into buildListEntries(), called by both createControl and the WS live-update path.
- 🐇 DevicesModule kMaxMissed: corrected the comment to describe the real drop threshold (two consecutive missed sweeps) precisely; CodeRabbit's suggested wording was itself off-by-one, so the value/logic are unchanged.
- 🐇 DevicesModule self row: added setSelfName (wired from SystemModule::deviceName in main.cpp) so the self row shows the device's real name, not "this device".
KPI Details:
Desktop: 16,384 lights | 363 KB | tests pass / scenarios pass / platform boundary PASS
ESP32 (P4, esp32/monitor.log): tick: 2612us (FPS: 382)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
CLAUDE.md (1)
114-114:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winName the actual default field.
entry[0]reads like the board object is indexed; the PR-wide contract is thatfirmwares[0]is the default firmware.Proposed wording fix
-8. Board catalog, `check_boards.py`, fast (<1s), if `docs/install/boards.json` or `scripts/check/check_boards.py` changed. Validates the installer catalog: required fields, `firmwares` a non-empty list of non-empty strings (entry[0] is the default), every `image` resolves on disk, each `Board.board` control equals its entry `name`, module `type`s are factory-registered (or boot-wired singletons), `pins` controls live only on `*LedDriver` modules, and `supported` capabilities stay within the known vocabulary. +8. Board catalog, `check_boards.py`, fast (<1s), if `docs/install/boards.json` or `scripts/check/check_boards.py` changed. Validates the installer catalog: required fields, `firmwares` a non-empty list of non-empty strings (`firmwares[0]` is the default), every `image` resolves on disk, each `Board.board` control equals its entry `name`, module `type`s are factory-registered (or boot-wired singletons), `pins` controls live only on `*LedDriver` modules, and `supported` capabilities stay within the known vocabulary.🤖 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 `@CLAUDE.md` at line 114, The documentation in CLAUDE.md references "entry[0]" as the default firmware, but this is unclear and doesn't reflect the actual field name used in the PR contract. Replace "entry[0] is the default" with "firmwares[0] is the default" to use the explicit field name that matches the documented contract, making the reference clearer and more accurate.
🤖 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 `@docs/moonmodules/core/DevicesModule.md`:
- Line 10: Update the documentation in DevicesModule.md to accurately reflect
the implemented behavior of the progress control and List persistence. The
progress control documentation should be corrected to indicate that it remains
present at all times rather than being shown only during sweeps, as the
implementation maintains the control and manages it through value changes.
Additionally, the List documentation should be updated to reflect that it
supports persisted restore via recursive JSON parsing rather than being
flat-parser only, matching the actual implementation behavior demonstrated in
the code and tests.
In `@src/core/Control.cpp`:
- Around line 293-300: The ControlType::List case handler calls
ListSource::restoreList() but ignores its return value, always returning
ApplyResult::Ok regardless of whether the restore succeeded. Capture the return
value from the restoreList() call on the ListSource pointer, check whether it
indicates success, and return an appropriate failure result if the restoration
fails (such as ApplyResult::InvalidOperation or similar). This ensures that
failures from parsing malformed persisted lists or invalid live writes are
properly propagated instead of being masked as successful operations.
In `@src/core/Control.h`:
- Around line 100-121: The `ListSource` struct creates a secondary
virtual-method hierarchy outside the `MoonModule` tree, violating the coding
guideline that the module tree should be the only class hierarchy. Replace the
`ListSource` interface with a non-inheriting callback/context table or
plain-old-data (POD) adapter pattern — convert the virtual methods
(listRowCount, writeListRow, writeListRowDetail, restoreList) into function
pointers or a callback struct that `DevicesModule` populates and passes, rather
than inheriting from. This keeps the virtual-dispatch hierarchy contained to the
module tree alone.
- Around line 248-251: In the addList() method, change the parameter from const
ListSource& source to ListSource& source to match the non-const nature of the
source being passed. Then remove the const_cast<ListSource*>(&source) and
replace it with just &source, since the source will no longer be const and no
casting will be needed. This eliminates the const-correctness violation and
undefined behavior from casting away constness of a method that mutates the
object.
In `@src/core/DevicesModule.h`:
- Around line 231-235: The scanProgress_ member variable is not being reset when
the network check fails (the "no network" status case at lines 231-235) or after
sweep completion (at lines 264-271), leaving stale progress values visible in
the UI. Add a reset of scanProgress_ to its idle state (typically 0) in both
locations: first, immediately after setting the "no network" status in the early
abort block where local address checks fail, and second, in the sweep completion
block at lines 264-271 where scanning concludes normally. This ensures the
progress indicator returns to an idle state in all code paths where scanning is
not active.
- Line 8: The DevicesModule.h header file includes "platform/platform.h" which
violates the core/platform separation principle—core-layer code must remain
platform-independent with no direct platform includes. Remove the include of
"platform/platform.h" from DevicesModule.h and refactor any platform-specific
API invocations in this file to use platform-agnostic abstractions or delegate
platform calls to a separate platform adaptation layer. This issue appears at
the include statement and also at additional locations within the file where
platform APIs are invoked (at lines 281-283 and 308), so ensure all direct
platform API calls throughout the file are either removed or replaced with
platform-independent equivalents.
- Around line 351-367: The upsertSelf method has an issue where the existing
entry path (when a device entry is already found via findByIp) does not update
critical fields like type and speaks, causing stale cached values to persist.
The type field is only set to DevType::ProjectMM inside the if (!d) block for
new entries. To fix this, move the type assignment and any speaks field updates
outside of the if (!d) conditional block so they apply to both newly created and
existing device entries, ensuring that when upsertSelf is called, all identity
fields are properly refreshed regardless of whether the entry already existed.
In `@src/core/JsonUtil.h`:
- Around line 233-235: The numeric tail-skip loop in the while condition at line
233 is too permissive and accepts malformed JSON numbers like `1-2` or `1e+`.
Replace the single overly-broad condition with validation logic that properly
enforces JSON number syntax: allow digits and a single decimal point before any
exponent marker, and only allow sign characters (+/-) immediately following an
exponent marker (e/E), which must be followed by at least one digit. This
ensures that invalid numeric sequences are rejected rather than silently
consumed, preventing corrupted payloads from being treated as valid JSON.
- Around line 397-406: The function template forEachListElement creates a
separate static JsonDoc instance for each distinct callback type, multiplying
.bss usage despite the intent to have one resident copy. Extract the JSON
parsing logic and the static JsonDoc variable into a separate non-template
helper function (with a name like parseJsonList or similar) that performs the
parse operation and returns the result. Then update forEachListElement to call
this helper function instead of doing the parsing inline. This way the static
variable is instantiated only once and shared across all template
instantiations, regardless of the callback type.
In `@src/main.cpp`:
- Around line 279-282: The inline comment for the Devices module in main.cpp
uses future-tense wording with "May be promoted to a top-level module later"
which violates the guideline of describing current system behavior in present
tense only. Remove or rewrite the speculative phrase about future promotion to
instead describe only the current state and structure of the Devices module,
keeping the comment focused on what it is now rather than what it might become.
In `@src/platform/desktop/platform_desktop.cpp`:
- Around line 510-516: The snprintf call in this HTTP request building code has
a truncation vulnerability where reqLen can exceed the 256-byte buffer size. The
current validation only checks if reqLen is non-positive or if send fails, but
does not detect when snprintf truncates the buffer. Add a length validation
check before calling send that ensures reqLen does not exceed sizeof(req) (256
bytes), rejecting the request if the formatted HTTP header would be truncated
due to an excessively long path parameter. This prevents reading past the stack
buffer bounds when send is called with reqLen.
In `@src/platform/platform.h`:
- Around line 183-185: The comment describing httpGet in src/platform/platform.h
(lines 183-185) claims the scan runs off the render hot path, but this
contradicts the actual implementation documented in docs/backlog/backlog.md Line
25, which shows the DevicesModule scan is blocking on the render task. Update
the comment in the httpGet function documentation to accurately describe the
current blocking behavior on the render task and include information about the
bounded blocking budget, ensuring the comment reflects the actual scan path
implementation rather than the stated off-hot-path design.
In `@src/ui/app.js`:
- Around line 1363-1375: The summary button element lacks the aria-expanded
attribute needed to expose its expanded/collapsed state to assistive
technologies. Initialize the aria-expanded attribute on the summary element
after creating it, setting it to the opposite value of the initial
detailPanel.hidden state. Then, update the toggle function to also set
summary.setAttribute("aria-expanded", !detailPanel.hidden) whenever the detail
panel visibility changes, ensuring the aria-expanded state stays in sync with
the actual visibility state of the detailPanel.
In `@src/ui/style.css`:
- Line 482: The CSS rule for `.list-detail-val` uses the deprecated `word-break:
break-word` property which is no longer standards-compliant. Replace the
`word-break: break-word` declaration with `overflow-wrap: anywhere` to achieve
the same text-wrapping behavior using modern, non-deprecated CSS syntax.
In `@test/scenarios/core/scenario_DevicesModule_scan.json`:
- Around line 5-6: The current scenario_DevicesModule_scan.json is live-only,
network-dependent, and timing-sensitive, which violates the coding guideline
that tests should not depend on timing or network. Create a parallel
deterministic test scenario that exercises the same scan functionality (scan
restart, classification, upsert, aging, and list serialization) using canned
httpGet responses instead of real network calls, allowing it to run in the
desktop runner without flakiness. Keep the existing live-only scenario for
manual hardware verification. Apply this change to both the anchor location
(line 5-6) and sibling location (line 50-52) by ensuring each scenario is
properly configured for its respective test type (deterministic vs. live-only).
---
Outside diff comments:
In `@CLAUDE.md`:
- Line 114: The documentation in CLAUDE.md references "entry[0]" as the default
firmware, but this is unclear and doesn't reflect the actual field name used in
the PR contract. Replace "entry[0] is the default" with "firmwares[0] is the
default" to use the explicit field name that matches the documented contract,
making the reference clearer and more accurate.
🪄 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: c83cb0af-d427-4266-8ab1-60c622c594de
📒 Files selected for processing (28)
CLAUDE.mddocs/architecture.mddocs/backlog/backlog.mddocs/install/index.htmldocs/moonmodules/core/DevicesModule.mdesp32/main/CMakeLists.txtsrc/core/Control.cppsrc/core/Control.hsrc/core/DeviceIdentify.hsrc/core/DevicesModule.hsrc/core/FilesystemModule.hsrc/core/JsonUtil.hsrc/core/NetworkModule.hsrc/main.cppsrc/platform/desktop/platform_desktop.cppsrc/platform/esp32/platform_esp32.cppsrc/platform/esp32/platform_esp32_httpget.cppsrc/platform/platform.hsrc/ui/app.jssrc/ui/style.csstest/CMakeLists.txttest/scenarios/core/scenario_DevicesModule_scan.jsontest/scenarios/core/scenario_MoonModule_control_change.jsontest/scenarios/light/scenario_AllEffects_grid_sizes.jsontest/scenarios/light/scenario_Audio_mutation.jsontest/unit/core/unit_Control_list.cpptest/unit/core/unit_DeviceIdentify.cpptest/unit/core/unit_JsonUtil_parse.cpp
| struct ListSource { | ||
| virtual ~ListSource() = default; | ||
| // Number of rows currently in the list (may change between calls — e.g. a | ||
| // device scan found more). Bounded small (uint8_t) — these are LAN devices, | ||
| // UI list rows, not bulk data. | ||
| virtual uint8_t listRowCount() const = 0; | ||
| // Append the row's SUMMARY as a JSON object — the fields shown in the | ||
| // collapsed row (e.g. {"name":"MM-70BC","ip":"192.168.1.156","type":"projectMM"}). | ||
| virtual void writeListRow(JsonSink& sink, uint8_t row) const = 0; | ||
| // Append the row's DETAIL as a JSON object — the fields shown when the row is | ||
| // expanded. Default: same as the summary (override to show more). | ||
| virtual void writeListRowDetail(JsonSink& sink, uint8_t row) const { | ||
| writeListRow(sink, row); | ||
| } | ||
| // Rebuild the list from persisted JSON. `json` is the full object FilesystemModule | ||
| // loaded; `key` is this control's name — the source parses `json` with the | ||
| // recursive mm::json reader, navigates to `key` (a JSON array of the same | ||
| // row-summary objects writeListRow produced), and repopulates its own storage. | ||
| // Default no-op: a List that doesn't override simply isn't restored. The model | ||
| // owns its (de)serialization — Control.h stays free of the JsonUtil include, and | ||
| // the control system stays generic. Returns true if it took. | ||
| virtual bool restoreList(const char* /*json*/, const char* /*key*/) { return false; } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Avoid adding a second virtual-dispatch hierarchy.
ListSource introduces virtual methods outside the MoonModule tree, and DevicesModule now overrides that interface. Keep the generic list source as a non-inheriting callback/context table or another POD adapter so the module tree remains the only class hierarchy. As per coding guidelines, “The one deliberate class hierarchy is the module tree… Do not add inheritance elsewhere.”
🤖 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/Control.h` around lines 100 - 121, The `ListSource` struct creates a
secondary virtual-method hierarchy outside the `MoonModule` tree, violating the
coding guideline that the module tree should be the only class hierarchy.
Replace the `ListSource` interface with a non-inheriting callback/context table
or plain-old-data (POD) adapter pattern — convert the virtual methods
(listRowCount, writeListRow, writeListRowDetail, restoreList) into function
pointers or a callback struct that `DevicesModule` populates and passes, rather
than inheriting from. This keeps the virtual-dispatch hierarchy contained to the
module tree alone.
Source: Coding guidelines
| #include "core/JsonSink.h" | ||
| #include "core/JsonUtil.h" // recursive reader — restoreList parses the persisted array | ||
| #include "core/DeviceIdentify.h" // DevType, classifyDevice, extractDeviceName (pure, unit-tested) | ||
| #include "platform/platform.h" |
There was a problem hiding this comment.
Keep DevicesModule core-layer code platform-independent.
src/core/DevicesModule.h directly includes and invokes platform APIs, which breaks the core/platform boundary and hard-couples this core module to platform implementations.
As per coding guidelines, src/core/** must be platform-independent and have no platform includes.
Also applies to: 281-283, 308-308
🤖 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/DevicesModule.h` at line 8, The DevicesModule.h header file includes
"platform/platform.h" which violates the core/platform separation
principle—core-layer code must remain platform-independent with no direct
platform includes. Remove the include of "platform/platform.h" from
DevicesModule.h and refactor any platform-specific API invocations in this file
to use platform-agnostic abstractions or delegate platform calls to a separate
platform adaptation layer. This issue appears at the include statement and also
at additional locations within the file where platform APIs are invoked (at
lines 281-283 and 308), so ensure all direct platform API calls throughout the
file are either removed or replaced with platform-independent equivalents.
Source: Coding guidelines
| "live_only": true, | ||
| "description": "Trigger the device-discovery sweep repeatedly on a running device and confirm the render loop survives every sweep. Pins the robustness principle for DevicesModule: pressing the `scan` button (a Button control on the Devices submodule of Network) re-runs the subnet sweep, whose HTTP probes block the render task up to the probe timeout per tick. No press, sweep state, or completion may crash or wedge the tick. Runs live only — discovery needs a real LAN to probe and the module only exists on a connected device, so the in-process desktop runner SKIPs it; on a device it presses the button over HTTP. The bound checks FPS stays within range across repeated sweeps (a sweep is a transient cost, not a permanent degradation), proving the scan never leaves the render loop in a wedged state. (Background: the sweep is boot-only + manual precisely because the blocking probe must not run continuously on the render task — see DevicesModule.md.)", |
There was a problem hiding this comment.
Make the scan coverage deterministic.
This scenario is live-only, depends on a real LAN, and asserts a timing-derived FPS percentage, so it is both skipped by the desktop runner and prone to network/load flake. Keep the hardware sweep as manual verification, but add a deterministic test path with canned httpGet responses that exercises scan restart, classification, upsert, aging, and list serialization without real network or timing. As per coding guidelines, test/** tests “should not depend on timing or network.”
Also applies to: 50-52
🤖 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/core/scenario_DevicesModule_scan.json` around lines 5 - 6, The
current scenario_DevicesModule_scan.json is live-only, network-dependent, and
timing-sensitive, which violates the coding guideline that tests should not
depend on timing or network. Create a parallel deterministic test scenario that
exercises the same scan functionality (scan restart, classification, upsert,
aging, and list serialization) using canned httpGet responses instead of real
network calls, allowing it to run in the desktop runner without flakiness. Keep
the existing live-only scenario for manual hardware verification. Apply this
change to both the anchor location (line 5-6) and sibling location (line 50-52)
by ensuring each scenario is properly configured for its respective test type
(deterministic vs. live-only).
Source: Coding guidelines
DevicesModule now finds peers over mDNS (devices advertise _http._tcp and browse for each other), shows how each was found (scan/mdns) and when (last seen), and remembers the list across reboots. Discovery freshness is a per-device timestamp so devices age out after a day unseen, regardless of which strategy found them. KPI: 16384lights | PC:363KB | tick:120/100/118/118/58/9/1/37/20/117/328/11us(FPS:8333/10000/8474/8474/17241/111111/1000000/27027/50000/8547/3048/90909) | ESP32:1243KB | tick:3579us(FPS:279) | heap:8338KB | src:92(18763) | test:61(9437) | lizard:74w Core: - DevicesModule: mDNS browse runs every tick (non-blocking async poll), cycling _http._tcp / _wled._tcp and merging hits with a raise-never-lower type rule (a generic _http hit can't downgrade a projectMM/WLED row). Each Device carries a `via` discovery-source bitmask (scan/mdns/udp) and a `lastSeenMs` timestamp; age-out moved to loop1s and drops a non-self device unseen for 24h (strategy-agnostic — HTTP sweep, mDNS lap, and a future UDP beacon each just stamp "now"). A `cached` flag marks a restored-but-not-yet-re-seen device so the UI shows "last seen: cached" instead of a fake recent time until a live strategy re-confirms it; detail rows serialize `via`, `ageSec`, and `cached`. - Control: addList takes a non-const ListSource& (restoreList mutates the source) — removed the const_cast; the List apply path now propagates a restore parse failure as Malformed instead of masking it as Ok. - JsonUtil: extracted the parse+navigate into a non-template parseListArray so the ~8 KB static JsonDoc is one .bss copy shared across all forEachListElement instantiations (was one per callback type); tightened the integer parser's number-tail skip to stop at a number's real end (no longer swallows a following token like the `-2` in `1-2`). - platform (ESP32): mdnsInit advertises _http._tcp:80 (add-once, refresh instance name on reconnect) so peers discover us by service type; mdnsStop removes the service + clears the hostname but keeps the stack up for browse; new mdnsShutdown does the full mdns_free at teardown; mdnsBrowseStart/Poll/Stop is the async non-blocking browse cycle. NetworkModule teardown uses mdnsShutdown. - platform (desktop): httpGet rejects an over-long request that would truncate the 256-byte header buffer; mDNS browse + mdnsShutdown are no-op stubs. - Sort: new generic core mm::insertionSort (bounded, stable, allocation-free) — DevicesModule keeps its list ordered by name as devices arrive. Light domain: - (none — discovery is domain-neutral core) UI: - app.js: list detail renders a scalar array (speaks / via) as chips, and a `*Sec` duration / `cached` flag as a relative "last seen 2m ago" / "cached"; summary row gained aria-expanded for accessibility. - style.css: list-detail chips + muted "cached" value; list-detail-val uses overflow-wrap: anywhere (not the deprecated word-break: break-word) and wraps chips. Tests: - unit_DevicesModule_ageout: pins the 24h timestamp age-out using the millis() test override (fresh device survives, stale device drops, malformed cache tolerated). - unit_Sort: pins insertionSort (empty / single / sorted / reverse / stable / case-insensitive comparator). - unit_Control_list: corrected the stale "non-persistable" header comment to match the persistable + restore round-trip the body already tests. Docs / CI: - DevicesModule.md: documents the two discovery strategies, the via/ageSec/cached wire fields, and the 24h timestamp age-out rationale. - CLAUDE.md: reworded "data over objects" to scope it (hot-path = flat/object-free; module tree = the one hierarchy; a recognizable adapter like ListSource is allowed off the hot path), reconciling it with the minimalism principle; fixed firmwares[0] wording. - backlog: marked mDNS browse shipped; added the off-render-task HTTP probe and a deterministic canned-httpGet scan scenario as follow-ups. Reviews: - 🐇 addList const_cast → non-const ListSource& (fixed) - 🐇 List restore result ignored → propagate Malformed (fixed) - 🐇 JSON number tail too permissive → precise fraction/exponent skip (fixed) - 🐇 forEachListElement static JsonDoc per instantiation → shared via parseListArray (fixed) - 🐇 desktop httpGet snprintf truncation → reject over-long request (fixed) - 🐇 platform.h httpGet comment claimed off-hot-path → corrected to bounded-on-render-task (fixed) - 🐇 scanProgress not reset on no-network / sweep-end → reset to 0 (fixed) - 🐇 upsertSelf didn't refresh type/speaks on existing entry → refresh on both paths (fixed) - 🐇 main.cpp future-tense comment → present tense (fixed) - 🐇 app.js summary missing aria-expanded → added (fixed) - 🐇 style.css word-break: break-word deprecated → overflow-wrap: anywhere (fixed) - 🐇 DevicesModule.md progress/List docs stale → corrected (fixed) - 🐇 CLAUDE.md entry[0] → firmwares[0] (fixed) - 🐇 ListSource is a second virtual hierarchy → skipped: it's the recognizable data-source/adapter pattern (UITableView/QAbstractItemModel) the reworded principle explicitly allows off the hot path; a callback table would be more bespoke - 🐇 deterministic canned-httpGet scan scenario → deferred to backlog: needs a new httpGet-mock platform seam; age-out/restore/serialize already covered by unit_DevicesModule_ageout, classify by unit_DeviceIdentify Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/core/DevicesModule.h (2)
89-114:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix premature return in
restoreList()so post-parse sorting executes.At Line 89,
return mm::json::forEachListElement(...)exits the function before Line 113, so restored entries never getsortByName().Suggested fix
- return mm::json::forEachListElement(json, key, + const bool ok = mm::json::forEachListElement(json, key, [&](const mm::json::JsonDoc& doc, const mm::json::JsonNode* el) { if (deviceCount_ >= kMaxDevices) return; if (mm::json::readBool(mm::json::member(doc, el, "self"))) return; // skip persisted self char ipStr[16] = {}, name[24] = {}, typeStr[12] = {}; mm::json::readString(mm::json::member(doc, el, "ip"), ipStr, sizeof(ipStr)); mm::json::readString(mm::json::member(doc, el, "name"), name, sizeof(name)); mm::json::readString(mm::json::member(doc, el, "type"), typeStr, sizeof(typeStr)); uint8_t octets[4]; if (!parseDottedQuad(ipStr, octets)) return; Device& d = devices_[deviceCount_++]; std::memcpy(d.ip, octets, 4); std::snprintf(d.name, sizeof(d.name), "%s", name); d.type = (std::strcmp(typeStr, "projectMM") == 0) ? DevType::ProjectMM : (std::strcmp(typeStr, "WLED") == 0) ? DevType::Wled : DevType::Generic; d.self = false; d.speaks = ProtoHttp; d.via = 0; // no live sighting yet — via fills in when a strategy re-sees it d.cached = true; // restored, not re-confirmed live → UI shows "cached", not a time // Stamp "now" so the cached entry gets a full kStaleMs grace window to // re-announce before age-out drops it (a still-alive but slow device). d.lastSeenMs = platform::millis(); }); + if (!ok) return false; sortByName(); // cached list shows alphabetically too, before the first sweep return true;🤖 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/DevicesModule.h` around lines 89 - 114, The restoreList() function has a premature return statement that exits before the sortByName() call can execute. Remove the return keyword from the mm::json::forEachListElement() call so it simply executes the loop without early exit, allowing sortByName() on the following line to sort the restored cached entries alphabetically, and then the function returns true at the end.
327-336:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdvance
scanProgress_inside the sweep loop.
scanProgress_is set at Line 313 and reset at Line 339, but never updated per host in Lines 327-336, so the progress bar does not reflect sweep progress.Suggested fix
for (uint8_t i = 0; i < kProbesPerTick && hostCursor_ <= 254; i++, hostCursor_++) { + scanProgress_ = static_cast<uint32_t>(hostCursor_); uint8_t ip[4] = {subnet_[0], subnet_[1], subnet_[2], static_cast<uint8_t>(hostCursor_)}; // Don't probe our own IP: upsertSelf already gave it the right identity // (projectMM, deviceName), and an HTTP request to ourselves mid-tick can // race the server / loopback and misclassify us as generic. Just keep it🤖 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/DevicesModule.h` around lines 327 - 336, The scanProgress_ variable is initialized and reset but never incremented within the host scanning loop in DevicesModule.h, causing the progress bar to not reflect sweep progress. Inside the for loop that iterates through hosts (the loop containing the ipEq check and probe call), increment scanProgress_ for each host processed to track the scanning progress. This should be done for each iteration regardless of whether the host is skipped via the continue statement or actually probed, so that the progress bar advances consistently as the sweep progresses.docs/moonmodules/core/DevicesModule.md (1)
25-30:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign probe table with the implemented HTTP status gating.
At Line 27 and Line 28, the table implies body-only matching; implementation classifies projectMM/WLED only when HTTP status is
200.Suggested fix
-| `GET /api/state` | body contains `"modules"` | projectMM | -| `GET /json/info` | body contains `WLED` | WLED | +| `GET /api/state` | status `200` and body contains `"modules"` | projectMM | +| `GET /json/info` | status `200` and body contains `WLED` | WLED |🤖 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 `@docs/moonmodules/core/DevicesModule.md` around lines 25 - 30, The probe matching table in the Devices Module documentation currently describes the projectMM and WLED detection criteria as body-only matching (checking if the response body contains specific strings), but the actual implementation gates these matches on HTTP status code 200 in addition to the body content check. Update the Match column descriptions for the projectMM probe (GET /api/state) and WLED probe (GET /json/info) rows to clarify that HTTP status 200 is a required condition along with the body content match, ensuring the documentation accurately reflects the implementation behavior.
🤖 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/platform/esp32/platform_esp32.cpp`:
- Around line 963-967: The code in the mdns_service_add block assumes any
failure from mdns_service_add means the service already exists, but the function
can fail for other reasons like OOM or invalid state. Instead of unconditionally
calling mdns_service_instance_name_set on failure, first check if the service
actually exists using mdns_service_exists before attempting to update the
instance name, or alternatively explicitly remove the service with
mdns_service_remove before adding it. This ensures the mDNS service is only
reported as started (the log message and mdnsRunning_ flag) when the service is
actually successfully registered or refreshed, not when it fails for unrelated
reasons.
In `@test/unit/core/unit_DevicesModule_ageout.cpp`:
- Around line 45-59: The test clock restoration at the end of `aPresentAfter()`
using `platform::setTestNowMs(0)` is not failure-safe. If either REQUIRE check
at line 53 or line 54 fails, the function will exit without executing the clock
restoration, leaving the test clock in a polluted state for subsequent tests.
Implement a failure-safe cleanup mechanism such as a scope guard object or RAII
wrapper that guarantees the call to `platform::setTestNowMs(0)` executes
regardless of whether REQUIRE assertions succeed or fail, ensuring the test
clock is always restored to its initial state before the function returns.
---
Outside diff comments:
In `@docs/moonmodules/core/DevicesModule.md`:
- Around line 25-30: The probe matching table in the Devices Module
documentation currently describes the projectMM and WLED detection criteria as
body-only matching (checking if the response body contains specific strings),
but the actual implementation gates these matches on HTTP status code 200 in
addition to the body content check. Update the Match column descriptions for the
projectMM probe (GET /api/state) and WLED probe (GET /json/info) rows to clarify
that HTTP status 200 is a required condition along with the body content match,
ensuring the documentation accurately reflects the implementation behavior.
In `@src/core/DevicesModule.h`:
- Around line 89-114: The restoreList() function has a premature return
statement that exits before the sortByName() call can execute. Remove the return
keyword from the mm::json::forEachListElement() call so it simply executes the
loop without early exit, allowing sortByName() on the following line to sort the
restored cached entries alphabetically, and then the function returns true at
the end.
- Around line 327-336: The scanProgress_ variable is initialized and reset but
never incremented within the host scanning loop in DevicesModule.h, causing the
progress bar to not reflect sweep progress. Inside the for loop that iterates
through hosts (the loop containing the ipEq check and probe call), increment
scanProgress_ for each host processed to track the scanning progress. This
should be done for each iteration regardless of whether the host is skipped via
the continue statement or actually probed, so that the progress bar advances
consistently as the sweep progresses.
🪄 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: 651eba90-e99b-4682-a331-173ecc39e1f7
📒 Files selected for processing (22)
CLAUDE.mddocs/backlog/backlog.mddocs/moonmodules/core/DevicesModule.mdsrc/core/Control.cppsrc/core/Control.hsrc/core/DevicesModule.hsrc/core/JsonUtil.hsrc/core/NetworkModule.hsrc/core/Sort.hsrc/main.cppsrc/platform/desktop/platform_desktop.cppsrc/platform/esp32/platform_esp32.cppsrc/platform/platform.hsrc/ui/app.jssrc/ui/style.csstest/CMakeLists.txttest/scenarios/light/scenario_AllEffects_grid_sizes.jsontest/scenarios/light/scenario_Audio_mutation.jsontest/scenarios/light/scenario_Driver_mutation.jsontest/unit/core/unit_Control_list.cpptest/unit/core/unit_DevicesModule_ageout.cpptest/unit/core/unit_Sort.cpp
Adds five light modules (a 3D sine field, WLED-style distortion waves, a wheel layout, a random-remap modifier, and a rotate modifier) — all integer-only, reusing the project sin8/cos8 LUT — plus the first dynamic-modifier mechanism that lets a modifier reshape its mapping on a timer. Also processes a second CodeRabbit batch and prunes shipped/absorbed entries from the backlog and history per the new subtraction rule. KPI: 16384lights | PC:363KB | tick:119/100/119/118/57/9/2/36/19/116/330/11us(FPS:8403/10000/8403/8474/17543/111111/500000/27777/52631/8620/3030/90909) | ESP32:1248KB | tick:5509us(FPS:181) | heap:8334KB | src:97(19286) | test:66(9793) | lizard:74w Core: - DevicesModule: fixed an unreachable `return` in restoreList that skipped sortByName (cached list was never sorted) — capture the result, sort, then return it; restored per-tick scanProgress advance so the progress bar tracks the sweep; upsertSelf refreshes type/speaks on the existing-entry path too. - platform (ESP32): mdnsInit now branches on mdns_service_exists before add-vs-refresh, so a real add failure (OOM / invalid state) surfaces instead of being misread as "already advertised". Light domain: - SineEffect: 3D R/G/B sine field, 120° channel offsets, frequency/amplitude/bpm — integer sin8. - DistortionWavesEffect: two interfering sines → hue (WLED port), freq_x/freq_y/speed (0 = frozen) — integer sin8. - WheelLayout: spokes × ledsPerSpoke radiating from a hub, centre-shifted integer cos8/sin8 spoke coordinates. - RandomMapModifier: first dynamic modifier — a true 1:1 random permutation reshuffled on a bpm timer (0–60, default 6; 0 = frozen) via a member Fisher-Yates buffer, no hot-path alloc; OOM degrades to identity. - RotateModifier: dynamic modifier — stepped integer rotation (inverse sin8/cos8, nearest-neighbour) baked into the LUT, rebuilt only on an angle-step change; no per-frame alloc, no dynamic_cast. - ModifierBase / Layer: a modifier may override MoonModule::loop(); Layer::loop() ticks enabled modifier children after the effect pass (the dynamic-modifier hook the two new modifiers use). UI: - (none — the modules render through the generic control/preview path) Tests: - unit_SineEffect, unit_DistortionWavesEffect, unit_WheelLayout, unit_RandomMapModifier, unit_RotateModifier: render/bijection/identity/determinism/empty-grid/degrade properties (+17 cases). - unit_DevicesModule_ageout: added a ClockGuard RAII so a failing assertion can't leave virtual time polluted for later cases. Docs / CI: - Promoted the 4 module specs to docs/moonmodules/light/ (Sine/DistortionWaves/Wheel/Rotate) and RandomMapModifier; documented the via/ageSec/cached wire fields and the HTTP-200 probe gate in DevicesModule.md. - CLAUDE.md: extended Mandatory subtraction so docs/backlog/ and docs/history/ shrink as well as grow (shipped items deleted, absorbed lessons pruned, git is the permanent record); removed the moonmodules_draft folder + concept (a draft spec is now a plain backlog .md); reworded the per-feature workflow and folder descriptions to match. - backlog: removed the shipped moonmodules_draft drafts; split the former UI draft into ui-deferred.md (forward) and history/v1-inventory.md (backward); deleted the shipped installer-3layer plan (its deferred items already live in backlog.md/building.md); combined the two shipped LED-driver increment plans into one deferred-only leddriver-deferred.md. - history: deleted the completed repo-transfer runbook; pruned decisions.md's abandoned prototype-cycle postmortem + dated cycle-1 scorecard (~130 lines), keeping the working-v3 lessons. Reviews: - 🐇 restoreList unreachable return → capture result, sort, return it (fixed) - 🐇 scanProgress not incremented mid-sweep → restored per-tick advance (fixed) - 🐇 upsertSelf stale type/speaks on existing entry → refresh on both paths (fixed) - 🐇 mdns_service_add failure assumed "already exists" → branch on mdns_service_exists (fixed) - 🐇 test clock restore not failure-safe → ClockGuard RAII (fixed) - 🐇 DevicesModule.md probe table omitted the HTTP-200 gate → documented (fixed) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docs/moonmodules/light/drivers/RmtLedDriver.md (1)
28-29:⚠️ Potential issue | 🟡 MinorThe documentation accurately reflects the current code, but the loopback pin controls diverge from the standard pin-control pattern.
loopbackTxPinandloopbackRxPinare correctly documented asuint16_tin the spec — they match the member variables in the header and are registered viacontrols_.addUint16(). However, the standard pin control pattern elsewhere (defined inControl.h::addPin()) usesint8_tstorage andControlType::Pin. The loopback pins intentionally useuint16_tandaddUint16()instead, which is a deliberate design choice. If this divergence is intentional, add a comment in the header explaining why; if unintentional, unify them with the standardaddPin()pattern and update the spec accordingly.🤖 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 `@docs/moonmodules/light/drivers/RmtLedDriver.md` around lines 28 - 29, The loopback pin controls loopbackTxPin and loopbackRxPin intentionally diverge from the standard pin control pattern by using uint16_t and addUint16() instead of the standard int8_t and addPin() approach defined in Control.h. Clarify this design decision by adding an explanatory comment in the header file where loopbackTxPin and loopbackRxPin are registered, documenting why they use the uint16_t pattern instead of adhering to the standard addPin() convention used elsewhere in the codebase. Alternatively, if this divergence was unintentional, refactor these two controls to use the standard addPin() pattern and update the documentation to reflect int8_t as the type instead of uint16_t.docs/moonmodules/core/DevicesModule.md (2)
10-10:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix the progress range.
progressstarts at0while idle, so1..254is off by one and misstates the control.🐛 Suggested edit
-- `progress` — a progress bar of the sweep position (host 1..254). Always present (the WebSocket state push patches control *values* but not *structure*, so a hide-while-idle flag would not update live); at rest its value is 0 (empty bar). +- `progress` — a progress bar of the sweep position (host 0..254). Always present (the WebSocket state push patches control *values* but not *structure*, so a hide-while-idle flag would not update live); at rest its value is 0 (empty bar).🤖 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 `@docs/moonmodules/core/DevicesModule.md` at line 10, The progress bar range documentation for the DevicesModule states the sweep position is 1..254, but the text clarifies that the value is 0 when idle, indicating the actual range should be 0..254. Update the documented progress range from 1..254 to 0..254 to accurately reflect the full range of values the progress bar can display.
21-21:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep roadmap notes out of the shipped doc.
This page should stay in present tense; the backlog/planned-next notes belong in
docs/backlog/instead.🧹 Suggested edit
- Moving the probe to its own task (the enabler for safe periodic sweeping + a UDP presence beacon) is in the backlog. ... - A UDP beacon / ArtPoll listener is the planned next non-HTTP strategy.As per coding guidelines:
**/*.mduses present tense only, and roadmaps live only indocs/backlog/ordocs/history/.Also applies to: 40-40
🤖 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 `@docs/moonmodules/core/DevicesModule.md` at line 21, The DevicesModule.md documentation contains a sentence describing future work ("Moving the probe to its own task... is in the backlog") which violates the guideline that shipped documentation should remain in present tense and roadmap/backlog items should only exist in docs/backlog/ or docs/history/. Remove the sentence about moving the probe to its own task and the backlog reference from the subnet sweep description, keeping only the current-state technical details about how the sweep operates. Apply the same fix to line 40 as mentioned in the comment.Source: Coding guidelines
🤖 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 `@docs/moonmodules/light/drivers/RmtLedDriver.md`:
- Around line 22-23: The RmtLedDriver.md file contains a future-work note about
dedicated core-1 driver tasks and core affinity that describes planned
improvements rather than current functionality. Remove the sentence beginning
with "A dedicated core-1 driver task..." from the end of the loop() description,
as module documentation should describe the system as it currently exists in
present tense. The deferred functionality is already documented in
docs/backlog/leddriver-deferred.md, so this future-work note does not belong in
the shipped module specification.
In `@docs/moonmodules/light/modifiers/RandomMapModifier.md`:
- Around line 5-6: Remove the word "first" from the description of
RandomMapModifier since it is no longer accurate. The sentence currently states
"This is the first **dynamic** modifier" but RotateModifier.md also introduces a
dynamic modifier in this PR. Update the sentence to simply describe
RandomMapModifier as a dynamic modifier without claiming it is the first one.
In `@src/light/modifiers/RotateModifier.h`:
- Around line 92-93: The comment describing the rotation timing in
RotateModifier.h incorrectly calculates the duration of one full turn. The
comment states the duration is (256*256/speed) ms, but the actual code uses step
= phaseNum_ >> 6, which divides by 64 rather than 256. Correct the comment to
reflect the actual timing: one full turn requires 256 steps times 64 units per
step, totaling 16384 units, which takes 16384/speed ms (or equivalently
256*64/speed ms) at speed S. This is 4 times shorter than what the current
comment states.
---
Outside diff comments:
In `@docs/moonmodules/core/DevicesModule.md`:
- Line 10: The progress bar range documentation for the DevicesModule states the
sweep position is 1..254, but the text clarifies that the value is 0 when idle,
indicating the actual range should be 0..254. Update the documented progress
range from 1..254 to 0..254 to accurately reflect the full range of values the
progress bar can display.
- Line 21: The DevicesModule.md documentation contains a sentence describing
future work ("Moving the probe to its own task... is in the backlog") which
violates the guideline that shipped documentation should remain in present tense
and roadmap/backlog items should only exist in docs/backlog/ or docs/history/.
Remove the sentence about moving the probe to its own task and the backlog
reference from the subnet sweep description, keeping only the current-state
technical details about how the sweep operates. Apply the same fix to line 40 as
mentioned in the comment.
In `@docs/moonmodules/light/drivers/RmtLedDriver.md`:
- Around line 28-29: The loopback pin controls loopbackTxPin and loopbackRxPin
intentionally diverge from the standard pin control pattern by using uint16_t
and addUint16() instead of the standard int8_t and addPin() approach defined in
Control.h. Clarify this design decision by adding an explanatory comment in the
header file where loopbackTxPin and loopbackRxPin are registered, documenting
why they use the uint16_t pattern instead of adhering to the standard addPin()
convention used elsewhere in the codebase. Alternatively, if this divergence was
unintentional, refactor these two controls to use the standard addPin() pattern
and update the documentation to reflect int8_t as the type instead of uint16_t.
🪄 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: 51e1636e-4d52-46c4-8473-374e959d57ae
📒 Files selected for processing (42)
CLAUDE.mddocs/backlog/README.mddocs/backlog/backlog.mddocs/backlog/installer-3layer-plan.mddocs/backlog/leddriver-deferred.mddocs/backlog/leddriver-increment-1-plan.mddocs/backlog/leddriver-increment-2-plan.mddocs/backlog/moonmodules_draft/.gitkeepdocs/backlog/moonmodules_draft/core/ui.mddocs/backlog/moonmodules_draft/light/effects/DistortionWavesEffect.mddocs/backlog/moonmodules_draft/light/effects/SineEffect.mddocs/backlog/moonmodules_draft/light/layouts/WheelLayout.mddocs/backlog/moonmodules_draft/light/modifiers/RotateModifier.mddocs/backlog/ui-deferred.mddocs/history/README.mddocs/history/decisions.mddocs/history/repo-transfer.mddocs/history/v1-inventory.mddocs/moonmodules/core/DevicesModule.mddocs/moonmodules/light/drivers/RmtLedDriver.mddocs/moonmodules/light/effects/DistortionWavesEffect.mddocs/moonmodules/light/effects/SineEffect.mddocs/moonmodules/light/layouts/WheelLayout.mddocs/moonmodules/light/modifiers/RandomMapModifier.mddocs/moonmodules/light/modifiers/RotateModifier.mdsrc/core/DevicesModule.hsrc/light/effects/DistortionWavesEffect.hsrc/light/effects/SineEffect.hsrc/light/layers/Layer.hsrc/light/layouts/WheelLayout.hsrc/light/modifiers/ModifierBase.hsrc/light/modifiers/RandomMapModifier.hsrc/light/modifiers/RotateModifier.hsrc/main.cppsrc/platform/esp32/platform_esp32.cpptest/CMakeLists.txttest/unit/core/unit_DevicesModule_ageout.cpptest/unit/light/unit_DistortionWavesEffect.cpptest/unit/light/unit_RandomMapModifier.cpptest/unit/light/unit_RotateModifier.cpptest/unit/light/unit_SineEffect.cpptest/unit/light/unit_WheelLayout.cpp
💤 Files with no reviewable changes (10)
- docs/backlog/moonmodules_draft/light/effects/SineEffect.md
- docs/backlog/leddriver-increment-2-plan.md
- docs/backlog/moonmodules_draft/light/modifiers/RotateModifier.md
- docs/history/repo-transfer.md
- docs/backlog/installer-3layer-plan.md
- docs/backlog/moonmodules_draft/light/layouts/WheelLayout.md
- docs/backlog/moonmodules_draft/light/effects/DistortionWavesEffect.md
- docs/backlog/leddriver-increment-1-plan.md
- docs/backlog/moonmodules_draft/core/ui.md
- docs/history/README.md
…ard sweep Moves every single-GPIO control to the standard Pin control type, compiles each LED driver only into the chips whose silicon can run it, and replaces three overlapping perf scenarios with two incremental "add one subsystem, measure the delta" sweeps — then runs them across the classic ESP32, S3, and P4 to populate a three-board performance analysis. Also processes a second CodeRabbit batch. KPI: 16384lights | PC:363KB | tick:118/99/118/9/1/317/35/16/18/114/10us(FPS:8474/10101/8474/111111/1000000/3154/28571/62500/55555/8771/100000) | src:97(19318) | test:66(9794) | lizard:74w (ESP32 KPI tick omitted: the S3 was unreachable for a live render capture at commit time. This change is pin-control type, per-chip driver gating, scenarios and docs — no render-path code — and all three boards were verified rendering via the perf scenarios earlier this session.) Core: - AudioModule: wsPin / sdPin / sckPin moved from uint16/addUint16 to int8/addPin (the standard single-GPIO Pin control). Unset sentinel is now -1 (was 0), so GPIO 0 is a usable mic pin — a latent bug fix; the idle-when-unset guard switched from ==0 to <0. Light domain: - main.cpp: each LED driver's #include + registerType is now gated on the SOC capability macro it needs — RmtLedDriver on CONFIG_SOC_RMT_SUPPORTED, LcdLedDriver on CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED, ParlioLedDriver on CONFIG_SOC_PARLIO_SUPPORTED. A board's binary now carries only the drivers its chip can run (verified: the classic ELF has 0 LCD/Parlio symbols, 26 RMT; the classic type picker offers only RmtLedDriver). No dead flash, and the picker no longer offers an unrunnable driver. - RmtLedDriver / ParallelLedDriver (LCD + Parlio base): loopbackTxPin / loopbackRxPin moved to int8/addPin, -1 = unset (so GPIO 0 is usable); the unset check is now >= 0. - LcdLedDriver: clockPin / dcPin moved to int8/addPin (the i80 bus pins); lastClockPin_ / lastDcPin_ follow. - RotateModifier: corrected the timing comment — the angle step is phaseNum_/64, so a full turn is 16384/speed ms (the prior comment said 256*256/speed, 4× too long). Tests: - unit_AudioModule: pin-default assertions updated 0 -> -1 to match the Pin-control unset sentinel. Docs / CI: - performance.md: new three-board incremental-cost analysis (classic / S3 / P4) — per-subsystem cost, light/heavy effect bracket across 16²..16K, and the MultiplyModifier compute-down/memory-up tables. Tables are board-columned so later P4/classic re-runs drop straight in. Records the key findings: linear pixel scaling (no fragmentation), the heavy effect is the 16K bottleneck (P4 57 / S3 20 / classic 16 FPS), the modifier roughly halves heavy tick (¼ logical grid) at a LUT-memory cost, and 16K Noise+Multiply runs on the no-PSRAM classic at 35 FPS render-only (confirming it is not a no-PSRAM blocker). - architecture.md: light-domain rule — an effect renders a pattern, it does not transform geometry; mirror/tile/rotate/scroll/mask belong in a modifier (WLED folds these into effect code; we don't), so any effect composes with any modifier. - new scenario_perf_light (fast, ≤64², driver-free) and scenario_perf_full (comprehensive: per-subsystem adds, all-drivers-this-board, light/heavy bracket to 16K, modifier sweep). Both mutate + canvas-preparing: clear_children to a clean start, keeping only pre-wired apparatus; disable Audio + Devices so neither pollutes the tick. They subsume and replace scenario_Layer_buildup, scenario_GridLayout_grid_sizes, and scenario_AllEffects_grid_sizes (deleted). scenario-tests.md / unit-tests.md regenerated; testing.md example + backlog references updated. - backlog: NoiseEffect cost extended with the S3 16K data point; new entries for MultiplyModifier LUT-memory (re-verify the working classic+ArtNet setup, not an impossibility), disabling a module should release its resources not just stop its loop (onEnabledChanged audit), and the future classic I2S driver following the per-chip gating pattern. Removed the now-moot "RipplesEffect in the AllEffects sweep" item. - .gitignore: ignore the generated test/scenario-baseline.json cache. Reviews: - 🐇 RmtLedDriver.md future-work core-1-task sentence -> removed (present tense; it's in leddriver-deferred.md) (fixed) - 🐇 RandomMapModifier.md "first dynamic modifier" -> "a dynamic modifier" (RotateModifier is also dynamic) (fixed) - 🐇 RotateModifier.h full-turn timing comment off by 4× -> corrected (fixed) - 🐇 DevicesModule.md progress range 1..254 -> 0..254 (fixed) - 🐇 DevicesModule.md two future-work/backlog sentences -> removed (present tense) (fixed) - 🐇 RmtLedDriver loopback pins used uint16/addUint16 instead of the standard addPin -> migrated to int8/addPin, and extended the same fix to every single-GPIO control (Audio, Lcd, Parlio/Parallel base) for consistency (fixed) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
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 (4)
docs/testing.md (1)
27-27:⚠️ Potential issue | 🟡 MinorAdd subject to sentence fragment for clarity.
The phrase "May be empty or shorter than
pins" lacks an explicit subject. Reword to either "It may be empty or shorter thanpins" or rewrite as "When empty, the value must matchpinsby position" to improve clarity.🤖 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 `@docs/testing.md` at line 27, The sentence fragment "May be empty or shorter than `pins`" lacks an explicit subject and needs to be reworded for clarity. Add a subject to complete the sentence by either changing it to "It may be empty or shorter than `pins`" or rewriting it as "When empty, the value must match `pins` by position" to improve readability and grammatical correctness.src/core/AudioModule.h (1)
58-61:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate stale pin-sentinel comments to match current behavior.
Line 58 still documents unset as
0, and Line 200 says GPIO 0 is invalid, but this code now uses-1as unset and keeps GPIO 0 usable.🛠 Proposed fix
- // two read-only read-outs. The pins default to UNSET (0): the module is user- + // two read-only read-outs. The pins default to UNSET (-1): the module is user- @@ - // attempt an I2S init. A 0 GPIO is not a valid mic pin, and initialising + // attempt an I2S init. An unset pin (<0) is invalid; GPIO 0 is valid, and initialisingAs per coding guidelines, comments should only be removed/changed when outdated or factually wrong; these are now factually wrong.
Also applies to: 200-201
🤖 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 58 - 61, Update the outdated comments in AudioModule.h that document pin sentinel values and GPIO 0 constraints. The comment describing the module behavior needs to be changed from stating pins default to UNSET as 0 to correctly reflect that the code now uses -1 as the unset sentinel value. Additionally, the comment around line 200 that says GPIO 0 is invalid should be updated to reflect that GPIO 0 is actually usable in the current implementation. Replace the factually incorrect statements with accurate descriptions of the current behavior where -1 represents unset and GPIO 0 is a valid pin option.Source: Coding guidelines
src/light/drivers/RmtLedDriver.h (2)
369-376:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard unset
loopbackRxPinbefore hardware loopback call.Line 375 converts the unset sentinel (
-1) to255, so loopback still executes with an invalid GPIO instead of short-circuiting the test path.Proposed fix
// The test reconfigures the first data pin as TX, so release ALL our TX // channels first — this also guarantees the test's RX channel can always // allocate RMT memory, even with every TX channel otherwise claimed; // reinit() restores them after. + if (loopbackRxPin < 0) { + clearFailBuf(); + setStatus("loopback: set loopbackRxPin", Severity::Warning); + return; + } deinitAll(); // TX override: when loopbackTxPin is set, transmit on it instead of the // first data pin, so the bench loopback runs on a dedicated jumper without // re-typing `pins`. Falls back to pins[0] when unset. const uint8_t txPin = loopbackTxPin >= 0 ? static_cast<uint8_t>(loopbackTxPin) : static_cast<uint8_t>(pinList_[0]); const uint8_t rxPin = static_cast<uint8_t>(loopbackRxPin);🤖 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/light/drivers/RmtLedDriver.h` around lines 369 - 376, The loopbackRxPin is being unconditionally cast to uint8_t on line 375, which converts the unset sentinel value of -1 to 255, resulting in an invalid GPIO pin being used for the hardware loopback call. Add a guard condition before the platform::RmtLoopbackResult initialization to check if loopbackRxPin is set (not equal to -1). Only proceed with the loopback execution if loopbackRxPin has a valid value, otherwise skip or short-circuit the test path similar to how loopbackTxPin is conditionally handled with the ternary operator.
102-107:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the
loopbackTxPinsentinel comment to match current behavior.Line 106 still says
0 = transmit on pins[0], but the new logic treats-1as unset and0as a valid explicit GPIO override.Proposed fix
- // flips the flag. txPin is the optional override (0 = transmit on pins[0]). + // flips the flag. txPin is the optional override + // (-1 = use pins[0], >=0 = force that GPIO).As per coding guidelines, comments should be corrected when they become factually wrong.
🤖 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/light/drivers/RmtLedDriver.h` around lines 102 - 107, Update the comment for txPin in the RmtLedDriver.h file (located near the loopbackTxPin control addition) to reflect the current sentinel value behavior. The comment currently states "0 = transmit on pins[0]" but the new logic treats -1 as the unset sentinel value and 0 as a valid explicit GPIO override. Revise the comment to accurately describe that -1 indicates an unset/default state and 0 is now a valid explicit GPIO pin override, not the default behavior.Source: Coding guidelines
🤖 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/light/layers/Layer.h`:
- Around line 155-159: The loop iterating through children starting at line 155
ticks all enabled modifiers, but the rebuildLUT() method (lines 223-229) only
applies the first enabled modifier to the lookup table. This creates a semantic
mismatch where non-first modifiers can trigger rebuilds that never use their
mapping. Modify the loop that checks child(i)->role() and child(i)->enabled() to
only tick the first enabled modifier with ModuleRole::Modifier role, stopping
after processing that first one, so the ticking behavior aligns with how
rebuildLUT() actually applies modifiers.
In `@src/light/modifiers/RandomMapModifier.h`:
- Around line 96-97: Remove the `lyr->onBuildState()` call from the render loop
path in RandomMapModifier to avoid triggering full Layer rebuilds with memory
allocation/deallocation during the render tick. The `reshuffle()` call on the
preceding line is sufficient to invalidate the LUT for the next rebuild cycle
without forcing an immediate full rebuild from the modifier's `loop()` method,
which violates render-loop memory allocation guidelines. Simply delete the
onBuildState() line to defer the actual rebuild until a safer, non-render-path
context.
In `@src/main.cpp`:
- Around line 37-45: The preprocessor directives checking
CONFIG_SOC_RMT_SUPPORTED, CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED, and
CONFIG_SOC_PARLIO_SUPPORTED in the include sections are leaking
platform-specific branching logic into main.cpp, which violates the architecture
rules. Instead of using `#if` defined(CONFIG_SOC_...) blocks, define corresponding
compile-time boolean flags in platform_config.h (e.g., PLATFORM_SUPPORTS_RMT,
PLATFORM_SUPPORTS_LCD_LED_DRIVER, PLATFORM_SUPPORTS_PARLO_LED_DRIVER) and
replace the preprocessor conditional includes with if constexpr statements that
check these flags, ensuring all driver selection logic is abstracted through the
platform configuration system rather than scattered across main.cpp.
In `@test/unit/light/unit_RandomMapModifier.cpp`:
- Around line 74-119: Add a new test case after the existing tests to cover the
loop() method's timer-driven behavior. The test should create a
RandomMapModifier instance and call loop() with time values that cross a beat
boundary, testing both when bpm==0 (no reshuffling) and when bpm is set to a
positive value. Verify that when a beat boundary is crossed, the reshuffle
occurs and the mapping changes, while tracking that the internal phaseNum_ and
lastBeat_ state variables are properly updated. Also verify that calling loop()
with bpm==0 does not trigger a reshuffle even when time advances significantly,
confirming the timer logic correctly controls when dynamic remapping happens.
---
Outside diff comments:
In `@docs/testing.md`:
- Line 27: The sentence fragment "May be empty or shorter than `pins`" lacks an
explicit subject and needs to be reworded for clarity. Add a subject to complete
the sentence by either changing it to "It may be empty or shorter than `pins`"
or rewriting it as "When empty, the value must match `pins` by position" to
improve readability and grammatical correctness.
In `@src/core/AudioModule.h`:
- Around line 58-61: Update the outdated comments in AudioModule.h that document
pin sentinel values and GPIO 0 constraints. The comment describing the module
behavior needs to be changed from stating pins default to UNSET as 0 to
correctly reflect that the code now uses -1 as the unset sentinel value.
Additionally, the comment around line 200 that says GPIO 0 is invalid should be
updated to reflect that GPIO 0 is actually usable in the current implementation.
Replace the factually incorrect statements with accurate descriptions of the
current behavior where -1 represents unset and GPIO 0 is a valid pin option.
In `@src/light/drivers/RmtLedDriver.h`:
- Around line 369-376: The loopbackRxPin is being unconditionally cast to
uint8_t on line 375, which converts the unset sentinel value of -1 to 255,
resulting in an invalid GPIO pin being used for the hardware loopback call. Add
a guard condition before the platform::RmtLoopbackResult initialization to check
if loopbackRxPin is set (not equal to -1). Only proceed with the loopback
execution if loopbackRxPin has a valid value, otherwise skip or short-circuit
the test path similar to how loopbackTxPin is conditionally handled with the
ternary operator.
- Around line 102-107: Update the comment for txPin in the RmtLedDriver.h file
(located near the loopbackTxPin control addition) to reflect the current
sentinel value behavior. The comment currently states "0 = transmit on pins[0]"
but the new logic treats -1 as the unset sentinel value and 0 as a valid
explicit GPIO override. Revise the comment to accurately describe that -1
indicates an unset/default state and 0 is now a valid explicit GPIO pin
override, not the default behavior.
🪄 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: 9adc4583-a6aa-4a5a-aaf9-e7a45790a931
📒 Files selected for processing (63)
.gitignoreCLAUDE.mddocs/architecture.mddocs/backlog/README.mddocs/backlog/backlog.mddocs/backlog/installer-3layer-plan.mddocs/backlog/leddriver-deferred.mddocs/backlog/leddriver-increment-1-plan.mddocs/backlog/leddriver-increment-2-plan.mddocs/backlog/moonmodules_draft/.gitkeepdocs/backlog/moonmodules_draft/core/ui.mddocs/backlog/moonmodules_draft/light/effects/DistortionWavesEffect.mddocs/backlog/moonmodules_draft/light/effects/SineEffect.mddocs/backlog/moonmodules_draft/light/layouts/WheelLayout.mddocs/backlog/moonmodules_draft/light/modifiers/RotateModifier.mddocs/backlog/ui-deferred.mddocs/history/README.mddocs/history/decisions.mddocs/history/repo-transfer.mddocs/history/v1-inventory.mddocs/moonmodules/core/AudioModule.mddocs/moonmodules/core/DevicesModule.mddocs/moonmodules/light/drivers/LcdLedDriver.mddocs/moonmodules/light/drivers/ParlioLedDriver.mddocs/moonmodules/light/drivers/RmtLedDriver.mddocs/moonmodules/light/effects/DistortionWavesEffect.mddocs/moonmodules/light/effects/SineEffect.mddocs/moonmodules/light/layouts/WheelLayout.mddocs/moonmodules/light/modifiers/RandomMapModifier.mddocs/moonmodules/light/modifiers/RotateModifier.mddocs/performance.mddocs/testing.mddocs/tests/scenario-tests.mddocs/tests/unit-tests.mdsrc/core/AudioModule.hsrc/core/DevicesModule.hsrc/light/drivers/LcdLedDriver.hsrc/light/drivers/ParallelLedDriver.hsrc/light/drivers/ParlioLedDriver.hsrc/light/drivers/RmtLedDriver.hsrc/light/effects/DistortionWavesEffect.hsrc/light/effects/SineEffect.hsrc/light/layers/Layer.hsrc/light/layouts/WheelLayout.hsrc/light/modifiers/ModifierBase.hsrc/light/modifiers/RandomMapModifier.hsrc/light/modifiers/RotateModifier.hsrc/main.cppsrc/platform/esp32/platform_esp32.cpptest/CMakeLists.txttest/scenarios/light/scenario_AllEffects_grid_sizes.jsontest/scenarios/light/scenario_Audio_mutation.jsontest/scenarios/light/scenario_GridLayout_grid_sizes.jsontest/scenarios/light/scenario_Layer_buildup.jsontest/scenarios/light/scenario_perf_full.jsontest/scenarios/light/scenario_perf_light.jsontest/unit/core/unit_DevicesModule_ageout.cpptest/unit/light/unit_AudioModule.cpptest/unit/light/unit_DistortionWavesEffect.cpptest/unit/light/unit_RandomMapModifier.cpptest/unit/light/unit_RotateModifier.cpptest/unit/light/unit_SineEffect.cpptest/unit/light/unit_WheelLayout.cpp
💤 Files with no reviewable changes (12)
- docs/backlog/installer-3layer-plan.md
- test/scenarios/light/scenario_Layer_buildup.json
- docs/backlog/moonmodules_draft/light/layouts/WheelLayout.md
- docs/backlog/moonmodules_draft/light/effects/SineEffect.md
- docs/backlog/leddriver-increment-2-plan.md
- docs/history/README.md
- test/scenarios/light/scenario_GridLayout_grid_sizes.json
- docs/backlog/moonmodules_draft/light/effects/DistortionWavesEffect.md
- docs/backlog/leddriver-increment-1-plan.md
- docs/backlog/moonmodules_draft/core/ui.md
- docs/history/repo-transfer.md
- docs/backlog/moonmodules_draft/light/modifiers/RotateModifier.md
| for (uint8_t i = 0; i < childCount(); i++) { | ||
| if (child(i)->role() != ModuleRole::Modifier) continue; | ||
| if (!child(i)->enabled()) continue; | ||
| static_cast<ModifierBase*>(child(i))->loop(); | ||
| } |
There was a problem hiding this comment.
Ticking all modifiers conflicts with first-modifier LUT semantics.
Line 155 loops every enabled modifier, but rebuildLUT() (Lines 223-229) applies only the first enabled modifier. Non-first dynamic modifiers can still force rebuilds that never use their mapping.
Proposed fix (align loop semantics with rebuildLUT)
- for (uint8_t i = 0; i < childCount(); i++) {
- if (child(i)->role() != ModuleRole::Modifier) continue;
- if (!child(i)->enabled()) continue;
- static_cast<ModifierBase*>(child(i))->loop();
- }
+ for (uint8_t i = 0; i < childCount(); i++) {
+ if (child(i)->role() != ModuleRole::Modifier) continue;
+ if (!child(i)->enabled()) continue;
+ static_cast<ModifierBase*>(child(i))->loop();
+ break; // rebuildLUT() also uses first enabled modifier
+ }📝 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.
| for (uint8_t i = 0; i < childCount(); i++) { | |
| if (child(i)->role() != ModuleRole::Modifier) continue; | |
| if (!child(i)->enabled()) continue; | |
| static_cast<ModifierBase*>(child(i))->loop(); | |
| } | |
| for (uint8_t i = 0; i < childCount(); i++) { | |
| if (child(i)->role() != ModuleRole::Modifier) continue; | |
| if (!child(i)->enabled()) continue; | |
| static_cast<ModifierBase*>(child(i))->loop(); | |
| break; // rebuildLUT() also uses first enabled modifier | |
| } |
🤖 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/light/layers/Layer.h` around lines 155 - 159, The loop iterating through
children starting at line 155 ticks all enabled modifiers, but the rebuildLUT()
method (lines 223-229) only applies the first enabled modifier to the lookup
table. This creates a semantic mismatch where non-first modifiers can trigger
rebuilds that never use their mapping. Modify the loop that checks
child(i)->role() and child(i)->enabled() to only tick the first enabled modifier
with ModuleRole::Modifier role, stopping after processing that first one, so the
ticking behavior aligns with how rebuildLUT() actually applies modifiers.
| reshuffle(); // ask the next rebuild to produce a fresh permutation | ||
| lyr->onBuildState(); // rebuild the LUT now (re-runs mapToPhysical → reshuffle) |
There was a problem hiding this comment.
Avoid full onBuildState() rebuilds from modifier loop().
Line 97 triggers a full Layer rebuild from the render tick path (Layer::loop() → ModifierBase::loop()), which can allocate/free LUT and buffer memory and introduce beat-time stalls.
As per coding guidelines, “In the render loop and anything it calls: no heap allocations … no blocking …”.
🤖 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/light/modifiers/RandomMapModifier.h` around lines 96 - 97, Remove the
`lyr->onBuildState()` call from the render loop path in RandomMapModifier to
avoid triggering full Layer rebuilds with memory allocation/deallocation during
the render tick. The `reshuffle()` call on the preceding line is sufficient to
invalidate the LUT for the next rebuild cycle without forcing an immediate full
rebuild from the modifier's `loop()` method, which violates render-loop memory
allocation guidelines. Simply delete the onBuildState() line to defer the actual
rebuild until a safer, non-render-path context.
Source: Coding guidelines
| #if defined(CONFIG_SOC_RMT_SUPPORTED) | ||
| #include "light/drivers/RmtLedDriver.h" | ||
| #endif | ||
| #if defined(CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED) | ||
| #include "light/drivers/LcdLedDriver.h" | ||
| #endif | ||
| #if defined(CONFIG_SOC_PARLIO_SUPPORTED) | ||
| #include "light/drivers/ParlioLedDriver.h" | ||
| #include "light/drivers/RmtLedDriver.h" | ||
| #endif |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Replace preprocessor SOC branching in main.cpp with platform_config-driven compile-time wiring.
These #if defined(CONFIG_SOC_...) blocks leak platform branching into src/main.cpp. This should be routed through platform_config.h flags with if constexpr (and, if needed, a small registry indirection) so platform selection stays aligned with repo architecture rules.
As per coding guidelines, "Use if constexpr on platform_config.h flags for compile-time platform branching; no #ifdef, platform-specific #include, or hardware API call outside src/platform/."
Also applies to: 104-112
🤖 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` around lines 37 - 45, The preprocessor directives checking
CONFIG_SOC_RMT_SUPPORTED, CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED, and
CONFIG_SOC_PARLIO_SUPPORTED in the include sections are leaking
platform-specific branching logic into main.cpp, which violates the architecture
rules. Instead of using `#if` defined(CONFIG_SOC_...) blocks, define corresponding
compile-time boolean flags in platform_config.h (e.g., PLATFORM_SUPPORTS_RMT,
PLATFORM_SUPPORTS_LCD_LED_DRIVER, PLATFORM_SUPPORTS_PARLO_LED_DRIVER) and
replace the preprocessor conditional includes with if constexpr statements that
check these flags, ensuring all driver selection logic is abstracted through the
platform configuration system rather than scattered across main.cpp.
Source: Coding guidelines
| TEST_CASE("RandomMapModifier is deterministic for a fixed generation") { | ||
| mm::RandomMapModifier a, b; | ||
| auto da = mapAll(a, 8, 8, 1); | ||
| auto db = mapAll(b, 8, 8, 1); | ||
| CHECK(da == db); | ||
| } | ||
|
|
||
| // Reshuffling (a beat) changes the mapping, and the result is still a bijection. | ||
| TEST_CASE("RandomMapModifier reshuffle changes the mapping, stays a bijection") { | ||
| mm::RandomMapModifier m; | ||
| const mm::lengthType w = 8, h = 8, d = 1; | ||
| const mm::nrOfLightsType n = static_cast<mm::nrOfLightsType>(w) * h * d; | ||
|
|
||
| auto before = mapAll(m, w, h, d); | ||
| m.reshuffle(); // what loop() does on a beat | ||
| auto after = mapAll(m, w, h, d); | ||
|
|
||
| CHECK(before != after); // a genuinely different permutation | ||
|
|
||
| std::vector<int> seen(n, 0); | ||
| for (auto dst : after) { REQUIRE(dst < n); seen[dst]++; } | ||
| for (mm::nrOfLightsType i = 0; i < n; i++) CHECK(seen[i] == 1); // still a bijection | ||
| } | ||
|
|
||
| // Robustness: an empty (0×0×0) grid must not crash — maxOut 0 yields no destination. | ||
| TEST_CASE("RandomMapModifier tolerates an empty grid") { | ||
| mm::RandomMapModifier m; | ||
| mm::nrOfLightsType phys[4]; | ||
| mm::nrOfLightsType count = 7; // sentinel | ||
| m.mapToPhysical(0, 0, 0, 0, 0, 0, phys, count, 0); | ||
| CHECK(count == 0); // nothing emitted, no crash | ||
| } | ||
|
|
||
| // A resize (different box count) rebuilds the permutation to the new size, still a bijection. | ||
| TEST_CASE("RandomMapModifier rebuilds on a grid resize") { | ||
| mm::RandomMapModifier m; | ||
| auto small = mapAll(m, 4, 4, 1); // 16 lights | ||
| CHECK(small.size() == 16); | ||
|
|
||
| auto big = mapAll(m, 8, 8, 1); // 64 lights — forces a resize+rebuild | ||
| CHECK(big.size() == 64); | ||
| const mm::nrOfLightsType n = 64; | ||
| std::vector<int> seen(n, 0); | ||
| for (auto dst : big) { REQUIRE(dst < n); seen[dst]++; } | ||
| for (mm::nrOfLightsType i = 0; i < n; i++) CHECK(seen[i] == 1); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Add direct coverage for the loop() timer path (bpm==0 and beat crossing).
Current cases validate reshuffle() and mapping properties, but not the runtime timer logic (phaseNum_/lastBeat_) that actually drives dynamic behavior.
As per coding guidelines, test/** should “verify tests cover edge cases and match the specifications in docs/moonmodules/.”
🧰 Tools
🪛 Clang (14.0.6)
[warning] 75-75: multiple declarations in a single statement reduces readability
(readability-isolate-declaration)
[warning] 75-75: variable name 'a' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 75-75: variable name 'b' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 76-76: variable name 'da' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 77-77: variable name 'db' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 83-83: variable name 'm' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 84-84: multiple declarations in a single statement reduces readability
(readability-isolate-declaration)
[warning] 84-84: variable name 'w' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 84-84: variable name 'h' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 84-84: variable name 'd' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 85-85: variable name 'n' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 95-95: statement should be inside braces
(readability-braces-around-statements)
[warning] 100-100: variable name 'm' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 101-101: do not declare C-style arrays, use std::array<> instead
(modernize-avoid-c-arrays)
[warning] 108-108: function 'DOCTEST_ANON_FUNC_14' has cognitive complexity of 28 (threshold 25)
(readability-function-cognitive-complexity)
[note] 111-111: +1, including nesting penalty of 0, nesting level increased to 1
(clang)
[note] 111-111: +2, including nesting penalty of 1, nesting level increased to 2
(clang)
[note] 111-111: +2, including nesting penalty of 1, nesting level increased to 2
(clang)
[note] 114-114: +1, including nesting penalty of 0, nesting level increased to 1
(clang)
[note] 114-114: +2, including nesting penalty of 1, nesting level increased to 2
(clang)
[note] 114-114: +2, including nesting penalty of 1, nesting level increased to 2
(clang)
[note] 117-117: +1, including nesting penalty of 0, nesting level increased to 1
(clang)
[note] 117-117: +2, including nesting penalty of 1, nesting level increased to 2
(clang)
[note] 117-117: +3, including nesting penalty of 2, nesting level increased to 3
(clang)
[note] 117-117: +3, including nesting penalty of 2, nesting level increased to 3
(clang)
[note] 118-118: +1, including nesting penalty of 0, nesting level increased to 1
(clang)
[note] 118-118: +2, including nesting penalty of 1, nesting level increased to 2
(clang)
[note] 118-118: +3, including nesting penalty of 2, nesting level increased to 3
(clang)
[note] 118-118: +3, including nesting penalty of 2, nesting level increased to 3
(clang)
[warning] 109-109: variable name 'm' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 115-115: variable name 'n' is too short, expected at least 3 characters
(readability-identifier-length)
[warning] 118-118: statement should be inside braces
(readability-braces-around-statements)
🪛 Cppcheck (2.21.0)
[style] 110-110: The function 'rebuildControls' is never used.
(unusedFunction)
[style] 113-113: The function 'hasLUT' is never used.
(unusedFunction)
[style] 114-114: The function 'isPaged' is never used.
(unusedFunction)
[style] 115-115: The function 'logicalCount' is never used.
(unusedFunction)
[style] 116-116: The function 'destinationCount' is never used.
(unusedFunction)
[style] 74-74: The function 'DOCTEST_ANON_FUNC_22' is never used.
(unusedFunction)
[style] 74-74: The function 'DOCTEST_ANON_FUNC_3' is never used.
(unusedFunction)
[style] 82-82: The function 'DOCTEST_ANON_FUNC_27' is never used.
(unusedFunction)
[style] 82-82: The function 'DOCTEST_ANON_FUNC_42' is never used.
(unusedFunction)
[style] 99-99: The function 'DOCTEST_ANON_FUNC_32' is never used.
(unusedFunction)
[style] 99-99: The function 'DOCTEST_ANON_FUNC_47' is never used.
(unusedFunction)
[style] 99-99: The function 'DOCTEST_ANON_FUNC_5' is never used.
(unusedFunction)
[style] 108-108: The function 'DOCTEST_ANON_FUNC_37' is never used.
(unusedFunction)
[style] 108-108: The function 'DOCTEST_ANON_FUNC_52' is never used.
(unusedFunction)
[style] 108-108: The function 'DOCTEST_ANON_FUNC_6' is never used.
(unusedFunction)
🤖 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/unit/light/unit_RandomMapModifier.cpp` around lines 74 - 119, Add a new
test case after the existing tests to cover the loop() method's timer-driven
behavior. The test should create a RandomMapModifier instance and call loop()
with time values that cross a beat boundary, testing both when bpm==0 (no
reshuffling) and when bpm is set to a positive value. Verify that when a beat
boundary is crossed, the reshuffle occurs and the mapping changes, while
tracking that the internal phaseNum_ and lastBeat_ state variables are properly
updated. Also verify that calling loop() with bpm==0 does not trigger a
reshuffle even when time advances significantly, confirming the timer logic
correctly controls when dynamic remapping happens.
Source: Coding guidelines
Makes the three pre-existing mutate scenarios run order-independently in a chained live sweep (the new perf scenarios had widened that footgun), and processes a CodeRabbit review batch including a real loopback-pin bug from the pin-control migration. KPI: 16384lights | PC:363KB | src:97(19318) | test:67(...) | lizard:74w (ESP32 KPI tick omitted: the S3 was unreachable for a live render capture at commit time. No render-path code changed here — scenario JSON, a loopback guard, comment fixes, and a test — and all three boards build clean + the perf scenarios ran on all three earlier this session.) Core: - AudioModule: corrected the stale pin comments (UNSET is -1 not 0; GPIO 0 IS a valid mic pin now — the guard tests < 0). Light domain: - RmtLedDriver / ParallelLedDriver: guard the loopback self-test on loopbackRxPin >= 0. The pin-control migration made -1 the unset sentinel, and the old unconditional uint8 cast turned -1 into GPIO 255 (a bogus pin); now an unset RX pin short-circuits with a status note. Also fixed the txPin comment (-1 = unset/use pins[0]; 0 is a valid explicit override). - Layer::loop(): tick only the FIRST enabled modifier (then break) — that's the one rebuildLUT() applies, so a later modifier's loop() can no longer drive rebuilds the LUT never reflects. - RandomMapModifier: expanded the loop() comment to record why the per-beat onBuildState() rebuild is the accepted, beat-gated cost (not a per-tick hot-path alloc) and why reshuffle() alone can't replace it. - main.cpp: documented why the per-chip LED-driver gating uses the preprocessor (SOC-capability #if excludes unused-driver code from flash; if constexpr can't) and that these are not the platform/OS #ifdefs the boundary rule forbids. Tests: - unit_RandomMapModifier: added a loop() timer test through a real Layer + the test clock — bpm 60 reshuffles across a beat boundary, bpm 0 stays frozen, observed via the modifier's mapping (not the rendered buffer, which an animating effect would mask). Docs / CI: - The three older live scenarios (scenario_GridLayout_resize, scenario_MoonModule_control_change, scenario_NetworkModule_mdns_toggle) now self-canvas: they clear_children + rebuild their pipeline in steps (keeping the pre-wired apparatus) instead of assuming the boot tree, so a chained `--module all` live run is order-independent after the new perf scenarios leave the tree bare. Verified in-process + chained on the P4. Removed the relative-to-baseline `min_pct` FPS bounds from those scenarios — they compared against the runner's idle pre-scenario baseline, not the prior render step, and false-failed on fast boards (P4). scenario-tests.md regenerated. - RmtLedDriver.md: `ledsPerPin` sentence given a subject (grammar). - backlog: recorded the JS test-harness attempt + revert (do as its own branch). Reviews: - 🐇 loopbackRxPin -1 cast to GPIO 255 → guard rxPin >= 0 (Rmt + Parallel) (fixed) - 🐇 Layer ticks all modifiers but rebuildLUT applies only the first → tick first enabled only (fixed) - 🐇 AudioModule UNSET=0 / GPIO-0-invalid comments stale → corrected to -1 / GPIO 0 valid (fixed) - 🐇 RmtLedDriver txPin "0 = pins[0]" comment stale → corrected (fixed) - 🐇 ledsPerPin sentence lacked a subject → reworded (fixed) - 🐇 add a loop() timer test → added (bpm 60 reshuffles, bpm 0 frozen) (fixed) - 🐇 remove onBuildState() from RandomMapModifier::loop() → skipped: reshuffle() alone never applies the new permutation (would break the feature); the rebuild is beat-gated + post-effect-pass + buffer-reused, the documented bounded cost, not a per-tick hot-path alloc. Comment strengthened. - 🐇 main.cpp #if vs if constexpr → skipped: if constexpr can't exclude unused-driver code from flash (the goal); these are SOC-capability macros, not the platform/OS #ifdef the boundary rule forbids (check_platform_boundary.py passes). Comment added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
A broad
next-iterationbatch on top of the runtime-Ethernet work inmain: device discovery (mDNS + HTTP sweep), four new light modules plus the first dynamic modifiers, a standardPincontrol type, per-chip LED-driver gating, two new performance scenarios with a three-board analysis, and the installer IP auto-detect this PR started as. Seven commits; all host gates green, all three ESP32 boards (classic / S3 / P4) build clean and were exercised live.What landed
DevicesModule + discovery (core, domain-neutral)
DevicesModule: discovers other devices on the LAN (projectMM / WLED / generic), shows them in a generic List control, marks self. Two strategies merge into one list: a non-blocking mDNS browse every tick (devices advertise_http._tcp; cycles_http/_wled) and a boot-once HTTP subnet sweep (the blocking probe stays off the periodic path).via(how found: scan/mdns/udp),ageSec(seconds since last seen, device-side), and acachedflag (restored-from-persistence, not yet re-seen live → "last seen: cached"). 24h timestamp age-out, strategy-agnostic.mm::jsonreader (JsonUtil.h) +ListSource::restoreList; a generic coreSort.h(insertion sort) keeps it name-ordered._http._tcp, so projectMM devices find each other over mDNS.Light modules + dynamic-modifier hook
sin8/cos8, no float.MoonModule::loop()override ticked byLayer::loop()(only the first enabled modifier, matchingrebuildLUT).Pin controls → a standard
PintypeaddUint16to the standardaddPin/int8_tPin control. Unset sentinel is now -1 (was 0), fixing a latent bug where GPIO 0 was unusable; loopback now guardsrxPin >= 0(the old cast turned -1 into GPIO 255).Per-chip LED-driver gating
#include+ registration is gated on its SOC capability macro (RMT / LCD_CAM / Parlio), so a board's binary carries only the drivers its silicon can run (verified: the classic ELF has 0 LCD/Parlio symbols; its type picker offers only RmtLedDriver). Not the platform/OS#ifdefthe boundary rule forbids —check_platform_boundary.pypasses.Performance scenarios + 3-board analysis
scenario_perf_light(fast, ≤64², driver-free) andscenario_perf_full(comprehensive: per-subsystem incremental cost, all-this-board drivers capped to 64 LEDs, light/heavy effect bracket to 16K, modifier sweep), replacing three overlapping scenarios (net −1765 scenario lines). The older mutate scenarios were made self-canvasing so chained live runs are order-independent.Installer IP auto-detection (the original PR scope)
NetworkModule::currentIp(uint8_t[4]);main.cppappends aMM_IP=<ip>token to the tick line (first-60s gated). The web installer'sreadBootIp()reads it post-flash and routes an already-online (Ethernet / saved-WiFi) device straight to the "Device is online!" success screen, auto-adding it. Board cards gained aboards.jsondetails popup; install gated on a port being picked.uint8_t[4]end to end (ethGetIPv4/wifiStaGetIPv4);default_firmwareremoved (firmwares[0]by convention).Verification
esp32,esp32s3-n16r8,esp32p4-ethall compile clean (-Werror).🤖 Generated with Claude Code