Runtime Ethernet PHY (RMII + W5500) + firmware-variant collapse + catalog provisioning#20
Conversation
The hardware catalog can now set a device up for its hardware at install time — not just record a board name, but create the modules it needs (a mic, extra drivers) and configure their pins. Boards/devices ship with only what is known and hardware-fixed injected; user-soldered pins stay unset for the user to set later. Also collapses three identical 16 MB partition tables into one shared file. tick:3312us(FPS:301) [ESP32-S3, 128x128] Core (src/, esp32/): - partitions: removed esp32_16mb.csv / esp32s3_n16r8.csv / esp32p4_16mb.csv (verified identical data rows), added the shared chip-neutral partitions/ota_16mb.csv, repointed the three sdkconfig fragments at it — one flash-offset map serves classic/S3/P4 since 16 MB is 16 MB on any of them UI (src/ui/): - app.js: consumePendingBoardParam() now adds a catalog entry's modules via POST /api/modules before the controls fan-out, so a fresh flash with no AudioModule no longer 404s the mic-pin writes; new sendAddModule() helper mirrors sendControl's best-effort style. Both arrays are optional (absent = inject nothing) Scripts / MoonDeck (scripts/): - moondeck.py: _push_board_to_device() adds the entry's modules before the controls loop; _post refactored to take a path + body so it serves both /api/modules and /api/control. Idempotent (existing module id returns 200) Tests (test/): - scenario_Driver_mutation: new mutate-mode scenario adding/removing output drivers while the pipeline renders, proving the driver-delete robustness rule (host-portable NetworkSend/Preview exercise the same Scheduler add/remove path the hardware drivers use; surfaced that PreviewDriver is userEditable=false) - scenario_Audio_mutation: extended the single sdPin write into the full three-pin install sequence (add module, then wsPin/sdPin/sckPin one at a time), proving the self-correcting partial-pin-fill keeps rendering - scenario_AllEffects_grid_sizes / scenario_Audio_mutation: observed-block writeback (drift tracking) from running the scenario gate Docs / CI (docs/): - install/boards.json: install-orchestrator.js + the schema doc gained the modules contract; added three projectMM testbench entries (S3: AudioModule + mic 4/5/6 + RmtLed 12/13 + LcdLed 18; ESP32-16MB: RmtLed 4/5; P4: RmtLed 32/33 + ParlioLed 20-27/64/32) — real boards verified on hardware - install/README.md: catalog schema (modules+controls, add-then-configure, opportunistic/partial injection, one-entry-type, reserved extends hierarchy, shared scenario op glossary) - backlog/installer-3layer-plan.md: new initiative doc tying the workstreams together; backlog.md three-level-model section collapsed to a pointer (no duplication) - moonmodules/core/AudioModule.md: Troy's prior-art notes on source types beyond the I2S MEMS mic (MCLK line-in, PDM, analog line-in, esp_codec_dev codecs, DSP boards) Verified end-to-end on hardware: injecting the testbench entries via the real MoonDeck push created the AudioModule from a clean slate on the S3 (idempotent on re-push) and set every pin on S3 + P4 (RmtLed, LcdLed, ParlioLed). All commit gates pass: spec check, desktop build (zero warnings), unit 1/1, scenarios 14/14, platform boundary, ESP32 build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/backlog/installer-3layer-plan.md`:
- Around line 299-305: Update the bench example docs so they match the actual
injects in docs/install/boards.json: in docs/backlog/installer-3layer-plan.md
(lines 299-305) change the S3 testbench description from "RmtLed-only" to show
AudioModule with mic pins (WS=4/SD=5/SCK=6), RmtLed loopback pins (pins=12,
rx=13) and the injected LcdLed; change the P4 description from "RmtLed-only" to
include the injected ParlioLed plus the RmtLed loopback (pins 4/5 and 32/33 as
applicable) and note that ESP32-16MB remains only RmtLed loopback; and in
docs/install/README.md (lines 66-104) make the corresponding edits so the S3 row
lists AudioModule + LcdLed + RmtLed (with mic pin values) and the P4 row lists
ParlioLed + RmtLed, ensuring all pin names and values match boards.json exactly.
In `@docs/moonmodules/core/AudioModule.md`:
- Line 5: The documentation uses future-tense phrases; update the sentences that
mention "as they are added" and any "will be experimenting" wording so the
description stays in present tense (e.g., change "as they are added" to "as they
exist" or remove the temporal implication) or move those roadmap statements into
a clearly marked "Future work" or "Roadmap" section; specifically, edit the
sentence beginning "It is named for what it does, audio acquisition plus
analysis..." and the block containing "will be experimenting" / lines describing
planned sources to be present-tense descriptions of current capabilities or
relocate them to a new future-work subsection.
In `@src/ui/app.js`:
- Around line 195-213: sendAddModule currently only logs failures; change it to
return a boolean (true on 2xx, false on non-2xx or exception) so callers can
react, and update consumePendingBoardParam to stop the injection batch and abort
further sendControl/sendAddModule calls when sendAddModule returns false.
Specifically, modify sendAddModule to return true after a successful res.ok and
return false in both the !res.ok branch and the catch block, and change
consumePendingBoardParam (the batch loop that calls sendAddModule and
sendControl) to check the return value and break/return immediately on a false
result to prevent continuing the batch.
🪄 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: 94da9811-eeb7-450e-b3f8-179a61bb8f31
⛔ Files ignored due to path filters (4)
esp32/partitions/esp32_16mb.csvis excluded by!**/*.csvesp32/partitions/esp32p4_16mb.csvis excluded by!**/*.csvesp32/partitions/esp32s3_n16r8.csvis excluded by!**/*.csvesp32/partitions/ota_16mb.csvis excluded by!**/*.csv
📒 Files selected for processing (15)
docs/backlog/README.mddocs/backlog/backlog.mddocs/backlog/installer-3layer-plan.mddocs/install/README.mddocs/install/boards.jsondocs/install/install-orchestrator.jsdocs/moonmodules/core/AudioModule.mdesp32/sdkconfig.defaults.16mbesp32/sdkconfig.defaults.esp32p4-ethesp32/sdkconfig.defaults.esp32s3-n16r8scripts/moondeck.pysrc/ui/app.jstest/scenarios/light/scenario_AllEffects_grid_sizes.jsontest/scenarios/light/scenario_Audio_mutation.jsontest/scenarios/light/scenario_Driver_mutation.json
| Acquires an audio source and publishes an **AudioFrame** — an overall sound **level**, a 16-band frequency **spectrum**, and the **dominant peak**. The frame is available to consumers every render tick, but its analysed values are *recomputed* only when a full sample block has accumulated (a 512-sample block at 22 kHz takes ~23 ms, longer than one tick), so a tick that doesn't complete a block re-publishes the previous `AudioFrame` unchanged rather than re-analysing. It is the producer half of the audio-reactive pipeline; [AudioVolumeEffect](../light/effects/AudioVolumeEffect.md) and [AudioSpectrumEffect](../light/effects/AudioSpectrumEffect.md) are the consumers. | ||
|
|
||
| It is named for what it does, audio acquisition plus analysis, not for one source: today the source is a digital I²S MEMS microphone (the only one wired), and the same analysis pipeline is built to serve other sources (line-in, USB audio) behind the platform read seam as they are added. Most of the module is the analysis (DC-blocker, RMS level, windowed FFT, band mapping), which is source-independent. | ||
| It is named for what it does, audio acquisition plus analysis, not for one source: today the source is a digital I²S MEMS microphone (the only one wired), and the same analysis pipeline is built to serve other sources (line-in, USB audio) behind the platform read seam as they are added. The candidate source types — I²S with an MCLK for line-in, PDM mics, analog line-in, and I²C-configured codecs — are surveyed in the [Troy (troyhacks)](#prior-art) prior-art notes below. Most of the module is the analysis (DC-blocker, RMS level, windowed FFT, band mapping), which is source-independent. |
There was a problem hiding this comment.
Keep this section strictly in present tense.
Line 5 and Lines 62-70 include forward-looking phrasing (for example, “as they are added” / “will be experimenting”). Reword to present tense or move roadmap statements into a clearly marked future-work section.
As per coding guidelines, "**/*.{cpp,h,hpp,js,ts,tsx,md}: Present tense only in code, comments, and documentation: describe the system as it is now, not future plans or changelogs".
Also applies to: 62-70
🤖 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/AudioModule.md` at line 5, The documentation uses
future-tense phrases; update the sentences that mention "as they are added" and
any "will be experimenting" wording so the description stays in present tense
(e.g., change "as they are added" to "as they exist" or remove the temporal
implication) or move those roadmap statements into a clearly marked "Future
work" or "Roadmap" section; specifically, edit the sentence beginning "It is
named for what it does, audio acquisition plus analysis..." and the block
containing "will be experimenting" / lines describing planned sources to be
present-tense descriptions of current capabilities or relocate them to a new
future-work subsection.
Source: Coding guidelines
| async function sendAddModule(m) { | ||
| // Best-effort add for the board-inject path, same no-retry contract as | ||
| // sendControl. Mirrors POST /api/modules {type, id, parent_id}; the | ||
| // endpoint is idempotent (an existing id returns 200), so re-running an | ||
| // inject re-adds nothing. Distinct from the interactive addModule() helper, | ||
| // which refetches state per call (wrong inside a batch loop). | ||
| try { | ||
| const res = await fetch("/api/modules", { | ||
| method: "POST", | ||
| headers: {"Content-Type": "application/json"}, | ||
| body: JSON.stringify({type: m.type, id: m.id, parent_id: m.parent_id}) | ||
| }); | ||
| if (!res.ok) { | ||
| console.warn(`[board-inject] add module ${m.type} failed (status=${res.status})`); | ||
| } | ||
| } catch (e) { | ||
| console.warn(`[board-inject] add module ${m.type} failed (error=${e && e.message ? e.message : e})`); | ||
| } | ||
| } |
There was a problem hiding this comment.
Stop the board-injection batch on a failed module create.
sendAddModule() only logs non-2xx responses, so consumePendingBoardParam() still sends controls after /api/modules rejects. That leaves the device half-injected and turns a single catalog/backend problem into a cascade of 404s. Return success/failure from the helper and abort the batch on the first failed module add.
🛠 Proposed fix
async function sendAddModule(m) {
@@
- if (!res.ok) {
- console.warn(`[board-inject] add module ${m.type} failed (status=${res.status})`);
- }
+ if (!res.ok) return false;
+ return true;
@@
for (const m of entry.modules ?? []) {
if (!m || typeof m !== "object" || !m.type) continue;
- await sendAddModule(m);
+ if (!(await sendAddModule(m))) return;
}Also applies to: 268-275
🤖 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/ui/app.js` around lines 195 - 213, sendAddModule currently only logs
failures; change it to return a boolean (true on 2xx, false on non-2xx or
exception) so callers can react, and update consumePendingBoardParam to stop the
injection batch and abort further sendControl/sendAddModule calls when
sendAddModule returns false. Specifically, modify sendAddModule to return true
after a successful res.ok and return false in both the !res.ok branch and the
catch block, and change consumePendingBoardParam (the batch loop that calls
sendAddModule and sendControl) to check the return value and break/return
immediately on a false result to prevent continuing the batch.
…ckTxPin
LED and network drivers are no longer baked into every device at boot — each board's catalog entry lists the drivers it actually has, so a device carries only its own outputs instead of every driver the chip can do. The catalog also nests each module's controls inside its module entry (one self-describing unit), and the LED-driver loopback self-test gains a TX-pin override so the bench jumper no longer has to share the operational LED pin. Includes the board-picker images and the open CodeRabbit review fixes.
PC tick:117/87/115/118/56/9/1/37/19/116/347/10us(FPS:8547/11494/8695/8474/17857/111111/1000000/27027/52631/8620/2881/100000) | ESP32(S3) tick:2976us(FPS:335). ESP32 tick captured live from the S3 over serial (collect_kpi's auto-port was absent); the boards ran the prior firmware, but this increment doesn't touch the render path (boot-wiring removal + a test-only control), so the tick is representative.
Core (src/):
- main.cpp: stopped boot-wiring NetworkSendDriver / RmtLedDriver / LcdLedDriver / ParlioLedDriver — they are added per board through the catalog now; only the Drivers container and PreviewDriver stay boot-wired (Preview needs the HTTP broadcaster the catalog can't supply). Removed the now-dangling netSend IP log. A bare flash with no inject has only Preview until a board is selected (the deliberate explicit-add model)
- RmtLedDriver / ParallelLedDriver (Lcd+Parlio base): added a loopbackTxPin control — an optional TX override the loopback transmits on instead of pins[0]/lane 0, so the bench self-test runs on a dedicated jumper without re-typing the operational pins; falls back to pins[0] when unset, test-only. The Parlio/Lcd loopback only drives lane 0, so one override pin suffices for all three drivers
UI (src/ui/):
- app.js / install-picker.js: parse the new nested catalog shape (each modules[] unit carries its own controls; Board is a module entry; a unit without parent_id is an already-existing module whose controls still apply). install-picker reads the WiFi TX-power cap from the Network module's nested controls
Scripts / MoonDeck (scripts/):
- moondeck.py: _push_board_to_device adds each module then sets its nested controls; custom/unknown boards push a synthesised Board unit
Tests (test/):
- unit_RmtLedDriver / unit_LcdLedDriver / unit_ParlioLedDriver: loopbackTxPin is a conditional control (bound always, shown only while loopbackTest is on), one case each
Docs / CI (docs/):
- install/boards.json: all 22 entries restructured to nested module-with-controls units; every board declares its default LED driver (classic+S3 RmtLedDriver, P4 ParlioLedDriver); three projectMM testbench entries with operational LED pins separated from the loopback tx/rx jumper (S3 LEDs=18 loop 13/12, ESP32-16MB LEDs=18 loop 4/5, P4 LEDs=20-27 loop 33/32)
- install/README.md: catalog schema rewritten for the nested shape; documents why type+parent_id stay explicit (offline-C++ scenario runner vs online JS/Python clients can't share an id->type table without triplicating it)
- moonmodules/light/drivers/*.md: the four driver specs updated from boot-wired to catalog-added; loopbackTxPin documented on Rmt/Lcd/Parlio
- backlog.md: 1..8-pin LCD output recorded as the future extension that would let S3 default to LCD (LcdLed needs all 8 i80 lanes today)
- assets/boards/: board-picker images for the installer
- AudioModule.md: present-tense fix on a Troy prior-art line
Reviews:
- 🐇 stale testbench bench-example text in installer-3layer-plan.md / README.md corrected to match boards.json — fixed
- 🐇 AudioModule.md future-tense ("will be experimenting") rewritten present-tense; the pre-existing "as they are added" line left as out-of-scope — fixed/accepted
- 🐇 sendAddModule now returns a boolean and consumePendingBoardParam skips a failed module's controls (narrower than the suggested whole-batch abort, preserving app.js's best-effort contract; orchestrator/moondeck already abort) — fixed
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (2)
docs/backlog/installer-3layer-plan.md (1)
303-315:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTestbench wiring details are stale versus the live catalog.
Line 304 documents S3 loopback as
pins=12/rx=12, and Lines 311-314 sayloopbackTxPinwas dropped. Current catalog values indocs/install/boards.jsonarepins="18",loopbackTxPin=13,loopbackRxPin=12 for S3, andloopbackTxPinis also present on the ESP32-16MB/P4 testbench entries. Please update this block to avoid drift in bench reproduction guidance.🤖 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/backlog/installer-3layer-plan.md` around lines 303 - 315, The documentation in this backlog file contains stale testbench wiring details that diverge from the live catalog in docs/install/boards.json. Update the S3 entry to reflect the current catalog values where pins is 18 (not 12), and acknowledge that loopbackTxPin is actually present in the current catalog entries for S3 (value 13), ESP32-16MB, and P4, contrary to the statement in lines 311-314 that says it was dropped. Revise the entire block to accurately describe the current board configurations to prevent confusion in bench reproduction guidance.docs/install/install-orchestrator.js (1)
260-268:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate
modules[]shape and required module identifiers before POST fan-out.At Line 260, iterating
entry.modules ?? []assumes an array; malformed catalog data can turn this into a runtime failure instead of a cleanfalsereturn.
At Line 262 and Line 283,m.idis not validated before/api/modulesand/api/control, which breaks the idempotent-create contract and can send control writes to an undefined module target.Suggested hardening
- for (const m of entry.modules ?? []) { + if (!Array.isArray(entry.modules)) return false; + for (const m of entry.modules) { if (!m || typeof m !== "object") continue; - if (m.parent_id && m.type) { + if (typeof m.id !== "string" || !m.id || typeof m.type !== "string" || !m.type) { + return false; + } + if (m.parent_id) { try { const res = await fetch(new URL("api/modules", deviceUrl), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: m.type, id: m.id, parent_id: m.parent_id }), @@ - const controls = m.controls; + const controls = m.controls; if (!controls || typeof controls !== "object") continue;Also applies to: 283-287
🤖 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/install/install-orchestrator.js` around lines 260 - 268, The code iterates over entry.modules without validating that it is actually an array, and it uses m.id in POST requests to both the /api/modules endpoint (around line 262) and /api/control endpoint (around line 283) without first verifying that m.id exists and is valid. Add validation to ensure entry.modules is an array before iteration, and add checks to confirm that m.id is a non-empty string or valid identifier before using it in the JSON body of either POST request. Apply these validations at all affected POST call sites to prevent runtime failures and ensure the idempotent-create contract is maintained.
🤖 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/backlog/backlog.md`:
- Around line 181-183: The heading and content of this backlog section use
future/conditional tense (such as "(future)" and "would let") which violates the
documentation style rule requiring present tense. Remove "(future)" from the
heading on line 181 and reword the entire section to use present tense
throughout, changing conditional phrases like "would let" to "lets" to describe
the proposed system as it would function in the present tense rather than as a
future possibility.
In `@docs/install/boards.json`:
- Line 436: Update the JSON control entry for ledsPerPin so it stays numeric
instead of a string; replace the quoted value with a number in the board control
definition where ledsPerPin is set, keeping the schema/type consistent with the
other controls and with the injector handling.
In `@docs/moonmodules/light/drivers/LcdLedDriver.md`:
- Line 31: Rewrite the affected driver docs to use present tense consistently:
update the LcdLedDriver, NetworkSendDriver, and ParlioLedDriver descriptions so
“Added as a child” becomes “is added as a child,” and revise the RmtLedDriver
wording so “the bench used 5” is expressed in present tense. Apply the wording
changes at the listed anchor and sibling sites only, keeping the rest of each
description unchanged.
In `@docs/moonmodules/light/drivers/RmtLedDriver.md`:
- Around line 28-30: The documentation for the `loopbackTest` field is
incomplete. The description lists `pins`, `loopbackRxPin`, and `loopbackFrame`
as the controls that trigger a re-run of the self-test, but `loopbackTxPin` also
triggers a re-run while test mode is on according to the source code. Add
`loopbackTxPin` to the list of controls mentioned in the `loopbackTest`
description to align the documentation with the actual implementation.
In `@test/unit/light/unit_LcdLedDriver.cpp`:
- Around line 201-214: The test "LcdLedDriver loopbackTxPin tracks the
loopbackTest toggle" claims to verify toggle behavior but only asserts the
default-hidden state. After the initial check that loopbackTxPin is hidden by
default (hidden == true), add code to enable the loopbackTest control by finding
and toggling it (or setting its value), then add a second assertion verifying
that loopbackTxPin.hidden becomes false when loopbackTest is active. This will
complete the test to match its stated contract of tracking toggle behavior.
In `@test/unit/light/unit_ParlioLedDriver.cpp`:
- Around line 248-259: The test for ParlioLedDriver loopbackTxPin toggle
tracking only verifies the default hidden state but does not actually test the
toggle behavior. Extend the test to also toggle the loopbackTest control to true
and then re-check that loopbackTxPin visibility changes accordingly. After the
initial CHECK for the default hidden state (hidden == true), locate the
loopbackTest control in the controls array, toggle its value to true, and then
verify that the loopbackTxPin control's hidden state changes to false to confirm
the toggle tracking works as intended.
---
Duplicate comments:
In `@docs/backlog/installer-3layer-plan.md`:
- Around line 303-315: The documentation in this backlog file contains stale
testbench wiring details that diverge from the live catalog in
docs/install/boards.json. Update the S3 entry to reflect the current catalog
values where pins is 18 (not 12), and acknowledge that loopbackTxPin is actually
present in the current catalog entries for S3 (value 13), ESP32-16MB, and P4,
contrary to the statement in lines 311-314 that says it was dropped. Revise the
entire block to accurately describe the current board configurations to prevent
confusion in bench reproduction guidance.
In `@docs/install/install-orchestrator.js`:
- Around line 260-268: The code iterates over entry.modules without validating
that it is actually an array, and it uses m.id in POST requests to both the
/api/modules endpoint (around line 262) and /api/control endpoint (around line
283) without first verifying that m.id exists and is valid. Add validation to
ensure entry.modules is an array before iteration, and add checks to confirm
that m.id is a non-empty string or valid identifier before using it in the JSON
body of either POST request. Apply these validations at all affected POST call
sites to prevent runtime failures and ensure the idempotent-create contract is
maintained.
🪄 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: af4252ab-685c-49bd-8da1-c3b00e99a4c6
⛔ Files ignored due to path filters (18)
docs/assets/boards/ch9102.jpgis excluded by!**/*.jpgdocs/assets/boards/cp210x-ch34x.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-c3-supermini.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-c3.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-d0-azdelivery.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-d0-pico2.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-d0-wrover.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-d0.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-p4-eth.pngis excluded by!**/*.pngdocs/assets/boards/esp32-p4-olimex.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-p4.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-s3-atoms3r.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-s3-lightcrafter16.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-s3-n8r8v.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-s3-seeed_xiao.pngis excluded by!**/*.pngdocs/assets/boards/esp32-s3-stephanelec-16p.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-s3-zero-n4r2.jpgis excluded by!**/*.jpgdocs/assets/boards/others.jpgis excluded by!**/*.jpg
📒 Files selected for processing (19)
docs/backlog/backlog.mddocs/backlog/installer-3layer-plan.mddocs/install/README.mddocs/install/boards.jsondocs/install/install-orchestrator.jsdocs/moonmodules/core/AudioModule.mddocs/moonmodules/light/drivers/LcdLedDriver.mddocs/moonmodules/light/drivers/NetworkSendDriver.mddocs/moonmodules/light/drivers/ParlioLedDriver.mddocs/moonmodules/light/drivers/RmtLedDriver.mdscripts/moondeck.pysrc/light/drivers/ParallelLedDriver.hsrc/light/drivers/RmtLedDriver.hsrc/main.cppsrc/ui/app.jssrc/ui/install-picker.jstest/unit/light/unit_LcdLedDriver.cpptest/unit/light/unit_ParlioLedDriver.cpptest/unit/light/unit_RmtLedDriver_lifecycle.cpp
| ### 1..8-pin LCD output (future) — would let S3 default to LCD | ||
|
|
||
| `LcdLedDriver` requires **all 8** i80 data lanes (`kExactLaneCount = true`, `LcdLedDriver.h`): the ESP-IDF `esp_lcd` i80 bus configures every data line of the bus width and rejects a partial set, so even a few WS2812 strands claim 8 GPIOs. That's why **S3 boards default to `RmtLedDriver`** in `boards.json` (RMT runs one channel per pin, 1..N) rather than LCD — a board with fewer than 8 strips can't sensibly use the LCD driver, and the 8-lane LCD bench wiring (`1,2,4,5,6,7,8,9`) collides with common peripheral pins (e.g. the mic on 4/5/6). A **1..8-pin LCD mode** (drive only the lanes named in `pins`, leave the rest unclaimed — matching Parlio's flexibility) would let the parallel S3 path run any lane count, at which point an S3 board entry could choose LCD vs RMT by intent. Parlio already does this (`kExactLaneCount = false`, 1..8 lanes), so the P4 default *is* the parallel driver. Until LCD gains the same flexibility, S3 stays on RMT by default. Low priority — RMT covers the few-strip S3 case today. |
There was a problem hiding this comment.
Use present tense phrasing in this heading/section.
Line 181 (“future”) and Line 183 (“would let”) are future/conditional; reword to present tense to match the docs style rule.
As per coding guidelines, “Present tense only in code, comments, and documentation: describe the system as it is now, not future plans or changelogs.”
🤖 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/backlog/backlog.md` around lines 181 - 183, The heading and content of
this backlog section use future/conditional tense (such as "(future)" and "would
let") which violates the documentation style rule requiring present tense.
Remove "(future)" from the heading on line 181 and reword the entire section to
use present tense throughout, changing conditional phrases like "would let" to
"lets" to describe the proposed system as it would function in the present tense
rather than as a future possibility.
Source: Coding guidelines
| "parent_id": "Drivers", | ||
| "controls": { | ||
| "pins": "20,21,22,23,24,25,26,27", | ||
| "ledsPerPin": "64", |
There was a problem hiding this comment.
Keep ledsPerPin numeric in JSON controls.
Line 436 uses a string ("64") while this control is numeric elsewhere. Prefer 64 (number) to keep injector/schema handling type-stable across clients.
🤖 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/install/boards.json` at line 436, Update the JSON control entry for
ledsPerPin so it stays numeric instead of a string; replace the quoted value
with a number in the board control definition where ledsPerPin is set, keeping
the schema/type consistent with the other controls and with the injector
handling.
| ## Cross-domain wiring | ||
|
|
||
| Added as a child of the `Drivers` container in `main.cpp` under `if constexpr (platform::lcdLanes > 0)` (SOC-derived: the S3 among current targets), wired by code like its siblings. The **slot encode** (`LcdSlots.h`) is domain code, host-testable; the **peripheral** (`platform_esp32_lcd.cpp`, ESP-IDF's [`esp_lcd` i80 bus](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/lcd/index.html) + GDMA) is the only IDF-touching part. | ||
| Added as a child of the `Drivers` container at runtime via the catalog (`POST /api/modules`, a board's [`boards.json`](../../../install/boards.json) `modules` entry) — not boot-wired, so it only exists on a board that selects it. The type is registered on every target, but the peripheral exists only where the SOC has the LCD_CAM i80 bus (the S3 among current targets): on a chip without it the driver is inert (`lanesAvailable()` is 0, so init / loopback report "not supported on this platform"), so a board entry only lists `LcdLedDriver` where it makes sense. Once added, `Drivers::passBufferToDrivers` wires it like any child. The **slot encode** (`LcdSlots.h`) is domain code, host-testable; the **peripheral** (`platform_esp32_lcd.cpp`, ESP-IDF's [`esp_lcd` i80 bus](https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/lcd/index.html) + GDMA) is the only IDF-touching part. |
There was a problem hiding this comment.
Normalize past-tense phrasing to present tense across updated driver docs. The same style-rule issue appears in multiple changed sentences; please rewrite each in present tense for consistency.
docs/moonmodules/light/drivers/LcdLedDriver.md#L31-L31: change “Added as a child …” to present tense (“The driver is added …”).docs/moonmodules/light/drivers/NetworkSendDriver.md#L37-L37: change “Added as a child …” to present tense.docs/moonmodules/light/drivers/ParlioLedDriver.md#L49-L49: change “Added as a child …” to present tense.docs/moonmodules/light/drivers/RmtLedDriver.md#L29-L29: rewrite “the bench used 5” in present tense wording.
As per coding guidelines, "Present tense only in code, comments, and documentation: describe the system as it is now, not future plans or changelogs".
📍 Affects 4 files
docs/moonmodules/light/drivers/LcdLedDriver.md#L31-L31(this comment)docs/moonmodules/light/drivers/NetworkSendDriver.md#L37-L37docs/moonmodules/light/drivers/ParlioLedDriver.md#L49-L49docs/moonmodules/light/drivers/RmtLedDriver.md#L29-L29
🤖 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/LcdLedDriver.md` at line 31, Rewrite the
affected driver docs to use present tense consistently: update the LcdLedDriver,
NetworkSendDriver, and ParlioLedDriver descriptions so “Added as a child”
becomes “is added as a child,” and revise the RmtLedDriver wording so “the bench
used 5” is expressed in present tense. Apply the wording changes at the listed
anchor and sibling sites only, keeping the rest of each description unchanged.
Source: Coding guidelines
Renames the "LOLIN WiFi fix" to a generic weak-power/brown-out TX-power cap (it applies to any weak-powered board, not just LOLIN), corrects a mislabeled bench board (the "LOLIN S3 N16R8" was never a LOLIN product — it's an ESP32-S3-DevKitC clone), and makes every board name and image filename follow the vendor's own name with a DevKit→Dev shorthand. Also de-duplicates the install-alt orchestrator, which was a byte-identical fork of the canonical one, and processes a round of CodeRabbit findings. KPI: tick:112/84/111/112/52/8/1/35/19/110/313/9us(FPS:8928/11904/9009/8928/19230/125000/1000000/28571/52631/9090/3194/111111) (PC; ESP32 live tick/FPS omitted — no bench device on a serial port this session, all three testbench variants built clean) Core: - NetworkModule / ImprovProvisioningModule / platform.h / platform_esp32_improv.cpp: reworded the TX-power-cap comments from "LOLIN WiFi fix" to a generic weak-power/brown-out cap; corrected the mislabeled board to "ESP32-S3-DevKitC clone"; kept the legitimate "LOLIN D32" and "LOLIN S3 mini" references (real boards). Light domain: - LcdLedDriver.h: bench-wiring comment now names the board "ESP32-S3 N16R8 Dev" instead of the old LOLIN mislabel. UI: - install-picker.js: TX-power-cap comment generalized (the nested-catalog reader is unchanged). Scripts / MoonDeck: - moondeck.py / moondeck_ui / improv_provision.py / MoonDeck.md: TX-power-cap wording generalized; CLI board-name example updated to "ESP32-S3 N16R8 Dev". - preview_installer.py: stage install-alt's orchestrator from the canonical /install/ copy (single-sourced, no fork). Tests: - unit_BoardModule.cpp: board-name assertion updated to "Generic ESP32 Dev" (free-text Board.board value, no enum gate). - unit_LcdLedDriver / unit_ParlioLedDriver: completed the loopbackTxPin toggle tests — they asserted only the default-hidden state despite their "tracks the toggle" name; now use the shared checkConditionalControl helper (toggles loopbackTest both ways, verifies visibility flips while the control stays bound), matching the RMT sibling and dropping the hand-rolled loop. Docs / CI: - boards.json: merged the duplicate mislabeled S3 entry into "ESP32-S3 N16R8 Dev"; renamed "Generic ESP32 DevKit" → "Generic ESP32 Dev"; wired lolin-d32.jpg (the real D32 photo, formerly misfiled as a generic) and generic-esp32-dev.jpg (a real DevKitC photo) as board images; image filenames now match each board's name slug. - board image assets renamed to the vendor-name slug standard (esp32-s3-n16r8-dev, esp32-s3-n8r8-dev, generic-esp32-dev, lolin-d32, lightcrafter-16, waveshare-esp32-p4-nano); removed the bogus lolin-s3-n16r8.jpg. - architecture.md / decisions.md / performance.md / driver specs / backlog: swept the LOLIN→generic and DevKit→Dev renames consistently. - driver specs (Lcd/Parlio/NetworkSend): "Added as a child" → "The driver is added as a child" (present tense); RmtLedDriver.md loopbackTest trigger list now includes loopbackTxPin to match the source. - release.yml: deploy install-alt with the canonical install-orchestrator.js (cp from /install/) instead of a forked copy. - install-orchestrator.js: deleted the byte-identical install-alt fork (−1109 lines); boot-log comment corrected to "ESP32-S3-DevKitC clone"; guard entry.modules with Array.isArray and skip catalog units missing a non-empty id before the /api/modules and /api/control POSTs. Reviews: - 👾 Reviewer (pre-commit, whole-branch diff): verdict ready-to-commit, no blockers. SHOULD-FIX — install-alt/install-orchestrator.js was a byte-identical 1109-line fork: fixed (de-duped, single-sourced from canonical via release.yml + preview_installer.py). NIT — :657 boot-log comment said "DevKit": fixed to "DevKitC clone". NIT — orphaned esp32-s3-n8r8-dev.jpg: accepted (intentional pre-staging for the future N8R8 board, not deployed). - 🐇 CodeRabbit — RmtLedDriver.md loopbackTest trigger list missing loopbackTxPin: fixed (source re-runs the test on it). Driver-spec "Added as a child" past tense: fixed to present tense (Lcd/Parlio/NetworkSend). unit_LcdLedDriver / unit_ParlioLedDriver toggle tests only checked the default state: fixed (full toggle contract via shared helper). install-orchestrator.js missing entry.modules array + m.id validation: fixed (guarded). backlog.md future tense: skipped (docs/backlog/ is exempt from the present-tense rule per CLAUDE.md). boards.json ledsPerPin should be numeric: skipped (it's a Text control in firmware — string is the correct type; the cited line was a type field, not ledsPerPin). installer-3layer-plan.md stale testbench wiring: skipped (already removed when the plan doc was trimmed earlier in the branch). 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 (4)
scripts/run/preview_installer.py (1)
229-236:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse
uv runfor manifest generation subprocess calls.Line 230 invokes Python via
sys.executableinstead ofuv run, which diverges from the script-invocation policy and can bypass the managed environment.Suggested fix
- result = subprocess.run( - [sys.executable, str(GENERATE_MANIFEST), + result = subprocess.run( + ["uv", "run", str(GENERATE_MANIFEST), "--firmware", firmware, "--version", LOCAL_VERSION, "--release-url", ".", "--flasher-args", str(flasher_args), "--out", str(manifest_path)],As per coding guidelines, “Use uv run for every Python invocation in scripts and CMake commands; never type python or python3 directly.”
🤖 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 `@scripts/run/preview_installer.py` around lines 229 - 236, The subprocess.run call for manifest generation is using sys.executable to invoke Python directly instead of using uv run as required by coding guidelines. In the command list passed to subprocess.run, replace the sys.executable approach with uv run by changing the command array to start with "uv" and "run" before the GENERATE_MANIFEST script path, ensuring the script is executed through the managed environment rather than the raw Python executable.Source: Coding guidelines
src/ui/install-picker.js (1)
717-724:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate TX-power bounds before returning the value.
This currently accepts any number. Invalid values can flow into pre-association provisioning and silently break the intended brown-out mitigation path. Enforce integer
0..21here.Suggested patch
getSelectedBoardTxPower() { if (!_lastState || !_lastState.selectedBoard || !_lastState.boards) return null; const entry = _lastState.boards.find(b => b.name === _lastState.selectedBoard); // New catalog shape: each board's modules list carries its own controls; // the WiFi TX-power cap lives on the Network module's controls block. const net = entry && (entry.modules || []).find(m => m && m.id === "Network"); const v = net && net.controls && net.controls.txPowerSetting; - return (typeof v === "number") ? v : null; + if (!Number.isInteger(v) || v < 0 || v > 21) return null; + return v; },🤖 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/ui/install-picker.js` around lines 717 - 724, The getSelectedBoardTxPower() function currently validates only that the TX-power value is a number, but does not enforce the required bounds. Update the return statement to validate that the value is an integer within the valid range of 0 to 21 inclusive. If the value fails validation (is not an integer, is negative, or exceeds 21), return null instead of the invalid value to prevent it from breaking the brown-out mitigation path.docs/history/decisions.md (1)
709-709:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix the repeated list number.
Line 709 restarts the ordered list at
3.immediately after the previous3.item. Renumber it so the sequence stays unambiguous.🤖 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/history/decisions.md` at line 709, The ordered list in decisions.md has a numbering error where item 3 appears twice in succession. Change the second occurrence of the list number from "3." to "4." to maintain proper sequential numbering throughout the list.src/light/drivers/LcdLedDriver.h (1)
32-39:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep the bench wiring note in present tense.
The new sentence uses "was", which violates the repo rule that comments stay in present tense. Please rephrase it as current state (for example, "is pins ..."). As per coding guidelines,
**/*.{cpp,h,hpp,js,ts,tsx,md}: Present tense only in code, comments, and documentation.🤖 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/LcdLedDriver.h` around lines 32 - 39, The comment about the ESP32-S3 N16R8 Dev bench wiring in the LcdLedDriver.h file violates the repository rule requiring present tense in all code comments and documentation. Change the phrase "The ESP32-S3 N16R8 Dev bench wiring was pins" to use present tense by replacing "was" with "is" to maintain consistency with the coding guidelines that mandate present tense only in comments.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/install-alt/index.html`:
- Line 506: The input element with id "board-search" lacks an accessible name
for screen readers. Add either a label element with a for attribute matching the
input's id, or add an aria-label attribute directly to the input element to
provide a descriptive accessible name such as "Search boards" so screen readers
can announce the field's purpose to users.
- Around line 1600-1604: The card function creates a non-focusable div element
that only responds to mouse clicks via onclick, making it inaccessible to
keyboard-only users. To fix this, add tabindex="0" to the div element to make it
keyboard-focusable, and add a keyboard event handler (such as onkeydown) that
triggers the pickBoard function when the user presses Enter or Space keys. This
should be done in addition to the existing onclick handler to ensure both mouse
and keyboard interactions work for selecting board cards.
---
Outside diff comments:
In `@docs/history/decisions.md`:
- Line 709: The ordered list in decisions.md has a numbering error where item 3
appears twice in succession. Change the second occurrence of the list number
from "3." to "4." to maintain proper sequential numbering throughout the list.
In `@scripts/run/preview_installer.py`:
- Around line 229-236: The subprocess.run call for manifest generation is using
sys.executable to invoke Python directly instead of using uv run as required by
coding guidelines. In the command list passed to subprocess.run, replace the
sys.executable approach with uv run by changing the command array to start with
"uv" and "run" before the GENERATE_MANIFEST script path, ensuring the script is
executed through the managed environment rather than the raw Python executable.
In `@src/light/drivers/LcdLedDriver.h`:
- Around line 32-39: The comment about the ESP32-S3 N16R8 Dev bench wiring in
the LcdLedDriver.h file violates the repository rule requiring present tense in
all code comments and documentation. Change the phrase "The ESP32-S3 N16R8 Dev
bench wiring was pins" to use present tense by replacing "was" with "is" to
maintain consistency with the coding guidelines that mandate present tense only
in comments.
In `@src/ui/install-picker.js`:
- Around line 717-724: The getSelectedBoardTxPower() function currently
validates only that the TX-power value is a number, but does not enforce the
required bounds. Update the return statement to validate that the value is an
integer within the valid range of 0 to 21 inclusive. If the value fails
validation (is not an integer, is negative, or exceeds 21), return null instead
of the invalid value to prevent it from breaking the brown-out mitigation path.
🪄 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: a6e12ade-d223-49a2-a37b-51667e152e69
⛔ Files ignored due to path filters (17)
docs/assets/boards/esp32-s3-n16r8-dev.jpgis excluded by!**/*.jpgdocs/assets/boards/esp32-s3-n8r8-dev.jpgis excluded by!**/*.jpgdocs/assets/boards/generic-esp32-dev.jpgis excluded by!**/*.jpgdocs/assets/boards/lightcrafter-16.jpgis excluded by!**/*.jpgdocs/assets/boards/lolin-d32.jpgis excluded by!**/*.jpgdocs/assets/boards/olimex-esp32-gateway-rev-g.jpgis excluded by!**/*.jpgdocs/assets/boards/quinled-dig-2-go.jpgis excluded by!**/*.jpgdocs/assets/boards/quinled-dig-next-2.jpgis excluded by!**/*.jpgdocs/assets/boards/quinled-dig-octa-32-8l.pngis excluded by!**/*.pngdocs/assets/boards/quinled-dig-quad-v3.pngis excluded by!**/*.pngdocs/assets/boards/quinled-dig-uno-v3.pngis excluded by!**/*.pngdocs/assets/boards/serg-minishield.jpgis excluded by!**/*.jpgdocs/assets/boards/waveshare-esp32-p4-nano.jpgis excluded by!**/*.jpgdocs/assets/screenshots/installer-board-picker-collapsed.pngis excluded by!**/*.pngdocs/assets/screenshots/installer-board-picker-expanded.pngis excluded by!**/*.pngdocs/install-alt/favicon.pngis excluded by!**/*.pngscripts/build/improv_provision.pyis excluded by!**/build/**
📒 Files selected for processing (37)
.github/workflows/release.ymldocs/architecture.mddocs/backlog/README.mddocs/backlog/backlog.mddocs/backlog/installer-3layer-plan.mddocs/backlog/leddriver-increment-2-plan.mddocs/history/decisions.mddocs/install-alt/devices.jsdocs/install-alt/index.htmldocs/install/README.mddocs/install/boards.jsondocs/install/index.htmldocs/install/install-orchestrator.jsdocs/moonmodules/core/ImprovProvisioningModule.mddocs/moonmodules/core/NetworkModule.mddocs/moonmodules/light/drivers/LcdLedDriver.mddocs/moonmodules/light/drivers/NetworkSendDriver.mddocs/moonmodules/light/drivers/ParlioLedDriver.mddocs/moonmodules/light/drivers/RmtLedDriver.mddocs/performance.mdscripts/MoonDeck.mdscripts/moondeck.pyscripts/moondeck_ui/index.htmlscripts/run/preview_installer.pysrc/core/ImprovProvisioningModule.hsrc/core/NetworkModule.hsrc/light/drivers/LcdLedDriver.hsrc/platform/esp32/platform_esp32_improv.cppsrc/platform/platform.hsrc/ui/install-picker.jstest/scenarios/core/scenario_MoonModule_control_change.jsontest/scenarios/light/scenario_AllEffects_grid_sizes.jsontest/scenarios/light/scenario_Audio_mutation.jsontest/scenarios/light/scenario_Driver_mutation.jsontest/unit/core/unit_BoardModule.cpptest/unit/light/unit_LcdLedDriver.cpptest/unit/light/unit_ParlioLedDriver.cpp
Each installer board now declares what the firmware drives today (supported) and what hardware it has but we can't drive yet (planned) — shown as chips in the board picker, and the planned list is the backlog seed for future modules. The five QuinLED boards get their real LED data pins so a flashed QuinLED drives LEDs with no manual setup, and a new check guards the whole catalog against drift. Also folds in a round of CodeRabbit fixes.
KPI: tick:128/92/326/148/64/11/3/43/20/147/403/35us(FPS:7812/10869/3067/6756/15625/90909/333333/23255/50000/6802/2481/28571) (PC; ESP32 live tick/FPS omitted — no bench device on a serial port this session, all three testbench variants built clean)
Core:
- install-picker.js (UI, firmware-embedded): getSelectedBoardTxPower now validates the cap is a whole number in 0..21 (the SET_TX_POWER RPC's range) and returns null otherwise, so a bad catalog value can't poison the brown-out mitigation path.
- LcdLedDriver.h: bench-wiring comment reworded to present tense ("is", not "was").
Scripts / MoonDeck:
- check_boards.py (new): validates the installer catalog — required fields, default_firmware ∈ firmwares, every image resolves on disk, each Board.board control equals its entry name, module types are factory-registered (or boot-wired singletons), pins controls live only on *LedDriver modules, and supported capabilities stay within the known vocabulary. Negative-tested against six fault classes.
- moondeck_config.json: expose the board-catalog check in the MoonDeck check group.
- preview_installer.py: invoke the manifest generator through `uv run python` instead of the raw interpreter (project uv-run rule); verified it still produces valid manifests.
Docs / CI:
- boards.json: added supported/planned capability arrays to all 21 boards (researched from each vendor's product page); added LED data pins to all five QuinLED boards (Dig-2-Go=16, Dig-Next-2=2,4, Dig-Uno V3=16,3, Dig-Quad V3=16,3,1,4, Dig-Octa=0,1,2,3,4,5,12,13). User-soldered dev boards and private boards stay pinless by design.
- install-alt/index.html: render supported (solid) and planned ("… soon", dashed) capability chips on each picker card; accessibility — aria-label on the board search, and keyboard-focusable cards (tabindex, role=option, Enter/Space select).
- install/README.md: document the supported/planned catalog fields.
- backlog.md: re-scope the I2S-parallel future driver to classic ESP32 (16-lane, beyond RMT's 8 channels) — it is the classic-ESP32 high-lane path, distinct from the S3 (LCD_CAM) and P4 (Parlio); no catalog board needs it today.
- CLAUDE.md: add the board-catalog check as commit gate 8 (runs when boards.json or the validator changes).
- decisions.md: fix a duplicated list number (3,3 → 3,4,5) in the LCD-driver lessons.
Reviews:
- 🐇 CodeRabbit — install-alt board-search input lacked an accessible name: fixed (aria-label). Board cards were mouse-only: fixed (keyboard-focusable, Enter/Space). decisions.md duplicate "3." numbering: fixed. preview_installer.py used sys.executable not uv run: fixed. LcdLedDriver.h comment past tense: fixed to present. install-picker.js getSelectedBoardTxPower didn't bound-check 0..21: fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rd catalog Two infrastructure wins plus a catalog round. (1) The firmware list, previously hand-copied six times (one copy wrong), is now generated from the FIRMWARES dict into docs/install/firmwares.json and read by CI, MoonDeck, and the docs — fixing a bug where MoonDeck couldn't offer the P4 variants. (2) The byte-identical release assets are shared: ota-data once globally, partition-table once per flash-size group, instead of one copy per firmware — verified end-to-end in a real browser flash. The board catalog gains capability chips, real LED pins from MoonLight's pin database, two new MHC/ABC boards, and 89%-smaller images. Scripts / MoonDeck: - generate_firmwares.py (new): projects build_esp32's FIRMWARES (now with a per-entry `ships` flag) into docs/install/firmwares.json. check_firmwares.py (new) guards the committed file against drift. - generate_manifest.py: emit shared asset names — `shared-ota-data.bin` (global) and `partition-table-<size>.bin` (per flash-size group, key read from flasher_args' flash_settings.flash_size); app + bootloader stay per-firmware. - moondeck.py / moondeck_config.json: read the shipping firmware list from firmwares.json instead of a hardcoded list that was missing both P4 variants (the bug fix). Added the check_firmwares entry to the check group. - preview_installer.py: stage the shared asset names so local flash-ready preview matches release; added the json import. Docs / CI: - release.yml: a `firmwares` job emits the shipping list; build-esp32's matrix reads it via fromJSON; both manifest loops select `.ships` from firmwares.json (three hand-copied lists gone). Stage step writes shared asset names; publish + Pages-download globs include them. - boards.json: added supported/planned capability arrays (short labels) to all boards; LED pins from MoonLight ModuleIO.h (Serg ×2, MHC ×2, Yves, Cube, QuinLED family); new boards MHC V5.7 PRO image+url and ABC-WLED ESP32-P4 shield; LightCrafter16/SE16 kept pinless with a "16-ch I2S (not yet)" planned note (their 16 outputs exceed RMT/LCD lane counts). - board images: compressed 7.3 MB -> 0.8 MB (89% smaller) — capped at 600px, JPEG q80; the three photo-PNGs converted to JPEG; Dig-2-Go re-fetched uncropped; small images that re-encode inflated were restored to originals. New vendor images (MHC, ABC P4, Serg UniShield) hosted locally, not hotlinked. - install-alt/index.html: capability chips — supported green, planned orange (colour, not text); show all chips with short labels (dropped the +N overflow); short tooltips; keyboard-accessible cards + search aria-label. - building.md: firmware-variant table replaced with a pointer to the FIRMWARES dict + firmwares.json (no duplicated list). - check_boards.py: capability vocab updated for the shortened "Audio" label. - CLAUDE.md: commit gate 9 (check_firmwares). - installer-3layer-plan.md: marked #2 (firmwares.json) and #8 (asset dedup) done; added §10 — the detected-chip-vs-boards.json-chip granularity mismatch (onDetect returns raw silicon on the web path, so the flash guard false-warns; lean toward normalising silicon→family at compare time rather than adding per-SKU precision to boards.json). Reviews: - 🐇 CodeRabbit (prior round, folded in earlier this session): a11y on the board cards + search input, decisions.md list numbering, preview_installer uv-run, LcdLedDriver tense, install-picker txPower bounds — all 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 (1)
docs/history/decisions.md (1)
772-780:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep this lesson in past tense.
will introducereads like roadmap text, which conflicts with the repo’s present-tense doc rule. Rephrase this section as a past event/lesson so the history file stays consistent.As per coding guidelines, docs stay in present tense.
🤖 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/history/decisions.md` around lines 772 - 780, The phrase "will introduce" in the opening sentence reads as future-tense roadmap text, which conflicts with the repository's present-tense documentation style. Rephrase the section starting with "Round 1 recorded that round 3 'will introduce a WiFi abstraction seam...'" to describe this as a past event that already occurred, converting future-tense language like "will introduce" to past tense to maintain consistency with the document's historical narrative and the repo's style guidelines for documentation.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 @.github/workflows/release.yml:
- Line 80: The actions/checkout action at line 80 in the `firmwares` job uses a
mutable version tag (`@v4`) and retains default credential persistence. Replace
the mutable tag with a pinned commit SHA and add the parameter
persist-credentials: false to the checkout action configuration to reduce
supply-chain and token-exposure risk.
In `@docs/install/boards.json`:
- Around line 723-727: The P4 board entries in docs/install/boards.json
incorrectly advertise WiFi support in the "supported" field when only an
Ethernet-only firmware variant (esp32p4-eth) is available. Remove "WiFi" from
the "supported" array at lines 723-727 (anchor location) and at lines 758-762
(sibling location) to accurately reflect that only Ethernet capability is
currently installable for P4 boards.
In `@scripts/check/check_boards.py`:
- Around line 123-124: The code at the isinstance(mods, list) check silently
skips invalid entries using continue, which allows schema violations to pass
undetected. Replace the continue statement with proper error handling by raising
an appropriate exception or logging an error to ensure that non-list modules
fields are caught and reported as schema validation failures rather than being
silently ignored.
- Around line 89-93: The validation at the location where firmwares membership
is checked only validates the default_firmware when firmwares is a list, but
skips validation entirely if firmwares is not a list type. This allows malformed
entries where firmwares is not an array to pass through unchecked. Add a
separate validation check that ensures the firmwares field obtained from
e.get("firmwares") is actually a list type, and if it is not a list, append an
error message to the errors list indicating that firmwares must be an array.
This validation should occur before or alongside the existing default_firmware
membership check.
---
Outside diff comments:
In `@docs/history/decisions.md`:
- Around line 772-780: The phrase "will introduce" in the opening sentence reads
as future-tense roadmap text, which conflicts with the repository's
present-tense documentation style. Rephrase the section starting with "Round 1
recorded that round 3 'will introduce a WiFi abstraction seam...'" to describe
this as a past event that already occurred, converting future-tense language
like "will introduce" to past tense to maintain consistency with the document's
historical narrative and the repo's style guidelines for documentation.
🪄 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: d1a9fe0f-706f-4763-96ae-84235fda40ca
⛔ Files ignored due to path filters (12)
docs/assets/boards/abc-wled-esp32-p4-shield.jpgis excluded by!**/*.jpgdocs/assets/boards/mhc-v57-pro.jpgis excluded by!**/*.jpgdocs/assets/boards/quinled-dig-2-go.jpgis excluded by!**/*.jpgdocs/assets/boards/quinled-dig-next-2.jpgis excluded by!**/*.jpgdocs/assets/boards/quinled-dig-octa-32-8l.jpgis excluded by!**/*.jpgdocs/assets/boards/quinled-dig-quad-v3.jpgis excluded by!**/*.jpgdocs/assets/boards/quinled-dig-uno-v3.jpgis excluded by!**/*.jpgdocs/assets/boards/serg-minishield.jpgis excluded by!**/*.jpgdocs/assets/boards/serg-unishield-v5.jpgis excluded by!**/*.jpgscripts/build/build_esp32.pyis excluded by!**/build/**scripts/build/generate_firmwares.pyis excluded by!**/build/**scripts/build/generate_manifest.pyis excluded by!**/build/**
📒 Files selected for processing (19)
.github/workflows/release.ymlCLAUDE.mddocs/backlog/backlog.mddocs/backlog/installer-3layer-plan.mddocs/building.mddocs/history/decisions.mddocs/install-alt/index.htmldocs/install/README.mddocs/install/boards.jsondocs/install/firmwares.jsonscripts/check/check_boards.pyscripts/check/check_firmwares.pyscripts/moondeck.pyscripts/moondeck_config.jsonscripts/run/preview_installer.pysrc/light/drivers/LcdLedDriver.hsrc/ui/install-picker.jstest/scenarios/core/scenario_MoonModule_control_change.jsontest/scenarios/light/scenario_AllEffects_grid_sizes.json
| outputs: | ||
| list: ${{ steps.gen.outputs.list }} | ||
| steps: | ||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify unpinned and credential-persisting checkout usage in this workflow.
rg -n 'uses:\s*actions/checkout@' .github/workflows/release.yml -A3
rg -n 'persist-credentials:\s*false' .github/workflows/release.ymlRepository: MoonModules/projectMM
Length of output: 1291
Harden checkout in the new firmwares job.
The checkout at line 80 uses a mutable tag (@v4) and retains default credential persistence. Pin to a commit SHA and disable persisted credentials to reduce supply-chain and token-exposure risk.
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 80-80: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 80-80: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 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 @.github/workflows/release.yml at line 80, The actions/checkout action at
line 80 in the `firmwares` job uses a mutable version tag (`@v4`) and retains
default credential persistence. Replace the mutable tag with a pinned commit SHA
and add the parameter persist-credentials: false to the checkout action
configuration to reduce supply-chain and token-exposure risk.
Source: Linters/SAST tools
Fixes a real bug seen flashing a board: the installer compared esptool's raw silicon name ("ESP32-D0WD-V3") against boards.json's coarse family ("ESP32"), so every classic-ESP32 device matched no board in the picker and the flash guard false-warned on a correct flash. Detection now normalises silicon → family. Also folds in a round of CodeRabbit fixes and the cheap half of a CI-hardening finding.
Core / UI:
- install-orchestrator.js: add chipFamily() — normalise esptool's getChipDescription() ("ESP32-S3 (QFN56) (revision …)", "ESP32-D0WD-V3", …) to the family vocabulary the picker compares against (boards.json `chip`). detect() returns the family; the raw silicon name is kept for the flash-progress log. The regex keeps the family token and drops the package/revision tail, so future S2/C3/C5/C6/H2 chips need no code change (projectMM aims to support every ESP32-family chip) — and classic names like D0WD don't mis-split.
Scripts / MoonDeck:
- build_esp32.py: add TARGET_TO_FAMILY — the one IDF-target → chip-family map, the single home for the family vocabulary (new SoCs are added here once and every consumer follows).
- generate_manifest.py: import TARGET_TO_FAMILY instead of its own duplicate copy.
- generate_firmwares.py: documented that the chip *family* is deliberately NOT stored in firmwares.json — it's derivable from `chip` and no consumer reads it (the manifest gets chipFamily from the map at generation time; the installer's runtime check uses boards.json + detection). Avoids data ahead of a consumer.
- check_boards.py: validate that `firmwares` and `modules` are lists (a non-list previously slipped through silently); both negative-tested.
Docs / CI:
- release.yml: persist-credentials: false on every checkout that doesn't push, so the GITHUB_TOKEN isn't left in .git/config for later steps (incl. third-party Docker actions) to read. The `release` job keeps the credential — its "Re-create latest" step force-pushes the tag with git.
- boards.json: Waveshare P4-NANO + ABC-WLED P4 shield listed WiFi as supported, but only the Ethernet-only esp32p4-eth variant ships (C6-WiFi is held out) — moved WiFi to `planned` so the card reflects what's actually flashable.
- backlog.md: added a "pin GitHub Actions to commit SHAs" item — the other half of the CI-hardening finding, deferred because SHA-pinning only pays off alongside Dependabot (else pins go stale and miss patches); the persist-credentials half is done.
Reviews:
- 🐇 CodeRabbit — boards.json P4 WiFi overstated: fixed (→ planned). check_boards firmwares/modules not list-validated: fixed. release.yml checkout hardening: persist-credentials done, SHA-pinning backlogged. decisions.md "will introduce" future tense: skipped — it's a verbatim quote of round 1's (wrong) prediction, and docs/history/ is exempt from the present-tense rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the installer plan Folds the install-alt picture board picker into the live /install/ page (install-alt was the unlinked build-beside test ground; the swap makes the visual card grid THE installer), adds MoonDeck save/restore of a device's pin profile, and closes out the installer initiative — the catalog-driven installer is now complete on this branch. Three picker-UX bugs surfaced by hardware testing are fixed. UI: - install/index.html: the picture board picker (card grid + supported/planned capability chips) replaces the plain board dropdown — the install-alt page folded in, install-alt/ deleted. Three fixes from real-device testing: (1) the grid now honours the detected chip — after Detect it shows only the matching family's boards, mirroring the dropdown's narrowing (was showing all families); (2) a "Show all boards" toggle as the escape hatch when detection is wrong or your board isn't in the detected family; (3) picking a board from another family via "Show all" now sets it correctly (the option is added to the hidden #rp-board before assigning, which previously silently no-op'd and left the firmware list unfiltered). Scripts / MoonDeck: - moondeck.py: device pin-profiles — /api/save-profile captures a device's pin/peripheral config (drivers, board, network, audio modules and their controls from /api/state; effects + the always-on Preview excluded) and stores it per-device in moondeck.json; /api/apply-profile re-pushes it via the shared add-then-configure fan-out (_apply_modules_to_device, factored out of _push_board_to_device). Restores GPIOs after a reflash, or clones a setup to a second identical rig. - moondeck_ui/app.js: Save / Apply profile controls on each device row. - preview_installer.py: single-installer staging (the install-alt mirror removed). Docs / CI: - release.yml: deploy only /install/ (the install-alt Pages block removed). - architecture.md: record the decided board-vs-device direction — board = devkit (bare MCU, user-soldered, Device-level pins unset), device = product (fixed wiring, often a carrier on a devkit, Device-level pins shipped). Both share one flat boards.json today; a kind tag / devices.json + extends split lands only when the catalog grows enough to need it. - installer-3layer-plan.md: rewritten as a closing index — shipped items point at permanent homes, the genuinely-gated future work (annotated-pin images, devices.json split, firmware-variant collapse + runtime PHY, EIM, CI SHA-pinning) points at its backlog.md homes. - README.md / MoonDeck.md / backlog.md: picture-picker is the installer (not a separate page); device-profile documented; board-vs-device standpoint cross-referenced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
scripts/check/check_boards.py (1)
90-93:⚠️ Potential issue | 🟠 MajorStrengthen firmware schema validation beyond container type.
At Line 90, this only enforces that
firmwaresis a list. A malformed entry likefirmwares: [null]plusdefault_firmware: nullcan still pass membership checks, while downstream catalog consumers expect firmware identifiers as strings.Proposed fix
fws = e.get("firmwares") - if not isinstance(fws, list): - errors.append(f"{where}: firmwares must be a list, got {type(fws).__name__}") - elif e.get("default_firmware") not in fws: + default_fw = e.get("default_firmware") + if ( + not isinstance(fws, list) + or not fws + or not all(isinstance(fw, str) and fw for fw in fws) + ): + errors.append(f"{where}: firmwares must be a non-empty list of non-empty strings") + elif not isinstance(default_fw, str) or not default_fw: + errors.append(f"{where}: default_firmware must be a non-empty string") + elif default_fw not in fws: errors.append(f"{where}: default_firmware '{e.get('default_firmware')}' " f"not in firmwares {fws}")🤖 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 `@scripts/check/check_boards.py` around lines 90 - 93, The firmware schema validation at the isinstance check for fws being a list is incomplete. Beyond verifying that fws is a list, you must also validate that each firmware identifier within that list is a string, since downstream catalog consumers expect firmware identifiers to be strings rather than null or other types. Add validation logic to iterate through the list and check that all items are strings, appending an appropriate error message to the errors list if any non-string entries are found (similar to how the current type checking is done with type(fws).__name__).
🤖 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/install/README.md`:
- Around line 42-44: The description of the board picker currently presents both
`image` and `url` as required catalog fields, but the schema documentation below
indicates that `url` is optional. Reword the sentence in the board picker
description to clarify that while the `image` field is used, the `url` field is
optional. This ensures the documentation doesn't create a contract mismatch
between the prose description and the schema definition below it.
In `@scripts/moondeck_ui/app.js`:
- Around line 976-981: The profile creation logic at lines 976-981 uses a
placeholder with empty modules array that will be persisted when saveState() is
called later, clobbering the real profile modules already saved to the server
via /api/save-profile. Instead of pushing a local placeholder with {name,
modules: []}, refresh the authoritative profile state from the server by making
a request to fetch the updated device state after the profile is created on the
server, then update device.profiles with the real data returned from the server
before calling renderDevices().
In `@scripts/moondeck.py`:
- Line 316: The nested helper functions in this file are missing return type
annotations, which triggers the Ruff ANN202 lint rule. Add return type
annotations of `-> None` to the private helper functions `_collect` and its
sibling nested helper that was flagged. Simply append `-> None` to the function
definition line after the closing parenthesis of the parameters and before the
colon to indicate these functions don't return a value.
- Around line 321-325: The control processing loop in moondeck.py needs to
filter out non-replayable control types before adding them to the controls
dictionary. Modify the loop that iterates through m.get("controls", []) to check
the control type (obtained via c.get("type")) and skip adding controls that have
type values of Password, ReadOnly, ReadOnlyInt, or Progress to the controls
dictionary. This prevents obfuscated password values and read-only/progress
controls from being saved and replayed, which would cause password encoding
issues or profile restore errors.
- Around line 1021-1025: The profile lookup in the nested loop (iterating
through networks, devices, and profiles) currently matches by name alone across
all devices, which causes incorrect pin maps to be restored when multiple
devices share the same profile name like "default". Scope the profile lookup to
the source device by first identifying which device the profile should come from
(either by capturing the source device/IP when the profile is initially selected
or by passing it as a parameter), then modify the profile matching logic to
verify that the found profile belongs to the correct source device before
retrieving its modules. This ensures that only profiles from the intended device
are used during restore operations.
---
Duplicate comments:
In `@scripts/check/check_boards.py`:
- Around line 90-93: The firmware schema validation at the isinstance check for
fws being a list is incomplete. Beyond verifying that fws is a list, you must
also validate that each firmware identifier within that list is a string, since
downstream catalog consumers expect firmware identifiers to be strings rather
than null or other types. Add validation logic to iterate through the list and
check that all items are strings, appending an appropriate error message to the
errors list if any non-string entries are found (similar to how the current type
checking is done with type(fws).__name__).
🪄 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: 9947c461-3252-4f2e-9297-ab5612694da6
⛔ Files ignored due to path filters (3)
scripts/build/build_esp32.pyis excluded by!**/build/**scripts/build/generate_firmwares.pyis excluded by!**/build/**scripts/build/generate_manifest.pyis excluded by!**/build/**
📒 Files selected for processing (13)
.github/workflows/release.ymldocs/architecture.mddocs/backlog/backlog.mddocs/backlog/installer-3layer-plan.mddocs/install/README.mddocs/install/boards.jsondocs/install/index.htmldocs/install/install-orchestrator.jsscripts/MoonDeck.mdscripts/check/check_boards.pyscripts/moondeck.pyscripts/moondeck_ui/app.jsscripts/run/preview_installer.py
Ethernet pin/PHY selection moves from compile-time constants to runtime config: each chip's firmware carries the driver(s) it can host (RMII EMAC on classic/P4, W5500 SPI on the S3), and which PHY/pins a board uses is set per board in boards.json and pushed at provision. The classic variants collapse — the default `esp32` is now WiFi+Ethernet in one binary (was `esp32-eth-wifi`), with `esp32-eth` kept as the WiFi-stripped option. Also fixes a persistence bug that zeroed any control's non-zero default when a saved file omitted its key — the root cause of the P4 losing Ethernet.
KPI: 16384lights | PC:340KB | tick:118/89/118/116/57/9/1/36/19/115/332/9us(FPS:8474/11235/8474/8620/17543/111111/1000000/27777/52631/8695/3012/111111) | ESP32:1331KB | src:88(16995) | test:56(8933)
Core:
- Control.cpp/JsonUtil.h: applyControlValue now skips absent JSON keys (new json::hasKey guard) instead of writing parseInt's 0-for-missing sentinel — a persisted file omitting a key no longer clobbers a control's non-zero default. This was the P4 no-DHCP root cause: an older NetworkModule.json without `ethType` zeroed it (2=IP101 → 0=none), so ethInit() dispatched to "no Ethernet" (link up, no lease).
- NetworkModule: runtime eth controls (ethType as a Select dropdown None/LAN8720/IP101/W5500, plus PHY-addr / RMII pins / W5500 SPI pins), seeded from the per-chip platform::ethConfigDefault and overridable per board; syncEthConfig() pushes them via platform::setEthConfig before ethInit(); onBuildControls() shows only the pin rows relevant to the chosen type; syncEthLive() hot-reconfigures W5500 (ethStop+ethInit, no reboot) but only where the W5500 driver is compiled in, so selecting it on an RMII board no longer tears down the working interface.
- platform: setEthConfig/ethStop seam (+ desktop stubs); ethInit() dispatches on phyType to ethInitRmii (gated CONFIG_ETH_USE_ESP32_EMAC) or ethInitSpi (gated MM_ETH_W5500); EthPinConfig + per-chip ethConfigDefault in platform_config.h (esp32/desktop mirrors kept in sync); hasEthW5500 capability flag.
Scripts / MoonDeck:
- build_esp32.py: collapse FIRMWARES (default esp32 = WiFi+Eth, drop the WiFi-only key, keep esp32-eth, add esp32-16mb); S3 variants gain the W5500 .eth-spi fragment; has_eth_fragment matches .eth-spi/-eth; new stale_feature_cache() guard wipes a build dir whose cached MM_NO_ETH/MM_ETH_ONLY no longer matches the firmware (a CMake -D isn't cleared by omission, which silently stubbed Ethernet out).
- generate_build_info.py / moondeck.py / app.js: variant-list comments + examples updated for the collapse; moondeck.py also filters non-replayable control types (password/display/progress) out of captured profiles and scopes apply-profile lookup to the source device; app.js refreshes profile state from the server after save instead of pushing a {modules:[]} placeholder.
- check_boards.py: firmwares[] entries must be strings.
Light domain:
- (none — eth is core/platform)
UI:
- install-picker.js / install-orchestrator.js: comments reframed for the collapse (esp32-eth-wifi is now a legacy OTA-mapped key, not a current variant); chipFamily wording corrected (it's in the manifest, not firmwares.json).
Tests:
- unit_Control_apply_absent_key: pins the persistence fix (absent key never mutates a control; present key, incl. explicit 0, still applies; hasKey detects presence vs value).
- unit_NetworkModule_ethernet: EthPhyType enum-ordering contract (dropdown index == dispatch == boards.json) + desktop eth seam is a safe no-op.
- scenario_NetworkModule_eth_reconfigure: cycles ethType through all four values live on a device and asserts the render loop survives every transition (robustness).
Docs / CI:
- boards.json/firmwares.json: per-board eth config (Olimex/Dig-Octa RMII, SE16/LightCrafter W5500 SPI, Waveshare/ABC P4 IP101); variant keys updated; SE16/LightCrafter gain Ethernet capability; P4 bench board ledsPerPin removed.
- esp32: new Kconfig.projbuild (MM_P4_WIFI) gating esp_hosted/esp_wifi_remote to the WiFi build only (was wrongly pulled into eth-only P4); espressif/w5500 component gated to S3; sdkconfig.defaults.eth-spi added; dead CONFIG_ETH_PHY_INTERFACE_RMII / PHY_ADDR / RST_GPIO symbols removed.
- README/architecture/building/MoonDeck/SystemModule/NetworkModule/BoardModule/performance: runtime-PHY model documented, dropped-variant references reframed as historical/legacy.
- backlog: variant-collapse marked done; live-RMII-reconfigure item added; resolved P4 no-DHCP entry removed. decisions.md: persistence absent-key lesson recorded.
Reviews:
- 👾 Reviewer (Opus, whole-branch): verdict coherent, no blockers; 6 should-fix stale comments/docs all fixed (ethPins→ethConfigDefault references, isEsp32S3 contradiction, architecture.md compile-time-pins claim, esp32-eth-wifi-as-current in install-picker/FirmwareUpdateModule, family-in-firmwares.json wording). Zero diagnostic residue from the P4 debugging confirmed.
- 🐇 CodeRabbit: check_boards firmware-entries-must-be-strings (fixed); README image/url-optional wording (fixed); moondeck non-replayable control filter (fixed); profile-lookup device scoping (fixed); app.js profile-placeholder clobber (fixed); moondeck.py ANN202 return annotations (skipped — Ruff not configured in the project and 33 pre-existing hits, partial annotation would be inconsistent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
README.md (1)
29-29:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix the P4 lane-count claim.
Line 29 says the P4 Parlio path can drive up to 20 strands, but the architecture doc caps Parlio at 1–8 lanes. Please reconcile the numbers so the headline matches the actual hardware limit.
🤖 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 `@README.md` at line 29, The README.md file currently claims that the P4's Parlio engine can drive up to 20 simultaneous strands, but this contradicts the actual hardware limit documented in the architecture documentation which caps Parlio at 1–8 lanes. Update the P4 Parlio claim in the parallel WS2812 output section to reflect the correct lane count of 1–8 instead of 20 to ensure the documentation accurately represents the hardware capabilities.docs/history/decisions.md (1)
820-823:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReconcile the
devices.jsondecision.Line 820 says there is no
devices.jsonsplit, butarchitecture.mdandbacklog.mdstill describe that split as deferred rather than rejected. Please make the docs say one thing here. As per coding guidelines, "Single source of truth. CLAUDE.md (rules), architecture.md (design), docs/modules/*.md (specs). Nothing else. If it's duplicated, delete one copy."🤖 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/history/decisions.md` around lines 820 - 823, The decision documented in decisions.md clearly states there is no `devices.json` split (one catalog, one entry type), but architecture.md and backlog.md still describe this `devices.json` split as deferred rather than rejected. Search for references to the `devices.json` split being deferred in both architecture.md and backlog.md, then update those references to either explicitly state it is rejected (to align with the decision in decisions.md), or alternatively update the statement in decisions.md to say it is deferred instead. Ensure only one consistent statement of this decision exists across all architecture documentation files to maintain single source of truth.Source: Coding guidelines
docs/install/index.html (1)
560-565:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the Ethernet note for runtime pin configuration.
This still points boards with different RMII pins at a local rebuild, but this PR moves Ethernet pin/PHY choice into the catalog-driven runtime config path.
Proposed wording
- but different pins (e.g. WT32-ETH01: reset on GPIO 16) need a local rebuild. + but different pins (e.g. WT32-ETH01: reset on GPIO 16) need a matching catalog entry + so the installer can apply the runtime Ethernet pin configuration.🤖 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/install/index.html` around lines 560 - 565, Update the Ethernet documentation note at the end of the paragraph to reflect that boards with different RMII pins (like WT32-ETH01) can now be configured at runtime through the catalog-driven configuration system instead of requiring a local rebuild. Replace the statement about needing a local rebuild with information about how to configure Ethernet pins and PHY selection at runtime.src/ui/install-picker.js (1)
710-713:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign the TX-power JSDoc with the modules schema.
The implementation no longer reads
controls.Network; leaving that path in the public helper docs can send the next catalog edit back to the removed schema. As per coding guidelines, comments document intent and context and outdated/factually wrong comments should not be carried forward.Proposed fix
- * The picked board's boards.json TX-power cap - * (controls.Network.txPowerSetting), or null when the board has none / + * The picked board's boards.json TX-power cap + * (`modules[].id === "Network"` → `controls.txPowerSetting`), or null when the board has none /🤖 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/ui/install-picker.js` around lines 710 - 713, The JSDoc comment for the TX-power property in the install-picker.js file (around line 710-713) contains an outdated reference to controls.Network.txPowerSetting, which is no longer part of the implementation schema. Update the JSDoc documentation to remove this incorrect schema path reference and ensure the comment accurately reflects the current implementation without mentioning the removed controls.Network path, so that future catalog edits are not misled by outdated documentation.Source: Coding guidelines
scripts/run/preview_installer.py (1)
91-91: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueRedundant import of
jsoninside function.
jsonis already imported at module level (line 37). The local import at line 91 is unnecessary.Suggested fix
def _stage_referenced_board_images(dst_dir: Path): """Stage only the board images a boards.json entry references (mirrors the deploy's filtered copy), under <dst_dir>/assets/boards/.""" boards_json = INSTALL_DIR / "boards.json" if not boards_json.exists(): return - import json try: boards = json.loads(boards_json.read_text())🤖 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 `@scripts/run/preview_installer.py` at line 91, The `json` module is imported twice: once at module level and again locally inside a function at line 91. Remove the redundant local import statement since the module is already available at the module scope and can be used directly without reimporting..github/workflows/release.yml (1)
283-283:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse
uv runfor Python invocations per coding guidelines.The release job invokes
python scripts/build/generate_manifest.pydirectly without going throughuv run. The coding guidelines require all Python invocations to useuv run. Addastral-sh/setup-uv@v3and change touv run python scripts/build/generate_manifest.py.Same issue at line 397 in the deploy-pages job.
Suggested fix for release job (lines 268-289)
Add setup-uv step and update the command:
steps: + - uses: astral-sh/setup-uv@v3 # NB: this checkout KEEPS persisted credentials ... - uses: actions/checkout@v4 ... - name: Generate ESP Web Tools manifests (release-asset URLs) ... run: | ... for F in $(jq -r '.firmwares[] | select(.ships) | .name' docs/install/firmwares.json); do - python scripts/build/generate_manifest.py \ + uv run python scripts/build/generate_manifest.py \🤖 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 @.github/workflows/release.yml at line 283, Update the release and deploy-pages jobs in the GitHub Actions workflow to comply with coding guidelines that require all Python invocations to use uv run. In the release job (around line 283), add a setup step using astral-sh/setup-uv@v3 and change the python scripts/build/generate_manifest.py invocation to uv run python scripts/build/generate_manifest.py. Apply the same fix to the deploy-pages job at line 397 by adding the setup-uv action and updating the command to use uv run.Source: Coding guidelines
♻️ Duplicate comments (2)
scripts/moondeck.py (1)
323-323:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd missing
-> Noneannotations for nested private helpers (ANN202).This lint finding is still present for
_collectand_store.#!/bin/bash uv run ruff check scripts/moondeck.py --select ANN202Also applies to: 1015-1015
🤖 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 `@scripts/moondeck.py` at line 323, The nested private helper functions `_collect` and `_store` are missing return type annotations, triggering the ANN202 lint rule. Add `-> None` return type annotations to the function signatures: add `-> None` after the closing parenthesis of the `_collect` function at line 323 and after the closing parenthesis of the `_store` function at line 1015 to explicitly declare that both functions return None.Source: Linters/SAST tools
scripts/check/check_boards.py (1)
93-99:⚠️ Potential issue | 🟠 Major | ⚡ Quick winEnforce non-empty firmware IDs in
firmwaresentries.Line 93 only type-checks entries, so empty strings still pass. That can let schema-invalid firmware keys through and break downstream firmware selection assumptions.
Proposed fix
- non_str = [f for f in fws if not isinstance(f, str)] + non_str = [f for f in fws if not isinstance(f, str) or not f] if non_str: - errors.append(f"{where}: firmwares entries must be strings, got " + errors.append(f"{where}: firmwares entries must be non-empty strings, got " f"{[type(f).__name__ for f in non_str]}")🤖 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 `@scripts/check/check_boards.py` around lines 93 - 99, The firmware validation in the firmwares entries check only validates that entries are strings but does not check if those strings are non-empty. Add a validation step after the existing type check (the non_str list comprehension) to identify any empty string entries in the firmwares list, and if found, append an appropriate error message to the errors list indicating that firmware entries must be non-empty strings. This ensures that invalid empty firmware IDs cannot slip through the validation.
🤖 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/architecture.md`:
- Around line 232-233: The documentation uses inconsistent casing for the WiFi
flag: line 232 contains `hasWifi` while an earlier section in the same document
uses `hasWiFi`. Choose one spelling convention (either `hasWifi` or `hasWiFi`)
and update all occurrences throughout the document to use that spelling
consistently.
In `@docs/backlog/backlog.md`:
- Around line 190-191: The documentation currently references a "short hardcoded
board list" as a fallback mechanism, but the implementation has moved to a
catalog-driven approach using boards.json for both the board picker and
fallback. Remove the "hardcoded list" wording and update the text to clarify
that MoonDeck now uses the boards.json catalog to both let users pick a board
and provide fallback options when the firmware does not uniquely identify the
hardware.
In `@docs/install/index.html`:
- Around line 1743-1747: In the MutationObserver callback, the line assigning to
`selected` uses a fallback that prevents clearing stale board selection when the
picker is reset. When `installPicker.getSelectedBoard()` returns an empty string
(indicating the picker was cleared for a detected family with multiple matches),
the `|| selected` causes the old board value to persist in the summary display,
misleading the user about the current selection state. Update the assignment to
`selected` to directly use the value from `installPicker.getSelectedBoard()`
without the fallback operator, so that an empty return value properly clears the
stale selection and reflects the actual generic mode state of the picker.
In `@scripts/moondeck.py`:
- Around line 1015-1023: The _store function matches devices by IP address
across all networks, which causes collisions when multiple networks have
overlapping private subnets (like 192.168.1.x). Modify the _store function to
accept or determine a specific network context, then only iterate through and
modify devices within that single network rather than searching across all
networks. This same scoping issue also needs to be applied to the similar
profile apply handler function to prevent replaying profiles from the wrong
network.
In `@src/core/JsonUtil.h`:
- Around line 43-52: The hasKey function uses a naive std::strstr search that
cannot distinguish between actual JSON object members and text that happens to
appear inside string values, causing false positives for absent keys when
escaped patterns exist in values. Implement a JSON-aware search in hasKey that
iterates through the JSON string while tracking whether the current position is
inside a string value (handling escaped quotes), and only matches the key
pattern when positioned at the object member level outside of any string values.
This ensures the function correctly identifies genuinely-present keys without
matching similar patterns embedded in value strings.
In `@src/core/NetworkModule.h`:
- Line 477: The `appliedEthSig_` member variable is declared as `long` but
experiences signed overflow during hash calculations in the `ethSig()` function,
causing undefined behavior. Change the type of `appliedEthSig_` from `long` to
`uint32_t` to guarantee deterministic wrapping behavior instead of undefined
signed overflow. Update the initialization value from -1 to an appropriate value
for the unsigned type (typically 0 or the maximum uint32_t value depending on
the logic intent).
In `@src/platform/esp32/platform_esp32.cpp`:
- Around line 526-543: The functions `setEthConfig()` and `ethStop()` are
exposed in the public platform API but lack corresponding stub implementations
in the `#else // MM_NO_ETH` fallback block, causing link errors in builds
without Ethernet support. Add no-op stub implementations for both
`setEthConfig()` and `ethStop()` within the `#else // MM_NO_ETH` preprocessor
block (at lines 564-573) to maintain API symmetry with the main implementation
and ensure shared code that calls these Ethernet functions can compile
successfully on all build configurations.
- Around line 334-336: The Ethernet initialization code in the RMII and W5500
bring-up paths uses fatal ESP_ERROR_CHECK calls and lacks null checks on
esp_netif_new() return values. Replace all ESP_ERROR_CHECK calls throughout the
initialization sequence with proper error checking that logs the issue and
returns false, and add explicit null checks after esp_netif_new() to verify the
ethNetif_ pointer is valid before proceeding; this pattern needs to be applied
consistently across all the Ethernet initialization code paths (including the
sections around lines 394-399, 434-436, and 487-489) to ensure the device fails
gracefully and allows NetworkModule to cascade to WiFi/AP fallback instead of
aborting under memory pressure or transient failures.
In `@test/unit/core/unit_NetworkModule_ethernet.cpp`:
- Around line 52-69: The test case "Desktop Ethernet seam is a safe no-op"
modifies shared platform state via multiple setEthConfig() calls but fails to
restore the configuration at the end, which can cause other tests to fail if
they depend on the default ethernet configuration. Add a cleanup statement at
the end of the test (after the final CHECK_FALSE call) to reset the ethernet
config to ethConfigDefault by calling setEthConfig(ethConfigDefault) to ensure
tests remain order-independent and prevent cross-test state leakage.
---
Outside diff comments:
In @.github/workflows/release.yml:
- Line 283: Update the release and deploy-pages jobs in the GitHub Actions
workflow to comply with coding guidelines that require all Python invocations to
use uv run. In the release job (around line 283), add a setup step using
astral-sh/setup-uv@v3 and change the python scripts/build/generate_manifest.py
invocation to uv run python scripts/build/generate_manifest.py. Apply the same
fix to the deploy-pages job at line 397 by adding the setup-uv action and
updating the command to use uv run.
In `@docs/history/decisions.md`:
- Around line 820-823: The decision documented in decisions.md clearly states
there is no `devices.json` split (one catalog, one entry type), but
architecture.md and backlog.md still describe this `devices.json` split as
deferred rather than rejected. Search for references to the `devices.json` split
being deferred in both architecture.md and backlog.md, then update those
references to either explicitly state it is rejected (to align with the decision
in decisions.md), or alternatively update the statement in decisions.md to say
it is deferred instead. Ensure only one consistent statement of this decision
exists across all architecture documentation files to maintain single source of
truth.
In `@docs/install/index.html`:
- Around line 560-565: Update the Ethernet documentation note at the end of the
paragraph to reflect that boards with different RMII pins (like WT32-ETH01) can
now be configured at runtime through the catalog-driven configuration system
instead of requiring a local rebuild. Replace the statement about needing a
local rebuild with information about how to configure Ethernet pins and PHY
selection at runtime.
In `@README.md`:
- Line 29: The README.md file currently claims that the P4's Parlio engine can
drive up to 20 simultaneous strands, but this contradicts the actual hardware
limit documented in the architecture documentation which caps Parlio at 1–8
lanes. Update the P4 Parlio claim in the parallel WS2812 output section to
reflect the correct lane count of 1–8 instead of 20 to ensure the documentation
accurately represents the hardware capabilities.
In `@scripts/run/preview_installer.py`:
- Line 91: The `json` module is imported twice: once at module level and again
locally inside a function at line 91. Remove the redundant local import
statement since the module is already available at the module scope and can be
used directly without reimporting.
In `@src/ui/install-picker.js`:
- Around line 710-713: The JSDoc comment for the TX-power property in the
install-picker.js file (around line 710-713) contains an outdated reference to
controls.Network.txPowerSetting, which is no longer part of the implementation
schema. Update the JSDoc documentation to remove this incorrect schema path
reference and ensure the comment accurately reflects the current implementation
without mentioning the removed controls.Network path, so that future catalog
edits are not misled by outdated documentation.
---
Duplicate comments:
In `@scripts/check/check_boards.py`:
- Around line 93-99: The firmware validation in the firmwares entries check only
validates that entries are strings but does not check if those strings are
non-empty. Add a validation step after the existing type check (the non_str list
comprehension) to identify any empty string entries in the firmwares list, and
if found, append an appropriate error message to the errors list indicating that
firmware entries must be non-empty strings. This ensures that invalid empty
firmware IDs cannot slip through the validation.
In `@scripts/moondeck.py`:
- Line 323: The nested private helper functions `_collect` and `_store` are
missing return type annotations, triggering the ANN202 lint rule. Add `-> None`
return type annotations to the function signatures: add `-> None` after the
closing parenthesis of the `_collect` function at line 323 and after the closing
parenthesis of the `_store` function at line 1015 to explicitly declare that
both functions return None.
🪄 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: 1b10adbc-0f6b-41a1-aee3-b317fa859476
⛔ Files ignored due to path filters (4)
scripts/build/build_esp32.pyis excluded by!**/build/**scripts/build/generate_build_info.pyis excluded by!**/build/**scripts/build/generate_firmwares.pyis excluded by!**/build/**scripts/build/generate_manifest.pyis excluded by!**/build/**
📒 Files selected for processing (44)
.github/workflows/release.ymlREADME.mddocs/architecture.mddocs/backlog/backlog.mddocs/backlog/installer-3layer-plan.mddocs/building.mddocs/history/decisions.mddocs/install/README.mddocs/install/boards.jsondocs/install/firmwares.jsondocs/install/index.htmldocs/install/install-orchestrator.jsdocs/moonmodules/core/BoardModule.mddocs/moonmodules/core/FirmwareUpdateModule.mddocs/moonmodules/core/NetworkModule.mddocs/moonmodules/core/SystemModule.mddocs/performance.mdesp32/main/Kconfig.projbuildesp32/main/idf_component.ymlesp32/sdkconfig.defaults.esp32p4-ethesp32/sdkconfig.defaults.esp32p4-eth-wifiesp32/sdkconfig.defaults.ethesp32/sdkconfig.defaults.eth-spiscripts/MoonDeck.mdscripts/check/check_boards.pyscripts/moondeck.pyscripts/moondeck_ui/app.jsscripts/run/preview_installer.pysrc/core/Control.cppsrc/core/JsonUtil.hsrc/core/NetworkModule.hsrc/platform/desktop/platform_config.hsrc/platform/desktop/platform_desktop.cppsrc/platform/esp32/platform_config.hsrc/platform/esp32/platform_esp32.cppsrc/platform/platform.hsrc/ui/install-picker.jstest/CMakeLists.txttest/scenarios/core/scenario_MoonModule_control_change.jsontest/scenarios/core/scenario_NetworkModule_eth_reconfigure.jsontest/scenarios/light/scenario_AllEffects_grid_sizes.jsontest/scenarios/light/scenario_Driver_mutation.jsontest/unit/core/unit_Control_apply_absent_key.cpptest/unit/core/unit_NetworkModule_ethernet.cpp
| The firmware-vs-board separation is now in place across the codebase (see [architecture.md § Firmware vs board](../architecture.md#firmware-vs-board)). `build_esp32.py --firmware <variant>` picks the compiled binary; MoonDeck deduces the physical board where the firmware uniquely identifies hardware (`esp32-eth*` ⇒ `olimex-esp32-gateway-rev-g`) and lets the user pick from a short hardcoded list otherwise. Firmware variants stay separate — `esp32-eth` saves ~670 KB flash + ~30 KB DRAM vs the default `esp32` (WiFi+Ethernet, measured); merging would erase that win. | ||
|
|
There was a problem hiding this comment.
Drop the hardcoded-list wording.
Line 190 still says MoonDeck falls back to a short hardcoded board list, but the new catalog-driven flow reads boards.json for the picker and fallback too. Please update the wording so this section doesn't imply a second source of truth.
🤖 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/backlog/backlog.md` around lines 190 - 191, The documentation currently
references a "short hardcoded board list" as a fallback mechanism, but the
implementation has moved to a catalog-driven approach using boards.json for both
the board picker and fallback. Remove the "hardcoded list" wording and update
the text to clarify that MoonDeck now uses the boards.json catalog to both let
users pick a board and provide fallback options when the firmware does not
uniquely identify the hardware.
| // True when `key` is present in the JSON object. Lets callers distinguish a | ||
| // genuinely-absent key from one whose value happens to be 0/false — parseInt and | ||
| // parseBool can't, so applying their result for an absent key would clobber a | ||
| // control's non-zero default (e.g. eth phyType=2) with 0 on a partial/older save. | ||
| inline bool hasKey(const char* json, const char* key) { | ||
| if (!json || !key) return false; | ||
| char search[48]; | ||
| std::snprintf(search, sizeof(search), "\"%s\":", key); | ||
| return std::strstr(json, search) != nullptr; | ||
| } |
There was a problem hiding this comment.
hasKey can match key-like text inside string values and still misapply absent-key overlays.
std::strstr("\"key\":") is not object-member aware; it can match escaped text inside a string value (e.g., {\"note\":\"...\\\"ethType\\\":0...\"}), causing hasKey to return true for an actually absent control key and reintroducing unintended value writes downstream.
Suggested fix (member-aware flat scan without allocations)
inline bool hasKey(const char* json, const char* key) {
if (!json || !key) return false;
- char search[48];
- std::snprintf(search, sizeof(search), "\"%s\":", key);
- return std::strstr(json, search) != nullptr;
+ const size_t keyLen = std::strlen(key);
+ if (keyLen == 0) return false;
+
+ bool inString = false;
+ for (const char* p = json; *p; ++p) {
+ if (*p == '"' && (p == json || p[-1] != '\\')) {
+ inString = !inString;
+ if (inString) {
+ const char* s = p + 1;
+ if (std::strncmp(s, key, keyLen) == 0 && s[keyLen] == '"') {
+ const char* q = s + keyLen + 1;
+ while (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r') ++q;
+ if (*q == ':') return true;
+ }
+ }
+ }
+ }
+ return false;
}🤖 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/JsonUtil.h` around lines 43 - 52, The hasKey function uses a naive
std::strstr search that cannot distinguish between actual JSON object members
and text that happens to appear inside string values, causing false positives
for absent keys when escaped patterns exist in values. Implement a JSON-aware
search in hasKey that iterates through the JSON string while tracking whether
the current position is inside a string value (handling escaped quotes), and
only matches the key pattern when positioned at the object member level outside
of any string values. This ensures the function correctly identifies
genuinely-present keys without matching similar patterns embedded in value
strings.
| esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH(); | ||
| ethNetif_ = esp_netif_new(&netif_cfg); | ||
|
|
There was a problem hiding this comment.
Avoid panic paths in runtime Ethernet init; fail closed and return false.
Both RMII and W5500 bring-up still use fatal ESP_ERROR_CHECK calls and skip esp_netif_new() null checks. Under memory pressure or transient init failures, this can abort the device instead of letting NetworkModule cascade to WiFi/AP.
Suggested fix pattern
static bool ethInitRmii() {
esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH();
ethNetif_ = esp_netif_new(&netif_cfg);
+ if (!ethNetif_) {
+ ESP_LOGE(NET_TAG, "Ethernet netif create failed");
+ return false;
+ }
@@
- ESP_ERROR_CHECK(esp_netif_attach(ethNetif_, esp_eth_new_netif_glue(eth_handle)));
- ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID,
- ðEventHandler, nullptr));
- ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP,
- ðEventHandler, nullptr));
+ esp_err_t err = esp_netif_attach(ethNetif_, esp_eth_new_netif_glue(eth_handle));
+ if (err != ESP_OK) {
+ esp_eth_driver_uninstall(eth_handle);
+ if (ethNetif_) { esp_netif_destroy(ethNetif_); ethNetif_ = nullptr; }
+ return false;
+ }
+ err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ðEventHandler, nullptr);
+ if (err != ESP_OK) {
+ esp_eth_driver_uninstall(eth_handle);
+ if (ethNetif_) { esp_netif_destroy(ethNetif_); ethNetif_ = nullptr; }
+ return false;
+ }
+ err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ðEventHandler, nullptr);
+ if (err != ESP_OK) {
+ esp_event_handler_unregister(ETH_EVENT, ESP_EVENT_ANY_ID, ðEventHandler);
+ esp_eth_driver_uninstall(eth_handle);
+ if (ethNetif_) { esp_netif_destroy(ethNetif_); ethNetif_ = nullptr; }
+ return false;
+ }
}Also applies to: 394-399, 434-436, 487-489
🤖 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.cpp` around lines 334 - 336, The Ethernet
initialization code in the RMII and W5500 bring-up paths uses fatal
ESP_ERROR_CHECK calls and lacks null checks on esp_netif_new() return values.
Replace all ESP_ERROR_CHECK calls throughout the initialization sequence with
proper error checking that logs the issue and returns false, and add explicit
null checks after esp_netif_new() to verify the ethNetif_ pointer is valid
before proceeding; this pattern needs to be applied consistently across all the
Ethernet initialization code paths (including the sections around lines 394-399,
434-436, and 487-489) to ensure the device fails gracefully and allows
NetworkModule to cascade to WiFi/AP fallback instead of aborting under memory
pressure or transient failures.
…eview
GPIO controls render as plain number inputs (and clock-direction as a toggle) instead of sliders — a slider over a GPIO number is meaningless. A new one-byte ControlType::Pin (int8, -1 = unused) carries that, saving a byte per pin on a DRAM-scarce chip and serving every future pin control. Also folds in a CodeRabbit review pass over the runtime-Ethernet branch.
Core:
- Control.h/.cpp: new ControlType::Pin — int8_t storage (a GPIO never exceeds ~54), -1 = unused, serialized/parsed as a plain integer, min/max a server-side write-clamp guard only. Distinct from Int16 because the UI's int16 case always draws a slider (unbounded int16 falls back to the -100..200 percentage slider Layer start/end rely on), so int16 can't mean both "position slider" and "pin number". addPin() binds it.
- NetworkModule: eth GPIO/addr controls → addPin (int8_t members, was int16 sliders); ethClockExtIn → addBool (a clock-direction toggle, was a 0/1 slider). ethSig() now returns uint32_t (the rolling hash multiply wrapped as signed long = UB) with a separate ethSigApplied_ flag for the never-applied case.
- JsonUtil.h: parseBool accepts a numeric 1 as well as the literal true — boards.json / the catalog historically wrote 0/1 for flags now bound as Bool (ethClockExtIn).
UI:
- app.js: render ControlType::Pin ("pin") as a plain number input (clamped to the valid-GPIO span, -1 = unused); the int16 slider path is untouched so Layer positions keep their slider.
- install-orchestrator.js / install-picker.js: comment-only — chipFamily wording, MutationObserver clears a stale board selection verbatim instead of `|| selected`.
Scripts / MoonDeck:
- moondeck.py: save-profile + apply-profile scope to the ACTIVE network only (overlapping private subnets across networks no longer collide); _collect/_store get -> None annotations.
- check_boards.py: firmwares[] entries must be non-empty strings.
- preview_installer.py: drop a redundant local `import json`.
Tests:
- unit_NetworkModule_ethernet: restore ethConfigDefault at the end so the test leaves no shared eth-config state.
Docs / CI:
- boards.json: ethClockExtIn values 0/1 → JSON booleans (false/true).
- NetworkModule.md: eth pins documented as the `pin` type (int8); ethClockExtIn as bool.
- release.yml: the release + deploy-pages jobs use `uv run python` for generate_manifest.py (+ astral-sh/setup-uv), per the uv-everywhere rule.
- README: P4 Parlio corrected to "up to 8 lanes" (was 20); architecture.md hasWiFi casing; index.html eth note reframed (runtime PHY/pins, no rebuild).
- backlog: P4 console-over-USB-JTAG dev-loop note; persistence partial-save/schema-change audit item; round-3 esp_hosted-gating note corrected (now CONFIG_MM_P4_WIFI, not "left as-is").
- decisions.md: Pin-type rationale; devices.json "rejected vs deferred" reconciled (rejects a second SCHEMA, not a future same-schema grouping file); persistence absent-key lesson (from 1161e77).
Reviews:
- 🐇 check_boards firmwares-non-empty-strings (fixed); README image/url-optional + parlio lane count (fixed); moondeck non-replayable-control filter, profile device+network scoping, ANN202 (fixed); app.js profile clobber + MutationObserver clear (fixed); JsonUtil parseBool 1/0 (fixed); ethSig signed overflow → uint32 (fixed); MM_NO_ETH setEthConfig/ethStop stubs (fixed); release.yml uv (fixed); index.html eth note (fixed); devices.json doc consistency (fixed).
- 🐇 hasKey JSON-aware string-scan — accepted/skipped: our persisted JSON is flat + self-written and hasKey is exactly as robust as the sibling parseInt/parseString (same strstr approach); a JSON-aware scan for one helper while the value-extractors stay naive is inconsistent over-engineering for this format.
- 🐇 ESP_ERROR_CHECK in ethInit aborts under OOM — accepted/deferred: the graceful-degradation path (MAC/PHY/driver-install → fail() → WiFi cascade) is already in place; the remaining esp_netif_attach / event-register ESP_ERROR_CHECK fail only on programmer-error/OOM, not the cable/PHY conditions the cascade exists for, and rewriting them across the freshly-hardware-verified RMII+W5500 paths is a hardening follow-up, not a pre-merge blocker.
- 🐇 install-picker txPower JSDoc "outdated" — skipped: not outdated; txPowerSetting is still a live control (NetworkModule + boards.json).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gate-4 doc-sync follow-up: add the Pin row to docs/moonmodules/core/Control.md's type table (int8 storage, -1=unused, number-input UI) and a note on why it's distinct from Int16 (which renders a slider). Also corrects the Int16 row's UI description to 'slider', matching the actual app.js rendering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two should-fix items from the Event-2 reviewer over the whole branch: Core: - Control.h: ControlType::Pin enum comment said 'int16 storage' — factually wrong, it's int8_t (everywhere else — addPin, the casts, decisions.md — already says int8). Corrected. Docs: - NetworkModule.md: the '## Ethernet' section still claimed board GPIO config is 'hardcoded per board via compile-time defines in sdkconfig.defaults.<board>' and 'firmware always includes Ethernet' — both pre-branch statements that contradicted the runtime-PHY model documented at the top of the same file. Rewritten to the runtime model (driver compiled per chip, pins/PHY runtime via the eth controls, MM_NO_ETH stub, cascade-on-no-PHY). Reviews: - 👾 Reviewer: verdict merge-ready, no blockers; both should-fix comment/doc items fixed. The 3 nits accepted as-is per the reviewer's own guidance (history-file superseded statement is allowed; the absent-key test's addInt16 exercises the same type-agnostic guard; LED-driver pins staying addUint16 is the deliberate concrete-first scope). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What this does
Makes Ethernet runtime-configurable per board and collapses the classic firmware matrix, on top of the catalog-driven install work this branch started with.
platform_config.h, overridable per board fromboards.json(ethType+ pin controls →platform::setEthConfig→ethInit()dispatch). No more Olimex-pins-baked-into-the-binary.espressif/w5500managed component, gated to the S3.esp32is now WiFi and Ethernet in one binary (wasesp32-eth-wifi),esp32-ethkept as the WiFi-stripped option, the WiFi-only key dropped.esp32-16mbadded.ControlType::Pin— GPIO controls render as plain number inputs (one-byteint8_tstorage, −1 = unused), not sliders.tick:unchanged — Ethernet is boot/loop1s-path, not the render tick.Why
parseInt, which returns0for an absent key — indistinguishable from a real0— and wrote that over the control's default. Harmless when defaults were 0 (the old controls), but the new eth controls have meaningful non-zero defaults (P4ethType= 2 = IP101), so a partial saved file silently disabled Ethernet. Fixed with ajson::hasKeypresence guard.Headline changes
Core / platform — runtime
EthPinConfig+setEthConfig/ethStop;ethInit()dispatches RMII (CONFIG_ETH_USE_ESP32_EMAC) vs W5500 (MM_ETH_W5500);NetworkModuleeth controls (live-applied for W5500, save-and-restart for RMII); newControlType::Pin;applyControlValueabsent-key guard +json::hasKey/parseBool(1/0).esp32 build —
FIRMWAREScollapse;espressif/w5500+CONFIG_MM_P4_WIFIgating inidf_component.yml(keeps the C6 WiFi stack out of the eth-only P4 build);sdkconfig.defaults.eth-spi;build_esp32.pystale-feature-cache guard (a cachedMM_NO_ETH-Disn't cleared by omission).UI / installer —
app.jsrenderspinas a number input; install-picker/orchestrator comments reframed for the collapse.Scripts / MoonDeck — catalog module-then-controls injection; profile save/apply scoped to the active network; control-type filtering on captured profiles.
Catalog — per-board eth config (Olimex/Dig-Octa RMII, SE16/LightCrafter W5500, Waveshare/ABC P4 IP101); variant keys updated; 30 board images.
Tests —
unit_Control_apply_absent_key(persistence regression),unit_NetworkModule_ethernet(enum/seam contract),scenario_NetworkModule_eth_reconfigure(live ethType cycle robustness), driver-mutation scenarios.Docs — runtime-PHY model across README/architecture/building/MoonDeck/SystemModule/NetworkModule/FirmwareUpdateModule; dropped variants framed as legacy/OTA-mapped;
decisions.mdlessons (persistence absent-key, Pin type, devices.json schema-vs-grouping).Verification
esp32-16mbtestbench (RMII no-cable → clean WiFi cascade → provisioned STA), ESP32-S3 (W5500 driver compiled+linked; no-eth → WiFi fallback + mic OK), Waveshare P4-NANO (IP101 RMII Ethernet up, DHCP, web UI — after the persistence fix). The P4 regression was bisected withgit worktreebuilds (round-1 ✓, main ✓) to prove it was our code, then root-caused via a stdout printf over the P4's secondary USB-JTAG console.-Werror), unit tests, scenarios, platform boundary, board/firmware checks; all 5 ESP32 variants build clean.Notes for review
esp32p4-eth-wifiremainsships:false(the C6 esp_hosted slave-firmware path isn't CI-reproducible yet) — tracked in the backlog.observed-block writeback (drift tracking) from running the scenario gate.🤖 Generated with Claude Code