Skip to content

Conversation

@ewowi
Copy link
Collaborator

@ewowi ewowi commented Nov 30, 2025

Back end

  • ModuleIO, add On off button pin
  • ModuleLightsControl: add pinToggleOnOff

Summary by CodeRabbit

  • New Features

    • Hardware On/Off button with debounce; Parallel LED Driver replaces previous driver split.
    • Per-layout LED-pin selection and panels-per-pin grouping; driver status shown in UI.
  • Improvements

    • Streamlined UI control registration and live UI update flow.
    • Consolidated board presets, pin usage and wiring presentation.
    • Wi‑Fi STA connect path re-enabled.
    • Frontend bundling set to single-file output.
  • Documentation

    • Updated drivers, installation, and new development standards guide.

✏️ Tip: You can customize this high-level summary in your review settings.

Back end
========
- ModuleIO, add On off button pin
- ModuleLightsControl: add pinToggleOnOff
@coderabbitai
Copy link

coderabbitai bot commented Nov 30, 2025

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'review', 'context'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Standardizes module/control APIs (renaming rootcontrols/stateJson/newData), replaces manual JSON control construction with addControl()/addControlValue(), renames PhysicalDriver → ParallelLEDDriver with pin-assignment tracking, adds an On/Off button with debounce, moves Char utilities into Char.h, and removes the VirtualDriver implementation.

Changes

Cohort / File(s) Summary
Core module & state APIs
src/MoonBase/Module.h, src/MoonBase/Module.cpp
Signatures renamed to use const JsonArray& controls, stateJson, newData; added requestUIUpdate and addControlValue helper; adjusted Module loop/update/invoke origins.
Module definitions (addControl/addControlValue)
src/MoonBase/Modules/ModuleIO.h, src/MoonBase/Modules/ModuleDevices.h, src/MoonBase/Modules/ModuleTasks.h, src/MoonLight/Modules/ModuleChannels.h, src/MoonLight/Modules/ModuleLiveScripts.h, src/MoonLight/Modules/ModuleMoonLightInfo.h, src/MoonLight/Modules/ModuleEffects.h, src/MoonBase/Modules/FileManager.*
setupDefinition now accepts controls; replaced in-place Json construction with addControl() and addControlValue() and introduced rows-style control population.
Nodes, NodeManager & control APIs
src/MoonBase/Nodes.h, src/MoonBase/Nodes.cpp, src/MoonBase/NodeManager.h
Added moduleNodes linkage, addControlValue helper; many method signatures changed to take const JsonArray& controls / const JsonObject& control; updateControl/onUpdate simplified; nextPin extended to accept optional ledPinDIO.
Parallel LED driver & PhysicalLayer
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h, src/MoonLight/Layers/PhysicalLayer.h, src/MoonLight/Layers/PhysicalLayer.cpp, src/MoonLight/Nodes/Drivers/parlio.cpp
PhysicalDriverParallelLEDDriver; version uses Char<32>, new status field; added ledPinsAssigned[]; nextPin()nextPin(uint8_t) with assigned-pin tracking and maxChannels adjustments.
Lights control: button & debounce
src/MoonLight/Modules/ModuleLightsControl.h
Added pinToggleOnOff, renamed pinRelaisBrightnesspinRelayBrightness, introduced debounce fields/logic, INPUT_PULLUP handling, and updates pushed to _moduleName + "server".
Layouts & pin mapping features
src/MoonLight/Nodes/Layouts/L_MoonLight.h, src/MoonLight/Layers/PhysicalLayer.*
Layouts now support ledPinDIO and panelsPerPin; layout logic updated to group panels per pin and call nextPin(ledPinDIO).
Driver/node registration & option population
src/MoonLight/Modules/ModuleDrivers.h, src/MoonLight/Nodes/Drivers/*
addNodes now accepts control objects; many drivers replaced manual values.add(...) with addControlValue(control, ...); node->moduleNodes is set for UI linkage; many onUpdate signatures now take const JsonObject&.
Removed Virtual driver
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h, docs/develop/drivers.md
Deleted the VirtualDriver implementation and removed the large drivers.md documentation file.
String utilities / Char
src/MoonBase/Utilities.h, src/MoonBase/Char.h
Removed Char template and related helpers from Utilities.h; added Char<N> implementation in Char.h; logging macros gated by CORE_DEBUG_LEVEL.
Minor drivers/effects/refactors
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h, src/MoonLight/Nodes/Drivers/D_Infrared.h, many src/*/Nodes/Effects/* and src/*/Nodes/Drivers/*
Replaced manual option arrays with addControlValue, changed several onUpdate signatures to pass const JsonObject& control, small naming/label tweaks.
Frontend & UI
interface/src/lib/components/moonbase/MultiRow.svelte, interface/svelte.config.js, interface/src/routes/moonbase/monitor/Monitor.svelte
MultiRow: CRUD-based gating for Add/Delete/drag handles and dragDisabled prop; Svelte kit output bundleStrategy set to 'single'; removed a commented variable.
WiFi behavior
lib/framework/WiFiSettingsService.cpp
Re-enabled STA-mode check: path now calls connectToWiFi() when WiFi mode lacks STA.
Build config & docs nav
platformio.ini, firmware/esp32-s3.ini, mkdocs.yml
Swapped I2SClocklessLedDriver dependency commit, bumped APP_DATE, comment edits, and added docs nav entry for standards/guidelines.
Documentation & guides
docs/develop/*, docs/moonbase/*, docs/moonlight/*, docs/gettingstarted/*
Added standardsguidelines.md, updated IO/pin docs, renamed driver terminology to Parallel LED Driver, and updated developer/process docs.
Small signature/const-ref fixes
many headers across src/* and misc/livescripts/*
Numerous small signature updates to use const JsonObject& control and other const-ref improvements.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas to focus on:

  • Module state read/update refactor in Module.cpp / ModuleState (JSON diff/merge correctness, originId handling).
  • Pin assignment and mapping changes: PhysicalLayer::nextPin(uint8_t), ledPinsAssigned[], and interactions with layouts (ledPinDIO, panelsPerPin).
  • ParallelLEDDriver rename and Char<32> adoption — memory/copy semantics and UI bindings.
  • ModuleLightsControl debounce logic and INPUT_PULLUP polarity/edge handling and the new server update routing.
  • Global migration to addControl/addControlValue — verify defaults, values arrays, and backward-compatible UI expectations.
  • Ensure removal of VirtualDriver does not leave unresolved references or missing expected behavior.

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 a rabbit notes in the build log:
controls renamed, rows lined up in a row,
pins now remember where their tiny currents go.
a button bounces softly, debounced in the night,
Char hopped to a new file and everything feels right.
Parallel drivers hum — the codebase took a leap.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.64% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Board presets tuning' is directly related to the main changeset, which focuses on tuning board-level presets including pin assignments, relay naming, button controls, and related configuration changes across ModuleIO and ModuleLightsControl modules.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch board-preset-tuning

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80a6496 and e9f0c39.

📒 Files selected for processing (2)
  • src/MoonBase/Modules/ModuleIO.h (3 hunks)
  • src/MoonLight/Modules/ModuleLightsControl.h (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
src/MoonBase/Modules/ModuleIO.h (2)

275-275: LGTM - Board preset updated correctly.

The SE16V1 board pin 46 usage is correctly updated to use the new pin_Button_OnOff type.


380-384: LGTM - Default LED pin assignments expanded.

The default wiring now includes LED outputs for pins 4-8, providing better out-of-box coverage for boards without specific presets.

src/MoonLight/Modules/ModuleLightsControl.h (3)

28-29: LGTM - Clearer initialization for unsigned pin tracking.

Changing from -1 to UINT8_MAX makes the sentinel value explicit for uint8_t types. Adding pinToggleOnOff follows the same pattern as pinRelaisBrightness.


74-84: LGTM - Pin configuration read correctly.

The logic properly resets pin tracking variables and scans the IO configuration to populate pinToggleOnOff when pin_Button_OnOff usage is found.


286-286: Note: Update origin changed to server suffix.

The update call now uses _moduleName + "server" instead of just _moduleName. This change also appears at line 298 for the button toggle. Verify this is the intended pattern for distinguishing server-originated updates from UI updates.

Back end
========
- Nodes: add addControlValue, add ledPinDIO as argument to nextPin
- Physical layer: add ledPinsAssigned, add ledPinDIO as argument to nextPin
- Physical driver: use ledPinsAssigned to fill pins array (to do for other drivers!)
- MoonLight layouts: add ledPinDIO in SingleLineLayout and SingleRowLayout
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/MoonLight/Nodes/Layouts/L_MoonLight.h (4)

259-259: Uninitialized member variable ledPinDIO.

The ledPinDIO member is uninitialized, which means it could hold a garbage value before setup() is called or if the control's default isn't properly set. Consider initializing to 0 or UINT8_MAX to ensure predictable behavior.

-  uint8_t ledPinDIO;
+  uint8_t ledPinDIO = 0;

266-271: Magic number 20 should reference MAXLEDPINS.

The loop uses a hardcoded 20 which corresponds to MAXLEDPINS. Consider using the constant directly to ensure consistency if the limit changes.

-    for (int i = 0; i < 20; i++) {
+    for (int i = 0; i < MAXLEDPINS; i++) {

299-299: Uninitialized member variable ledPinDIO.

Same issue as in SingleLineLayout - initialize to a default value.

-  uint8_t ledPinDIO;
+  uint8_t ledPinDIO = 0;

306-311: Consider extracting duplicate control setup logic.

The LED pin DIO control setup (lines 306-311) is identical to SingleLineLayout (lines 266-271). Consider extracting this into a helper function to reduce duplication.

src/MoonLight/Layers/PhysicalLayer.cpp (1)

175-193: LGTM with minor logging suggestion.

The implementation correctly handles both explicit pin assignment (ledPinDIO != UINT8_MAX) and default sequential ordering. Consider enhancing the log message to include the assigned pin value for easier debugging.

-      EXT_LOGD(ML_TAG, "nextPin #%d ledsPerPin:%d of %d", i, ledsPerPin[i], MAXLEDPINS);
+      EXT_LOGD(ML_TAG, "nextPin #%d ledsPerPin:%d of %d assigned:%d", i, ledsPerPin[i], MAXLEDPINS, ledPinsAssigned[i]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d79aaf and 705e0a8.

📒 Files selected for processing (5)
  • src/MoonBase/Nodes.h (2 hunks)
  • src/MoonLight/Layers/PhysicalLayer.cpp (2 hunks)
  • src/MoonLight/Layers/PhysicalLayer.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (2 hunks)
  • src/MoonLight/Nodes/Layouts/L_MoonLight.h (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/MoonBase/Nodes.h (1)
src/MoonLight/Layers/PhysicalLayer.cpp (2)
  • nextPin (175-194)
  • nextPin (175-175)
src/MoonLight/Nodes/Layouts/L_MoonLight.h (2)
src/MoonBase/Nodes.h (1)
  • nextPin (188-188)
src/MoonLight/Layers/PhysicalLayer.cpp (2)
  • nextPin (175-194)
  • nextPin (175-175)
src/MoonLight/Layers/PhysicalLayer.cpp (1)
src/MoonBase/Nodes.h (1)
  • nextPin (188-188)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
src/MoonLight/Nodes/Layouts/L_MoonLight.h (2)

285-285: LGTM!

The nextPin(ledPinDIO) call correctly passes the user-selected pin index to the physical layer for pin assignment mapping.


325-325: LGTM!

The nextPin(ledPinDIO) call is consistent with the SingleLineLayout implementation.

src/MoonBase/Nodes.h (1)

188-188: LGTM!

The updated nextPin signature with the default UINT8_MAX parameter maintains backward compatibility while enabling explicit pin DIO mapping when needed.

src/MoonLight/Layers/PhysicalLayer.h (1)

129-129: LGTM!

The updated nextPin signature correctly accepts an optional ledPin parameter with a sensible default of UINT8_MAX (sentinel value).

src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (2)

106-106: LGTM!

The initled call correctly uses the mapped pins array instead of the raw ledPins, ensuring the LED DIO assignment is respected during initialization.


119-119: LGTM!

The updateDriver call consistently uses the mapped pins array, maintaining alignment with the initialization path.

Back end
========
- ModuleIO.h: bugfix add Button OnOff to select
- Nodes.h: addControlValue multiple types
- Physical layer: onLayoutPre: memset ledPinsAssigned
- Lights control: pinToggleOnOff pinmode pull up, loop pinToggleOnOff check on debounce and LOW
- Layout SingleLine/Row: ledPinDIO default
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
src/MoonBase/Utilities.h (1)

402-410: Good refactor, but remove commented-out code.

The refactoring to use allocMB<T>(1, "object") improves consistency and adds error logging. However, the commented-out old implementation (line 403) should be removed to keep the codebase clean.

Apply this diff to remove the commented-out code:

 T* allocMBObject(Args&&... args) {
   void* res = allocMB<T>(1, "object");
-  // T* res = (T*)heap_caps_malloc_prefer(sizeof(T), 2, MALLOC_CAP_SPIRAM, MALLOC_CAP_8BIT);  // MALLOC_CAP_8BIT General-purpose byte-addressable RAM (safe for objects)
-
   if (res) {
     return new (res) T(std::forward<Args>(args)...);
   } else {
lib/framework/EventSocket.cpp (1)

144-164: Good defensive programming for heap-constrained devices.

The heap-aware reservation logic prevents crashes on ESP32-D0 by checking available memory before reserving. The try/catch block and graceful degradation to dynamic growth is well-designed.

Consider extracting the 4096-byte safety margin as a named constant for clarity:

+        constexpr size_t HEAP_SAFETY_MARGIN = 4096;
+        
         // Only reserve if we have enough memory + safety margin
-        if (requiredSize > 0 && largestBlock > requiredSize + 4096) {
+        if (requiredSize > 0 && largestBlock > requiredSize + HEAP_SAFETY_MARGIN) {
             try {
                 outBuffer.reserve(requiredSize);
             } catch (const std::bad_alloc& e) {
src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (1)

90-98: LED pin remapping via ledPinsAssigned looks correct and bounds-safe

nrOfPins = min(layerP.nrOfLedPins, layerP.nrOfAssignedPins) and the assignedPin < layerP.nrOfLedPins guard ensure all accesses to layerP.ledPins[assignedPin] and pins[i] stay within initialized ranges, while the fallback to layerP.ledPins[i] keeps behavior sane for out-of-range assignments. Using pins for both initled and updateDriver cleanly wires the driver to the new mapping.

Optionally, you could log assignedPin (and/or ledPinsAssigned[i]) in the EXT_LOGD to make it easier to diagnose mismatches between logical DIO indices and physical pins during debugging.

Also applies to: 106-107, 119-119

src/MoonLight/Nodes/Layouts/L_MoonLight.h (2)

259-272: SingleLineLayout: LED DIO selection correctly wired into nextPin

The ledPinDIO select and its values (0 = "Default", 1..MAXLEDPINS = "LED NN") map cleanly into nextPin(ledPinDIO == 0 ? UINT8_MAX : ledPinDIO - 1), which aligns with the updated nextPin(uint8_t ledPinDIO) API (using UINT8_MAX as “no override”). Label generation via Char<8> and MAXLEDPINS is safe and straightforward.

You might slightly expand the comment on ledPinDIO to spell out the value semantics (e.g., // 0 = default order, 1..N = explicit LED DIO index) for future readers.

Also applies to: 286-287


300-313: SingleRowLayout: mirrors SingleLineLayout DIO override as expected

This layout mirrors the ledPinDIO handling from SingleLineLayout, with the same select options and the same nextPin(ledPinDIO == 0 ? UINT8_MAX : ledPinDIO - 1) conversion. That keeps behavior consistent between horizontal and vertical single-strip layouts and cleanly hooks into the PhysicalLayer override logic.

As above, consider a brief clarifying comment on the meaning of the ledPinDIO values to make the override behavior obvious at a glance.

Also applies to: 327-328

src/MoonLight/Layers/PhysicalLayer.cpp (1)

41-47: Mappings allocation and nextPin(uint8_t) override behavior are coherent with the new LED pin model

Splitting PSRAM (or fixed heap) between lights.maxChannels and lights.maxMappings, then allocating both lights.channels and lights.mappings, brings the physical layer in line with the richer mapping model. Resetting ledsPerPin to 0xFFFF and ledPinsAssigned to 0 in onLayoutPre() (for pass 1, non-monitor) matches the sentinel logic used in nextPin and avoids stale data between layouts.

The new nextPin(uint8_t ledPinDIO) correctly:

  • Finds the first free pin slot via the UINT16_MAX sentinel in ledsPerPin.
  • Stores the number of lights on that pin as lights.header.nrOfLights - prevNrOfLights.
  • Uses ledPinDIO != UINT8_MAX to distinguish explicit overrides from default wiring, assigning either the provided DIO index or the default i.

This lines up with the updated Node nextPin signature and with how ledPinsAssigned is later consumed by the driver. If you ever need to be extremely tight on heap, you could consider freeing lights.channels when lights.mappings fails to allocate (or vice versa), but that’s a minor, one-shot optimization rather than a correctness issue.

Also applies to: 50-53, 56-57, 141-143, 181-198

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 705e0a8 and 4cff6c2.

📒 Files selected for processing (14)
  • lib/framework/EventSocket.cpp (3 hunks)
  • src/MoonBase/Modules/ModuleIO.h (4 hunks)
  • src/MoonBase/Nodes.h (2 hunks)
  • src/MoonBase/SharedHttpEndpoint.h (1 hunks)
  • src/MoonBase/SharedWebSocketServer.h (1 hunks)
  • src/MoonBase/Utilities.h (4 hunks)
  • src/MoonLight/Layers/PhysicalLayer.cpp (4 hunks)
  • src/MoonLight/Layers/PhysicalLayer.h (2 hunks)
  • src/MoonLight/Layers/VirtualLayer.cpp (9 hunks)
  • src/MoonLight/Layers/VirtualLayer.h (2 hunks)
  • src/MoonLight/Modules/ModuleLightsControl.h (3 hunks)
  • src/MoonLight/Modules/ModuleMoonLightInfo.h (3 hunks)
  • src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (2 hunks)
  • src/MoonLight/Nodes/Layouts/L_MoonLight.h (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/MoonBase/Modules/ModuleIO.h
  • src/MoonBase/Nodes.h
  • src/MoonLight/Layers/PhysicalLayer.h
🧰 Additional context used
🧬 Code graph analysis (4)
src/MoonLight/Nodes/Layouts/L_MoonLight.h (1)
src/MoonBase/Nodes.h (1)
  • nextPin (190-190)
src/MoonLight/Modules/ModuleMoonLightInfo.h (1)
src/MoonBase/Module.cpp (2)
  • addControl (355-364)
  • addControl (355-355)
src/MoonLight/Layers/PhysicalLayer.cpp (1)
src/MoonBase/Nodes.h (1)
  • nextPin (190-190)
src/MoonLight/Modules/ModuleLightsControl.h (3)
src/MoonBase/Modules/FileManager.cpp (2)
  • update (58-139)
  • update (58-58)
src/MoonBase/Module.cpp (2)
  • update (262-293)
  • update (262-262)
src/MoonBase/Module.h (1)
  • ModuleState (47-137)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (20)
src/MoonLight/Modules/ModuleLightsControl.h (4)

24-29: Good use of UINT8_MAX sentinels for pin fields

Initializing pinRelaisBrightness and pinToggleOnOff to UINT8_MAX matches the uint8_t type and the later != UINT8_MAX checks, avoiding the old signed/unsigned mismatch.


72-85: Pin scanning reset and button pin configuration look correct

Resetting both pin members to UINT8_MAX before iterating state.data["pins"] prevents stale GPIO usage, and configuring pinToggleOnOff as INPUT_PULLUP is consistent with the active‑low handling in loop(). Logging both pins should also help diagnose wiring/mapping issues.


283-288: Origin ID change to "<module>server" is reasonable

Routing preset auto‑advance updates through update(newState, ModuleState::update, _moduleName + "server") is consistent with the new on/off button path and should help distinguish server‑initiated changes from UI ones, assuming listeners are aware of / expect this origin ID.


292-308: Debounced on/off button handling is robust

The new block correctly:

  • Guards on pinToggleOnOff != UINT8_MAX.
  • Debounces using lastDebounceTime and a 50 ms window.
  • Triggers only on the HIGH→LOW edge (for INPUT_PULLUP), then toggles lightsOn via update(..., _moduleName + "server").

This addresses the earlier concerns about missing pinMode, no debounce, and double‑edge triggering.

src/MoonLight/Layers/VirtualLayer.h (1)

70-70: Heap optimization: raw pointer for mappingTable.

The change from std::vector to a raw pointer aligns with the stated heap optimization goals. The memory is now managed externally via layerP->lights.mappings, which is appropriate for embedded systems with limited RAM.

src/MoonLight/Layers/VirtualLayer.cpp (7)

35-38: Good use of reference in range-based for loop.

Using std::vector<uint16_t>& instead of copying each inner vector improves efficiency during cleanup.


41-44: Cast assumes proper alignment of layerP->lights.mappings.

The cast to PhysMap* is acceptable assuming the source buffer is properly aligned. Since PhysMap is a 2-byte structure, this should work on typical embedded platforms.


74-96: Improved state transition comments.

The added comments clearly document the mapping state transitions, improving code readability.


121-150: Bounds check updated to use nrOfLights.

The change from mappingTableSizeUsed to nrOfLights is consistent with the new mapping table design where nrOfLights represents the number of valid virtual light entries.


152-185: Consistent bounds check in getLight.

The update to use nrOfLights as the upper bound is consistent with the setLight function changes.


309-326: Proper bounds validation in addLight.

The null check and bounds validation indexV < layerP->lights.maxMappings / sizeof(PhysMap) correctly converts byte size to element count. This confirms maxMappings is in bytes.


359-377: Minor formatting improvement.

The added inline comment clarifies the single-pixel case handling.

src/MoonBase/Utilities.h (2)

361-361: LGTM! Error message now matches the actual operation.

The error log correctly identifies heap_caps_realloc as the failing operation, improving debugging accuracy.


425-427: LGTM! Formatting improvement.

Minor formatting adjustments improve readability without changing logic.

lib/framework/EventSocket.cpp (1)

179-179: LGTM! Enhanced logging improves debugging.

Adding len and originId to the warning message provides valuable context when troubleshooting unregistered event emissions.

src/MoonBase/SharedWebSocketServer.h (1)

122-122: LGTM! Error logging improves debugging.

Adding explicit error logging when a module is not found makes troubleshooting easier and aligns with the similar improvement in SharedHttpEndpoint.h.

src/MoonBase/SharedHttpEndpoint.h (3)

36-38: LGTM! Standard CORS preflight handling.

The OPTIONS handler is properly feature-gated and implements standard CORS preflight response.


93-93: LGTM! Consistent error logging.

Error logging when module is not found improves debugging and aligns with SharedWebSocketServer.h.


64-86: This is a new endpoint, not a modification to existing behavior.

SharedHttpEndpoint.h is a new file introducing unified HTTP endpoint handling for modules. The POST handler returning updated state is the intentional design of this new implementation, consistent with the GET handler pattern. No clients previously existed for this endpoint, so this cannot be a breaking change.

Likely an incorrect or invalid review comment.

src/MoonLight/Modules/ModuleMoonLightInfo.h (1)

24-47: MoonLight info schema refactor and maxMappings exposure look consistent

Refactoring setupDefinition to use addControl for the root metrics plus a layers rows control makes the schema much clearer and more in line with the rest of the module framework. Adding maxMappings both to the schema and to the readHook (data["maxMappings"] = layerP.lights.maxMappings;) keeps the UI in sync with the new backend capacity field.

Within the per-layer loop, counting nrOfZeroLights/nrOfOneLight/nrOfMoreLights over i < layer->nrOfLights is consistent with a per-light mappingTable and matches how the rest of the struct is populated, so the derived stats should be accurate given the current data model.

Also applies to: 58-59, 68-95

{
// 🌙 adding semaphore wait too long logging
if (xSemaphoreTake(clientSubscriptionsMutex, pdMS_TO_TICKS(100))==pdFALSE) {
if (xSemaphoreTake(clientSubscriptionsMutex, pdMS_TO_TICKS(200)) == pdFALSE) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent mutex timeout values across similar code paths.

The mutex wait timeout in onWSClose is increased to 200ms, while emitEvent at line 185 remains at 100ms. If the original 100ms timeout was problematic enough to warrant increasing it to 200ms in one location, the same issue likely applies to both code paths.

Consider standardizing the timeout value:

-    if (xSemaphoreTake(clientSubscriptionsMutex, pdMS_TO_TICKS(100)) == pdFALSE) {
+    if (xSemaphoreTake(clientSubscriptionsMutex, pdMS_TO_TICKS(200)) == pdFALSE) {
         ESP_LOGW(SVK_TAG, "clientSubscriptionsMutex wait too long");
         xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY);
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/framework/EventSocket.cpp around line 45 (and also update emitEvent at
line 185), the mutex wait timeouts are inconsistent (200ms at onWSClose vs 100ms
in emitEvent); replace the magic numbers with a single named constant (e.g.
CLIENT_SUBSCRIPTIONS_MUTEX_TIMEOUT_MS = 200) and use
pdMS_TO_TICKS(CLIENT_SUBSCRIPTIONS_MUTEX_TIMEOUT_MS) in both places (and any
other similar code paths) to standardize behavior and make future adjustments
easy.

Comment on lines 345 to 347
T* res = (T*)heap_caps_malloc_prefer(n * sizeof(T), 2, MALLOC_CAP_SPIRAM, MALLOC_CAP_DEFAULT); // calloc is malloc + memset(0);
if (res) {
memset(res, 0, n * sizeof(T)); //calloc not always save so do it explicit
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for heap_caps_calloc_prefer usage
echo "=== Searching for heap_caps_calloc_prefer usage ==="
rg -n 'heap_caps_calloc' --type cpp --type c -C 2

echo -e "\n=== Searching for calloc-related safety comments ==="
rg -n 'calloc.*safe|calloc.*bug|calloc.*issue' --type cpp --type c -C 3

echo -e "\n=== Searching for comments about this specific change ==="
rg -n 'calloc not always' --type cpp --type c -C 3

echo -e "\n=== Checking the full context of allocMB function ==="
rg -n 'allocMB|heap_caps_malloc_prefer' src/MoonBase/Utilities.h -B 5 -A 10

Repository: MoonModules/MoonLight

Length of output: 5222


Fix typo and reconsider the malloc+memset replacement—no documented issue with heap_caps_calloc_prefer found.

The claim that "calloc not always save" (typo: "safe") is unsubstantiated. The codebase itself uses heap_caps_calloc_prefer elsewhere (e.g., src/MoonLight/Nodes/Drivers/parlio.cpp lines 328–333) without any documented issues. The change to malloc + memset appears unnecessary unless there's a specific platform issue that should be documented.

Additionally, fix the typo in the comment: "save" → "safe".

For consistency, either:

  • Revert allocMB to use heap_caps_calloc_prefer (preferred, simpler and equivalent), or
  • Document the specific issue/platform limitation with a reference if the change is required.
🤖 Prompt for AI Agents
In src/MoonBase/Utilities.h around lines 345 to 347, the replacement of
heap_caps_calloc_prefer with malloc + memset contains a typo in the comment
("save" -> "safe") and is undocumented/unnecessary; revert allocMB to use
heap_caps_calloc_prefer for zero-initialized allocation (to match other usages
like src/MoonLight/Nodes/Drivers/parlio.cpp) OR if malloc+memset is truly
required, add a clear comment documenting the specific platform/implementation
bug with a reference, then fix the comment typo to "safe".

Comment on lines 298 to 307

// resetMapping

for (std::vector<uint16_t>& mappingTableIndex : mappingTableIndexes) {
mappingTableIndex.clear();
}
mappingTableIndexesSizeUsed = 0; // do not clear mappingTableIndexes, reuse it

memset(mappingTable, 0, layerP->lights.maxMappings); // reset mappingTable
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for maxMappings definition and usage
rg -n "maxMappings" --type=cpp -B2 -A2 | head -100

Repository: MoonModules/MoonLight

Length of output: 4884


🏁 Script executed:

# Find mappingTable declaration and type
rg -n "mappingTable\s*=" --type=cpp -B2 -A2 | head -100

Repository: MoonModules/MoonLight

Length of output: 1310


🏁 Script executed:

# Find PhysMap type definition
rg -n "struct\s+PhysMap|class\s+PhysMap|typedef.*PhysMap" --type=cpp -B2 -A2

Repository: MoonModules/MoonLight

Length of output: 378


🏁 Script executed:

# Look at the context around the code in question
sed -n '280,320p' src/MoonLight/Layers/VirtualLayer.cpp

Repository: MoonModules/MoonLight

Length of output: 1386


🏁 Script executed:

# Search for onLayoutPre calls and setup order
rg -n "onLayoutPre|\.setup\(\)" --type=cpp src/MoonLight/Layers/VirtualLayer.cpp | head -20

Repository: MoonModules/MoonLight

Length of output: 103


🏁 Script executed:

# Search for when onLayoutPre is called
rg -n "onLayoutPre" --type=cpp -B2 -A2 | head -100

Repository: MoonModules/MoonLight

Length of output: 3727


🏁 Script executed:

# Check Layer base class or initialization patterns
rg -n "class VirtualLayer|class Layer" --type=h src/MoonLight/Layers/ -B2 -A5

Repository: MoonModules/MoonLight

Length of output: 1461


Add null check before memset on mappingTable.

The mappingTable is initialized as nullptr and only assigned in setup(), but onLayoutPre() may be called during layout mapping before setup completes. Without a null check, calling memset on a null pointer causes undefined behavior. This pattern is already used defensively elsewhere in the code (line 318: if (mappingTable && ...)).

The size parameter (layerP->lights.maxMappings) is correct—it represents bytes allocated for the mappingTable buffer, not a count.

  // resetMapping
  for (std::vector<uint16_t>& mappingTableIndex : mappingTableIndexes) {
    mappingTableIndex.clear();
  }
  mappingTableIndexesSizeUsed = 0;  // do not clear mappingTableIndexes, reuse it

-  memset(mappingTable, 0, layerP->lights.maxMappings);  // reset mappingTable
+  if (mappingTable) memset(mappingTable, 0, layerP->lights.maxMappings);  // reset mappingTable
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// resetMapping
for (std::vector<uint16_t>& mappingTableIndex : mappingTableIndexes) {
mappingTableIndex.clear();
}
mappingTableIndexesSizeUsed = 0; // do not clear mappingTableIndexes, reuse it
memset(mappingTable, 0, layerP->lights.maxMappings); // reset mappingTable
}
// resetMapping
for (std::vector<uint16_t>& mappingTableIndex : mappingTableIndexes) {
mappingTableIndex.clear();
}
mappingTableIndexesSizeUsed = 0; // do not clear mappingTableIndexes, reuse it
if (mappingTable) memset(mappingTable, 0, layerP->lights.maxMappings); // reset mappingTable
}
🤖 Prompt for AI Agents
In src/MoonLight/Layers/VirtualLayer.cpp around lines 298 to 307, the code calls
memset(mappingTable, 0, layerP->lights.maxMappings) without ensuring
mappingTable is non-null; add a null check so the memset runs only if
mappingTable is not null (e.g. if (mappingTable) { memset(...); }), leaving the
size parameter layerP->lights.maxMappings unchanged and preserving the existing
comment about resetting mappingTable.


// checks if a virtual light is mapped to a physical light (use with XY() or XYZ() to get the indexV)
bool isMapped(int indexV) const { return indexV < mappingTableSizeUsed && (mappingTable[indexV].mapType == m_oneLight || mappingTable[indexV].mapType == m_moreLights); }
bool isMapped(int indexV) const { return indexV < nrOfLights && (mappingTable[indexV].mapType == m_oneLight || mappingTable[indexV].mapType == m_moreLights); }
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential null pointer dereference in isMapped().

If isMapped() is called before setup() initializes mappingTable, accessing mappingTable[indexV] will dereference a null pointer. Consider adding a null check:

-  bool isMapped(int indexV) const { return indexV < nrOfLights && (mappingTable[indexV].mapType == m_oneLight || mappingTable[indexV].mapType == m_moreLights); }
+  bool isMapped(int indexV) const { return mappingTable && indexV < nrOfLights && (mappingTable[indexV].mapType == m_oneLight || mappingTable[indexV].mapType == m_moreLights); }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool isMapped(int indexV) const { return indexV < nrOfLights && (mappingTable[indexV].mapType == m_oneLight || mappingTable[indexV].mapType == m_moreLights); }
bool isMapped(int indexV) const { return mappingTable && indexV < nrOfLights && (mappingTable[indexV].mapType == m_oneLight || mappingTable[indexV].mapType == m_moreLights); }
🤖 Prompt for AI Agents
In src/MoonLight/Layers/VirtualLayer.h around line 211, isMapped() dereferences
mappingTable without checking it; add a guard to first verify mappingTable is
non-null and indexV is within valid range (indexV >= 0 && indexV < nrOfLights)
and return false if either check fails, then perform the existing mapType
comparison; optionally add an assertion or error log for unexpected states.

@ewowi ewowi force-pushed the board-preset-tuning branch from 4cff6c2 to a1341fd Compare December 1, 2025 08:27
Front end
========
- svelte config: add build strategy single

Back end
=======
- WiFi settings service: revert  check on getmode and wifi sta
- Module: move requestUIUpdate from Node manager to module (used also in driver)
- Nodes: add moduleNodes , add updateControl by control name
- Physical layer: increase max channels for non psram to 8192
- Drivers: readPins: do not check on ledsPerPin! add moduleNodes (for UI Update)
- Physical driver: add status control and set onLayout
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (2)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1)

50-56: Const-ref control is fine; fix DDP vs ddp name mismatch.

The new onUpdate(const JsonObject& control) matches the base API and is fine. However, setup() registers the checkbox as "DDP" (upper-case), while onUpdate checks for "ddp", so the condition never fires and the handler is effectively dead code.

Consider aligning the names:

-    if (control["name"] == "ddp") {
+    if (control["name"] == "DDP") {
       if (control["value"] == 0) {
       }
     }
lib/framework/WiFiSettingsService.cpp (1)

314-321: The WiFi.getMode() gate blocks reconnection in AP+STA mode

This condition prevents reconnection after WiFi disconnects when the device is in AP+STA mode. After a STA disconnection, WiFi.getMode() remains WIFI_AP_STA (value 3), so (3 & 1) evaluates to 1 (non-zero), causing the condition to fail and connectToWiFi() to be skipped. This is the exact issue reported in ESP32-sveltekit theelims#109.

The existing guards already handle the critical cases: WiFi.isConnected(), _state.wifiSettings.empty(), and _state.staConnectionMode == OFFLINE prevent spurious reconnection attempts. Combined with the caller's throttling via WIFI_RECONNECTION_DELAY, these are sufficient safeguards.

Remove the getMode() conditional to allow reconnection:

-    // Connect or reconnect as required
-    if ((WiFi.getMode() & WIFI_STA) == 0) // 🌙 , see https://github.com/theelims/ESP32-sveltekit/issues/109 🚧
-    {
-#ifdef SERIAL_INFO
-        Serial.println("Connecting to WiFi...");
-#endif
-        connectToWiFi();
-    }
+    // Connect or reconnect as required
+#ifdef SERIAL_INFO
+    Serial.println("Connecting to WiFi...");
+#endif
+    connectToWiFi();
🧹 Nitpick comments (6)
src/MoonLight/Layers/PhysicalLayer.cpp (1)

176-193: nextPin overload and assignment logic are consistent; consider future‑proofing the index type

The new nextPin(uint8_t ledPinDIO) overload cleanly supports explicit pin ordering:

  • You scan for the first UINT16_MAX slot, accumulate prevNrOfLights, and store the delta in ledsPerPin[i] as before.
  • ledPinsAssigned[i] is set either to the explicit ledPinDIO or to the default sequential index i when ledPinDIO == UINT8_MAX, which lines up with the wrapper in Nodes.h using UINT8_MAX as the sentinel.
  • Logging now reports the assigned pin, which should simplify debugging mismatched wiring vs. layout.

Given the comment that you target up to 120 pins, the uint8_t i index is fine today, but if MAXLEDPINS ever increases beyond 255 this could overflow and produce incorrect iteration. As a low‑priority hardening, you could widen the index type:

-    uint8_t i = 0;
+    uint16_t i = 0;

Everything else in this block looks logically sound and aligned with the new per‑pin assignment model.

src/MoonBase/Modules/ModuleIO.h (1)

381-387: Commented-out default LED pins could be simplified into a TODO

The commented assignments for LED_02LED_06 and the note about esp32-d0-16MB are harmless, but commented-out code tends to age poorly. Consider replacing these lines with a single // TODO that briefly describes the incompatibility and links to an issue, or remove them once the pin selection is finalized.

src/MoonLight/Modules/ModuleEffects.h (1)

283-286: Wiring moduleNodes to the owning module is appropriate.

Hooking node->moduleNodes = (Module*)this; ensures effects can request UI updates via the parent module, consistent with the new Module::requestUIUpdate flow. You might prefer static_cast<Module*>(this) for clearer intent, but behavior is correct as-is.

src/MoonBase/Module.h (1)

142-143: Module-level requestUIUpdate flow looks consistent with the new node linkage.

Centralizing UI refresh via Module::requestUIUpdate and triggering a StateUpdateResult::CHANGED in loop() aligns with the new node->moduleNodes wiring and removes the NodeManager-specific flag. Assuming this flag is only set from the same task (or benign races are acceptable), the approach is fine; if you ever start toggling it from other tasks, consider a slightly more robust signaling mechanism (e.g., using the existing semaphore-based update path) to avoid lost UI updates.

Also applies to: 155-165

src/MoonLight/Modules/ModuleDrivers.h (1)

54-56: Pin compaction and node–module wiring look sound.

  • The compaction loop now keeps any ledPins[readPos] != UINT8_MAX, which is consistent with using UINT8_MAX as a sentinel and keeps nrOfLedPins in sync with the compacted prefix.
  • Assigning both node->moduleIO and node->moduleNodes = (Module*)this correctly exposes IO configuration and the module-level UI update hook to driver/layout nodes.

If you ever change the type of ledPins away from uint8_t, consider swapping sizeof(layerP.ledPins) for an explicit element-count to avoid subtle bugs, but it's fine with the current definition.

Also applies to: 160-164

src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (1)

86-87: Pin remapping and driver updates look correct; consider guarding moduleNodes.

  • nrOfPins = min(layerP.nrOfLedPins, layerP.nrOfAssignedPins) combined with assignedPin < layerP.nrOfLedPins ensures you never index ledPins out of bounds; the fallback to ledPins[i] keeps behavior sensible if an assignment index is invalid.
  • Using a local pins[MAX_PINS] and passing it (with nrOfPins) into initled and updateDriver keeps the original layerP.ledPins array untouched while still honoring the per-pin mapping.
  • Building a human-readable statusString from pins[i] and ledsPerPin[i] is useful for debugging and is correctly pushed back into the control before requesting a UI update.

To make this a bit more robust, it would be safer to guard the UI signal:

updateControl("status", statusString.c_str());
if (moduleNodes) {
  moduleNodes->requestUIUpdate = true;
}

so that accidental use of PhysicalDriver outside a ModuleDrivers context (where moduleNodes may not be initialized) cannot crash.

Also applies to: 93-105, 117-118, 130-131

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4cff6c2 and a0b3805.

📒 Files selected for processing (20)
  • interface/svelte.config.js (1 hunks)
  • lib/framework/WiFiSettingsService.cpp (1 hunks)
  • platformio.ini (2 hunks)
  • src/MoonBase/Module.h (2 hunks)
  • src/MoonBase/Modules/ModuleIO.h (4 hunks)
  • src/MoonBase/NodeManager.h (1 hunks)
  • src/MoonBase/Nodes.cpp (2 hunks)
  • src/MoonBase/Nodes.h (6 hunks)
  • src/MoonBase/Utilities.h (1 hunks)
  • src/MoonLight/Layers/PhysicalLayer.cpp (4 hunks)
  • src/MoonLight/Modules/ModuleDrivers.h (2 hunks)
  • src/MoonLight/Modules/ModuleEffects.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_Infrared.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (4 hunks)
  • src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D__Sandbox.h (1 hunks)
  • src/MoonLight/Nodes/Effects/E_MoonLight.h (2 hunks)
  • src/MoonLight/Nodes/Effects/E_MoonModules.h (1 hunks)
  • src/MoonLight/Nodes/Effects/E__Sandbox.h (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • platformio.ini
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/interface/svelte.config.js : Configure SvelteKit build and adapter settings in `interface/svelte.config.js`

Applied to files:

  • interface/svelte.config.js
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/interface/**/*.{svelte,ts,tsx,js,json} : Modify frontend code in the `interface/` directory using SvelteKit

Applied to files:

  • interface/svelte.config.js
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/interface/vite.config.ts : Configure Vite build tool, proxy settings, and LittleFS compatibility in `interface/vite.config.ts`

Applied to files:

  • interface/svelte.config.js
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/interface/src/routes/**/*.svelte : Create Svelte routes in `interface/src/routes/` for different pages of the web application

Applied to files:

  • interface/svelte.config.js
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/lib/framework/**/*.{h,cpp} : Leverage the framework services in `lib/framework/` for WiFi management, MQTT, NTP, security, firmware updates, and system utilities

Applied to files:

  • lib/framework/WiFiSettingsService.cpp
🧬 Code graph analysis (5)
src/MoonLight/Layers/PhysicalLayer.cpp (1)
src/MoonBase/Nodes.h (1)
  • nextPin (199-199)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (5)
src/MoonBase/Nodes.cpp (2)
  • onUpdate (401-541)
  • onUpdate (401-401)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
  • onUpdate (926-930)
  • onUpdate (1122-1126)
src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
  • onUpdate (121-125)
src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
  • onUpdate (35-41)
src/MoonLight/Nodes/Effects/E_MoonModules.h (7)
src/MoonBase/Modules/ModuleIO.h (1)
  • onUpdate (401-421)
src/MoonBase/NodeManager.h (1)
  • onUpdate (133-284)
src/MoonBase/Nodes.cpp (2)
  • onUpdate (401-541)
  • onUpdate (401-401)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
  • onUpdate (31-37)
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
  • onUpdate (926-930)
  • onUpdate (1122-1126)
src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
  • onUpdate (35-41)
src/MoonLight/Nodes/Effects/E_MoonLight.h (4)
src/MoonBase/Nodes.cpp (2)
  • onUpdate (401-541)
  • onUpdate (401-401)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
  • onUpdate (121-125)
src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
  • onUpdate (35-41)
src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (2)
src/MoonBase/Module.cpp (2)
  • addControl (355-364)
  • addControl (355-355)
src/MoonBase/Nodes.cpp (2)
  • updateControl (17-66)
  • updateControl (17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (18)
src/MoonLight/Layers/PhysicalLayer.cpp (2)

44-44: Non‑PSRAM channel capacity increase looks safe

Bumping the non‑PSRAM lights.maxChannels to 8192 * 3 (≈24 KB) stays comfortably below the ~30 KB single‑alloc note in the header comment while doubling the LED capacity. Behavior and intent remain clear and consistent with the comment; no functional issues spotted here.


136-138: Good: pin mapping state is fully reset between layouts

Zeroing ledPinsAssigned alongside resetting ledsPerPin avoids stale pin assignments leaking across layout remaps when !monitorPass. This keeps nextPin’s per‑pin mapping logic operating on a clean slate each pass and matches how nrOfAssignedPins is recomputed.

src/MoonBase/Modules/ModuleIO.h (3)

76-78: New IO_PinUsage entry is added safely before the sentinel

Placing pin_Button_OnOff immediately before pin_count preserves all existing enum values and keeps pin_count as a valid upper bound. This is a safe, backward‑compatible extension.


148-207: UI usage list stays aligned with enum ordinals

Appending "Button OnOff" at the end of the usage select values matches the new IO_PinUsage ordinal, so stored integer usages won’t shift. The mapping between enum and UI label remains consistent.


270-280: SE16V1 preset correctly wires GPIO 46 to the new On/Off button usage

Mapping pins[46]["usage"] to pin_Button_OnOff cleanly introduces the dedicated On/Off button for the SE16V1 preset without conflicting with the LED pin array or other button assignments. This looks coherent with the new button semantics.

src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)

31-31: LGTM! Const-correctness improvement.

The signature change to pass control by const-reference avoids unnecessary copies and aligns with the broader refactoring pattern across the codebase.

interface/svelte.config.js (1)

11-13: LGTM! Single bundle strategy appropriate for embedded deployment.

The bundleStrategy: 'single' configuration is well-suited for embedded systems where filesystem constraints (limited file count in LittleFS) make a single bundle preferable over multiple chunks.

src/MoonLight/Nodes/Effects/E__Sandbox.h (1)

35-35: LGTM! Const-correctness improvement.

The signature change to pass control by const-reference avoids unnecessary copies and aligns with the broader refactoring pattern.

src/MoonLight/Nodes/Drivers/D_Infrared.h (1)

126-126: LGTM! Const-correctness improvement.

The signature change to pass control by const-reference avoids unnecessary copies and is consistent with the broader refactoring pattern across driver and effect nodes.

src/MoonBase/Utilities.h (1)

230-234: LGTM! Useful cross-size concatenation operator.

The templated operator+= enables concatenation between Char instances of different sizes, mirroring the existing operator+=(const char*) pattern. The use of strlcat ensures buffer safety.

src/MoonLight/Nodes/Effects/E_MoonModules.h (1)

121-121: LGTM! Const-correctness improvement.

The signature change to pass control by const-reference avoids unnecessary copies and aligns with the broader refactoring pattern.

src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)

69-69: LGTM! Const-correctness improvement.

The signature change to pass control by const-reference avoids unnecessary copies and is consistent with the broader refactoring pattern across the codebase.

src/MoonBase/Nodes.cpp (2)

17-17: LGTM! Const-correctness improvement.

The signature change to pass control by const-reference avoids unnecessary copies. The method body only reads from the control object, making const-correctness appropriate.


401-401: LGTM! Const-correctness improvement.

The signature change to pass control by const-reference avoids unnecessary copies and aligns with the broader refactoring pattern. The method implementation only reads from the control object.

src/MoonLight/Nodes/Effects/E_MoonLight.h (1)

926-930: Const-ref onUpdate overrides look correct.

Both RubiksCubeEffect::onUpdate and ParticlesEffect::onUpdate now match the base Node::onUpdate signature and only read from control, so passing const JsonObject& is appropriate and avoids copies.

Also applies to: 1122-1126

src/MoonBase/NodeManager.h (1)

260-261: Control object plumbing now correctly uses a shared JsonObject reference.

Using a local JsonObject control = nodeState["controls"][updatedItem.index[1]]; and passing it by const reference into updateControl(control) and onUpdate(oldValue, control) matches the updated Node API and avoids redundant lookups/copies.

Also applies to: 267-269

src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (1)

29-31: version/status controls integrate cleanly with the Node control API.

Exposing version and status as read-only "text" controls and refreshing them via updateControl("version", HP_ALL_VERSION) / "status" lines up with the new Node::updateControl(const char*, T) helper and should give the UI immediate visibility into driver state.

Also applies to: 42-45

src/MoonBase/Nodes.h (1)

56-57: Node–module linkage and control helpers align well with the new architecture.

  • Adding Module* moduleNodes and using it (via callers) to flip Module::requestUIUpdate cleanly connects nodes to the centralized UI update path.
  • Switching constructor and onUpdate/updateControl to take const references avoids unnecessary Json copies and matches how you now pass control objects around from NodeManager.
  • addControl’s explicit JsonObject control = JsonObject(); plus the "valid" flagging continues to support reuse of existing controls.
  • The new addControlValue helper with a size-0 guard safely appends values to the last control, addressing the previous potential underflow.
  • updateControl(const char* name, const T value) is a nice utility that you’re already using in PhysicalDriver for "version"/"status".
  • Extending nextPin to accept an optional ledPinDIO and delegating to layer->layerP->nextPin(ledPinDIO) fits the new per-pin assignment flow.

Overall these changes look consistent and low-risk; any future nodes that rely on moduleNodes just need to ensure their owning NodeManager initializes it, as you’ve done in ModuleDrivers and ModuleEffects.

Also applies to: 65-68, 82-105, 145-147, 152-159, 162-172, 174-175, 199-200, 286-287

@ewowi
Copy link
Collaborator Author

ewowi commented Dec 3, 2025

@coderabbitai,

I have this in my logging:

[ 1465][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3504) status: ok
[ 1473][D][Nodes.h:149] addControl(): [💫] loopTask (3504) status t:text p:0x3c25fb9a ps:32 {"name":"status","type":"text","default":"ok","p":1009122202,"ro":true,"max":32,"valid":true,"value":"ok","size":32}
[ 1493][D][Nodes.cpp:22] updateControl(): [💫] loopTask (3504) version = 20251120 t:text p:0x3c25fb7a
[ 1504][D][NodeManager.h:230] onUpdate(): [💫] loopTask (3504) Physical Driver ☸️ on: true (#2)
[ 1515][W][NodeManager.h:260] onUpdate(): [💫] loopTask (3504) dev control name is null nodes[1]controls[3].value = ok -> null ({"size":32})

You can see status is created as {"name":"status","type":"text","default":"ok","p":1009122202,"ro":true,"max":32,"valid":true,"value":"ok","size":32}

But then a moment later there is only {"size":32}.

Status is a new control in the Physical driver node, so the json for physical driver doesn't contain the status, All other - already existing controls in physical driver work fine.
Idea is that status should be added

This is managed by NodeManager.h. Can you check what could be wrong?

@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read NodeManager.h to examine the onUpdate implementation
cat src/MoonBase/NodeManager.h

Length of output: 14390


🏁 Script executed:

#!/bin/bash
# Read D_PhysicalDriver.h to see how status control is being used
cat src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h

Length of output: 7551


🏁 Script executed:

#!/bin/bash
# Read Nodes.h to see the updateControl implementation
cat src/MoonBase/Nodes.h

Length of output: 12713


🏁 Script executed:

#!/bin/bash
# Read Nodes.cpp to see the updateControl implementation
cat src/MoonBase/Nodes.cpp

Length of output: 24180


🏁 Script executed:

#!/bin/bash
# Search for the warning message "dev control name is null"
rg -n "dev control name is null" --type=cpp

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for "control name is null" more broadly
rg -n "control name" --type=cpp -C 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search around line 260 in NodeManager.h and look for any commented code or recent changes
cat src/MoonBase/NodeManager.h | head -270 | tail -30

Length of output: 2004


🏁 Script executed:

#!/bin/bash
# Look for any TODO or debug statements related to controls
rg -n "nodes\[.*\]controls" --type=cpp -C 2

Length of output: 0


@ewowi, I found the issue!

The problem is in the template updateControl function in Nodes.h around line 189. When you call updateControl("status", statusString.c_str()), it searches through the controls and modifies the JsonObject, but there's a subtle issue with how the control is being accessed and passed.

Here's the problematic code:

template <typename T>
void updateControl(const char* name, const T value) {
  for (JsonObject control : controls) {
    if (control["name"] == name) {
      control["value"] = value;
      updateControl(control);
      break;
    }
  }
}

The issue is that when iterating with for (JsonObject control : controls), you're getting a reference to each control, which is correct. However, when updateControl(control) is called, the control object reference might not be stable depending on how ArduinoJson handles the reference internally during the iteration.

The fix is to use indexed access instead of range-based iteration to ensure we're working with a stable reference:

template <typename T>
void updateControl(const char* name, const T value) {
  for (size_t i = 0; i < controls.size(); i++) {
    JsonObject control = controls[i];
    if (control["name"] == name) {
      control["value"] = value;
      updateControl(control);
      break;
    }
  }
}

This ensures that when we access controls[i], we get a proper JsonObject reference from the array that remains valid throughout the operation.

The reason your existing controls work fine is that they already exist in the state before the first access, but the new "status" control is being added dynamically and might not be fully synchronized between the node's control array and the state's control array when updateControl is called.

Can you try this fix and let me know if it resolves the issue?

@ewowi
Copy link
Collaborator Author

ewowi commented Dec 3, 2025

Can you try this fix and let me know if it resolves the issue?
@coderabbitai, no this didn't solve it, by the way there are more

for (JsonObject control ...

statements in the repo. From what I understand all of these are okay to use like that.

Please dig deeper

@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Let's search for where controls are being added to or synced with the state in NodeManager
rg -n "controls\[" src/MoonBase/NodeManager.h -A 5 -B 5

Length of output: 1389


🏁 Script executed:

#!/bin/bash
# Let's also look at where addControl is defined and how it interacts with state
rg -n "void addControl" src/MoonBase/Nodes.h -A 20

Length of output: 916


🏁 Script executed:

#!/bin/bash
# Search for the specific warning message pattern
rg -n "control name is null" src/MoonBase/NodeManager.h -A 5 -B 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's see how the state is populated initially with controls
rg -n "nodeState\[\"controls\"\]" src/MoonBase/NodeManager.h -A 3 -B 3

Length of output: 2048


🏁 Script executed:

#!/bin/bash
# Let's look at the setup method in NodeManager to see how initial controls are handled
cat src/MoonBase/NodeManager.h | grep -A 50 "void setup()"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for the full addControl implementation
rg -n "void.*addControl" src/MoonBase/Nodes.cpp -A 30

Length of output: 3707


🏁 Script executed:

#!/bin/bash
# Search for the warning message - it's from the user's log, let me search more broadly
rg -n "dev control name" src/MoonBase/ -A 2 -B 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's look at the exact area around line 260 in NodeManager.h where the issue occurs
sed -n '255,275p' src/MoonBase/NodeManager.h

Length of output: 1195


🏁 Script executed:

#!/bin/bash
# Let's look at how addNode is defined and what it does with the controls parameter
rg -n "Node.*addNode" src/MoonBase/NodeManager.h -A 10

Length of output: 1189


🏁 Script executed:

#!/bin/bash
# Let's also check the constructor signature in Nodes.h to see how controls are passed
rg -n "virtual void constructor" src/MoonBase/Nodes.h -A 3

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Let's look at the full addControl implementation in Nodes.h
rg -n "template.*addControl" src/MoonBase/Nodes.h -A 30

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's see when onLayout is called and how it relates to control setup
rg -n "onLayout" src/MoonBase/NodeManager.h -A 5 -B 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's look at the PhysicalDriver to see where updateControl("status") is called
rg -n "updateControl.*status" src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h -A 3 -B 3

Length of output: 216


🏁 Script executed:

#!/bin/bash
# Let's see the full context of how controls array is used in Node
sed -n '130,180p' src/MoonBase/Nodes.h

Length of output: 1946


🏁 Script executed:

#!/bin/bash
# Let's see the full addControl implementation from the beginning
sed -n '90,155p' src/MoonBase/Nodes.h

Length of output: 2830


🏁 Script executed:

#!/bin/bash
# Let's also look at when setup is called vs when onLayout is called in PhysicalDriver
sed -n '1,50p' src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h

Length of output: 1538


🏁 Script executed:

#!/bin/bash
# Let's trace through to see when the control array reference might become stale
rg -n "JsonArray controls" src/MoonBase/Nodes.h -B 2 -A 2

Length of output: 271


🏁 Script executed:

#!/bin/bash
# Look for where controls might be cleared or reassigned
rg -n "controls =" src/MoonBase/Nodes.h -B 2 -A 2

Length of output: 152


🏁 Script executed:

#!/bin/bash
# Let's see the updateControl implementation in Nodes.cpp that takes a JsonObject
sed -n '1,100p' src/MoonBase/Nodes.cpp

Length of output: 5202


🏁 Script executed:

#!/bin/bash
# Let's see lines around where updateControl is implemented
rg -n "void Node::updateControl" src/MoonBase/Nodes.cpp -A 40

Length of output: 2602


🏁 Script executed:

#!/bin/bash
# Let's also check if there's anything in the log that shows when controls are being manipulated
rg -n "controls\[" src/MoonBase/Nodes.cpp -B 3 -A 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for compareRecursive - this is mentioned in the comment as removing control properties
rg -n "compareRecursive" src/MoonBase/ -A 10 -B 5

Length of output: 11462


🏁 Script executed:

#!/bin/bash
# Also search in framework directory
rg -n "compareRecursive" lib/framework/ -A 10 -B 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's also check the Module base class for state comparison logic
rg -n "compare" src/MoonBase/Module.h -A 5 -B 5

Length of output: 1200


@ewowi, I found the root cause! The issue is in the compareRecursive function in Module.cpp.

Here's what's happening:

  1. Status control is added in onLayout (line 47 in D_PhysicalDriver.h) with all its properties
  2. updateControl("status", ...) is called (line 108), which updates the value
  3. UI update is requested with moduleNodes->requestUIUpdate = true
  4. Before the UI receives the update, another state synchronization happens (likely from the UI sending its current state)
  5. compareRecursive is called (Module.cpp, line 281) to sync the incoming state with the backend state
  6. The problem: The incoming state from the UI doesn't have the new "status" control yet (or has partial info), and compareRecursive OVERWRITES the backend state properties with the incoming ones

Look at Module.cpp, lines 240-246:

} else {  // if property is key/value
  if (key != "p") {
    changed = true;
    updatedItem.name = key;
    updatedItem.oldValue = stateValue;
    stateData[key.c_str()] = newValue;           // ← THIS LINE OVERWRITES!
    updatedItem.value = stateData[key.c_str()];

When newValue.isNull() (because the incoming data doesn't have that property), it sets stateData[key.c_str()] to null, effectively stripping the property!

The fix: Controls added after initial setup (in onLayout, loop, etc.) get partially wiped when state synchronization happens. We need to protect read-only controls or controls marked with certain flags from being overwritten by incoming data that doesn't include them.

Suggested solution: In compareRecursive, when processing control objects within a controls array, skip the comparison if the control is marked as ro (read-only) and the incoming data doesn't have complete control information. Alternatively, only update control values, not the control structure itself.

Try this fix in Module.cpp, around line 240-246:

} else {  // if property is key/value
  if (key != "p") {
    // NEW: For controls array items, preserve control structure if incoming data is incomplete
    bool isControlInArray = (parent == "controls" && depth == 1);
    bool shouldPreserve = isControlInArray && 
                          (key == "name" || key == "type" || key == "ro" || 
                           key == "max" || key == "min" || key == "desc" || key == "default") &&
                          newValue.isNull();
    
    if (!shouldPreserve) {
      changed = true;
      updatedItem.name = key;
      updatedItem.oldValue = stateValue;
      stateData[key.c_str()] = newValue;
      updatedItem.value = stateData[key.c_str()];

This preserves control metadata properties when the incoming data doesn't have them, preventing them from being nulled out.

Can you try this fix?

@ewowi
Copy link
Collaborator Author

ewowi commented Dec 3, 2025

Can you try this fix?
@coderabbitai, nope, didn't help

@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Let's look at the exact code around line 260 in NodeManager.h where the warning is generated
sed -n '240,280p' src/MoonBase/NodeManager.h

Length of output: 2510


🏁 Script executed:

#!/bin/bash
# Let's see how controls are initially synced to state in addNode
rg -n "nodeState\[\"controls\"\] =" src/MoonBase/NodeManager.h -B 5 -A 10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's trace when controls are accessed in NodeManager during onUpdate
rg -n "controls\[index\]" src/MoonBase/NodeManager.h -B 10 -A 10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's see the full context of how onLayout is called and when controls might be added
rg -n "->onLayout" src/MoonBase/ -B 3 -A 3

Length of output: 404


🏁 Script executed:

#!/bin/bash
# Check if there's a mismatch between node->controls.size() and nodeState["controls"].size()
rg -n "controls.size()" src/MoonBase/NodeManager.h -B 5 -A 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the entire onUpdate method in NodeManager to understand the flow
sed -n '200,300p' src/MoonBase/NodeManager.h

Length of output: 5359


🏁 Script executed:

#!/bin/bash
# Find where the warning message comes from
rg -n "dev control name" src/MoonBase/NodeManager.h -B 5 -A 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for addNode implementation to see how controls are initially synced
rg -n "Node.*addNode" src/MoonBase/NodeManager.h -A 50

Length of output: 4477


🏁 Script executed:

#!/bin/bash
# Search for where controls array is copied/synced to nodeState
rg -n 'nodeState\["controls"\]' src/MoonBase/NodeManager.h -B 3 -A 3

Length of output: 2048


🏁 Script executed:

#!/bin/bash
# Look at the entire NodeManager.h file to understand the structure
cat src/MoonBase/NodeManager.h | head -150

Length of output: 6178


🏁 Script executed:

#!/bin/bash
# Search for where node->controls is accessed in NodeManager
rg -n "node->controls" src/MoonBase/NodeManager.h -B 3 -A 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for warning messages with "control" and "null" broadly
rg -n "control.*null" src/MoonBase/ --type=cpp -C 3

Length of output: 1272


🏁 Script executed:

#!/bin/bash
# Let's look at the Module.cpp compareRecursive implementation in detail
cat src/MoonBase/Module.cpp | grep -A 100 "void Module::compareRecursive"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for where controls might be cleared or reset
rg -n "controls.*clear\|controls.*to<JsonArray>" src/MoonBase/ --type=cpp -C 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's see the exact addNode implementation in ModuleDrivers to understand the flow
rg -n "Node.*addNode.*const" src/MoonLight/Modules/ModuleDrivers.h -A 30

Length of output: 1571


🏁 Script executed:

#!/bin/bash
# Check how node constructor receives and stores controls
rg -n "void.*constructor.*controls" src/MoonBase/Nodes.cpp -A 20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Read the entire Module.cpp to find compareRecursive
cat src/MoonBase/Module.cpp

Length of output: 18492


🏁 Script executed:

#!/bin/bash
# Search for the exact warning text more broadly across all files
rg -n "dev control" . --type=cpp --type=h -C 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for where "size" property is set or handled in controls
rg -n '"size"' src/MoonBase/Nodes.h -C 3

Length of output: 1610


🏁 Script executed:

#!/bin/bash
# Look at Nodes.h addControl to see where size comes from
sed -n '140,160p' src/MoonBase/Nodes.h

Length of output: 791


🏁 Script executed:

#!/bin/bash
# Search for where Node constructor stores the controls reference
rg -n "this->controls" src/MoonBase/Nodes.cpp -B 5 -A 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's look at the Node constructor implementation
sed -n '1,80p' src/MoonBase/Nodes.cpp

Length of output: 4078


🏁 Script executed:

#!/bin/bash
# Search for any warning or log about "null" in NodeManager more broadly
rg -n "null.*control|control.*null" src/MoonBase/NodeManager.h -C 3

Length of output: 281


🏁 Script executed:

#!/bin/bash
# Let's see if there's a version mismatch - check git log or recent changes
cat src/MoonBase/NodeManager.h | sed -n '255,265p'

Length of output: 810


🏁 Script executed:

#!/bin/bash
# Search across entire codebase for the warning pattern
rg -n "control name" --type=cpp --type=h -C 2

Length of output: 0


@ewowi, I think I've found the root cause! Let me explain what's happening:

When onLayout adds the new "status" control to nodeState["controls"], the UI doesn't know about it yet. Then, before the UI receives the update, another state synchronization happens (likely the UI sending its current state for some other change).

The key issue is in NodeManager.h lines 161-179:

// invalidate controls
if (nodeState["controls"].isNull()) {
  nodeState["controls"].to<JsonArray>();
} else {
  for (JsonObject control : nodeState["controls"].as<JsonArray>()) {
    control["valid"] = false;  // ← Mark all controls as invalid
  }
}

Node* nodeClass = addNode(updatedItem.index[0], updatedItem.value, nodeState["controls"]);  // set controls to valid

// remove invalid controls
for (int i = nodeState["controls"].as<JsonArray>().size() - 1; i >= 0; i--) {
  JsonObject control = nodeState["controls"][i];
  if (!control["valid"].as<bool>()) {
    EXT_LOGD(ML_TAG, "remove control %d", i);
    nodeState["controls"].remove(i);  // ← Remove invalid controls!
  }
}

This logic is designed to sync controls when a node name changes. But here's the problem:

  1. onLayout adds the "status" control AFTER setup() has already marked controls as valid
  2. When compareRecursive processes an update from the UI (even for unrelated changes), it might trigger this node name processing
  3. All controls are marked valid=false
  4. Only controls that addNode recreates in setup() get marked valid=true
  5. Controls added in onLayout() (like "status") don't get marked valid because they're added AFTER setup
  6. They get removed by the cleanup loop

The fix: In D_PhysicalDriver.h, after adding the status control in onLayout, you need to mark it as valid:

void onLayout() override {
  // ... existing code ...
  
  // Add status control
  addControl(status, "status", "text", 0, 32, true);
  status["valid"] = true;  // ← ADD THIS LINE!
  
  // ... rest of code ...
}

Or better yet, modify the cleanup logic in NodeManager.h to only remove controls that are explicitly marked invalid, not controls that don't have the "valid" key at all. Change line 175:

if (!control["valid"].as<bool>()) {

To:

if (control.containsKey("valid") && !control["valid"].as<bool>()) {

This way, controls without the "valid" key (newly added ones) won't be removed.

Can you try either of these fixes?

@ewowi
Copy link
Collaborator Author

ewowi commented Dec 3, 2025

Can you try either of these fixes?
@coderabbitai, you are looking into the right direction only you have to better understand the code. First of all onLayout () is not calling addControl() but setup() does, you can see that in the logging. Furthermore you have to look at the logging statements and compare that with the output, as there is no 'remove control' in the output. For your convenience I add the complete logging:

[ 547][I][WiFiSettingsService.cpp:103] reconfigureWiFiConnection(): [🐼] Reconfiguring WiFi connection to: STRENGTH
[ 559][I][WiFiSettingsService.cpp:105] reconfigureWiFiConnection(): [🐼] Reconfiguring WiFi TxPower to: 34
[ 569][I][WiFiSettingsService.cpp:107] reconfigureWiFiConnection(): [🐼] Hostname: esp32-s3-n16r8
[ 588][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: notification
[ 615][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: features
[ 624][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: features
[ 633][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: rssi
[ 641][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: reconnect
[ 650][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: ethernet
[ 659][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: otastatus
[ 705][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: battery
[ 713][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: analytics
[ 722][D][SharedWebSocketServer.h:34] registerModule(): [💫] loopTask (5372) devices
[ 732][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: devices
[ 740][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: devices
[ 749][D][SharedWebSocketServer.h:34] registerModule(): [💫] loopTask (5372) tasks
[ 758][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: tasks
[ 767][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: tasks
[ 775][D][SharedWebSocketServer.h:34] registerModule(): [💫] loopTask (5372) inputoutput
[ 785][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: inputoutput
[ 794][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: inputoutput
[ 803][D][SharedWebSocketServer.h:34] registerModule(): [💫] loopTask (5372) effects
[ 812][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: effects
[ 821][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: effects
[ 830][D][SharedWebSocketServer.h:34] registerModule(): [💫] loopTask (5372) drivers
[ 839][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: drivers
[ 847][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: drivers
[ 856][D][SharedWebSocketServer.h:34] registerModule(): [💫] loopTask (5372) lightscontrol
[ 866][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: lightscontrol
[ 875][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: lightscontrol
[ 884][D][SharedWebSocketServer.h:34] registerModule(): [💫] loopTask (5372) channels
[ 894][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: channels
[ 902][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: channels
[ 912][D][SharedWebSocketServer.h:34] registerModule(): [💫] loopTask (5372) moonlightinfo
[ 921][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: moonlightinfo
[ 930][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: moonlightinfo
[ 940][D][SharedWebSocketServer.h:34] registerModule(): [💫] loopTask (5372) livescripts
[ 949][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: livescripts
[ 958][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: livescripts
[ 968][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: FileManager
[ 977][I][EventSocket.cpp:252] onSubscribe(): [🐼] onSubscribe for event: FileManager
[ 986][E][PsychicHttpServer.cpp:141] on(): [🔮] Add endpoint /ws/FileManager failed (ESP_ERR_HTTPD_HANDLER_EXISTS)
[ 1092][D][ModuleIO.h:407] onUpdate(): [🌙] loopTask (5088) [255][255].modded = -> false
[ 1103][D][FSPersistence.h:97] writeToFS(): [🐼] delayedWrites: Add /.config/inputoutput.json
[ 1134][D][ModuleEffects.h:267] addNode(): [💫] loopTask (3836) Bouncing Balls ⏹️ 🔥🎨🐙 (p:0x3c25e450 pr:1)
[ 1146][D][Nodes.h:149] addControl(): [💫] loopTask (3836) grav t:slider p:0x3c25e46d ps:1 {"name":"grav","type":"slider","default":128,"p":1009116269,"valid":true,"value":128,"size":8}
[ 1164][D][Nodes.h:149] addControl(): [💫] loopTask (3728) numBalls t:slider p:0x3c25e46e ps:1 {"name":"numBalls","type":"slider","default":8,"p":1009116270,"min":1,"max":16,"valid":true,"value":8,"size":8}
[ 1184][W][Utilities.h:379] freeMB(): [🌙] loopTask (3728) Nothing to free: pointer is null
[ 1194][D][NodeManager.h:230] onUpdate(): [💫] loopTask (3728) Bouncing Balls ⏹️ 🔥🎨🐙 on: true (#1)
[ 1206][D][EventSocket.cpp:28] registerEvent(): [🐼] Registering event: monitor
[ 1238][D][ModuleDrivers.h:158] addNode(): [💫] loopTask (3728) Single Line 📏 🚥 (p:0x3c25f390 pr:1)
[ 1249][D][Nodes.h:149] addControl(): [💫] loopTask (3728) starting X t:slider p:0x3c25f3ad ps:1 {"name":"starting X","type":"slider","default":0,"p":1009120173,"valid":true,"value":0,"size":8}
[ 1268][D][Nodes.h:149] addControl(): [💫] loopTask (3712) width t:number p:0x3c25f3ae ps:2 {"name":"width","type":"number","default":30,"p":1009120174,"min":1,"max":1000,"valid":true,"value":30,"size":16}
[ 1288][D][Nodes.h:149] addControl(): [💫] loopTask (3712) Y position t:number p:0x3c25f3b0 ps:2 {"name":"Y position","type":"number","default":0,"p":1009120176,"valid":true,"value":0,"size":16}
[ 1307][D][Nodes.h:149] addControl(): [💫] loopTask (3712) reversed order t:checkbox p:0x3c25f3b2 ps:1 {"name":"reversed order","type":"checkbox","default":false,"p":1009120178,"valid":true,"value":false,"size":1}
[ 1327][D][Nodes.h:149] addControl(): [💫] loopTask (3712) LED pin DIO t:select p:0x3c25f3b3 ps:1 {"name":"LED pin DIO","type":"select","default":0,"p":1009120179,"valid":true,"value":0,"size":8}
[ 1347][D][NodeManager.h:230] onUpdate(): [💫] loopTask (3712) Single Line 📏 🚥 on: true (#1)
[ 1358][D][ModuleDrivers.h:158] addNode(): [💫] loopTask (3712) Physical Driver ☸️ (p:0x3c25fbc4 pr:1)
[ 1369][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3712) lightPreset: 2
[ 1377][I][Nodes.cpp:516] onUpdate(): [💫] loopTask (3504) setLightPreset 2 (cPL:3, o:1,0,2,255)
[ 1387][D][Nodes.h:149] addControl(): [💫] loopTask (3504) lightPreset t:select p:0x3c25fbe9 ps:1 {"name":"lightPreset","type":"select","default":2,"p":1009122281,"valid":true,"value":2,"size":8}
[ 1406][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3504) dmaBuffer: 75
[ 1414][D][Nodes.h:149] addControl(): [💫] loopTask (3504) dmaBuffer t:slider p:0x3c25fc2a ps:1 {"name":"dmaBuffer","type":"slider","default":75,"p":1009122346,"min":1,"max":100,"valid":true,"value":75,"size":8}
[ 1435][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3504) version: 20251120
[ 1444][D][Nodes.h:149] addControl(): [💫] loopTask (3504) version t:text p:0x3c25fbea ps:32 {"name":"version","type":"text","default":"20251120","p":1009122282,"ro":true,"max":32,"valid":true,"value":"20251120","size":32}
[ 1465][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3504) status: ok
[ 1473][D][Nodes.h:149] addControl(): [💫] loopTask (3504) status t:text p:0x3c25fc0a ps:32 {"name":"status","type":"text","default":"ok","p":1009122314,"ro":true,"max":32,"valid":true,"value":"ok","size":32}
[ 1493][D][Nodes.cpp:22] updateControl(): [💫] loopTask (3504) version = 20251120 t:text p:0x3c25fbea
[ 1504][D][NodeManager.h:230] onUpdate(): [💫] loopTask (3504) Physical Driver ☸️ on: true (#2)
[ 1515][W][NodeManager.h:260] onUpdate(): [💫] loopTask (3504) dev control name is null nodes[1]controls[3].value = ok -> null ({"size":32})
[ 1529][D][ModuleDrivers.h:158] addNode(): [💫] loopTask (3504) AudioSync ☸️♫ (p:0x3c26037c pr:1)
[ 1540][D][NodeManager.h:230] onUpdate(): [💫] loopTask (3504) AudioSync ☸️♫ on: true (#3)
[ 1568][I][ModuleLightsControl.h:41] begin(): [💫] loopTask (3504) Lights:48(Header:40) L-H:8 Node:32 PL:228(PL-L:180) VL:112 PM:2 C3D:12
[ 1582][I][ModuleLightsControl.h:43] begin(): [💫] loopTask (3504) isInPSRAM: mt:1 mti:1 ch:0
[ 1609][D][ModuleChannels.h:65] onUpdate(): [💫] loopTask (3504) set count 512
[ 1618][D][FSPersistence.h:97] writeToFS(): [🐼] delayedWrites: Add /.config/channels.json
[ 1628][D][ModuleChannels.h:65] onUpdate(): [💫] loopTask (3504) set count 256
[ 1677][D][PhysicalLayer.cpp:49] setup(): [💫] AppEffectTask (3304) allocated 184320 bytes in PSRAM
[ 1687][D][VirtualLayer.cpp:58] loop(): [💫] AppEffectTask (2252) onSizeChanged V 0,0,0 -> 16,16,1
[ 1698][D][PhysicalLayer.cpp:81] loopDrivers(): [💫] AppDriverTask (3572) mapLayout physical requested
[ 1708][D][PhysicalLayer.cpp:124] onLayoutPre(): [💫] AppDriverTask (2372) pass 1 mp:0
[ 1717][D][PhysicalLayer.cpp:129] onLayoutPre(): [💫] AppDriverTask (2292) positions in progress (0 -> 1)
=========== After Setup Start ============
INTERNAL Memory Info:

Total Size : 321316 B ( 313.8 KB)
Free Bytes : 184760 B ( 180.4 KB)
Allocated Bytes : 122900 B ( 120.0 KB)
Minimum Free Bytes: 181972 B ( 177.7 KB)
Largest Free Block: 139252 B ( 136.0 KB)

SPIRAM Memory Info:

Total Size : 8388608 B (8192.0 KB)
Free Bytes : 8131172 B (7940.6 KB)
Allocated Bytes : 244708 B ( 239.0 KB)
Minimum Free Bytes: 8131148 B (7940.6 KB)
Largest Free Block: 8126452 B (7936.0 KB)

GPIO Info:

GPIO : BUS_TYPE[bus/unit][chan]

43 : UART_TX[0]
44 : UART_RX[0]

============ After Setup End =============
[ 1833][D][PhysicalLayer.cpp:192] nextPin(): [💫] AppDriverTask (2292) nextPin #0 ledsPerPin:30 of 20 assigned:0
[ 1844][D][D_PhysicalDriver.h:82] onLayout(): [💫] AppDriverTask (2180) return: lightpresetsaved:1 initDone:0 #:0
[ 1856][D][PhysicalLayer.cpp:200] onLayoutPost(): [💫] AppDriverTask (2164) pass 1 mp:0 #:30 s:30,1,1
[ 1866][D][PhysicalLayer.cpp:202] onLayoutPost(): [💫] AppDriverTask (2164) positions stored (1 -> 2)
[ 1877][D][PhysicalLayer.cpp:90] loopDrivers(): [💫] AppDriverTask (2164) mapLayout virtual requested
[ 1887][D][PhysicalLayer.cpp:124] onLayoutPre(): [💫] AppDriverTask (2164) pass 2 mp:0
[ 1896][D][PhysicalLayer.cpp:209] onLayoutPost(): [💫] AppDriverTask (2164) pass 2 30
[ 1905][D][VirtualLayer.cpp:362] onLayoutPost(): [🌙] AppDriverTask (2164) V:30 x 1 x 1 = v:30 = 1:0:0 + 1:1:30 + mti:0 (1:m:0)
[ 3521][I][WiFiSettingsService.cpp:344] connectToWiFi(): [🐼] 16 networks found.
[ 3530][I][WiFiSettingsService.cpp:403] connectToWiFi(): [🐼] Connecting to strongest network: ewtr, BSSID: da:8c:ab:71:6f:1e
[ 3542][D][WiFiSettingsService.cpp:440] configureNetwork(): [🐼] Connecting to SSID: ewtr, Channel: 11, BSSID: da:8c:ab:71:6f:1e, Hostname: esp32-s3-n16r8
[ 3559][I][WiFiSettingsService.cpp:471] configureNetwork(): [🐼] WiFi setTxPower to: 34
[ 3735][D][ModuleIO.h:390] setBoardPresetDefaults(): [💫] ESP32 SvelteKit (5356) boardID 0
[ 3747][I][WiFiStatus.cpp:39] onStationModeConnected(): [🐼] WiFi Connected.
[ 3813][D][ModuleDrivers.h:41] operator()(): [💫] ESP32 SvelteKit (5356) maxPower 10
[ 3822][D][ModuleDrivers.h:61] operator()(): [💫] ESP32 SvelteKit (5356) ledPins[0-1] = 16 (#30)
[ 3832][D][PhysicalLayer.cpp:81] loopDrivers(): [💫] AppDriverTask (2164) mapLayout physical requested
[ 3832][D][Module.h:157] loop(): [💫] ESP32 SvelteKit (5356) requestUIUpdate
k (2164) pass 1 msist[n0c;e3.6hm:[9 7 ]3 8w5r0i]t[eDT]o[FPSh(y)s:i c[a�l�L�a�y]e rd.eclpapy:e1d2W4r]i toensL:a yAodudt P/r.eco()nf: ig[�/�ef��fe] ctAspp.jDrsiovne[0Tams
p:0
[ 3866][D][PhysicalLayer.cpp:129] onLayoutPre(): [💫] AppDriverTask (2164) positions in progress (2 -> 1)
[ 3868][D][Module.h:157] loop(): [💫] ESP32 SvelteKit (5288) requestUIUpdate
[ 3885][D][FSPersistence.h:97] writeToFS(): [🐼] delayedWrites: Add /.config/drivers.json
[ 3897][D][ModuleDrivers.h:41] operator()(): [💫] ESP32 SvelteKit (5052) maxPower 10
[ 3906][D][ModuleDrivers.h:61] operator()(): [💫] ESP32 SvelteKit (5052) ledPins[0-1] = 16 (#30)
[ 3990][D][PhysicalLayer.cpp:192] nextPin(): [💫] AppDriverTask (2164) nextPin #0 ledsPerPin:30 of 20 assigned:0
[ 4002][D][D_PhysicalDriver.h:86] onLayout(): [💫] AppDriverTask (2164) nrOfLedPins 1 1 1
[ 4011][D][D_PhysicalDriver.h:101] onLayout(): [💫] AppDriverTask (1972) onLayout pin#0 of 1: 16 -> 16 #30
[ 4022][D][D_PhysicalDriver.h:106] onLayout(): [💫] AppDriverTask (1972) status: 16#30
[ 4031][D][D_PhysicalDriver.h:116] onLayout(): [💫] AppDriverTask (1972) init Physical Driver 3 1 0 2
[ 4032][D][Module.h:157] loop(): [💫] ESP32 SvelteKit (5052) requestUIUpdate
[ 4050][D][I2SClocklessLedDriver.h:1095] initled(): [🐸] maximum leds 30
[ 4061][D][PhysicalLayer.cpp:200] onLayoutPost(): [💫] AppDriverTask (1972) pass 1 mp:0 #:30 s:30,1,1
[ 4072][D][PhysicalLayer.cpp:202] onLayoutPost(): [💫] AppDriverTask (1972) positions stored (1 -> 2)
[ 4082][D][PhysicalLayer.cpp:90] loopDrivers(): [💫] AppDriverTask (1972) mapLayout virtual requested
[ 4088][D][ModuleLightsControl.h:316] operator()(): [💫] ESP32 SvelteKit (5052) positions sent to monitor (2 -> 3, noL:30 noC:184320)
[ 4105][D][PhysicalLayer.cpp:124] onLayoutPre(): [💫] AppDriverTask (1972) pass 2 mp:0
[ 4115][D][PhysicalLayer.cpp:209] onLayoutPost(): [💫] AppDriverTask (1972) pass 2 30
[ 4124][D][VirtualLayer.cpp:362] onLayoutPost(): [🌙] AppDriverTask (1972) V:30 x 1 x 1 = v:30 = 1:0:0 + 1:1:30 + mti:0 (1:m:0)
[ 4136][D][VirtualLayer.cpp:58] loop(): [💫] AppEffectTask (2252) onSizeChanged V 16,16,1 -> 30,1,1
[ 4147][D][PhysicalLayer.cpp:99] loopDrivers(): [💫] AppDriverTask (1972) positions done (3 -> 0)
[ 4157][D][PhysicalLayer.cpp:104] loopDrivers(): [💫] AppDriverTask (1972) onSizeChanged P 0,0,0 -> 30,1,1
[ 5266][I][WiFiStatus.cpp:58] onStationModeGotIP(): [🐼] WiFi Got IP. localIP=http://192.168.1.105, hostName=http://esp32-s3-n16r8.local
[ 5281][I][D_AudioSync.h:36] loop(): [💫] AppDriverTask (1972) AudioSync: Initialized
[363575][D][PsychicHttpServer.cpp:226] openCallback(): [🔮] New client connected 52
[363822][D][PsychicHttpServer.cpp:226] openCallback(): [🔮] New client connected 53
[363884][D][SharedHttpEndpoint.h:53] handleGet(): [💫] httpd (5296) search module /rest/drivers
[363937][D][PsychicHttpServer.cpp:226] openCallback(): [🔮] New client connected 54
[363948][I][EventSocket.cpp:39] onWSOpen(): [🐼] ws[192.168.1.108][54] connect
[364033][D][PhysicalLayer.cpp:124] onLayoutPre(): [💫] httpd (4320) pass 1 mp:1
[364042][D][PhysicalLayer.cpp:129] onLayoutPre(): [💫] httpd (4320) positions in progress (0 -> 1)
[364157][D][PhysicalLayer.cpp:200] onLayoutPost(): [💫] httpd (4320) pass 1 mp:1 #:30 s:30,1,1
[364166][D][PhysicalLayer.cpp:202] onLayoutPost(): [💫] httpd (4320) positions stored (1 -> 2)
[364186][D][ModuleLightsControl.h:316] operator()(): [💫] ESP32 SvelteKit (5052) positions sent to monitor (2 -> 3, noL:30 noC:184320)
[364200][D][PhysicalLayer.cpp:99] loopDrivers(): [💫] AppDriverTask (1972) positions done (3 -> 0)
[364935][D][FSPersistence.h:97] writeToFS(): [🐼] delayedWrites: Add /.config/tasks.json
[368255][D][SharedHttpEndpoint.h:53] handleGet(): [💫] httpd (4320) search module /rest/inputoutput
[370036][D][ModuleDevices.h:64] loop10s(): [💫] ESP32 SvelteKit (4056) deviceUDPConnected 1 i:0 p:65506
[373124][D][ModuleDrivers.h:41] operator()(): [💫] httpd (4320) maxPower 10
[373133][D][ModuleDrivers.h:61] operator()(): [💫] httpd (4320) ledPins[0-1] = 16 (#30)
[373146][D][PhysicalLayer.cpp:81] loopDrivers(): [💫] AppDriverTask (1972) mapLayout physical requested
[373156][D][PhysicalLayer.cpp:124] onLayoutPre(): [💫] AppDriverTask (1972) pass 1 mp:0
[373166][D][PhysicalLayer.cpp:129] onLayoutPre(): [💫] AppDriverTask (1972) positions in progress (0 -> 1)
[373282][D][PhysicalLayer.cpp:192] nextPin(): [💫] AppDriverTask (1972) nextPin #0 ledsPerPin:30 of 20 assigned:0
[373294][D][D_PhysicalDriver.h:86] onLayout(): [💫] AppDriverTask (1972) nrOfLedPins 1 1 1
[373303][D][D_PhysicalDriver.h:101] onLayout(): [💫] AppDriverTask (1972) onLayout pin#0 of 1: 16 -> 16 #30
[373314][D][D_PhysicalDriver.h:106] onLayout(): [💫] AppDriverTask (1972) status: 16#30
[373323][D][D_PhysicalDriver.h:129] onLayout(): [💫] AppDriverTask (1972) update Physical Driver 3 1 0 2
[373333][D][Module.h:157] loop(): [💫] ESP32 SvelteKit (4056) requestUIUpdate
[373344][D][I2SClocklessLedDriver.cpp:71] updateDriver(): [🐸] updateLeds 1 x 30 (75)
[373353][D][PhysicalLayer.cpp:200] onLayoutPost(): [💫] AppDriverTask (1956) pass 1 mp:0 #:30 s:30,1,1
[373363][D][PhysicalLayer.cpp:202] onLayoutPost(): [💫] AppDriverTask (1956) positions stored (1 -> 2)
[373374][D][PhysicalLayer.cpp:90] loopDrivers(): [💫] AppDriverTask (1956) mapLayout virtual requested
[373384][D][PhysicalLayer.cpp:124] onLayoutPre(): [💫] AppDriverTask (1956) pass 2 mp:0
[373389][D][ModuleLightsControl.h:316] operator()(): [💫] ESP32 SvelteKit (4056) positions sent to monitor (2 -> 3, noL:30 noC:184320)
[373407][D][PhysicalLayer.cpp:209] onLayoutPost(): [💫] AppDriverTask (1956) pass 2 30
[373416][D][VirtualLayer.cpp:362] onLayoutPost(): [🌙] AppDriverTask (1956) V:30 x 1 x 1 = v:30 = 1:0:0 + 1:1:30 + mti:0 (1:m:0)
[373429][D][PhysicalLayer.cpp:99] loopDrivers(): [💫] AppDriverTask (1956) positions done (3 -> 0)
[397265][D][PsychicHttpServer.cpp:252] closeCallback(): [🔮] Client disconnected 52
[397311][D][PsychicHttpServer.cpp:252] closeCallback(): [🔮] Client disconnected 53

@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the exact warning message "dev control name is null"
rg -n "dev control name is null" --type=cpp -C 10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at NodeManager.h around line 260 where the warning appears
sed -n '250,270p' src/MoonBase/NodeManager.h

Length of output: 1348


🏁 Script executed:

#!/bin/bash
# Search for "control name" in NodeManager.h more broadly
rg -n "control name" src/MoonBase/NodeManager.h -C 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for warning logs with "null" in NodeManager.h
rg -n "EXT_LOGW.*null" src/MoonBase/NodeManager.h -C 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for all warning log statements in NodeManager.h
rg -n "EXT_LOGW" src/MoonBase/NodeManager.h -B 2 -A 2

Length of output: 1177


🏁 Script executed:

#!/bin/bash
# Search for "dev control" across entire codebase
rg -n "dev control" . --type=cpp --type=h -C 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's read the entire NodeManager.h file to find all logging statements
cat src/MoonBase/NodeManager.h | grep -n "EXT_LOG" -B 2 -A 2

Length of output: 9108


🏁 Script executed:

#!/bin/bash
# Read the full NodeManager.h to understand the control handling section better
sed -n '255,275p' src/MoonBase/NodeManager.h

Length of output: 1195


🏁 Script executed:

#!/bin/bash
# Let's look at the Module.cpp compareRecursive function to understand state sync
sed -n '220,290p' src/MoonBase/Module.cpp

Length of output: 3612


🏁 Script executed:

#!/bin/bash
# Check if there's a version of NodeManager with debug logging
rg -n "dev control" src/ lib/ -C 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the Module.cpp section that removes properties not in newData
sed -n '205,225p' src/MoonBase/Module.cpp

Length of output: 1629


🏁 Script executed:

#!/bin/bash
# Look at how compareRecursive handles array items, especially the part that updates array items
sed -n '180,250p' src/MoonBase/Module.cpp

Length of output: 5212


🏁 Script executed:

#!/bin/bash
# Search for where properties might be removed from controls in compareRecursive
sed -n '140,180p' src/MoonBase/Module.cpp

Length of output: 2747


🏁 Script executed:

#!/bin/bash
# Let's look at the full compareRecursive function to understand property handling
sed -n '130,260p' src/MoonBase/Module.cpp

Length of output: 8413


🏁 Script executed:

#!/bin/bash
# Let's look at how node->controls is assigned - it's a JsonArray reference
sed -n '1,50p' src/MoonBase/Nodes.cpp

Length of output: 2807


🏁 Script executed:

#!/bin/bash
# Look at where node->controls is assigned in the constructor
sed -n '70,120p' src/MoonBase/Nodes.cpp

Length of output: 2388


@ewowi, You're absolutely right - I apologize for not analyzing the logs carefully enough. Let me trace through what's actually happening:

Key observations from the logs:

  1. [1473] - Status control is created with all properties: {"name":"status","type":"text","default":"ok",...}
  2. [1504] - NodeManager.h:230 logs "Physical Driver ☸️ on: true (#2)" - The Update ModuleAnimations.h #2 indicates this is the SECOND time the node's "on" property is being processed
  3. [1515] - The warning shows the control now only has {"size":32}

The (#2) is the smoking gun! This means state synchronization is happening again—likely from the UI sending its current state back to the backend after connecting.

Root cause: When the UI connects (around timestamp [363948]), it sends its cached state to sync with the backend. That cached state was persisted before the new "status" control existed. When compareRecursive in Module.cpp processes this incoming state, it's comparing:

  • Backend state (stateData): has 4 controls including the new "status" with all properties
  • UI state (newData): has only 3 controls (no "status" yet) OR has "status" with incomplete data

The problem is in Module.cpp around lines 230-240. When comparing array items (like controls arrays), if the arrays have different sizes or if an object exists in both but has different properties, compareRecursive is called recursively.

Here's the actual bug: Look at Module.cpp lines 131-135:

for (JsonPair newProperty : newData.as<JsonObject>()) {
  if (stateData[newProperty.key()].isNull()) {
    stateData[newProperty.key()] = nullptr;  // Initialize the key in stateData if it doesn't exist
  }
}

This initializes keys from newData into stateData. So if the UI sends controls[3] = {"size":32} (maybe from a previous partial state or file), it initializes all those keys in stateData to nullptr if they don't exist. Then when the comparison loop runs, it updates stateData properties with newData properties.

But wait—there's another path: If the persisted state file (.config/drivers.json) doesn't have the status control yet, when it's loaded and compared, the backend's in-memory state (which HAS status) gets overwritten by the file state (which doesn't have complete status).

The fix: In D_PhysicalDriver.h, the status control is being added dynamically in setup(), but it's not being marked as persistent or protected from state sync overwrites. The issue is that when state loads from the file or UI, it doesn't know about this new control.

Try this solution:

In D_PhysicalDriver.h, after adding the status control (around line 44-45), you need to ensure it's added to the node's controls array which will be synced to nodeState["controls"]. But more importantly, ensure the control is actually in the nodeState before addNode returns.

Check if there's a timing issue where:

  1. setup() adds controls to node->controls JsonArray reference
  2. But that reference might not be pointing to the final nodeState["controls"] yet

Look at NodeManager.h line 168 where addNode is called:

Node* nodeClass = addNode(updatedItem.index[0], updatedItem.value, nodeState["controls"]);

The controls JsonArray is passed to setup(), but if there's a state sync happening immediately after, it could overwrite it.

Quick fix to test: Can you add this logging right after the status control is added in setup():

addControl(status, "status", "text", 0, 32, true);
// ADD THIS:
EXT_LOGD(ML_TAG, "status control added, controls array size: %d", controls.size());
String dbg;
serializeJson(controls[controls.size()-1], dbg);
EXT_LOGD(ML_TAG, "last control: %s", dbg.c_str());

This will help us see if the control is properly added to the array, and whether it survives until the state sync happens.

@ewowi
Copy link
Collaborator Author

ewowi commented Dec 3, 2025

Can you add this logging right after the status control is added in setup()

@coderabbitai, added:

if the control is properly added to the array, and whether it survives until the state sync happens.

Yes looks like it!

here is the updated logging:

[ 1358][D][ModuleDrivers.h:158] addNode(): [💫] loopTask (3568) Physical Driver ☸️ (p:0x3c25fb54 pr:1)
[ 1369][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3568) lightPreset: 2
[ 1377][I][Nodes.cpp:516] onUpdate(): [💫] loopTask (3424) setLightPreset 2 (cPL:3, o:1,0,2,255)
[ 1387][D][Nodes.h:149] addControl(): [💫] loopTask (3424) lightPreset t:select p:0x3c25fb79 ps:1 {"name":"lightPreset","type":"select","default":2,"p":1009122169,"valid":true,"value":2,"size":8}
[ 1406][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3424) dmaBuffer: 75
[ 1414][D][Nodes.h:149] addControl(): [💫] loopTask (3424) dmaBuffer t:slider p:0x3c25fbba ps:1 {"name":"dmaBuffer","type":"slider","default":75,"p":1009122234,"min":1,"max":100,"valid":true,"value":75,"size":8}
[ 1435][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3424) version: 20251120
[ 1443][D][Nodes.h:149] addControl(): [💫] loopTask (3424) version t:text p:0x3c25fb7a ps:32 {"name":"version","type":"text","default":"20251120","p":1009122170,"ro":true,"max":32,"valid":true,"value":"20251120","size":32}
[ 1465][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3424) status: ok
[ 1473][D][Nodes.h:149] addControl(): [💫] loopTask (3424) status t:text p:0x3c25fb9a ps:32 {"name":"status","type":"text","default":"ok","p":1009122202,"ro":true,"max":32,"valid":true,"value":"ok","size":32}
[ 1493][D][Nodes.cpp:22] updateControl(): [💫] loopTask (3424) version = 20251120 t:text p:0x3c25fb7a
[ 1503][D][Nodes.h:149] addControl(): [💫] loopTask (3424) status t:text p:0x3c25fb9a ps:32 {"name":"status","type":"text","default":"ok","p":1009122202,"ro":true,"max":32,"valid":true,"value":"ok","size":32}
[ 1523][D][D_PhysicalDriver.h:48] setup(): [💫] loopTask (3424) status control added, controls array size: 4
[ 1534][D][D_PhysicalDriver.h:51] setup(): [💫] loopTask (3424) last control: {"name":"status","type":"text","default":"ok","p":1009122202,"ro":true,"max":32,"valid":true,"value":"ok","size":32}
[ 1553][D][NodeManager.h:230] onUpdate(): [💫] loopTask (3424) Physical Driver ☸️ on: true (#2)
[ 1565][W][NodeManager.h:260] onUpdate(): [💫] loopTask (3424) dev control name is null nodes[1]controls[3].value = ok -> null ({"size":32})

Also note this is all happening during boot, running in loopTask, so even before the app tasks are created and loopTask is destroyed, so not much concurrency going on yet (read: other tasks interfering)

@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the compareRecursive function to see how it handles array items
sed -n '130,260p' src/MoonBase/Module.cpp

Length of output: 8413


🏁 Script executed:

#!/bin/bash
# Look at the context around the warning in NodeManager.h line 260
sed -n '220,280p' src/MoonBase/NodeManager.h

Length of output: 3679


🏁 Script executed:

#!/bin/bash
# Search for "null" warnings in NodeManager.h
rg -n "null" src/MoonBase/NodeManager.h | head -20

Length of output: 449


🏁 Script executed:

#!/bin/bash
# Search for "dev" or warning messages in NodeManager.h
rg -n "EXT_LOGW" src/MoonBase/NodeManager.h -A 2 -B 2

Length of output: 1177


🏁 Script executed:

#!/bin/bash
# Let's look at the full onUpdate method in NodeManager.h
sed -n '100,280p' src/MoonBase/NodeManager.h

Length of output: 9095


🏁 Script executed:

#!/bin/bash
# Search for where compareRecursive is called
rg -n "compareRecursive" src/MoonBase/Module.cpp src/MoonBase/Module.h -B 3 -A 3

Length of output: 4285


🏁 Script executed:

#!/bin/bash
# Look at the update function in Module.cpp where compareRecursive is called
sed -n '260,320p' src/MoonBase/Module.cpp

Length of output: 2560


@ewowi, I think I've found the issue! The problem is in how compareRecursive syncs the persisted state from the file with the in-memory state.

Here's what's happening:

  1. At [1534]: setup() completes and the status control is fully added to nodeState["controls"] with all properties

  2. Immediately after: The module's persisted state is being re-synced from the .config/drivers.json file

  3. The file sync: Since the file was saved BEFORE you added the status control, it either:

    • Doesn't have controls[3] at all, OR
    • Has controls[3] with only partial data like {"size":32}
  4. compareRecursive runs: It compares the file data (newData) with the in-memory state (stateData) and updates stateData based on what's in the file

The key question: What does .config/drivers.json contain for the Physical Driver's controls? Can you check the file and share what controls[3] looks like (or if it exists at all)?

To verify this theory, can you:

  1. Check the persisted file:

  2. Or add this debug logging right before line 281 in Module.cpp where compareRecursive is called:

// Add before line 281
if (_moduleName == "drivers") {
  String rootStr, stateStr;
  serializeJson(root, rootStr);
  serializeJson(state.data, stateStr);
  EXT_LOGD(ML_TAG, "BEFORE compareRecursive - root: %s", rootStr.c_str());
  EXT_LOGD(ML_TAG, "BEFORE compareRecursive - state: %s", stateStr.c_str());
}

if (state.compareRecursive("", state.data, root, updatedItem)) {

This will show us exactly what's in the file vs what's in memory before they're compared.


🏁 Script executed:

cat .config/drivers.json

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Let's check if there's any logic in compareRecursive that removes properties
# Look for code that removes or sets properties to null
rg -n "remove\|= null" src/MoonBase/Module.cpp | grep -A 2 -B 2 "compareRecursive\|stateData"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the Node constructor to see how controls array is assigned
sed -n '1,100p' src/MoonBase/Nodes.cpp

Length of output: 5202


🏁 Script executed:

#!/bin/bash
# Search for where this->controls is assigned in Node constructor
rg -n "this->controls" src/MoonBase/Nodes.cpp -B 5 -A 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the Node constructor signature and where controls is defined
rg -n "JsonArray.*controls|constructor.*controls" src/MoonBase/Nodes.h -B 2 -A 5

Length of output: 673


🏁 Script executed:

#!/bin/bash
# Let's look at where node->controls gets assigned in the constructor
sed -n '63,70p' src/MoonBase/Nodes.h

Length of output: 236


@ewowi, I need your help to pinpoint exactly where the properties are being stripped. Based on my analysis of compareRecursive, it should NOT remove properties that exist in stateData but not in newData. The logic only updates properties when they exist in both and have different values.

However, something IS stripping the properties. Can you add this specific logging to help us track it down?

Add this logging in NodeManager.h around line 247 (right before the log that shows "on: true (#2)"):

else if (updatedItem.name == "on" && updatedItem.parent[1] == "") {  // nodes[i].on
  // ADD THIS BLOCK:
  if (strcmp(nodeState["name"].as<const char*>(), "Physical Driver ☸️") == 0) {
    String ctrlStr;
    serializeJson(nodeState["controls"], ctrlStr);
    EXT_LOGD(ML_TAG, "BEFORE on handler - controls: %s", ctrlStr.c_str());
  }
  
  if (updatedItem.index[0] < nodes->size()) {
    EXT_LOGD(ML_TAG, "%s on: %s (#%d)", nodeState["name"].as<const char*>(), updatedItem.value.as<String>().c_str(), nodes->size());
    
    // ADD THIS BLOCK TOO:
    if (strcmp(nodeState["name"].as<const char*>(), "Physical Driver ☸️") == 0) {
      String ctrlStr2;
      serializeJson(nodeState["controls"], ctrlStr2);
      EXT_LOGD(ML_TAG, "AFTER on log - controls: %s", ctrlStr2.c_str());
    }

Also, add this at the END of Module.cpp's compareRecursive function (right before the return changed; at line 257):

  // ADD THIS before return:
  if (parent == "controls" && depth == 1) {
    String stateStr, newStr;
    serializeJson(stateData, stateStr);
    serializeJson(newData, newStr);
    EXT_LOGD(MB_TAG, "compareRecursive controls[%d] - state: %s", index, stateStr.c_str());
    EXT_LOGD(MB_TAG, "compareRecursive controls[%d] - new: %s", index, newStr.c_str());
  }
  
  return changed;
}

This will show us:

  1. Exactly what's in the controls array before and after the "on" handler runs
  2. What compareRecursive is comparing for each control object
  3. Whether the stripping happens during compareRecursive or somewhere else

Can you add this logging and share the new output?

@ewowi
Copy link
Collaborator Author

ewowi commented Dec 3, 2025

Can you add this logging and share the new output?
@coderabbitai, I didn't add this logging but I added this in compare recursive:

[ 10070][D][ModuleDrivers.h:158] addNode(): [💫] loopTask (3056) Physical Driver ☸️ (p:0x3c26107c pr:1)
[ 10081][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3056) lightPreset: 2
[ 10090][I][Nodes.cpp:516] onUpdate(): [💫] loopTask (3056) setLightPreset 2 (cPL:3, o:1,0,2,255)
[ 10100][D][Nodes.h:149] addControl(): [💫] loopTask (3056) lightPreset t:select p:0x3c2610a1 ps:1 {"name":"lightPreset","type":"select","default":2,"p":1009127585,"valid":true,"value":2,"size":8}
[ 10119][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3056) dmaBuffer: 75
[ 10127][D][Nodes.h:149] addControl(): [💫] loopTask (3056) dmaBuffer t:slider p:0x3c2610e2 ps:1 {"name":"dmaBuffer","type":"slider","default":75,"p":1009127650,"min":1,"max":100,"valid":true,"value":75,"size":8}
[ 10148][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3056) version: 20251120
[ 10156][D][Nodes.h:149] addControl(): [💫] loopTask (3056) version t:text p:0x3c2610a2 ps:32 {"name":"version","type":"text","default":"20251120","p":1009127586,"ro":true,"max":32,"valid":true,"value":"20251120","size":32}
[ 10178][D][Nodes.cpp:403] onUpdate(): [💫] loopTask (3056) status: ok
[ 10186][D][Nodes.h:149] addControl(): [💫] loopTask (3056) status t:text p:0x3c2610c2 ps:32 {"name":"status","type":"text","default":"ok","p":1009127618,"ro":true,"max":32,"valid":true,"value":"ok","size":32}
[ 10206][D][Nodes.cpp:22] updateControl(): [💫] loopTask (3056) version = 20251120 t:text p:0x3c2610a2
[ 10216][D][D_PhysicalDriver.h:47] setup(): [💫] loopTask (3056) status control added, controls array size: 4
[ 10228][D][D_PhysicalDriver.h:50] setup(): [💫] loopTask (3056) last control: {"name":"status","type":"text","default":"ok","p":1009127618,"ro":true,"max":32,"valid":true,"value":"ok","size":32}
[ 10246][D][Module.cpp:248] compareRecursive(): [🌙] loopTask (3056) kv nodes.on v: true d: 0
[ 10256][D][Module.cpp:249] compareRecursive(): [🌙] loopTask (3056) kv nodes[1][255].on = -> true
[ 10267][D][NodeManager.h:230] onUpdate(): [💫] loopTask (3056) Physical Driver ☸️ on: true (#2)
[ 10278][D][Module.cpp:248] compareRecursive(): [🌙] loopTask (3056) kv controls.name v: Version d: 1
[ 10288][D][Module.cpp:249] compareRecursive(): [🌙] loopTask (3056) kv nodes[1]controls[2].name = version -> Version
[ 10300][D][Module.cpp:248] compareRecursive(): [🌙] loopTask (3056) kv controls.max v: 30 d: 1
[ 10310][D][Module.cpp:249] compareRecursive(): [🌙] loopTask (3056) kv nodes[1]controls[2].max = 32 -> 30
[ 10321][D][Module.cpp:248] compareRecursive(): [🌙] loopTask (3056) kv controls.size v: 30 d: 1
[ 10331][D][Module.cpp:249] compareRecursive(): [🌙] loopTask (3056) kv nodes[1]controls[2].size = 32 -> 30
[ 10343][W][NodeManager.h:260] onUpdate(): [💫] loopTask (3056) dev control name is null nodes[1]controls[3].value = ok -> null ({"size":32})
[ 10357][D][Module.cpp:209] compareRecursive(): [🌙] loopTask (3056) remove nodes.controls[3] d: 0 ({})
[ 10368][D][Module.cpp:166] compareRecursive(): [🌙] loopTask (3056) add .nodes[2] (2/3) d: 255

I think I know what is going on: compare recursive reads from file and there is no status (yet) so it removes the just added status (remove nodes.controls[3] d: 0 ({})). This is no issue if there is a new node added (there is nothing on file), but for existing nodes it needs to remove if things are not added in addControl, and it needs to add if it is in addControl. So this looks like a matter of the sequence of things, e.g. first complete compare recursive so it matches what is on file and do addControl() statements after that. Can you suggest how to do that?

@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Tip

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

@ewowi, Excellent analysis! You've identified the root cause perfectly. The issue is that compareRecursive removes the newly added "status" control because it doesn't exist in the persisted file yet.

The good news is that the "valid" flag mechanism already exists for exactly this purpose! Looking at your logs, the status control is added with "valid":true at line 10228. The "valid" flag is designed to protect controls during state synchronization.

The fix: Modify compareRecursive in Module.cpp to skip removing array items (like controls) that are marked with valid=true.

Around line 209 in Module.cpp where it says:

EXT_LOGD(MB_TAG, "remove %s%s[%d] d: %d (%s)", parent.c_str(), stateArray.name().c_str(), index, depth, dbg2.c_str());
stateArray.remove(index);

Change it to:

// Don't remove array items that are marked as valid (e.g., newly added controls)
JsonObject itemObj = stateData.as<JsonObject>();
bool isValid = itemObj.containsKey("valid") && itemObj["valid"].as<bool>();

if (!isValid) {
  EXT_LOGD(MB_TAG, "remove %s%s[%d] d: %d (%s)", parent.c_str(), stateArray.name().c_str(), index, depth, dbg2.c_str());
  stateArray.remove(index);
} else {
  EXT_LOGD(MB_TAG, "skip remove %s%s[%d] d: %d - marked valid (%s)", parent.c_str(), stateArray.name().c_str(), index, depth, dbg2.c_str());
}

This way:

  1. setup() adds the new "status" control with valid=true
  2. compareRecursive runs and sees the control isn't in the file
  3. Instead of removing it, it checks the valid flag and skips the removal
  4. The control survives the synchronization

Can you try this fix?

@ewowi
Copy link
Collaborator Author

ewowi commented Dec 3, 2025

Can you try this fix?

@coderabbitai, nice suggestion but I think this is better:

else if (i >= newArray.size()) { // newArray has deleted a row
// newArray.add(); //add dummy row
changed = true; // compareRecursive(key, stateArray[i], newArray[i], updatedItem, depth+1, i) || changed;

        // EXT_LOGD(MB_TAG, "remove %s.%s[%d] (%d/%d) d: %d", parent.c_str(), key.c_str(), i, stateArray.size(), newArray.size(), depth);

        // set all the values to null
        //  UpdatedItem updatedItem; //create local updatedItem
        updatedItem.parent[1] = "";  // reset deeper levels when coming back from recursion (repeat in loop)
        updatedItem.index[1] = UINT8_MAX;
        // depth 255: set parent en index 0
        // depth 0: set parent en index 1
        // depth 1: set parent en index 2 not possible yet
        if ((uint8_t)(depth + 1) < 2) {
          // EXT_LOGD(MB_TAG, "kv %s.%s v: %s d: %d", parent.c_str(), key.c_str(), newValue.as<String>().c_str(), depth);
          // EXT_LOGD(ML_TAG, "%s[%d]%s[%d].%s = %s -> %s", updatedItem.parent[0].c_str(), updatedItem.index[0], updatedItem.parent[1].c_str(), updatedItem.index[1], updatedItem.name.c_str(), updatedItem.oldValue.c_str(), updatedItem.value.as<String>().c_str());
          updatedItem.parent[(uint8_t)(depth + 1)] = key;
          updatedItem.index[(uint8_t)(depth + 1)] = i;
        }
        // }
        bool isValid = !stateArray[i]["valid"].isNull() && stateArray[i]["valid"].as<bool>();
        if (!isValid) {
          for (JsonPair control : stateArray[i].as<JsonObject>()) {
            // EXT_LOGD(MB_TAG, "     remove %s[%d] %s %s", key.c_str(), i, control.key().c_str(), control.value().as<const char*>());
            // newArray[i][control.key()] = nullptr; // Initialize the keys in newArray so comparerecusive can compare them
            updatedItem.name = control.key();
            updatedItem.oldValue = control.value();
            updatedItem.value = JsonVariant();    // Assign an empty JsonVariant
            stateArray[i].remove(control.key());  // remove the control from the state row so onUpdate see it as empty

            postUpdate(updatedItem);
          }

          String dbg;
          serializeJson(stateArray[i], dbg);
          EXT_LOGD(MB_TAG, "remove %s.%s[%d] d: %d (%s)", parent.c_str(), key.c_str(), i, depth, dbg.c_str());

          stateArray.remove(i);  // remove the state row entirely
                                 // ([{"name":"xFrequency","type":"slider","default":64,"p":1009144377,"value":173,"size":8},{"name":"fadeRate","type":"slider","default":128,"p":1009144378,"value":128,"size":8},{"name":"speed","type":"slider","default":128,"p":1009144379,"value":128,"size":8},{"name":"brightness","type":"slider","default":255,"p":1009144380,"value":255,"size":8}])
        } else {
          String dbg;
          serializeJson(stateArray[i], dbg);
          EXT_LOGD(MB_TAG, "skip remove %s.%s[%d] d: %d (%s)", parent.c_str(), key.c_str(), i, depth, dbg.c_str());
        }
        
        and indeed, this works 💫

Can you do an after action review if this fits the general architecture of the compareRecursive mechanism and it is not only a fix for this particular case?: Why is this needed for controls to work and will it work for other (edge) cases as well, e.g. if a node will remove an earlier saved control? Or for non-node related controls : we have also module controls (see Module.h/.cpp): what if a control will be added or removed there?

@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how module controls are added in Module.h and Module.cpp
echo "=== Module.h control handling ==="
rg -n "addControl" src/MoonBase/Module.h -B 2 -A 10

echo -e "\n=== Module.cpp control handling ==="
rg -n "addControl" src/MoonBase/Module.cpp -B 2 -A 10

echo -e "\n=== Check for 'valid' flag usage in Module ==="
rg -n "valid" src/MoonBase/Module.h src/MoonBase/Module.cpp -C 3

echo -e "\n=== Check NodeManager control invalidation/validation logic ==="
sed -n '160,180p' src/MoonBase/NodeManager.h

Length of output: 2307


🏁 Script executed:

#!/bin/bash
# Look for all places where controls arrays might be modified
echo "=== Places where control arrays are modified ==="
rg -n "controls.*remove|controls.*add|controls.*clear" src/MoonBase/ --type=cpp -C 2

echo -e "\n=== Check if module controls use same mechanism as node controls ==="
rg -n "class Module" src/MoonBase/Module.h -A 50 | head -60

Length of output: 4449


🏁 Script executed:

#!/bin/bash
# Check the full compareRecursive context around the fix area
echo "=== compareRecursive array handling (around line 180-220) ==="
sed -n '180,230p' src/MoonBase/Module.cpp

Length of output: 3864


@ewowi, Excellent fix! 🎉 Let me provide a comprehensive architectural review:

Architectural Fit

Your fix is architecturally sound and leverages the existing "valid" flag mechanism that's already central to the node control lifecycle. Here's why:

The "valid" flag pattern:

  1. NodeManager invalidates all controls (valid=false) when a node changes (line 164)
  2. Node::setup() re-adds controls and marks them valid=true
  3. NodeManager removes controls that remain invalid (lines 174-179)
  4. Your fix extends this pattern to compareRecursive, preventing removal of newly added valid controls during state synchronization

This is a natural extension of the existing architecture, not a one-off patch.

🎯 Edge Case Analysis

1. Node removes a previously saved control

Scenario: A node's setup() no longer adds a control that exists in the persisted file.

What happens:

  • NodeManager invalidates all controls before calling setup() (line 164)
  • The old control is NOT marked valid=true because setup() doesn't add it
  • NodeManager's cleanup removes it (lines 174-179) BEFORE compareRecursive runs
  • Your fix doesn't interfere because the control is already gone

Verdict: ✅ Works correctly

2. Module controls (non-node controls) ⚠️

Observation: Module::addControl() (Module.cpp line 355) doesn't set the "valid" flag.

Current behavior:

  • Module controls are added directly to arrays without valid=true
  • Your fix treats them as non-valid (since the flag is missing)
  • They can still be removed by compareRecursive during state sync

Question: Are module controls ever added dynamically like node controls? Or are they only added during initial setup() before any persistence happens?

If module controls are added dynamically, you should also mark them with valid=true after creation:

JsonObject Module::addControl(const JsonArray& root, const char* name, const char* type, int min, int max, bool ro, const char* desc) {
  JsonObject control = root.add<JsonObject>();
  control["name"] = name;
  control["type"] = type;
  if (min != 0) control["min"] = min;
  if (max != UINT8_MAX) control["max"] = max;
  if (ro) control["ro"] = true;
  if (desc) control["desc"] = desc;
  control["valid"] = true;  // ← ADD THIS if controls can be added dynamically
  return control;
}

3. Persistence and subsequent boots

Scenario: After first boot, the "status" control is saved with valid=true in the file.

What happens:

  • On next boot, NodeManager invalidates all controls (valid=false) before calling setup()
  • setup() re-adds the control and marks it valid=true
  • Old persisted controls without matching setup() entries remain invalid and are removed

Verdict: ✅ Works correctly—the invalidation step ensures controls are re-validated each boot

4. Concurrent state syncs

Scenario: Multiple state synchronizations happen (UI connecting, file loading, etc.)

What happens:

  • Your fix protects controls marked valid=true from removal
  • Once setup() completes, controls have valid=true and survive any subsequent syncs
  • If a control shouldn't exist, it won't be marked valid in setup() and will be removed

Verdict: ✅ Works correctly

📋 Recommendations

  1. Document the pattern: Add a comment above your fix explaining that the "valid" flag protects dynamically added controls:
// Don't remove array items marked as valid (e.g., controls added in setup() that don't exist in persisted state yet)
bool isValid = !stateArray[i]["valid"].isNull() && stateArray[i]["valid"].as<bool>();
  1. Module controls: Verify if any modules add controls dynamically. If so, ensure Module::addControl() also sets valid=true, or manually set it after calling addControl().

  2. Consider flag cleanup: The "valid" flag is only needed during synchronization. You could optionally remove it after sync to keep persisted state cleaner, but this is low priority.

Conclusion

Your fix is architecturally correct and handles the primary use case (node controls) perfectly. The only potential gap is module controls, but this depends on whether they're added dynamically. Overall, this is a robust solution that properly extends the existing control lifecycle pattern! 💫

🐰✨

Back end
========
- Module: compareRecursive: do not remove valid node controls
- modules: add addControlValue, rename root to controls or stateJson or newData, rename property to control, details to rows, use addControl() and addControlValue()
- Module drivers and effects: addNodes argument from values to control
- Layout MoonLight: PanelsLayout: add panelsPerPin
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
src/MoonBase/Nodes.h (1)

135-139: Fix the coord3D type check in addControl

The coord3D branch currently reads:

} else if (control["type"] == "coord3D") {
  if (!sizeof(!std::is_same<ControlType, Coord3D>::value)) {
    EXT_LOGE(ML_TAG, "type for %s is not Coord3D", name);
  } else
    control["size"] = sizeof(Coord3D);
}

sizeof(!std::is_same<...>::value) is always 1, so the if condition is always false and the error log never triggers, even if ControlType is not Coord3D.

Suggested fix to mirror the checkbox branch:

-    } else if (control["type"] == "coord3D") {
-      if (!sizeof(!std::is_same<ControlType, Coord3D>::value)) {
-        EXT_LOGE(ML_TAG, "type for %s is not Coord3D", name);
-      } else
-        control["size"] = sizeof(Coord3D);
-    } else
+    } else if (control["type"] == "coord3D") {
+      if (!std::is_same<ControlType, Coord3D>::value) {
+        EXT_LOGE(ML_TAG, "type for %s is not Coord3D", name);
+      } else {
+        control["size"] = sizeof(Coord3D);
+      }
+    } else

This keeps existing behavior for correct uses and makes the error path actually reachable if someone passes the wrong type in the future.

src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1)

137-169: Fix DDP offset handling: use ledIndex for the destination channel index

In handleDDP() you correctly compute:

uint32_t offset = ...;           // byte offset in the LED array
uint16_t dataLen = ...;
int startPixel = offset / 3;
int numPixels = min((uint16_t)(dataLen / 3),
                    (uint16_t)(layer->layerP->lights.header.nrOfLights - startPixel));

but then write using i instead of ledIndex:

for (int i = 0; i < numPixels; i++) {
  int ledIndex = startPixel + i;
  if (ledIndex < layer->layerP->lights.header.nrOfLights) {
    memcpy(&layer->layerP->lights.channels[i * layer->layerP->lights.header.channelsPerLight],
           &pixelData[i * layer->layerP->lights.header.channelsPerLight],
           layer->layerP->lights.header.channelsPerLight);
  }
}

This ignores startPixel, so any packet with a non-zero offset overwrites from channel 0 instead of the intended LED range.

Suggested fix:

-        for (int i = 0; i < numPixels; i++) {
-          int ledIndex = startPixel + i;
-          if (ledIndex < layer->layerP->lights.header.nrOfLights) {
-            memcpy(&layer->layerP->lights.channels[i * layer->layerP->lights.header.channelsPerLight],
-                   &pixelData[i * layer->layerP->lights.header.channelsPerLight],
-                   layer->layerP->lights.header.channelsPerLight);
-          }
-        }
+        for (int i = 0; i < numPixels; i++) {
+          int ledIndex = startPixel + i;
+          if (ledIndex < layer->layerP->lights.header.nrOfLights) {
+            memcpy(&layer->layerP->lights.channels[ledIndex * layer->layerP->lights.header.channelsPerLight],
+                   &pixelData[i * layer->layerP->lights.header.channelsPerLight],
+                   layer->layerP->lights.header.channelsPerLight);
+          }
+        }

This preserves the existing bounds check while correctly honoring the DDP byte offset.

🧹 Nitpick comments (3)
src/MoonLight/Modules/ModuleLiveScripts.h (1)

93-99: Unnecessary initialization on line 96.

The rows variable is initialized to controls on line 96, but is immediately reassigned on line 99. This initialization serves no purpose and may confuse readers.

Apply this diff:

   void setupDefinition(const JsonArray& controls) override {
     EXT_LOGV(ML_TAG, "");
     JsonObject control;       // state.data has one or more properties
-    JsonArray rows = controls;  // if a control is an array, this is the rows of the array
+    JsonArray rows;  // if a control is an array, this is the rows of the array
 
     control = addControl(controls, "scripts", "rows");
     rows = control["n"].to<JsonArray>();
src/MoonBase/Modules/ModuleTasks.h (1)

67-72: Consider renaming controls to data for clarity.

The variable controls on line 69 represents the full JSON data object (containing tasks, core0, core1), not a controls array as used in setupDefinition. This naming may cause confusion.

Apply this diff:

     JsonDocument doc;
     doc["tasks"].to<JsonArray>();
-    JsonObject controls = doc.as<JsonObject>();
+    JsonObject data = doc.as<JsonObject>();
 
     for (UBaseType_t i = 0; i < taskCount; i++) {
-      JsonObject task = controls["tasks"].as<JsonArray>().add<JsonObject>();
+      JsonObject task = data["tasks"].as<JsonArray>().add<JsonObject>();

And update the remaining references on lines 123-124 and 128-131.

src/MoonLight/Modules/ModuleLightsControl.h (1)

81-85: Button handling with INPUT_PULLUP + debounce is sound; consider avoiding static locals

The new pin_Button_OnOff path correctly:

  • Configures the pin as INPUT_PULLUP when discovered.
  • Checks pinToggleOnOff != UINT8_MAX before reading.
  • Debounces and triggers only on the HIGH→LOW transition, then pushes a minimal lightsOn diff via update(..., _moduleName + "server").

Given there is effectively a single ModuleLightsControl instance, the static debounce state in loop() is fine. If you ever support multiple instances or hot-swapping pins, it would be cleaner to move lastDebounceTime / lastState into member fields so their lifetime and ownership are explicit.

Also applies to: 289-305

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0b3805 and 792e11b.

📒 Files selected for processing (24)
  • docs/develop/modules.md (1 hunks)
  • src/MoonBase/Module.cpp (10 hunks)
  • src/MoonBase/Module.h (5 hunks)
  • src/MoonBase/Modules/FileManager.cpp (3 hunks)
  • src/MoonBase/Modules/FileManager.h (1 hunks)
  • src/MoonBase/Modules/ModuleDevices.h (1 hunks)
  • src/MoonBase/Modules/ModuleIO.h (4 hunks)
  • src/MoonBase/Modules/ModuleTasks.h (3 hunks)
  • src/MoonBase/NodeManager.h (2 hunks)
  • src/MoonBase/Nodes.cpp (3 hunks)
  • src/MoonBase/Nodes.h (6 hunks)
  • src/MoonLight/Modules/ModuleChannels.h (1 hunks)
  • src/MoonLight/Modules/ModuleDrivers.h (3 hunks)
  • src/MoonLight/Modules/ModuleEffects.h (2 hunks)
  • src/MoonLight/Modules/ModuleLightsControl.h (3 hunks)
  • src/MoonLight/Modules/ModuleLiveScripts.h (1 hunks)
  • src/MoonLight/Modules/ModuleMoonLightInfo.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_FastLED.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_Infrared.h (2 hunks)
  • src/MoonLight/Nodes/Effects/E_MoonLight.h (5 hunks)
  • src/MoonLight/Nodes/Effects/E_MoonModules.h (2 hunks)
  • src/MoonLight/Nodes/Layouts/L_MoonLight.h (9 hunks)
  • src/MoonLight/Nodes/Modifiers/M_MoonLight.h (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (20)
src/MoonLight/Nodes/Drivers/D_FastLED.h (1)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Modifiers/M_MoonLight.h (1)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Modules/ModuleLiveScripts.h (4)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/Modules/ModuleDevices.h (1)
  • setupDefinition (33-47)
src/MoonBase/Modules/ModuleTasks.h (1)
  • setupDefinition (23-46)
src/MoonLight/Modules/ModuleChannels.h (1)
  • setupDefinition (24-54)
src/MoonLight/Modules/ModuleMoonLightInfo.h (6)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/Modules/ModuleDevices.h (1)
  • setupDefinition (33-47)
src/MoonBase/Modules/ModuleTasks.h (1)
  • setupDefinition (23-46)
src/MoonBase/NodeManager.h (1)
  • setupDefinition (83-112)
src/MoonLight/Modules/ModuleChannels.h (1)
  • setupDefinition (24-54)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
  • setupDefinition (93-114)
src/MoonBase/Nodes.cpp (6)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
  • onUpdate (920-924)
  • onUpdate (1116-1120)
src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
  • onUpdate (120-124)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
  • onUpdate (35-41)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
  • onUpdate (31-37)
src/MoonLight/Nodes/Drivers/D_Infrared.h (3)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
  • onUpdate (31-37)
src/MoonLight/Nodes/Layouts/L_MoonLight.h (3)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/Nodes.h (1)
  • nextPin (201-201)
src/MoonLight/Layers/PhysicalLayer.cpp (2)
  • nextPin (176-195)
  • nextPin (176-176)
src/MoonLight/Modules/ModuleChannels.h (1)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Effects/E_MoonModules.h (3)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
  • onUpdate (35-41)
src/MoonLight/Modules/ModuleEffects.h (3)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/NodeManager.h (2)
  • setupDefinition (83-112)
  • NodeManager (20-34)
src/MoonLight/Modules/ModuleChannels.h (1)
  • setupDefinition (24-54)
src/MoonLight/Modules/ModuleLightsControl.h (1)
src/MoonBase/Module.h (1)
  • ModuleState (47-137)
src/MoonBase/Modules/ModuleDevices.h (5)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/Modules/ModuleIO.h (1)
  • setupDefinition (110-214)
src/MoonBase/Modules/ModuleTasks.h (1)
  • setupDefinition (23-46)
src/MoonBase/NodeManager.h (1)
  • setupDefinition (83-112)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
  • setupDefinition (93-114)
src/MoonBase/Modules/ModuleIO.h (2)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/Modules/ModuleDevices.h (1)
  • setupDefinition (33-47)
src/MoonBase/Modules/ModuleTasks.h (4)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/Modules/ModuleDevices.h (1)
  • setupDefinition (33-47)
src/MoonBase/Modules/ModuleIO.h (1)
  • setupDefinition (110-214)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
  • setupDefinition (93-114)
src/MoonBase/NodeManager.h (1)
src/MoonBase/Module.h (3)
  • void (150-166)
  • void (194-194)
  • void (195-195)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (2)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonBase/Module.h (2)
lib/framework/NTPSettingsService.h (1)
  • update (66-73)
lib/framework/StatefulService.h (3)
  • update (123-134)
  • update (144-155)
  • StateUpdateResult (26-106)
src/MoonLight/Nodes/Effects/E_MoonLight.h (3)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
  • onUpdate (35-41)
src/MoonBase/Module.cpp (4)
src/MoonBase/Module.h (1)
  • postUpdate (116-127)
src/MoonBase/Modules/ModuleDevices.h (1)
  • setupDefinition (33-47)
src/MoonBase/Modules/ModuleTasks.h (1)
  • setupDefinition (23-46)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
  • setupDefinition (93-114)
src/MoonBase/Nodes.h (1)
src/MoonBase/Nodes.cpp (4)
  • onUpdate (400-540)
  • onUpdate (400-400)
  • updateControl (17-66)
  • updateControl (17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (44)
docs/develop/modules.md (1)

41-59: Documentation accurately reflects the new control-definition API refactoring.

The example correctly illustrates the parameter rename (rootcontrols), the rows pattern for managing nested arrays, and the builder-style addControl() / addControlValue() API. Verification confirms this pattern is consistently used across all setupDefinition implementations in the codebase (ModuleMoonLightInfo, ModuleLiveScripts, ModuleEffects, NodeManager, ModuleLightsControl, ModuleChannels, ModuleTasks, ModuleIO, ModuleDevices). The inline comment and the builder approach align well with the PR's shift toward helper-driven control construction.

src/MoonBase/Modules/ModuleDevices.h (1)

33-45: setupDefinition refactor is consistent with other modules

The updated signature and the controls/rows usage match the shared pattern used in ModuleTasks, NodeManager, and ModuleLiveScripts. Control definitions for devices (name/ip/time/mac) remain read‑only and behaviorally unchanged. No issues from this change.

src/MoonLight/Nodes/Drivers/D_FastLED.h (1)

50-56: LGTM! Label naming is now consistent.

The "version" label is now lowercase, matching the naming convention of other controls in this setup() method (chipSet, colorOrder, etc.).

src/MoonLight/Nodes/Effects/E_MoonModules.h (2)

99-106: LGTM! Clean migration to addControlValue pattern.

The ruleset select control now uses the addControl/addControlValue builder pattern, which is cleaner and consistent with the PR-wide refactor.


120-124: LGTM! Const-reference parameter improves efficiency.

Changing from const JsonObject control to const JsonObject& control avoids an unnecessary copy. This aligns with the same signature change in other files (e.g., D_VirtualDriver.h, E__Sandbox.h).

src/MoonLight/Modules/ModuleLiveScripts.h (1)

101-112: LGTM! Control definitions migrated to addControl pattern.

The scripts row controls are now properly defined using addControl calls, consistent with other modules like ModuleDevices.h and ModuleTasks.h.

src/MoonLight/Modules/ModuleMoonLightInfo.h (1)

22-44: LGTM! Clean migration to addControl pattern.

The setupDefinition method now uses the standardized controls parameter and addControl/addControlValue pattern, consistent with other modules across the PR. The layers row definition is properly structured.

src/MoonBase/Modules/ModuleIO.h (4)

76-77: LGTM! New enum value correctly positioned.

The pin_Button_OnOff enum value is added at the correct position (after pin_Ethernet), and the corresponding UI label "Button OnOff" is added at line 204 in matching order.


110-213: LGTM! Clean migration to addControl/addControlValue pattern.

The setupDefinition method is properly refactored to use the new builder pattern. The "Button OnOff" label at line 204 correctly matches the pin_Button_OnOff enum position.


273-276: LGTM! SE16V1 board preset uses the new On/Off button pin.

Pin 46 is now assigned to pin_Button_OnOff, enabling the new on/off toggle functionality for this board.


375-384: Informative comment about ESP32-D0-16MB compatibility.

The commented-out pin assignments with the note about ESP32-D0-16MB limitations are helpful for future maintainers. Consider converting this to a more formal documentation format if these limitations become relevant.

src/MoonBase/Modules/ModuleTasks.h (2)

23-45: LGTM! Clean migration to the new pattern.

The setupDefinition method correctly uses the controls parameter and addControl calls with rows for nested task properties.


123-131: LGTM! Core task names and update call are correct.

The core0/core1 names are properly stored and the update call correctly emits the state changes.

src/MoonLight/Modules/ModuleLightsControl.h (1)

28-29: Sentinel initialization for pin fields is appropriate

Using UINT8_MAX as the “not configured” value for pinRelaisBrightness and pinToggleOnOff matches the later checks (!= UINT8_MAX) and avoids the old signed/unsigned mismatch you’d get with -1 on a uint8_t.

src/MoonLight/Nodes/Layouts/L_MoonLight.h (1)

249-281: Per-pin selection for SingleLine/SingleRow layouts is coherent

The new ledPinDIO select (with “Default” + LED 01..MAXLEDPINS) combined with:

nextPin(ledPinDIO == 0 ? UINT8_MAX : ledPinDIO - 1);

fits the PhysicalLayer::nextPin contract (0-based pin index vs. UINT8_MAX = default ordering) and keeps default behavior when left at 0.

Given MAXLEDPINS is the compile-time ceiling and ModuleDrivers::readPins compacts actual pins into ledPins[], this is a reasonable first version of per-layout pin override.

Also applies to: 290-322

src/MoonBase/Nodes.h (1)

64-68: Control plumbing improvements look good

The changes here tighten up the Node control API:

  • constructor(VirtualLayer*, const JsonArray&) keeps a live handle to the controls array without copying.
  • addControlValue(const T& value) now guards against an empty controls array and appends to the last control’s values list, which is exactly what the various layout/effect select controls expect.
  • The updateControl(const char* name, const T value) overload is a useful helper: it updates "value" and delegates to updateControl(const JsonObject&), ensuring all update side effects stay centralized.
  • nextPin(uint8_t ledPinDIO = UINT8_MAX) simply forwards to layerP->nextPin(ledPinDIO), aligning Node code with the new per-pin mapping semantics.

No behavioral concerns here.

Also applies to: 154-161, 163-174, 200-202

src/MoonLight/Nodes/Modifiers/M_MoonLight.h (1)

278-287: Rotate direction select wiring matches runtime logic

The new direction select with values "Clockwise", "Counter-Clockwise", and "Alternate" correctly maps to the existing logic (direction == 0/1/2 in loop()), and the use of addControlValue keeps it consistent with other selects in the codebase.

src/MoonLight/Nodes/Effects/E_MoonLight.h (1)

222-245: Select control refactors and const-correct onUpdate are consistent

  • ScrollingTextEffect’s font and preset selects now enumerate options via addControlValue, and the resulting indices line up with the choice/switch logic (cases 1–9).
  • WaveEffect’s type select defines six entries, matching the switch (type) cases 0–5.
  • FreqSawsEffect’s method select defines three entries, matching the if (method == 0/1/2) paths.
  • RubiksCubeEffect::onUpdate and ParticlesEffect::onUpdate now take const JsonObject&, which matches the updated base signature and avoids copying control objects.

No functional issues spotted in these refactors.

Also applies to: 526-536, 599-610, 919-924, 1116-1120

src/MoonBase/Modules/FileManager.h (1)

41-44: Renamed parameters clarify intent of FileManager state APIs

Switching the FilesState::read / update parameter names to stateJson and newData makes their roles clearer and aligns with the broader naming cleanup in Module/ModuleState. Assuming the .cpp definitions were updated accordingly, this is a straight semantic win.

src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1)

33-45: Art-Net/DDP controls and onUpdate signature are wired correctly

  • DDP checkbox, port number, and view select (with “Physical layer” + per-layer entries) are now built via the standard Node control helpers, matching patterns elsewhere in the codebase.
  • Updating onUpdate to take const JsonObject& control is consistent with the new base signature.

The current onUpdate body is a no-op aside from checking "DDP"; that’s fine if protocol switching is handled elsewhere or still WIP.

Also applies to: 47-53

src/MoonLight/Modules/ModuleDrivers.h (1)

51-57: Driver module node wiring and pin compaction look consistent

  • readPins() now compacts layerP.ledPins purely on != UINT8_MAX, which keeps only configured LED pins and simplifies the logic. Given ledsPerPin[] is computed later during mapping, this is acceptable.
  • addNodes(const JsonObject& control) correctly populates the driver/layout selector using addControlValue(control, ...) for all supported nodes, including SE16Layout, and aligns with the updated NodeManager API.
  • In addNode(), wiring moduleControl, moduleIO, and the new moduleNodes = (Module*)this ensures each Node has access to lights control, IO, and its owning Module for UI updates.

No issues spotted in these changes.

Also applies to: 78-102, 160-164

src/MoonLight/Nodes/Drivers/D_Infrared.h (2)

86-89: LGTM! Clean migration to addControl/addControlValue pattern.

The refactor from manual JsonObject construction to the helper-based approach is consistent with the codebase-wide changes and improves maintainability.


123-123: Consistent signature update to const reference.

The const JsonObject& parameter improves const-correctness and avoids unnecessary copies. This aligns with other onUpdate implementations across the codebase (e.g., D_VirtualDriver.h, D__Sandbox.h).

src/MoonLight/Modules/ModuleChannels.h (1)

24-54: LGTM! Properly migrated to controls-based API.

The refactor correctly:

  • Renames parameter to controls for clarity
  • Uses addControl() to create controls and addControlValue() to populate select options
  • Maintains the same functional behavior while improving consistency with the Module base class API
src/MoonBase/Modules/FileManager.cpp (2)

45-56: Consistent parameter naming in read().

The rename from root to stateJson improves clarity and aligns with the broader API refactor across ModuleState::read implementations.


58-132: Consistent parameter naming in update().

The rename from root to newData clarifies the purpose of the parameter (incoming data to be applied) and aligns with the ModuleState::update signature changes.

src/MoonBase/Nodes.cpp (3)

17-66: Simplified updateControl signature.

The removal of oldValue parameter and use of const JsonObject& is appropriate since the function operates on the control object directly and doesn't need the old value for its current implementation.


348-362: Clean migration to addControlValue pattern for light presets.

The refactor from manual array construction to sequential addControlValue() calls is more readable and consistent with the codebase convention.


400-400: Consistent onUpdate signature.

The const JsonObject& parameter aligns with other driver implementations.

src/MoonLight/Modules/ModuleEffects.h (3)

60-75: LGTM! Clean setupDefinition refactor.

The migration to addControl() and addControlValue() patterns is consistent with the base class API. The layer select control properly populates options via addControlValue(control, i).


77-147: Signature change enables cleaner value population.

Changing addNodes(const JsonArray& values) to addNodes(const JsonObject& control) allows direct use of addControlValue(control, ...) which is more consistent with the module-wide pattern.


270-272: Commented module assignments are intentional and correct for effect nodes.

Effect nodes only require moduleNodes to request UI updates. Driver nodes (like D_Infrared) require both moduleControl and moduleIO and receive these assignments in ModuleDrivers.h. Effect nodes do not use these members, so the commented assignments at lines 270-271 are intentional and should remain commented out.

src/MoonBase/NodeManager.h (3)

78-78: Consistent addNodes signature change.

The change from const JsonArray& values to const JsonObject& control aligns with the pattern where the control object contains the values array, enabling addControlValue() usage.


83-112: Clean setupDefinition refactor with nested control structure.

The refactor properly builds the nodes/controls hierarchy using addControl() and stores nested arrays in control["n"]. The pattern is consistent with other module implementations.


241-257: Improved control object passing to node methods.

Extracting the control object (nodeState["controls"][updatedItem.index[1]]) and passing it directly to updateControl() and onUpdate() is cleaner than passing the entire nodeState. This aligns with the simplified updateControl(const JsonObject& control) signature.

src/MoonBase/Module.h (4)

99-99: Consistent setupDefinition signature in std::function.

The parameter rename from root to controls in the std::function type matches the virtual method signature.


111-112: Clear parameter naming for read/update signatures.

Renaming to stateJson and newData clarifies the purpose of each parameter in the static methods.


142-166: Centralized UI update handling in Module::loop().

Moving requestUIUpdate handling to the base Module class is a good design decision. This eliminates the need for derived classes (like NodeManager) to implement their own loop override for this purpose. The implementation correctly:

  • Resets the flag before processing
  • Logs the action
  • Triggers an update with the appropriate origin ID

185-190: Useful generic addControlValue template.

The template function provides a clean, type-safe way to add values to a control's values array. The lazy initialization of the values array (to<JsonArray>() if null) is a nice defensive touch.

src/MoonBase/Module.cpp (5)

18-36: Consistent setDefaults refactor.

The parameter rename from root to controls and definition maintains clarity. The function correctly iterates over the definition array and sets defaults on the controls object.


63-67: Clear read() implementation with renamed parameter.

The stateJson parameter name clearly indicates this is the output JSON being populated from state.


196-221: Key fix: Preserve valid controls during state sync.

This is the critical fix addressing the PR objectives. The logic correctly:

  1. Checks if the array item is marked as valid (line 198)
  2. If not valid, proceeds with the existing removal logic (lines 200-215)
  3. If valid, skips removal and logs the action (lines 217-220)

This prevents newly added controls (marked valid in setup()) from being incorrectly removed when the persisted/UI state lacks those controls.


271-302: Clean update() implementation with clear parameter naming.

The rename to newData clarifies that this is the incoming data to be compared and merged with existing state. The logic flow remains unchanged.


352-370: Consistent setupDefinition and addControl implementations.

The base implementations correctly use the controls parameter name and demonstrate the expected pattern for derived classes.

Back end
========
- Nodes: addControl coord3D fix
- Artnet in: channel offset fix
- Panales: PanelsPerPin fix
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1)

159-167: Align DDP offset/length math with channelsPerLight to prevent overruns

startPixel and numPixels are computed with / 3, but the updated memcpy uses channelsPerLight bytes per pixel. When channelsPerLight != 3, this can overread pixelData and corrupt LED mapping.

Suggested fix:

-        // Calculate starting pixel from byte offset (3 bytes per pixel)
-        int startPixel = offset / 3;
-        int numPixels = min((uint16_t)(dataLen / 3), (uint16_t)(layer->layerP->lights.header.nrOfLights - startPixel));
+        // Calculate starting pixel from byte offset (channelsPerLight bytes per pixel)
+        uint8_t cpl = layer->layerP->lights.header.channelsPerLight;
+        if (cpl == 0) return;  // defensive guard
+        int startPixel = offset / cpl;
+        int numPixels = min(
+            (uint16_t)(dataLen / cpl),
+            (uint16_t)(layer->layerP->lights.header.nrOfLights - startPixel));
@@
-          int ledIndex = startPixel + i;
-          if (ledIndex < layer->layerP->lights.header.nrOfLights) {
-            memcpy(&layer->layerP->lights.channels[ledIndex * layer->layerP->lights.header.channelsPerLight], &pixelData[i * layer->layerP->lights.header.channelsPerLight], layer->layerP->lights.header.channelsPerLight);
-          }
+          int ledIndex = startPixel + i;
+          if (ledIndex < layer->layerP->lights.header.nrOfLights) {
+            memcpy(
+                &layer->layerP->lights.channels[ledIndex * cpl],
+                &pixelData[i * cpl],
+                cpl);
+          }

This keeps DDP parsing consistent with the configured channel layout per LED and avoids source-buffer overruns.

🧹 Nitpick comments (1)
src/MoonLight/Modules/ModuleLightsControl.h (1)

241-244: Consider using static constexpr instead of #define for debounce delay.

The #define debounceDelay 50 macro works but doesn't respect scope and can cause issues if this identifier is used elsewhere. A typed constant would be safer.

-  //see pinToggleOnOff
-  unsigned long lastDebounceTime = 0;
-  #define debounceDelay 50  // 50ms debounce
-  int lastState = HIGH;
+  // see pinToggleOnOff
+  unsigned long lastDebounceTime = 0;
+  static constexpr unsigned long debounceDelay = 50;  // 50ms debounce
+  int lastState = HIGH;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 792e11b and 35c0dbf.

📒 Files selected for processing (7)
  • docs/develop/modules.md (1 hunks)
  • src/MoonBase/NodeManager.h (2 hunks)
  • src/MoonBase/Nodes.h (6 hunks)
  • src/MoonLight/Modules/ModuleLightsControl.h (4 hunks)
  • src/MoonLight/Modules/ModuleLiveScripts.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (3 hunks)
  • src/MoonLight/Nodes/Layouts/L_MoonLight.h (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/MoonLight/Modules/ModuleLiveScripts.h (4)
src/MoonLight/Modules/ModuleLightsControl.h (1)
  • setupDefinition (92-138)
src/MoonBase/Modules/ModuleDevices.h (1)
  • setupDefinition (33-47)
src/MoonLight/Modules/ModuleMoonLightInfo.h (1)
  • setupDefinition (22-45)
src/MoonLight/Modules/ModuleEffects.h (1)
  • setupDefinition (60-147)
src/MoonLight/Nodes/Layouts/L_MoonLight.h (2)
src/MoonBase/Nodes.h (1)
  • nextPin (202-202)
src/MoonLight/Layers/PhysicalLayer.cpp (2)
  • nextPin (176-195)
  • nextPin (176-176)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (5)
src/MoonBase/NodeManager.h (1)
  • onUpdate (115-266)
src/MoonLight/Modules/ModuleLightsControl.h (1)
  • onUpdate (141-212)
src/MoonLight/Nodes/Drivers/D_Infrared.h (1)
  • onUpdate (123-193)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
  • onUpdate (31-37)
src/MoonLight/Modules/ModuleLightsControl.h (2)
src/MoonBase/Module.cpp (6)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
  • update (271-302)
  • update (271-271)
src/MoonBase/Module.h (1)
  • ModuleState (47-137)
src/MoonBase/NodeManager.h (3)
src/MoonBase/Nodes.h (9)
  • void (65-68)
  • void (71-71)
  • void (191-191)
  • void (192-192)
  • void (193-193)
  • void (196-196)
  • void (205-205)
  • Node (46-153)
  • Node (209-209)
src/MoonBase/Module.h (3)
  • void (150-166)
  • void (194-194)
  • void (195-195)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/Nodes.h (4)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
  • onUpdate (117-142)
src/MoonBase/Nodes.cpp (4)
  • onUpdate (400-540)
  • onUpdate (400-400)
  • updateControl (17-66)
  • updateControl (17-17)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
  • onUpdate (31-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (21)
src/MoonBase/NodeManager.h (3)

83-111: LGTM! Clean refactor to addControl/addControlValue pattern.

The setupDefinition implementation now uses the consistent builder pattern with addControl and addControlValue helpers. The nested structure for nodes and controls is correctly established using the rows array pattern.


241-256: LGTM! Proper control extraction before update.

The code correctly extracts the specific control object before calling updateControl and onUpdate, ensuring the node receives the actual control data rather than raw state elements.


78-78: All callers and implementations of addNodes are correctly updated for the new signature.

The signature change from const JsonArray& values to const JsonObject& control is consistently applied:

  • Base virtual declaration at line 78 has the new signature
  • Call site at line 94 passes the correct JsonObject parameter
  • All overrides in ModuleDrivers.h and ModuleEffects.h match the new signature

No signature mismatches detected.

src/MoonLight/Modules/ModuleLightsControl.h (4)

28-29: LGTM! Proper initialization to invalid pin value.

Using UINT8_MAX as the invalid/unassigned sentinel is consistent and appropriate for GPIO pin variables.


72-89: LGTM! Pin configuration properly handles INPUT_PULLUP.

The readPins() function correctly resets both pins to UINT8_MAX before scanning, and configures pinToggleOnOff with INPUT_PULLUP mode as recommended in the previous review.


92-138: LGTM! Clean refactor to addControl/addControlValue pattern.

The setupDefinition now uses the consistent builder pattern. All control definitions follow the established convention with proper defaults and value options.


293-306: LGTM! Debounce logic correctly implemented.

The implementation properly:

  1. Checks if the pin is configured (!= UINT8_MAX)
  2. Applies debounce timing before accepting state changes
  3. Triggers only on button press (HIGH→LOW for INPUT_PULLUP)
  4. Updates lastState after processing

This addresses the previous review feedback about debouncing and edge detection.

src/MoonBase/Nodes.h (6)

56-56: LGTM! Added module reference for UI updates.

The moduleNodes pointer enables nodes to request UI updates when needed, aligning with the refactored update signaling approach.


65-68: LGTM! Const-correctness improvement.

Changing the controls parameter to const JsonArray& correctly reflects that the constructor shouldn't modify the passed array reference.


155-162: LGTM! Guard against empty controls addressed.

The guard if (controls.size() == 0) return; prevents undefined behavior from the underflow issue identified in the previous review. The implementation correctly appends values to the last control's values array.


165-175: LGTM! Convenient template for updating controls by name.

The updateControl(const char* name, const T value) template provides a clean API to update a control's value and propagate the change through updateControl(control).


177-177: LGTM! Const-correctness for onUpdate.

Changing to const JsonObject& correctly indicates the control should not be modified by the handler.


202-202: LGTM! Extended nextPin with optional LED pin assignment.

The optional ledPinDIO parameter enables per-pin LED assignment, forwarding to the physical layer's implementation.

docs/develop/modules.md (1)

41-61: LGTM! Documentation accurately reflects the new API pattern.

The updated example correctly demonstrates:

  • The renamed controls parameter
  • The rows pattern for nested arrays
  • Proper use of addControl and addControlValue
  • Capturing the control before setting defaults (addressing previous review feedback)
src/MoonLight/Nodes/Layouts/L_MoonLight.h (5)

94-96: LGTM: Cleaner control definition pattern.

The refactor from JSON-based options to explicit addControlValue calls improves clarity and aligns with the broader API changes across the codebase.


131-185: Past review concerns resolved.

The implementation correctly addresses both issues flagged in the previous review:

  1. Division by zero prevented: Line 151 enforces a minimum value of 1 for panelsPerPin, eliminating the possibility of modulo-by-zero.
  2. Final partial group handled: Line 184 ensures any remaining panels after the loop are assigned to a pin.

The solution is cleaner than the originally suggested fix—enforcing the constraint at the control definition level rather than adding runtime guards. The panel grouping logic is correct.


201-207: LGTM: Complete axis ordering options.

The explicit enumeration of all six axis permutations is clear and matches the axisOrders array. Consistent with the refactoring pattern applied across other layouts.


254-281: LGTM: LED pin selection implemented correctly.

The ledPinDIO field and UI control properly map user selections to pin indices:

  • "Default" (0) → UINT8_MAX → default pin ordering in PhysicalLayer::nextPin
  • "LED 01" through "LED N" (1–N) → pins 0 through N-1

The Char<8> buffer is sufficient for the format string, and PhysicalLayer::nextPin validates pin indices, ensuring safe handling even if out-of-range values are set via the JSON API.


295-322: LGTM: Consistent LED pin selection.

The implementation mirrors SingleLineLayout correctly, maintaining consistency across the codebase. The same safe mapping and validation logic applies.

src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (2)

33-44: view select control population looks consistent with addControl/addControlValue pattern

addControl(view, "view", "select"); followed by addControlValue("Physical layer"); and then one addControlValue per layerP.layers entry matches the usual “define select then enumerate options” pattern and decouples options from any persisted state. No functional issues spotted here.


47-53: onUpdate signature and DDP control name alignment

Changing onUpdate to take const JsonObject& control and checking control["name"] == "DDP" aligns this driver with the other drivers’ onUpdate signatures and the label passed to addControl(ddp, "DDP", ...). The method body is still a stub, but that’s acceptable if no side effects are needed when toggling DDP beyond the standard state update.

Docs
====
- Add Standards and Guidelines

Back end
========
- Move Char out of Utilities to separate file
- Artnet in: use channelsPerLight
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1)

120-130: Add defensive guard for channelsPerLight == 0 (avoid division by zero).

While the code now uses channelsPerLight instead of hardcoded 3 (partially addressing the previous review), it still lacks a defensive guard against division by zero. If channelsPerLight is 0, line 123 will crash.

Additionally, the comment on line 122 is now misleading—it still says "3 channels per LED: RGB" but the code uses the dynamic channelsPerLight.

Apply this diff to add the guard and update the comment:

-          // Map DMX channels to LEDs (3 channels per LED: RGB)
-          int numPixels = min((uint16_t)(dataLength / layerP.lights.header.channelsPerLight), (uint16_t)(layerP.lights.header.nrOfLights));
+          // Map DMX channels to LEDs (channelsPerLight channels per LED)
+          uint8_t cpl = layerP.lights.header.channelsPerLight;
+          if (cpl == 0) return;  // defensive guard
+          int numPixels = min(
+              (uint16_t)(dataLength / cpl),
+              (uint16_t)(layerP.lights.header.nrOfLights));
 
           for (int i = 0; i < numPixels; i++) {
-            memcpy(&layerP.lights.channels[i * layerP.lights.header.channelsPerLight], &dmxData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight);
+            memcpy(
+                &layerP.lights.channels[i * cpl],
+                &dmxData[i * cpl],
+                cpl);
           }
🧹 Nitpick comments (10)
docs/develop/standardsguidelines.md (5)

38-38: Capitalize "GitHub".

Line 38 has "github reposistory"; should be "GitHub repository" (also fixes typo: reposistory → repository).

- ...consequences on our github reposistory. >
+ ...consequences on our GitHub repository. >

55-58: Apply hyphenation to compound adjectives.

Several compound adjectives before nouns lack hyphens: "AI generated code" → "AI-generated code", "front end files" → "front-end files", "AI generated source code" → "AI-generated source code". Apply consistently throughout per standard English grammar.

- * best practice: add a comment like ``'// below section of my code was generated by an AI``, when larger parts of your source code were not written by you personally.
+ * best practice: add a comment like ``'// below section of my code was generated by an AI``, when larger parts of your AI-generated source code were not written by you personally.
- We use Clang-format for c-files (see .clang-format in the repo for the definition) and Prettier for front end files.
+ We use Clang-format for c-files (see .clang-format in the repo for the definition) and Prettier for front-end files.
- Right click Format Document on each file you edit before committing.
+ Right-click Format Document on each file you edit before committing.

Also applies to: 65-67


29-29: Specify language for fenced code block.

Line 29 has an unspecified code fence. Specify a language hint for syntax highlighting.

- ```
+ ```markdown

62-62: Use fenced code blocks instead of indented.

Line 62 uses indented code block format; convert to fenced blocks with language specifier for consistency (MD046).

- Code block style
- Expected: fenced; Actual: indented
+ ```markdown
+ // This is a comment.

17-17: Fix markdown list indentation.

Multiple list items use 4-space indentation instead of 2-space standard (MD007). Lines 17, 26, 78–87 and others. Normalize indentation throughout.

- * Want to make changes: fork the repo (see installation)and submit pull requests, see [creating-a-pull-request-from-a-fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork):
+ * Want to make changes: fork the repo (see installation) and submit pull requests, see [creating-a-pull-request-from-a-fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork):
-     * Press Branches...
+   * Press Branches...

Also applies to: 26-26, 78-87

src/MoonLight/Modules/ModuleLiveScripts.h (2)

109-109: Document or remove commented-out "free" button.

The free button control is commented out here and its handler is commented at line 129. If this feature is temporarily disabled, add a TODO comment explaining when/why it should be re-enabled. If it's permanently unused, remove the commented code.


127-131: Consider error handling for LiveScript operations.

The calls to kill(), execute(), and killAndDelete() lack error handling. If these methods can fail, consider checking return values or wrapping calls in try-catch to provide user feedback on failures.

src/MoonBase/Char.h (2)

14-20: Include <functional> to make this header self‑contained.

std::function is used in split() but <functional> isn’t included here. Relying on transitive includes is brittle; please add the header explicitly.

You can verify with a clean build where no other headers include <functional> indirectly. A minimal fix:

-#include <Arduino.h>
-#include "ArduinoJson.h"
+#include <Arduino.h>
+#include <functional>
+#include "ArduinoJson.h"

116-119: Avoid std::string::npos sentinel to keep this type STL‑free.

indexOf() returns std::string::npos when the token isn’t found, which requires <string> and pulls in the STL on an embedded target. Since Char is a lightweight helper, it’s better to use a raw size_t sentinel instead.

A minimal change:

-  size_t indexOf(const char* token) const {
-    const char* pos = strnstr(s, token, sizeof(s));
-    return pos ? (pos - s) : std::string::npos;  // or SIZE_MAX
-  }
+  size_t indexOf(const char* token) const {
+    const char* pos = strnstr(s, token, sizeof(s));
+    return pos ? static_cast<size_t>(pos - s)
+               : static_cast<size_t>(-1);  // npos sentinel without std::string
+  }

If you prefer, you can also define a static constexpr size_t npos = static_cast<size_t>(-1); inside Char and return that instead.

src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (1)

86-111: Status string construction is safe; consider capacity if many pins are used.

The statusString assembly ("#" + nrOfPins + ": " + " pin#leds" per pin) is bounded by Char<32>and usesstrlcat, so it’s safe but may truncate once you have ~4+ pins. If you expect setups with many LED pins and want the full mapping visible in the UI, bumping this to Char<64>` (and adjusting the control’s max length) would make it more future‑proof.

A minimal change if you decide to expand it:

-  Char<32> status = "ok";
+  Char<64> status = "ok";
...
-    addControl(status, "status", "text", 0, 32, true);    // read only
+    addControl(status, "status", "text", 0, 64, true);    // read only
...
-      Char<32> statusString = "#";
+      Char<64> statusString = "#";

Re‑run a layout with many pins to verify the full status string appears as expected.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35c0dbf and 4d44b48.

📒 Files selected for processing (12)
  • docs/develop/development.md (1 hunks)
  • docs/develop/standardsguidelines.md (1 hunks)
  • interface/src/routes/moonbase/monitor/Monitor.svelte (0 hunks)
  • mkdocs.yml (1 hunks)
  • src/MoonBase/Char.h (1 hunks)
  • src/MoonBase/NodeManager.h (2 hunks)
  • src/MoonBase/Utilities.h (1 hunks)
  • src/MoonLight/Modules/ModuleLightsControl.h (4 hunks)
  • src/MoonLight/Modules/ModuleLiveScripts.h (4 hunks)
  • src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (5 hunks)
  • src/MoonLight/Nodes/Drivers/D_ArtnetOut.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (4 hunks)
💤 Files with no reviewable changes (1)
  • interface/src/routes/moonbase/monitor/Monitor.svelte
✅ Files skipped from review due to trivial changes (1)
  • src/MoonLight/Nodes/Drivers/D_ArtnetOut.h
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Modify C++ backend code in the `src/` and `lib/` directories for ESP32 development

Applied to files:

  • docs/develop/development.md
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Use ArduinoJson for JSON serialization and deserialization in C++ backend code

Applied to files:

  • src/MoonBase/Utilities.h
🧬 Code graph analysis (5)
src/MoonBase/NodeManager.h (3)
src/MoonBase/Nodes.h (9)
  • void (65-68)
  • void (71-71)
  • void (191-191)
  • void (192-192)
  • void (193-193)
  • void (196-196)
  • void (205-205)
  • Node (46-153)
  • Node (209-209)
src/MoonBase/Module.h (3)
  • void (150-166)
  • void (194-194)
  • void (195-195)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (1)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (3)
src/MoonBase/Nodes.cpp (2)
  • onUpdate (400-540)
  • onUpdate (400-400)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
  • onUpdate (69-73)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
  • onUpdate (31-37)
src/MoonBase/Char.h (1)
src/MoonBase/NodeManager.h (2)
  • begin (36-74)
  • void (78-78)
src/MoonLight/Modules/ModuleLiveScripts.h (2)
src/MoonBase/NodeManager.h (1)
  • setupDefinition (83-112)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
🪛 Clang (14.0.6)
src/MoonBase/Char.h

[error] 14-14: 'Arduino.h' file not found

(clang-diagnostic-error)

🪛 LanguageTool
docs/develop/standardsguidelines.md

[style] ~12-~12: Consider changing the order of words to improve your wording.
Context: ...ould contain compilable code and tested to not crash the system at minimal and support...

(TO_NOT_VB)


[uncategorized] ~38-~38: The official name of this software platform is spelled with a capital “H”.
Context: ...btle and unexpected consequences on our github reposistory. > For example, we regularl...

(GITHUB)


[style] ~52-~52: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ... descriptions into English. AI bots are very good at understanding language. However, alw...

(EN_WEAK_ADJECTIVE)


[grammar] ~55-~55: Use a hyphen to join words.
Context: ... sure you understand exactly what the AI generated code does * best practice: a...

(QB_NEW_EN_HYPHEN)


[grammar] ~58-~58: Use a hyphen to join words.
Context: ...nts for correctness * always review AI generated source code * If the AI has ...

(QB_NEW_EN_HYPHEN)


[grammar] ~65-~65: Use a hyphen to join words.
Context: ...r the definition) and Prettier for front end files. Right click Format Document ...

(QB_NEW_EN_HYPHEN)


[grammar] ~67-~67: Use a hyphen to join words.
Context: ...and Prettier for front end files. Right click Format Document on each file you e...

(QB_NEW_EN_HYPHEN)


[style] ~71-~71: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... what it does, including its arguments. Comments are also formatted using Clang-format a...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.18.1)
docs/develop/standardsguidelines.md

17-17: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


19-19: Images should have alternate text (alt text)

(MD045, no-alt-text)


26-26: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


29-29: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


62-62: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


78-78: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


79-79: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


80-80: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


81-81: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


82-82: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


85-85: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


86-86: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


87-87: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (19)
docs/develop/development.md (1)

10-11: Navigation update looks good.

The link to Standards and Guidelines improves user experience by directing readers to a centralized resource for development governance.

mkdocs.yml (1)

39-39: Navigation entry is correctly placed.

The new standards-guidelines entry is properly positioned in the Develop section and follows the existing naming convention.

src/MoonBase/NodeManager.h (3)

83-112: setupDefinition UI schema looks consistent with control helpers

The new setupDefinition builds the nodesname/on/controls structure using addControl/addControlValue and defaults that match the described model (selectFile for node name, checkbox on, nested controls rows with name/type/value). This is coherent with the helper patterns in Module and Nodes.h.


241-251: Passing full control object into Node control update is appropriate

Using

JsonObject control = nodeState["controls"][updatedItem.index[1]];
nodeClass->updateControl(control);
nodeClass->onUpdate(updatedItem.oldValue, control);

aligns with the Node::updateControl(const JsonObject&) / onUpdate(oldValue, control) pattern and ensures node logic has access to the entire control (name, type, value, metadata) rather than just the new value.


78-78: No action needed. All existing addNodes overrides in the codebase already use the correct signature void addNodes(const JsonObject& control). Both ModuleDrivers and ModuleEffects have matching overrides with the override keyword. No implementations using the old JsonArray signature were found.

src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (3)

36-44: LGTM: Clean refactor to helper methods.

The migration from manual JSON construction to addControl/addControlValue helpers improves maintainability and aligns with the codebase pattern described in the PR objectives.


47-47: LGTM: Const-correctness improvement.

Passing control by const reference avoids unnecessary copying and matches the pattern used elsewhere in the codebase (as shown in relevant snippets from D_VirtualDriver.h and D__Sandbox.h).


49-49: LGTM: Fixes control name mismatch.

The comparison now correctly matches the control name "DDP" defined on line 34. The previous lowercase "ddp" would never have matched, so this fixes a bug where the onUpdate handler would not trigger for this control.

src/MoonLight/Modules/ModuleLightsControl.h (7)

28-29: LGTM! Clean pin field declarations.

Both pins use UINT8_MAX as the sentinel value for "no pin assigned," which is consistent and appropriate for optional GPIO configuration.


74-75: LGTM! Proper pin reset before scanning.

Resetting both pins to UINT8_MAX before scanning the configuration ensures clean state and prevents stale assignments if the configuration changes.


81-84: LGTM! Button pin properly configured.

The pinMode configuration is now present (addressed from past review), and INPUT_PULLUP is the correct mode for a button input with internal pull-up resistor.


92-138: LGTM! Clean refactor to addControl helpers.

The migration from manual JSON construction to addControl and addControlValue helpers significantly improves readability and maintainability while preserving the original control definitions.


288-288: LGTM! Consistent server update routing.

The change to _moduleName + "server" as the update destination is consistent with the same pattern used for button toggles (line 302), establishing clear routing for automated/server-side updates.


293-306: LGTM! Debouncing correctly implemented.

The button handling now includes:

  • 50ms debounce delay to prevent contact bounce
  • Edge detection (triggers only on button press, HIGH→LOW transition with INPUT_PULLUP)
  • State tracking to avoid repeated triggers

This addresses all concerns from the past review and implements the debouncing correctly.


241-244: Initialize lastDebounceTime to prevent undefined behavior.

The lastDebounceTime field is declared without an initializer, giving it an indeterminate value. While the debounce logic may work in practice due to unsigned arithmetic behavior, this is a latent bug that could cause unexpected behavior on the first button press.

Apply this diff to initialize the field:

   unsigned long lastPresetTime = 0;
   // see pinToggleOnOff
-  unsigned long lastDebounceTime;
+  unsigned long lastDebounceTime = 0;
   static constexpr unsigned long debounceDelay = 50;  // 50ms debounce
   int lastState = HIGH;

Likely an incorrect or invalid review comment.

src/MoonLight/Modules/ModuleLiveScripts.h (1)

61-61: LGTM: Formatting consistency.

The spacing adjustment in as<const char*>() improves const-correctness formatting without changing functionality.

Also applies to: 79-79

src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (2)

28-45: Version/status controls wiring looks consistent with the new Char API.

Using Char<32> for version and status, exposing them as read‑only text controls, and calling updateControl("version", HP_ALL_VERSION) on setup aligns well with the module control helpers and should keep existing UIs in sync.


93-107: Pins remapping and driver updates look correct and bounded by nrOfPins.

Using assignedPin < layerP.nrOfLedPins before indexing layerP.ledPins[assignedPin] and falling back to layerP.ledPins[i] keeps pins[] within the compacted valid range, and passing pins + nrOfPins into both initled() and updateDriver() ensures the physical mapping and the status string stay in sync.

Please sanity‑check a few boards with non‑trivial ledPinsAssigned mappings (and reconfigure presets) to confirm that the reported status string exactly matches the actual physical pin order used by the driver.

Also applies to: 119-133

src/MoonBase/Utilities.h (1)

17-18: Char utilities extraction into Char.h is wired correctly here.

Including "Char.h" from Utilities.h maintains backward compatibility for code that only includes Utilities.h while centralizing the Char implementation in its own header, which is a good clean‑up.

Comment on lines 102 to 110
Char<N> substring(uint16_t begin, uint16_t end = sizeof(s) - 1) {
Char<N> sub;
if (begin >= sizeof(s) || end >= sizeof(s))
sub = "";
else {
strlcpy(sub.s, s + begin, end - begin + 1);
}
return sub;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "Char.h" -type f

Repository: MoonModules/MoonLight

Length of output: 86


🏁 Script executed:

cat -n ./src/MoonBase/Char.h

Repository: MoonModules/MoonLight

Length of output: 7980


🏁 Script executed:

find . -name "*.cpp" -o -name "*.h" -o -name "*.ino" | xargs grep -l "substring" | head -20

Repository: MoonModules/MoonLight

Length of output: 474


🏁 Script executed:

find . -name "*test*" -o -name "*Test*" | grep -E "\.(cpp|h|ino)$" | head -20

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

rg "substring\(" --type cpp --type h -B 2 -A 2 | head -60

Repository: MoonModules/MoonLight

Length of output: 3513


🏁 Script executed:

cat -n ./src/MoonBase/Char.h | sed -n '102,110p'

Repository: MoonModules/MoonLight

Length of output: 363


Fix off‑by‑one truncation and missing bounds check in substring().

The substring() method has two bugs: (1) substring(0, 0) returns an empty string instead of a single character because strlcpy is called with size 0 - 0 + 1 = 1, which only leaves room for a NUL terminator. (2) There is no guard for end < begin, which causes uint16_t underflow and undefined behavior. These are behavioral bugs, though memory-safe.

The proposed fix correctly adds the missing bounds check and properly calculates the buffer size for strlcpy. Consider adding unit tests for substring(0, 0), substring(1, 3), and out-of-range cases.

Comment on lines +100 to +102
addControl(rows, "isRunning", "checkbox", false, true, true);
addControl(rows, "isHalted", "checkbox", false, true, true);
addControl(rows, "exeExist", "checkbox", false, true, true);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find where addControl is defined
fd "\.h$|\.cpp$" | head -20

# Search for addControl function definitions
echo "=== Looking for addControl definitions ==="
rg -n "void.*addControl|addControl\s*\(" --type=cpp --type=h -A 2 | head -100

Repository: MoonModules/MoonLight

Length of output: 8450


🏁 Script executed:

# Find the Module files to understand addControl
find . -name "Module*.h" -o -name "Module*.cpp" | head -20

Repository: MoonModules/MoonLight

Length of output: 481


🏁 Script executed:

# Search for addControl signatures in header files
rg -n "addControl" --type=h -B 1 -A 3

Repository: MoonModules/MoonLight

Length of output: 50377


🏁 Script executed:

# Look at addControl implementation in Module.cpp
cat -n src/MoonBase/Module.cpp | grep -A 20 "^.*addControl.*{" | head -40

Repository: MoonModules/MoonLight

Length of output: 631


🏁 Script executed:

# Check the pattern more specifically - look at checkbox usage with parameters
rg -n 'addControl.*checkbox.*true.*true' --type=cpp --type=h

Repository: MoonModules/MoonLight

Length of output: 712


Use standard 3-parameter addControl for checkbox controls.

Lines 100-102 unnecessarily pass false, true, true as min/max/ro parameters for checkbox controls. This generates "max": 1 in the JSON output and is inconsistent with how all other checkboxes are defined in the codebase. Use the standard 3-parameter form: addControl(rows, "isRunning", "checkbox") and set the read-only flag separately if needed, or verify that these checkboxes actually require explicit min/max constraints.

🤖 Prompt for AI Agents
In src/MoonLight/Modules/ModuleLiveScripts.h around lines 100 to 102, the
checkbox controls call addControl(rows, "isRunning", "checkbox", false, true,
true) (and similar) which incorrectly supplies min/max/ro parameters causing a
"max": 1 JSON entry; change each to use the standard 3-parameter form
addControl(rows, "isRunning", "checkbox") (and likewise for isHalted and
exeExist), and if any need read-only behavior set the read-only flag via the
established separate API or property after creation rather than passing extra
min/max args.

Comment on lines +107 to +110
addControl(rows, "stop", "button");
addControl(rows, "start", "button");
// addControl(rows, "free", "button");
addControl(rows, "delete", "button");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find getScriptsJson implementation
echo "=== Searching for getScriptsJson implementation ==="
rg -n "getScriptsJson" --type=cpp -B 2 -A 20

echo -e "\n=== Look for button/control field definitions in the file ==="
fd . -name "*.h" -o -name "*.cpp" | xargs rg -l "ModuleLiveScripts" | head -5

echo -e "\n=== Check related files in ModuleLiveScripts directory ==="
fd "ModuleLiveScripts" -type f

Repository: MoonModules/MoonLight

Length of output: 5787


Button name mismatch with getScriptsJson output requires clarification.

The setupDefinition defines buttons named stop, start, and delete (lines 107-110), but getScriptsJson emits fields named kill, execute, delete, and free (src/MoonBase/Nodes.cpp lines 332-335). While the onUpdate handler maps user clicks (stop → kill, start → execute), typical UI frameworks expect consistent naming between button names and JSON field names for proper control binding. Verify that the UI framework correctly handles this name mismatch, or consider renaming buttons to match the JSON field names (kill, execute, delete, free).

Comment on lines +159 to 167
int startPixel = offset / layerP.lights.header.channelsPerLight;
int numPixels = min((uint16_t)(dataLen / layerP.lights.header.channelsPerLight), (uint16_t)(layerP.lights.header.nrOfLights - startPixel));

// Update LEDs
for (int i = 0; i < numPixels; i++) {
int ledIndex = startPixel + i;
if (ledIndex < layer->layerP->lights.header.nrOfLights) {
memcpy(&layer->layerP->lights.channels[i * layer->layerP->lights.header.channelsPerLight], &pixelData[i * layer->layerP->lights.header.channelsPerLight], layer->layerP->lights.header.channelsPerLight);
if (ledIndex < layerP.lights.header.nrOfLights) {
memcpy(&layerP.lights.channels[ledIndex * layerP.lights.header.channelsPerLight], &pixelData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add defensive guard for channelsPerLight == 0 in DDP handler.

The DDP handler has the same division-by-zero vulnerability as the ArtNet handler. Lines 159-160 divide by channelsPerLight without checking if it's zero.

Apply this diff to add the guard:

         uint8_t* pixelData = packetBuffer + sizeof(DDPHeader);
 
-        // Calculate starting pixel from byte offset (3 bytes per pixel)
-        int startPixel = offset / layerP.lights.header.channelsPerLight;
-        int numPixels = min((uint16_t)(dataLen / layerP.lights.header.channelsPerLight), (uint16_t)(layerP.lights.header.nrOfLights - startPixel));
+        // Calculate starting pixel from byte offset (channelsPerLight bytes per pixel)
+        uint8_t cpl = layerP.lights.header.channelsPerLight;
+        if (cpl == 0) return;  // defensive guard
+        int startPixel = offset / cpl;
+        int numPixels = min((uint16_t)(dataLen / cpl), (uint16_t)(layerP.lights.header.nrOfLights - startPixel));
 
         // Update LEDs
         for (int i = 0; i < numPixels; i++) {
           int ledIndex = startPixel + i;
           if (ledIndex < layerP.lights.header.nrOfLights) {
-            memcpy(&layerP.lights.channels[ledIndex * layerP.lights.header.channelsPerLight], &pixelData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight);
+            memcpy(&layerP.lights.channels[ledIndex * cpl], &pixelData[i * cpl], cpl);
           }
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/MoonLight/Nodes/Drivers/D_ArtnetIn.h around lines 159 to 167, add a
defensive guard to handle layerP.lights.header.channelsPerLight == 0 before any
division or use; detect zero channelsPerLight and bail out (return or skip
processing) with an early exit so you don't perform offset / channelsPerLight or
dataLen / channelsPerLight, and avoid further memcpy operations when
channelsPerLight is invalid; keep behavior consistent with the ArtNet handler’s
guard and ensure no further computations run when the value is zero.

Front end
========
- Multirow: add crud(s) to display add, delete and sort/drag

Back end
=======
- Devices, IO, Tasks, Live scripts, Info: add crud
- Nodes: addControl: always update definition
- Virtual driver: remove (will be part of Physical driver -> rename...)
- Utilities: EXT_LOGx: conditional (saves flash)
- Physical layer: layoutPre: set nrOfAssignedPins = 0
- Channels: check if channels defined
- Infrared: no logging if symbol is 1
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/MoonBase/Modules/ModuleIO.h (1)

19-78: LED color select values are reversed relative to the enum.

The enum defines RGB pins as pin_LED_B (23), pin_LED_G (24), pin_LED_R (25), but the select control adds them as "LED R" (23), "LED G" (24), "LED B" (25). Since select controls use sequential array indices that map directly to enum values, a user selecting "LED R" from the UI will store index 23, which the code interprets as pin_LED_B, causing red/blue to swap.

Reorder the select values to match the enum:

-      addControlValue(control, "LED R");
-      addControlValue(control, "LED G");
-      addControlValue(control, "LED B");
+      addControlValue(control, "LED B");
+      addControlValue(control, "LED G");
+      addControlValue(control, "LED R");
♻️ Duplicate comments (1)
src/MoonLight/Modules/ModuleLiveScripts.h (1)

108-111: Button names vs LiveScriptNode::getScriptsJson: confirm consistency

The builder defines stop, start, and delete buttons, and onUpdate maps them to kill(), execute(), and killAndDelete() respectively, which is internally consistent here. However, a prior review already flagged a possible mismatch with LiveScriptNode::getScriptsJson field names (kill, free, delete, execute). Please confirm that getScriptsJson now emits fields compatible with these button names, or that the UI framework does not depend on matching state keys for buttons.

🧹 Nitpick comments (10)
src/MoonLight/Layers/PhysicalLayer.cpp (1)

177-196: LGTM! Consider adding range validation if ledPinDIO is constrained.

The function signature change and conditional assignment logic are correct. The use of UINT8_MAX as a sentinel for default behavior is appropriate and maintains backward compatibility through the default parameter. The array bounds are properly checked (i < MAXLEDPINS).

If ledPinDIO values are expected to be within a specific range (e.g., valid GPIO pin numbers), consider adding validation or documenting the expected range in the function/parameter documentation. However, since the value is only stored here and not used as an array index, the current implementation is safe.

interface/src/lib/components/moonbase/MultiRow.svelte (1)

187-187: Consider simplifying the double-negative logic.

The expression !(findItemInDefinition.crud == null || findItemInDefinition.crud.includes('s')) is correct but uses a double negative. Applying De Morgan's law improves readability:

-dragDisabled={!(findItemInDefinition.crud == null || findItemInDefinition.crud.includes('s'))}
+dragDisabled={findItemInDefinition?.crud != null && !findItemInDefinition?.crud?.includes('s')}

This also incorporates the null safety fix from the previous comment.

src/MoonLight/Modules/ModuleLiveScripts.h (2)

91-113: setupDefinition refactor to controls/rows pattern looks correct

The new setupDefinition(const JsonArray& controls) implementation follows the shared builder pattern:

  • Defines a single "scripts" control of type "rows" with "crud": "ru", then populates rows = control["n"].to<JsonArray>(), which is consistent with other modules’ array controls.
  • Per-row fields (name, isRunning, isHalted, exeExist, handle, binary_size, data_size, error, stop, start, delete) align with what loop1s() pushes into _state.data["scripts"] via LiveScriptNode::getScriptsJson(...) and with the onUpdate handler below (at least for stop, start, delete).

The checkbox addControl calls pass explicit min=0, max=1, and ro=true. That’s functionally fine (booleans as 0/1) but still slightly inconsistent with the simpler three‑parameter checkbox uses elsewhere; consider simplifying to the common form if you want consistency, but it’s not required.


100-103: Checkbox min/max/ro usage is functionally fine; style-only concern

Using addControl(rows, "...", "checkbox", false, true, true) correctly results in [0,1] range with read‑only flag set through the min/max/ro parameters of Module::addControl. If the rest of the codebase mostly uses the 3‑arg form for checkboxes, you could switch to that for style consistency, but there’s no correctness issue here.

docs/develop/nodes.md (1)

57-57: Typo: "choosen" should be "chosen".

-* Uses ESPLiveScripts, see compileAndRun. compileAndRun is started when in Nodes a file.sc is choosen
+* Uses ESPLiveScripts, see compileAndRun. compileAndRun is started when in Nodes a file.sc is chosen
src/MoonBase/Modules/ModuleTasks.h (1)

68-73: Consider renaming local variable to avoid conceptual shadowing.

The local JsonObject controls in loop1s() could be confused with the controls parameter in setupDefinition(). Consider renaming to state or taskData for clarity:

     JsonDocument doc;
     doc["tasks"].to<JsonArray>();
-    JsonObject controls = doc.as<JsonObject>();
+    JsonObject taskData = doc.as<JsonObject>();

And update subsequent references accordingly.

src/MoonBase/Modules/ModuleIO.h (2)

269-279: SE16V1 preset: pin_Button_OnOff mapping is reasonable, but verify hardware expectation.

For board_SE16V1, the preset now assigns:

  • Buttons: GPIO 0 → pin_Button_01, GPIO 45 → pin_Button_02, GPIO 46 → pin_Button_OnOff.

This appears coherent, but it’s hardware-specific. Please confirm that GPIO 46 on SE16V1 is indeed the intended On/Off button input and that no other module expects it to retain a previous usage (e.g., pin_Button_03).


270-377: Looping over fixed arrays with sizeof works but could be made more robust.

Patterns like:

uint8_t ledPins[16] = { ... };
for (int i = 0; i < sizeof(ledPins); i++)
  pins[ledPins[i]]["usage"] = pin_LED_01 + i;

rely on sizeof(uint8_t) == 1. That’s safe today, but slightly brittle. Consider using an element-count idiom for future-proofing and readability:

- for (int i = 0; i < sizeof(ledPins); i++)
+ for (size_t i = 0; i < sizeof(ledPins) / sizeof(ledPins[0]); i++)

(and similarly for ethernetPins and other arrays).

src/MoonBase/Nodes.h (2)

18-19: Including ModuleIO.h directly in Nodes.h is functional but creates a tighter coupling.

Pulling in ModuleIO.h to get IO_PinUsage and Module also brings in all of ModuleIO’s dependencies into this core header. If layering becomes a concern, consider:

  • Forward-declaring class Module; here, and
  • Moving IO_PinUsage into a lighter-weight shared header (e.g., IO_Pins.h) that both ModuleIO and nodes/drivers can include.

This is non-blocking but worth considering to keep Nodes.h lean.


52-57: New moduleNodes pointer is fine; ensure it’s initialized by the owner.

Adding Module* moduleNodes = nullptr; for UI-update signaling is reasonable. Just ensure the code that constructs/configures nodes (likely NodeManager or the owning Module) assigns this pointer appropriately before any logic that relies on it, to avoid null dereferences when requesting UI updates.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d44b48 and 014564c.

📒 Files selected for processing (16)
  • docs/develop/nodes.md (1 hunks)
  • docs/moonbase/devices.md (1 hunks)
  • interface/src/lib/components/moonbase/MultiRow.svelte (4 hunks)
  • src/MoonBase/Modules/ModuleDevices.h (1 hunks)
  • src/MoonBase/Modules/ModuleIO.h (4 hunks)
  • src/MoonBase/Modules/ModuleTasks.h (3 hunks)
  • src/MoonBase/Nodes.h (6 hunks)
  • src/MoonBase/Utilities.h (2 hunks)
  • src/MoonLight/Layers/PhysicalLayer.cpp (4 hunks)
  • src/MoonLight/Modules/ModuleChannels.h (2 hunks)
  • src/MoonLight/Modules/ModuleDrivers.h (3 hunks)
  • src/MoonLight/Modules/ModuleLiveScripts.h (4 hunks)
  • src/MoonLight/Modules/ModuleMoonLightInfo.h (1 hunks)
  • src/MoonLight/Nodes/Drivers/D_Infrared.h (3 hunks)
  • src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (0 hunks)
  • src/MoonLight/Nodes/Drivers/D__Sandbox.h (2 hunks)
💤 Files with no reviewable changes (1)
  • src/MoonLight/Nodes/Drivers/D_VirtualDriver.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/MoonBase/Modules/ModuleDevices.h
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Use ArduinoJson for JSON serialization and deserialization in C++ backend code

Applied to files:

  • src/MoonBase/Utilities.h
🧬 Code graph analysis (7)
src/MoonBase/Modules/ModuleTasks.h (3)
src/MoonBase/Modules/ModuleDevices.h (1)
  • setupDefinition (33-48)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
  • setupDefinition (91-113)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Modules/ModuleChannels.h (2)
src/MoonBase/Modules/ModuleDevices.h (1)
  • setupDefinition (33-48)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Layers/PhysicalLayer.cpp (1)
src/MoonBase/Nodes.h (1)
  • nextPin (210-210)
src/MoonLight/Nodes/Drivers/D_Infrared.h (5)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
  • onUpdate (31-37)
src/MoonBase/NodeManager.h (1)
  • onUpdate (115-266)
src/MoonBase/Nodes.cpp (2)
  • onUpdate (400-540)
  • onUpdate (400-400)
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
  • onUpdate (920-924)
  • onUpdate (1116-1120)
src/MoonLight/Modules/ModuleMoonLightInfo.h (2)
src/MoonBase/Modules/ModuleDevices.h (1)
  • setupDefinition (33-48)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/Nodes.h (5)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
  • onUpdate (116-141)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
  • onUpdate (31-37)
src/MoonBase/Nodes.cpp (4)
  • onUpdate (400-540)
  • onUpdate (400-400)
  • updateControl (17-66)
  • updateControl (17-17)
src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
  • onUpdate (120-124)
src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
  • onUpdate (35-41)
src/MoonLight/Modules/ModuleLiveScripts.h (2)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Modules/ModuleEffects.h (1)
  • setupDefinition (60-147)
🪛 LanguageTool
docs/develop/nodes.md

[style] ~17-~17: In American English, abbreviations like “etc.” require a period.
Context: ...pes (sliders/range, checkboxes, numbers etc). They are added with the addControl() ...

(ETC_PERIOD)


[grammar] ~19-~19: Use a hyphen to join words.
Context: ...e supports. They control when a physical to virtual mapping is recalculated *...

(QB_NEW_EN_HYPHEN)


[grammar] ~19-~19: Use a hyphen to join words.
Context: ...upports. They control when a physical to virtual mapping is recalculated * **...

(QB_NEW_EN_HYPHEN)


[grammar] ~29-~29: Use a hyphen to join words.
Context: ...support different lights e.g a mix of 15 channel lights and 32 channel lights etc...

(QB_NEW_EN_HYPHEN)


[grammar] ~29-~29: Use a hyphen to join words.
Context: ...ts e.g a mix of 15 channel lights and 32 channel lights etc. You could set channe...

(QB_NEW_EN_HYPHEN)


[style] ~31-~31: Consider using polite language here.
Context: ...wheel could be used to really go crazy. Let us know when you have one of these setups 🚨 * Movin...

(INSERT_PLEASE)


[grammar] ~45-~45: Use a hyphen to join words.
Context: ...he LEDs / channels array and the virtual to physical layer mappings. * specif...

(QB_NEW_EN_HYPHEN)


[grammar] ~45-~45: Use a hyphen to join words.
Context: ...LEDs / channels array and the virtual to physical layer mappings. * specify w...

(QB_NEW_EN_HYPHEN)


[grammar] ~57-~57: Ensure spelling is correct
Context: ...n is started when in Nodes a file.sc is choosen * To do: kill running scripts, e.g. when c...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~62-~62: In American English, abbreviations like “etc.” require a period.
Context: ...tPan, setTilt, setZoom etc. Also getRGB etc functions exists. ## Drivers ### Init...

(ETC_PERIOD)

🪛 markdownlint-cli2 (0.18.1)
docs/moonbase/devices.md

9-9: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

docs/develop/nodes.md

20-20: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


21-21: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


39-39: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


42-42: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


43-43: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


44-44: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


45-45: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


46-46: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


47-47: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


48-48: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


49-49: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


50-50: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


51-51: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


52-52: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


53-53: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


58-58: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


62-62: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (32)
docs/moonbase/devices.md (1)

5-5: Documentation updates are clear and accurate.

The reworded description better explains the Devices module's purpose, and the added WiFi station link helps users locate where to configure the device name. The changes align well with the PR's UI control refactoring.

Also applies to: 7-7

src/MoonLight/Modules/ModuleMoonLightInfo.h (3)

22-25: LGTM! Signature and variable declarations align with the new API pattern.

The parameter rename to controls matches the base class signature, and the local variables follow the established pattern for the addControl-based API.


27-31: LGTM! Top-level controls properly defined.

The addControl calls correctly define read-only info controls that are populated by the readHook (lines 54-60). The pattern is clean and consistent.


33-45: LGTM! Nested rows control follows the established pattern.

The layers control is properly structured as a read-only rows type, and the nested control definitions match the data population in the readHook (lines 83-92). The use of control["n"].to<JsonArray>() to obtain the rows array is consistent with the pattern shown in ModuleDevices.h.

src/MoonLight/Layers/PhysicalLayer.cpp (2)

136-139: LGTM!

The initialization of ledPinsAssigned and nrOfAssignedPins is correct and consistent with the existing pattern for ledsPerPin. The array is properly cleared and the counter is reset before the layout pass.


44-44: Allocation is within the stated heap limit and includes proper failure handling.

The 24,576-byte allocation (8192 × 3) is under the 30KB max allocation limit mentioned in the comments. The code already includes allocation failure detection (lines 48–53): if the allocation fails, it logs an error, sets maxChannels to 0, and continues. This graceful degradation means the concern about OOM is mitigated by existing error handling. No additional verification is required.

interface/src/lib/components/moonbase/MultiRow.svelte (1)

101-101: LGTM – Type annotations improve clarity.

The explicit type annotations on the callback parameters are good for readability and type safety.

Also applies to: 105-105, 107-107

src/MoonLight/Modules/ModuleLiveScripts.h (2)

40-84: FileManager update handler logging change is safe

The added debug logs using nodeState["name"].as<const char*>() look correct and match common patterns elsewhere; no functional impact beyond improved traceability.


128-133: onUpdate action mapping is coherent with defined buttons

The updated onUpdate logic cleanly maps:

  • "stop"liveScriptNode->kill()
  • "start"liveScriptNode->execute()
  • "delete"liveScriptNode->killAndDelete()

combined with the updatedItem.oldValue != "" guard to avoid firing at boot. This matches the button names defined in setupDefinition and looks correct.

docs/develop/nodes.md (1)

17-24: Documentation aligns well with the addControl API changes.

The terminology updates from setControl() to addControl() correctly reflect the code refactoring in this PR. The expanded explanations for hasOnLayout() and driver ordering are helpful for developers.

src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)

31-37: LGTM! Signature change to const reference is correct.

The onUpdate signature change from const JsonObject control to const JsonObject& control is consistent with the broader API refactor visible in src/MoonBase/Nodes.cpp and other driver files. Passing by const reference avoids unnecessary copies.

src/MoonLight/Nodes/Drivers/D_Infrared.h (3)

86-89: LGTM! Clean migration to addControlValue pattern.

The setup now uses the streamlined addControlValue() pattern instead of intermediate JsonObject/JsonArray construction, which is cleaner and consistent with the broader API refactor.


123-123: LGTM! Signature change to const reference is correct.

Consistent with D__Sandbox.h and the base class signature in src/MoonBase/Nodes.cpp.


408-408: Verify logging condition logic.

The condition rx_data.num_symbols != 1 will log for valid NEC frames (34 symbols for standard frames, 2 for repeat codes), since the comment indicates only 34 and 2 are processed. Did you intend to log only for invalid symbol counts?

If the intent is to log unexpected frames, consider:

-        if (rx_data.num_symbols != 1) EXT_LOGD(IR_DRIVER_TAG, "Received symbols: #%d", rx_data.num_symbols); // will not be processed (only 34 and 2)
+        if (rx_data.num_symbols != 34 && rx_data.num_symbols != 2) EXT_LOGD(IR_DRIVER_TAG, "Received symbols: #%d (unexpected)", rx_data.num_symbols);
src/MoonLight/Modules/ModuleDrivers.h (3)

54-57: LGTM! Simplified compacting logic.

The condition now only checks for valid pins (!= UINT8_MAX) without the previous ledsPerPin checks, which is cleaner and correctly focuses on pin validity during array compaction.


78-101: LGTM! Consistent migration to addControlValue pattern.

The signature change and use of addControlValue(control, ...) aligns with the broader API refactor across the codebase.


159-160: LGTM! Node wiring for UI updates.

Assigning moduleIO and moduleNodes enables proper pin allocation tracking and UI update signaling, which aligns with the PR objectives for module-level UI updates.

src/MoonBase/Utilities.h (2)

18-18: LGTM! Clean extraction of Char template to separate header.

Moving Char<N> and related utilities to Char.h improves modularity and reduces the size of this utility header.


26-77: LGTM! Compile-time logging level gates.

Gating logging macros by CORE_DEBUG_LEVEL eliminates logging overhead entirely when disabled, rather than relying on runtime checks. This is a good optimization for embedded systems.

src/MoonLight/Modules/ModuleChannels.h (2)

24-54: LGTM! Consistent migration to addControl/addControlValue pattern.

The signature change and control population align with the broader API refactor seen in ModuleDevices.h and the base Module::setupDefinition in src/MoonBase/Module.cpp.


77-77: Good defensive null check to prevent crash during init.

The early return when layerP.lights.channels is null prevents potential null pointer dereference during initialization.

src/MoonBase/Modules/ModuleTasks.h (1)

23-46: LGTM! Consistent migration to controls/rows pattern.

The refactored setupDefinition follows the same pattern as ModuleDevices.h and ModuleLiveScripts.h, using addControl with a "rows" type and populating nested fields via control["n"].to<JsonArray>().

src/MoonBase/Modules/ModuleIO.h (3)

110-215: setupDefinition refactor to addControl/addControlValue looks correct and aligns with the valid-flag pattern.

The refactored setupDefinition(const JsonArray& controls) cleanly rebuilds:

  • boardPreset as a "select" with ordered board names,
  • modded as a "checkbox",
  • maxPower as a "number",
  • pins as a "rows" control with nested GPIO, usage, summary, Level, and DriveCap.

Using addControl + addControlValue is consistent with the Node/control pattern elsewhere, and the pins rows configuration (filter !Unused, crud = "ru") matches the expected semantics for editing pin usages and viewing derived summaries.

No functional issues stand out here, assuming the underlying Module::addControl/addControlValue implementations mirror the Node versions described in Nodes.h.


148-207: New “Button OnOff” usage is wired consistently into the pins-usage select.

The "usage" select under the pins rows now includes "Button OnOff" as the last value (Line 206), which matches the placement of pin_Button_OnOff at the end of IO_PinUsage. This keeps the index/value mapping consistent for the new usage type.

Once the LED RGB ordering noted earlier is settled, this values list looks structurally correct.


377-386: Default-board preset comments are clear and non-invasive.

The added comments around the default board’s extra LED pin mappings clarify ESP32-D0-16MB limitations without affecting behavior (pins remain commented out). Looks good as documentation-only change.

src/MoonBase/Nodes.h (7)

64-68: Constructor constructor(VirtualLayer*, const JsonArray&) change is sensible.

Accepting const JsonArray& controls and simply storing it into this->controls avoids unnecessary copies while keeping the interface clear. As long as the underlying JsonDocument outlives the node (which is how these arrays are usually managed), this is a safe and more efficient signature.


75-160: addControl refactor: good reuse of existing controls and correct use of valid flag; minor behavior notes.

Key points in the new addControl implementation:

  • It first searches controls by name and reuses an existing control if found, only creating a new one when missing (Lines 80–94). This preserves persisted value while updating metadata.
  • For a new control, it sets name, initial value, and default from variable, then updates type, pointer p, valid = true, and optional ro/min/max/desc fields (Lines 96–117).
  • It computes size based on type and ControlType, including the corrected Coord3D handling (Lines 119–147), aligning with how updateControl interprets pointers.
  • When newControl is true, it calls onUpdate with an empty oldValue to allow nodes to react to the initial value (Lines 151–154).
  • control["valid"] = true ensures that newly added controls participate correctly in the "valid flag" lifecycle described in the PR (NodeManager invalidates then cleans up only invalid controls), which is important for avoiding the status-control truncation issue you debugged.

Two behavioral nuances to be aware of:

  1. For existing controls, control["value"] is not overwritten with variable, which is desirable to preserve user state across setups.
  2. default is always reset to variable, so changing defaults in code will update the metadata even for existing controls.

Overall this looks consistent with the intended control lifecycle and the compareRecursive/NodeManager behavior you outlined.


163-170: New templated addControlValue with empty-controls guard fixes the underflow risk.

The new template:

if (controls.size() == 0) return;
JsonObject control = controls[controls.size() - 1];
...
values.add(value);

prevents the previous potential underflow when addControlValue was called without any prior addControl. Making it templated also improves usability across value types.

This resolves the earlier bounds-check concern.


172-183: updateControl by name correctly reuses the shared update path.

The new overload:

template <typename T>
void updateControl(const char* name, const T value) {
  for (JsonObject control : controls) {
    if (control["name"] == name) {
      control["value"] = value;
      updateControl(control);
      break;
    }
  }
}

keeps a single implementation of the pointer write logic in updateControl(const JsonObject& control) and ensures the Json state is updated before propagating the change. This is straightforward and avoids code duplication.

Just be sure that name comparisons here are consistent with how names are declared in addControl (including exact case/spacing).


185-186: onUpdate signature change to const JsonObject& is compatible with new control handling.

Changing the base hook to:

virtual void onUpdate(const Char<20>& oldValue, const JsonObject& control) {}

matches the way controls are now passed around (read-only JsonObjects) and aligns with the implementation of updateControl. Subclasses can still read all metadata from control while relying on Node::updateControl to perform the actual pointer writes.

Ensure all overrides (drivers, effects, layouts, sandbox nodes) have been updated to this new signature, which it appears you’ve done based on the PR summary.


209-210: nextPin convenience overload with ledPinDIO parameter is a nice extension.

Forwarding an optional ledPinDIO down to layer->layerP->nextPin(ledPinDIO) gives callers fine-grained control while keeping the default behavior (UINT8_MAX) unchanged. As long as layer and layer->layerP are always set before use (which is the existing assumption), this is a safe change.


296-298: DriverNode onUpdate override now matches the base signature.

Updating DriverNode::onUpdate to:

void onUpdate(const Char<20>& oldValue, const JsonObject& control) override;

keeps it consistent with the new base Node::onUpdate and ensures polymorphic dispatch works correctly. This is necessary for driver nodes to participate in the unified control-update flow.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/moonlight/drivers.md (1)

33-33: Consider clarifying Virtual LED Driver relationship.

The table row mentions "Virtual LED Driver will be part of the Parallel LED driver," but lines 41-43 present Virtual LED Driver as a separate standalone section. This could confuse readers about whether Virtual LED Driver is:

  1. A sub-component of Parallel LED Driver, or
  2. A separate driver type

Consider adding a clarifying sentence such as:

Virtual LED Driver: Driving max 120! outputs. E.g. 48 panels of 256 LEDs each run at 50-100 FPS using shift registers. Currently integrated within the Parallel LED Driver architecture. Not implemented yet as a standalone option.
src/MoonBase/Nodes.h (1)

176-185: Consider returning a success indicator.

The template method silently does nothing if no control matches the given name. If callers might need to know whether the update succeeded, consider returning bool to indicate whether a matching control was found.

-  void updateControl(const char* name, const T value) {
+  bool updateControl(const char* name, const T value) {
     for (JsonObject control : controls) {
       if (control["name"] == name) {
         control["value"] = value;
         updateControl(control);
-        break;
+        return true;
       }
     }
+    return false;
   }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 014564c and ebc2e53.

📒 Files selected for processing (20)
  • docs/develop/development.md (1 hunks)
  • docs/develop/drivers.md (0 hunks)
  • docs/develop/installation.md (1 hunks)
  • docs/develop/nodes.md (1 hunks)
  • docs/develop/standardsguidelines.md (1 hunks)
  • docs/gettingstarted/installation.md (1 hunks)
  • docs/moonbase/inputoutput.md (1 hunks)
  • docs/moonlight/drivers.md (2 hunks)
  • firmware/esp32-s3.ini (1 hunks)
  • platformio.ini (3 hunks)
  • src/MoonBase/Modules/ModuleIO.h (4 hunks)
  • src/MoonBase/NodeManager.h (3 hunks)
  • src/MoonBase/Nodes.cpp (4 hunks)
  • src/MoonBase/Nodes.h (8 hunks)
  • src/MoonLight/Layers/PhysicalLayer.h (2 hunks)
  • src/MoonLight/Modules/ModuleDrivers.h (4 hunks)
  • src/MoonLight/Nodes/Drivers/D_FastLED.h (2 hunks)
  • src/MoonLight/Nodes/Drivers/D_Infrared.h (4 hunks)
  • src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h (7 hunks)
  • src/MoonLight/Nodes/Drivers/parlio.cpp (1 hunks)
💤 Files with no reviewable changes (1)
  • docs/develop/drivers.md
✅ Files skipped from review due to trivial changes (2)
  • firmware/esp32-s3.ini
  • src/MoonLight/Nodes/Drivers/parlio.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • platformio.ini
  • src/MoonBase/Nodes.cpp
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Modify C++ backend code in the `src/` and `lib/` directories for ESP32 development

Applied to files:

  • docs/develop/development.md
🧬 Code graph analysis (6)
src/MoonBase/Modules/ModuleIO.h (4)
src/MoonLight/Modules/ModuleMoonLightInfo.h (1)
  • setupDefinition (22-46)
src/MoonLight/Modules/ModuleLightsControl.h (1)
  • setupDefinition (92-138)
src/MoonBase/Module.cpp (4)
  • setupDefinition (352-359)
  • setupDefinition (352-352)
  • addControl (361-370)
  • addControl (361-361)
src/MoonLight/Modules/ModuleEffects.h (1)
  • setupDefinition (60-147)
src/MoonLight/Nodes/Drivers/D_Infrared.h (3)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/Nodes.cpp (2)
  • onUpdate (400-540)
  • onUpdate (400-400)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
  • onUpdate (31-37)
src/MoonLight/Nodes/Drivers/D_FastLED.h (1)
src/MoonBase/Module.cpp (2)
  • addControl (361-370)
  • addControl (361-361)
src/MoonBase/NodeManager.h (3)
src/MoonBase/Nodes.h (9)
  • void (67-70)
  • void (73-73)
  • void (201-201)
  • void (202-202)
  • void (203-203)
  • void (206-206)
  • void (215-215)
  • Node (48-163)
  • Node (219-219)
src/MoonBase/Module.h (3)
  • void (150-166)
  • void (194-194)
  • void (195-195)
src/MoonBase/Char.h (2)
  • contains (115-115)
  • contains (209-214)
src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h (3)
src/MoonLight/Nodes/Drivers/D_FastLED.h (2)
  • dim (33-33)
  • tags (34-34)
src/MoonLight/Nodes/Drivers/D_Infrared.h (2)
  • dim (34-34)
  • tags (35-35)
src/MoonBase/Nodes.cpp (2)
  • updateControl (17-66)
  • updateControl (17-17)
src/MoonLight/Modules/ModuleDrivers.h (2)
src/MoonLight/Nodes/Drivers/D_Hub75.h (1)
  • HUB75Driver (16-27)
src/MoonBase/Char.h (1)
  • equalAZaz09 (194-207)
🪛 LanguageTool
docs/moonbase/inputoutput.md

[grammar] ~38-~38: Ensure spelling is correct
Context: ...on On/Off * Planned soon * Battery * Relais brightness * DMX (in) * Planned l...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

docs/develop/nodes.md

[style] ~17-~17: In American English, abbreviations like “etc.” require a period.
Context: ...pes (sliders/range, checkboxes, numbers etc). They are added with the addControl() ...

(ETC_PERIOD)


[grammar] ~19-~19: Use a hyphen to join words.
Context: ...e supports. They control when a physical to virtual mapping is recalculated *...

(QB_NEW_EN_HYPHEN)


[grammar] ~19-~19: Use a hyphen to join words.
Context: ...upports. They control when a physical to virtual mapping is recalculated * **...

(QB_NEW_EN_HYPHEN)


[grammar] ~29-~29: Use a hyphen to join words.
Context: ...support different lights e.g a mix of 15 channel lights and 32 channel lights etc...

(QB_NEW_EN_HYPHEN)


[grammar] ~29-~29: Use a hyphen to join words.
Context: ...ts e.g a mix of 15 channel lights and 32 channel lights etc. You could set channe...

(QB_NEW_EN_HYPHEN)


[style] ~31-~31: Consider using polite language here.
Context: ...wheel could be used to really go crazy. Let us know when you have one of these setups 🚨 * Movin...

(INSERT_PLEASE)


[grammar] ~45-~45: Use a hyphen to join words.
Context: ...he LEDs / channels array and the virtual to physical layer mappings. * specif...

(QB_NEW_EN_HYPHEN)


[grammar] ~45-~45: Use a hyphen to join words.
Context: ...LEDs / channels array and the virtual to physical layer mappings. * specify w...

(QB_NEW_EN_HYPHEN)


[grammar] ~57-~57: Ensure spelling is correct
Context: ...n is started when in Nodes a file.sc is choosen * To do: kill running scripts, e.g. when c...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~62-~62: In American English, abbreviations like “etc.” require a period.
Context: ...tPan, setTilt, setZoom etc. Also getRGB etc functions exists. ## Drivers ### Init...

(ETC_PERIOD)

docs/develop/standardsguidelines.md

[style] ~12-~12: Consider changing the order of words to improve your wording.
Context: ...ould contain compilable code and tested to not crash the system at minimal and support...

(TO_NOT_VB)


[uncategorized] ~38-~38: The official name of this software platform is spelled with a capital “H”.
Context: ...btle and unexpected consequences on our github reposistory. > For example, we regularl...

(GITHUB)


[style] ~52-~52: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ... descriptions into English. AI bots are very good at understanding language. However, alw...

(EN_WEAK_ADJECTIVE)


[grammar] ~55-~55: Use a hyphen to join words.
Context: ... sure you understand exactly what the AI generated code does * best practice: a...

(QB_NEW_EN_HYPHEN)


[grammar] ~58-~58: Use a hyphen to join words.
Context: ...nts for correctness * always review AI generated source code * If the AI has ...

(QB_NEW_EN_HYPHEN)


[grammar] ~65-~65: Use a hyphen to join words.
Context: ...r the definition) and Prettier for front end files. Right click Format Document ...

(QB_NEW_EN_HYPHEN)


[grammar] ~67-~67: Use a hyphen to join words.
Context: ...and Prettier for front end files. Right click Format Document on each file you e...

(QB_NEW_EN_HYPHEN)


[style] ~71-~71: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... what it does, including its arguments. Comments are also formatted using Clang-format a...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.18.1)
docs/develop/development.md

10-10: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


13-13: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


14-14: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


15-15: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

docs/moonbase/inputoutput.md

18-18: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


19-19: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


20-20: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


21-21: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


25-25: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


32-32: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


33-33: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


34-34: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


37-37: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


38-38: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


39-39: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


42-42: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


43-43: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

docs/moonlight/drivers.md

11-11: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


33-33: Images should have alternate text (alt text)

(MD045, no-alt-text)


33-33: Table pipe style
Expected: leading_only; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)


34-34: Images should have alternate text (alt text)

(MD045, no-alt-text)


34-34: Table pipe style
Expected: leading_only; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)


35-35: Images should have alternate text (alt text)

(MD045, no-alt-text)


35-35: Table pipe style
Expected: leading_only; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)


36-36: Images should have alternate text (alt text)

(MD045, no-alt-text)


36-36: Table pipe style
Expected: leading_only; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)


37-37: Images should have alternate text (alt text)

(MD045, no-alt-text)


37-37: Images should have alternate text (alt text)

(MD045, no-alt-text)


37-37: Table pipe style
Expected: leading_only; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)


38-38: Images should have alternate text (alt text)

(MD045, no-alt-text)


38-38: Table pipe style
Expected: leading_only; Actual: leading_and_trailing; Unexpected trailing pipe

(MD055, table-pipe-style)


42-42: Images should have alternate text (alt text)

(MD045, no-alt-text)


42-42: Images should have alternate text (alt text)

(MD045, no-alt-text)

docs/develop/standardsguidelines.md

20-20: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


21-21: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


39-39: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


42-42: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


43-43: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


44-44: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


45-45: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


46-46: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


47-47: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


48-48: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


49-49: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


50-50: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


51-51: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


52-52: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


53-53: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


58-58: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


62-62: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (48)
src/MoonLight/Nodes/Drivers/D_FastLED.h (2)

50-56: UI label change in setup() is safe and consistent

Changing the control label from "Version" to "version" (while keeping the backing version buffer and other parameters the same) is purely cosmetic and aligns with the lower‑case control naming used elsewhere in the PR. No behavioral or API impact here.


89-92: onLayout guard condition remains correct after cleanup

The if (layerP.pass == 1 && !layerP.monitorPass) guard still clearly restricts FastLED layout work to the primary layout pass and skips monitor passes. Removing the trailing comment/whitespace does not change behavior; the control flow is still appropriate.

docs/develop/development.md (2)

10-11: LGTM! Documentation consolidation is well-executed.

The link to the centralized Standards and Guidelines document improves maintainability by reducing duplication across documentation files. The change aligns with the new standardsguidelines.md file introduced in this PR.


17-17: Clear and actionable branching guidance.

The instruction "Only branch from the main branch!" is prominently placed and reinforces the workflow described in the PR objectives and related documentation.

docs/moonbase/inputoutput.md (2)

6-16: Documentation update aligns with PR architecture changes.

The ESP32-focused content accurately reflects the new board presets and pin assignment tracking introduced in this PR. The structure with Modded, Max Power, and Pins sections provides clear guidance for users.


29-44: Pin usage roadmap is well-organized.

The categorization into Supported/Planned soon/Planned later provides clear expectations for feature availability. The inclusion of "Button On/Off" aligns with the PR description mentioning "add On off button pin" in ModuleIO.

src/MoonLight/Layers/PhysicalLayer.h (3)

25-25: Terminology update consistent with PR-wide driver renaming.

The comment change from "Physical driver" to "Parallel LED Driver" aligns with the broader renaming effort visible across documentation and code files in this PR.


129-129: Well-designed API extension for explicit pin assignment.

The default parameter allows nextPin() to maintain backward compatibility (auto-assignment when called without arguments) while enabling explicit pin specification when needed. This is a clean, non-breaking API evolution.


134-134: Pin assignment tracking array added appropriately.

The ledPinsAssigned array complements the existing ledPins and ledsPerPin arrays to track which GPIO pins are assigned to which slots. The past review comment confirms initialization has been properly addressed.

docs/gettingstarted/installation.md (2)

142-142: Driver recommendation updated with new terminology.

The guidance correctly replaces "Physical driver" with "Parallel LED Driver" and provides clear use cases (>4 strips/panels or non-standard LEDs). The recommendation structure remains clear and actionable.


146-146: ESP32-P4-Nano guidance includes proper attribution.

The P4-Nano-specific instructions now reference the "Parallel LED Driver" and correctly attribute the "Parallel IO driver" to @troyhacks. The wiring diagram reference enhances clarity for users.

docs/develop/installation.md (1)

41-41: Branch workflow documentation accurately reflects current practice.

The updated description clarifies that development occurs in branches from main rather than a dedicated dev branch. This aligns with the guidance in docs/develop/development.md (line 17) and the new standardsguidelines.md file.

docs/moonlight/drivers.md (2)

11-11: Driver list terminology updated consistently.

The change from "Physical LED Drivers" to "Parallel LED Drivers" maintains consistency with the PR-wide renaming effort.


46-46: Max Power relocation documented correctly.

The note about Max Power moving to board presets in Module IO aligns with the PR objectives and the changes visible in docs/moonbase/inputoutput.md.

docs/develop/standardsguidelines.md (4)

1-12: Comprehensive development principles clearly articulated.

The principles section effectively communicates key architectural decisions:

  • One firmware per board (3MB flash constraint)
  • Documentation integrated with code changes
  • Minimal upstream modifications with //🌙 markers
  • Main-based branching workflow

These align with the PR objectives and provide valuable guidance for contributors.


36-39: Excellent emphasis on force-push dangers.

The CAUTION block appropriately warns about force-push consequences, specifically mentioning lost review comments. This is a common pain point in collaborative development and the strong warning is justified.


44-60: Pragmatic and balanced AI guidance.

The AI-generated code section strikes the right balance:

  • Acknowledges AI assistance is acceptable
  • Emphasizes contributor responsibility and understanding
  • Requires transparency (commenting AI-generated sections)
  • Warns about AI limitations ("Often-Wrong")

This guidance is well-suited for modern development practices while maintaining code quality standards.


65-72: Code formatting requirements clearly specified.

The documentation correctly references the project's formatting tools (Clang-format for C/C++, Prettier for frontend) and emphasizes the "Right click Format Document" workflow in VS Code. The comment documentation requirements are appropriate for maintaining code quality.

src/MoonLight/Nodes/Drivers/D_Infrared.h (4)

33-35: Driver naming and status appropriately updated.

The name change to "Infrared Driver" (from "IR Driver") improves clarity, and removing the "🚧" tag signals the driver is no longer considered work-in-progress. Both changes align with the driver's maturity.


86-89: Excellent refactoring to addControl/addControlValue pattern.

The new control-building approach is cleaner and more maintainable than explicit JsonObject construction. This aligns with the pattern introduced in Module.cpp (see relevant snippet showing addControl implementation) and used throughout this PR.

Before: Manual JsonObject/JsonArray construction
After: Declarative control building with helpers


123-123: Const-correctness improvement in onUpdate signature.

Changing the parameter from JsonObject control to const JsonObject& control prevents unnecessary copies and adds const-correctness. This matches the pattern seen in other drivers (e.g., D__Sandbox.h in relevant snippets) and the base class signature update.


408-408: Smart logging guard reduces noise for standard frames.

The condition if (rx_data.num_symbols != 1) ensures that only non-standard frames are logged, as standard NEC frames (34 or 2 symbols) are processed normally. This reduces log clutter while preserving diagnostic information for unexpected frame types.

src/MoonBase/NodeManager.h (3)

78-78: LGTM - API signature change aligns with broader refactoring.

The signature change from const JsonArray& values to const JsonObject& control is consistent with the new addControlValue(control, ...) pattern used across other modules (ModuleEffects, ModuleDrivers).


83-111: Clean refactoring to the addControl/addControlValue pattern.

The control definition structure is well-organized with proper nesting for the "nodes" → "controls" hierarchy. The pattern matches other modules in the codebase (ModuleMoonLightInfo, ModuleLightsControl).


253-268: LGTM - Control update handling refactored correctly.

The control object is properly fetched by index and passed to updateControl and onUpdate. The bounds check on line 253 ensures safe access.

src/MoonBase/Modules/ModuleIO.h (3)

76-76: LGTM - New button pin type added correctly.

The pin_Button_OnOff enum value is added at the correct position (after pin_Ethernet), and the corresponding UI label "Button On/Off" is added at line 206 in the correct order to match the enum.


110-215: Clean refactoring to addControl/addControlValue pattern.

The control definitions are well-structured and the UI labels correctly match the IO_PinUsage enum order. The read-only flags for summary, Level, and DriveCap fields are appropriate.


275-275: LGTM - SE16V1 board preset updated for on/off button.

Pin 46 is correctly assigned to the new pin_Button_OnOff type for the SE16V1 board.

docs/develop/nodes.md (3)

17-17: LGTM - Documentation updated for addControl() API.

The terminology update from setControl() to addControl() aligns with the code refactoring.


36-63: Good technical documentation expansion.

The new technical section provides useful implementation details about Node class structure, live scripts, and lights. The content aligns well with the codebase.


68-80: Documentation correctly reflects driver rename.

The "Parallel LED Driver" terminology is consistently used throughout, matching the code rename from PhysicalDriver.

src/MoonLight/Modules/ModuleDrivers.h (4)

54-56: Verify the simplified pin validation is intentional.

The validation condition was simplified from checking ledsPerPin values to only checking ledPins[readPos] != UINT8_MAX. The old validation is visible in the comment. Confirm that ledsPerPin validation is handled elsewhere or is no longer needed for correct pin compaction.


78-101: LGTM - addNodes refactored to use addControlValue pattern.

The implementation correctly uses addControlValue(control, getNameAndTags<...>()) for all layout and driver registrations, consistent with the new API.


127-128: LGTM - PhysicalDriver correctly replaced with ParallelLEDDriver.

The driver class rename is consistently applied in both the registration (line 91) and instantiation (lines 127-128).


159-160: LGTM - Node wiring extended for UI updates.

The new moduleNodes assignment enables driver nodes to trigger UI updates via requestUIUpdate, which is used by the ParallelLEDDriver to refresh the status display.

src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h (4)

22-30: LGTM - Class renamed with new status member.

The class is cleanly renamed to ParallelLEDDriver with:

  • Updated name() method returning "Parallel LED Driver"
  • version changed from char[30] to Char<32> for consistency
  • New status member to display pin assignment status in UI

38-45: LGTM - UI controls for version and status added correctly.

The read-only text controls for version and status are properly defined, and the initial updateControl call ensures the version is displayed for existing nodes.


97-107: LGTM - Pin assignment bounds check correctly implemented.

The bounds check at line 99 properly validates against nrOfLedPins (not MAX_PINS), addressing the previous review concern. The fallback to ledPins[i] is safe.


179-187: LGTM - Destructor correctly renamed.

The destructor is properly renamed to ~ParallelLEDDriver() and the comment is updated accordingly.

src/MoonBase/Nodes.h (9)

40-43: LGTM!

Good defensive check to avoid appending an empty string and trailing space when no tags are present.


56-58: LGTM!

Consistent with the existing pattern for module references (moduleControl, moduleIO). The null initialization ensures safe default state.


67-70: LGTM!

Passing JsonArray by const reference avoids an unnecessary copy.


98-120: Control metadata management looks solid.

Setting control["valid"] = true (line 102) is essential for the state synchronization fix described in the PR objectives. The conditional property management (lines 104-119) properly adds only non-default values and cleans up stale properties.

Consider adding a brief comment above line 102 explaining the "valid" flag's role in preventing premature control removal during state sync, as discussed in the PR comments.


165-172: Bounds check addressed.

The guard at line 167 properly handles the edge case of an empty controls array, preventing undefined behavior from underflow. The template approach allows flexible value types.


187-187: LGTM!

Const references for both parameters are appropriate. The empty default implementation allows derived classes to optionally override.


212-212: LGTM!

The optional ledPinDIO parameter enables explicit LED pin assignment while maintaining backward compatibility via the UINT8_MAX default.


299-299: LGTM!

Signature correctly matches the updated base class virtual method with proper override specifier.


340-340: New driver header exists and old references are cleaned up.

The include path change to D_ParallelLEDDriver.h is valid. The header file exists at the expected location, and no remaining references to the old D_PhysicalDriver.h or D_VirtualDriver.h headers remain in the codebase.

Comment on lines 138 to 148
// conversion 20251204
if (contains(updatedItem.value.as<const char*>(), "Physical Driver")) {
EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
nodeState["name"] = getNameAndTags<ParallelLEDDriver>(); // set to current combination of name and tags
EXT_LOGD(ML_TAG, "... to [%s]", updatedItem.value.as<const char*>());
}
if (contains(updatedItem.value.as<const char*>(), "IR Driver")) {
EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
nodeState["name"] = getNameAndTags<IRDriver>(); // set to current combination of name and tags
EXT_LOGD(ML_TAG, "... to [%s]", updatedItem.value.as<const char*>());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Logging uses wrong variable for the updated name.

The second log statement logs updatedItem.value instead of nodeState["name"], which means it will show the old value rather than the newly assigned name.

Apply this diff to fix the logging:

          if (contains(updatedItem.value.as<const char*>(), "Physical Driver")) {
            EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
            nodeState["name"] = getNameAndTags<ParallelLEDDriver>();  // set to current combination of name and tags
-            EXT_LOGD(ML_TAG, "... to [%s]", updatedItem.value.as<const char*>());
+            EXT_LOGD(ML_TAG, "... to [%s]", nodeState["name"].as<const char*>());
          }
          if (contains(updatedItem.value.as<const char*>(), "IR Driver")) {
            EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
            nodeState["name"] = getNameAndTags<IRDriver>();  // set to current combination of name and tags
-            EXT_LOGD(ML_TAG, "... to [%s]", updatedItem.value.as<const char*>());
+            EXT_LOGD(ML_TAG, "... to [%s]", nodeState["name"].as<const char*>());
          }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// conversion 20251204
if (contains(updatedItem.value.as<const char*>(), "Physical Driver")) {
EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
nodeState["name"] = getNameAndTags<ParallelLEDDriver>(); // set to current combination of name and tags
EXT_LOGD(ML_TAG, "... to [%s]", updatedItem.value.as<const char*>());
}
if (contains(updatedItem.value.as<const char*>(), "IR Driver")) {
EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
nodeState["name"] = getNameAndTags<IRDriver>(); // set to current combination of name and tags
EXT_LOGD(ML_TAG, "... to [%s]", updatedItem.value.as<const char*>());
}
// conversion 20251204
if (contains(updatedItem.value.as<const char*>(), "Physical Driver")) {
EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
nodeState["name"] = getNameAndTags<ParallelLEDDriver>(); // set to current combination of name and tags
EXT_LOGD(ML_TAG, "... to [%s]", nodeState["name"].as<const char*>());
}
if (contains(updatedItem.value.as<const char*>(), "IR Driver")) {
EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
nodeState["name"] = getNameAndTags<IRDriver>(); // set to current combination of name and tags
EXT_LOGD(ML_TAG, "... to [%s]", nodeState["name"].as<const char*>());
}
🤖 Prompt for AI Agents
In src/MoonBase/NodeManager.h around lines 138 to 148, the second debug log
prints updatedItem.value (the old name) after assigning nodeState["name"];
change those log calls to log nodeState["name"] instead so the message shows the
newly assigned name and tags; ensure both the "Physical Driver" and "IR Driver"
branches use nodeState["name"] in the trailing EXT_LOGD call.

Comment on lines +110 to +111
updateControl("status", statusString.c_str());
moduleNodes->requestUIUpdate = true;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add null check before accessing moduleNodes.

If moduleNodes is not assigned (e.g., in a different usage context), this line will cause a null pointer dereference.

Apply this diff:

      updateControl("status", statusString.c_str());
-      moduleNodes->requestUIUpdate = true;
+      if (moduleNodes) moduleNodes->requestUIUpdate = true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
updateControl("status", statusString.c_str());
moduleNodes->requestUIUpdate = true;
updateControl("status", statusString.c_str());
if (moduleNodes) moduleNodes->requestUIUpdate = true;
🤖 Prompt for AI Agents
In src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h around lines 110-111, the
code assumes moduleNodes is non-null before calling updateControl and setting
requestUIUpdate; add a null check to guard access: only call updateControl(...)
and set moduleNodes->requestUIUpdate = true when moduleNodes is not null,
otherwise skip those operations or handle the null case appropriately (e.g.,
return or log) to avoid a potential null pointer dereference.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
interface/src/lib/components/moonbase/MultiRow.svelte (1)

51-55: Reordering while a filter is active will permanently drop non-matching items.

filteredItems (lines 94–125) maps items as { item } only—it lacks originalIndex tracking. When a filter is applied, DraggableList receives only the filtered subset:

// Both paths skip originalIndex
return data[property.name].map((item: any) => ({ item }))  // no filter
// or
return data[property.name]
  .map((item: any) => ({ item }))  // with filter
  .filter(({ item }: { item: any }) => { /* ... */ });

When handleReorder is called (line 51), it receives only the reordered filtered items and overwrites the entire array:

data[property.name] = reorderedItems.map((wrapper: any) => wrapper.item);

Any items that did not match the filter are permanently lost.

Fix: Preserve originalIndex in filteredItems and use it to reorder only the filtered positions within the full array:

-let filteredItems = $derived.by(() => {
+let filteredItems = $derived.by(() => {
-	if (!query) return data[property.name].map((item: any) => ({ item }));
+	if (!query) return data[property.name].map((item: any, index: number) => ({ item, originalIndex: index }));

-	return data[property.name]
-		.map((item: any) => ({ item }))
+	return data[property.name]
+		.map((item: any, index: number) => ({ item, originalIndex: index }))
		.filter(({ item }: { item: any }) => {

Then update handleReorder:

-function handleReorder(reorderedItems: any[]) {
+function handleReorder(reorderedItems: { item: any; originalIndex: number }[]) {
 	console.log('handleReorder', property.name, reorderedItems);
-	data[property.name] = reorderedItems.map((wrapper: any) => wrapper.item);
+	const full = [...data[property.name]];
+	reorderedItems.forEach(({ item }, pos) => {
+		const originalIndex = filteredItems[pos].originalIndex;
+		full[originalIndex] = item;
+	});
+	data[property.name] = full;
 	onChange();
 }

Also affects: lines 94–125 (filteredItems definition) and 183–188 (DraggableList usage).

♻️ Duplicate comments (5)
src/MoonBase/Char.h (1)

102-110: Missing bounds check for end < begin.

When end < begin, the expression end - begin + 1 underflows (uint16_t), causing unintended behavior. While strlcpy limits the copy to sizeof(sub.s), the function would copy the wrong content instead of returning an empty string.

This was flagged in a previous review. Consider applying:

 Char<N> substring(uint16_t begin, uint16_t end = sizeof(s) - 1) {
   Char<N> sub;
-  if (begin >= sizeof(s) || end >= sizeof(s))
+  if (begin >= sizeof(s) || end >= sizeof(s) || end < begin)
     sub = "";
   else {
     strlcpy(sub.s, s + begin, end - begin + 1);
   }
   return sub;
 }
src/MoonBase/NodeManager.h (1)

138-148: Log message still prints the old node name instead of the migrated one.

In the migration block you assign:

nodeState["name"] = getNameAndTags<ParallelLEDDriver>();

but the second log line still prints updatedItem.value:

EXT_LOGD(ML_TAG, "... to [%s]", updatedItem.value.as<const char*>());

So the log does not show the newly assigned name/tags.

You likely want to log nodeState["name"] instead:

-          if (contains(updatedItem.value.as<const char*>(), "Physical Driver")) {
-            EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
-            nodeState["name"] = getNameAndTags<ParallelLEDDriver>();  // set to current combination of name and tags
-            EXT_LOGD(ML_TAG, "... to [%s]", updatedItem.value.as<const char*>());
-          }
+          if (contains(updatedItem.value.as<const char*>(), "Physical Driver")) {
+            EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
+            nodeState["name"] = getNameAndTags<ParallelLEDDriver>();  // set to current combination of name and tags
+            EXT_LOGD(ML_TAG, "... to [%s]", nodeState["name"].as<const char*>());
+          }
@@
-          if (contains(updatedItem.value.as<const char*>(), "IR Driver")) {
-            EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
-            nodeState["name"] = getNameAndTags<IRDriver>();  // set to current combination of name and tags
-            EXT_LOGD(ML_TAG, "... to [%s]", updatedItem.value.as<const char*>());
-          }
+          if (contains(updatedItem.value.as<const char*>(), "IR Driver")) {
+            EXT_LOGD(ML_TAG, "update [%s] to ...", updatedItem.value.as<const char*>());
+            nodeState["name"] = getNameAndTags<IRDriver>();  // set to current combination of name and tags
+            EXT_LOGD(ML_TAG, "... to [%s]", nodeState["name"].as<const char*>());
+          }
docs/develop/nodes.md (1)

17-62: Documentation updates accurately reflect API changes.

The updates to use addControl() terminology, the expanded sections on hasOnLayout(), moving heads, and technical details all align well with the codebase changes. The restructured content improves clarity for developers implementing nodes.

Note: The spelling error "choosen" → "chosen" on line 57 was already flagged in a previous review.

docs/moonbase/inputoutput.md (1)

6-43: ESP32 board documentation is clear and comprehensive.

The new sections on board presets, pin usage, and naming conventions provide valuable hardware-specific guidance that aligns with the codebase changes in ModuleIO.

Note: The spelling "Relais brightness" on line 39 was already flagged in a previous review (should likely be "Relay brightness").

src/MoonLight/Modules/ModuleLightsControl.h (1)

241-244: LGTM: Button debouncing correctly implemented.

The debounce logic is sound:

  • 50ms debounce delay is appropriate for mechanical button bounce
  • Only triggers on button press (HIGH→LOW transition with INPUT_PULLUP)
  • lastState is updated only after the debounce period, preventing spurious toggles during bounce
  • pinMode configuration is handled in readPins() (line 83)

The past review concerns have been properly addressed.

Note: The past review comment about button debouncing and pinMode configuration has been fully addressed.

Also applies to: 293-307

🧹 Nitpick comments (4)
src/MoonBase/Char.h (2)

130-141: strncpy doesn't guarantee null-termination.

If the source string fills the buffer, strncpy won't null-terminate the destination. While the class's use of strlcpy elsewhere ensures s is always properly terminated, using strlcpy here would be more defensive.

 void split(const char* splitter, std::function<void(const char*, uint8_t)> callback) {
   char savedS[N];
-  strncpy(savedS, s, N);              // save s
+  strlcpy(savedS, s, N);              // save s
   char* token = strtok(s, splitter);  // eat s
   uint8_t sequence = 0;
   while (token != nullptr) {
     callback(token, sequence);
     sequence++;
     token = strtok(nullptr, splitter);
   }
-  strncpy(s, savedS, N);  // restore s
+  strlcpy(s, savedS, N);  // restore s
 }

172-200: Prefer inline over static for header-defined free functions.

static gives each translation unit its own copy of these functions, causing code bloat if the header is included in multiple files. Using inline allows the linker to merge duplicates.

-static bool equal(const char* a, const char* b) {
+inline bool equal(const char* a, const char* b) {
   if (a == nullptr || b == nullptr) {
     return false;
   }
   return strcmp(a, b) == 0;
 }

-static bool equalAZaz09(const char* a, const char* b) {
+inline bool equalAZaz09(const char* a, const char* b) {
   // ...
 }

-static bool contains(const char* a, const char* b) {
+inline bool contains(const char* a, const char* b) {
   // ...
 }
docs/develop/standardsguidelines.md (1)

12-12: Polish language in standards doc (optional but recommended).

A few small wording/consistency fixes would make this standards document crisper:

  • Line 12: sentence is a bit awkward.
  • Line 38: capitalize “GitHub”.
  • Lines 55, 58: prefer “AI-generated”.
  • Line 67: prefer “Right-click”.

Example patch:

-* A pull request should contain compilable code and tested to not crash the system at minimal and support also boards without PSRAM, e.g. ESP32-D0. Code may be work in progress.
+* A pull request should contain compilable code that is tested to, at minimum, not crash the system and also supports boards without PSRAM, e.g. ESP32-D0. Code may be work in progress.
@@
-> Do not use "force-push" while your PR is open!
-> It has many subtle and unexpected consequences on our github repository.
+> Do not use "force-push" while your PR is open!
+> It has many subtle and unexpected consequences on our GitHub repository.
@@
-  * As the person who contributes source code to MoonLight, make sure you understand exactly what the AI generated code does
+  * As the person who contributes source code to MoonLight, make sure you understand exactly what the AI-generated code does
@@
-  * always review AI generated source code
+  * always review AI-generated source code
@@
-Right click Format Document on each file you edit before committing.
+Right-click Format Document on each file you edit before committing.

Also applies to: 38-38, 55-55, 58-58, 67-67

src/MoonBase/Modules/ModuleIO.h (1)

44-45: Enum/string alignment and new Button On/Off preset mapping look correct.

  • The IO_PinUsage enum order matches the usage select’s addControlValue entries, including the new "Button On/Off" entry for pin_Button_OnOff.
  • Board presets correctly use the new usages:
    • board_SE16V1: pins[46]["usage"] = pin_Button_OnOff;
    • Relay-related presets now refer to pin_Relay / pin_Relay_Brightness and corresponding labels.
  • setupDefinition(const JsonArray& controls) is consistent with the new controls-based API elsewhere.

If you touch this code again, you might consider using std::size(ledPins) (or sizeof(ledPins) / sizeof(ledPins[0])) in the LED pin loops for future-proofing, but it’s not urgent.

Also applies to: 76-77, 110-115, 148-207, 269-276, 312-333

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebc2e53 and 77646f0.

📒 Files selected for processing (17)
  • docs/develop/development.md (3 hunks)
  • docs/develop/installation.md (2 hunks)
  • docs/develop/nodes.md (2 hunks)
  • docs/develop/standardsguidelines.md (1 hunks)
  • docs/develop/sveltekit.md (1 hunks)
  • docs/gettingstarted/overview.md (1 hunks)
  • docs/moonbase/devices.md (1 hunks)
  • docs/moonbase/inputoutput.md (1 hunks)
  • docs/moonlight/drivers.md (2 hunks)
  • interface/src/lib/components/moonbase/MultiRow.svelte (4 hunks)
  • misc/livescripts/E_PanTilt.sc (1 hunks)
  • platformio.ini (3 hunks)
  • src/MoonBase/Char.h (1 hunks)
  • src/MoonBase/Module.h (5 hunks)
  • src/MoonBase/Modules/ModuleIO.h (8 hunks)
  • src/MoonBase/NodeManager.h (3 hunks)
  • src/MoonLight/Modules/ModuleLightsControl.h (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/MoonBase/Module.h
  • docs/moonbase/devices.md
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/interface/**/*.{svelte,ts,tsx,js,json} : Modify frontend code in the `interface/` directory using SvelteKit

Applied to files:

  • docs/develop/development.md
  • docs/develop/sveltekit.md
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Assume the Vite development server (`npm run dev`) is running during frontend development

Applied to files:

  • docs/develop/development.md
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Be aware of the integrated build process where the PlatformIO build automatically builds and embeds the SvelteKit frontend into the ESP32 firmware

Applied to files:

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

Applied to files:

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

Applied to files:

  • platformio.ini
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/interface/svelte.config.js : Configure SvelteKit build and adapter settings in `interface/svelte.config.js`

Applied to files:

  • docs/develop/sveltekit.md
🧬 Code graph analysis (2)
src/MoonBase/Char.h (1)
src/MoonBase/NodeManager.h (2)
  • begin (36-74)
  • void (78-78)
src/MoonBase/NodeManager.h (3)
src/MoonBase/Module.h (3)
  • void (150-166)
  • void (194-194)
  • void (195-195)
src/MoonBase/Nodes.h (9)
  • void (67-70)
  • void (73-73)
  • void (201-201)
  • void (202-202)
  • void (203-203)
  • void (206-206)
  • void (215-215)
  • Node (48-163)
  • Node (219-219)
src/MoonBase/Char.h (2)
  • contains (115-115)
  • contains (195-200)
🪛 Clang (14.0.6)
src/MoonBase/Char.h

[error] 14-14: 'Arduino.h' file not found

(clang-diagnostic-error)

🪛 LanguageTool
docs/develop/development.md

[style] ~40-~40: To form a complete sentence, be sure to include a subject.
Context: ...w components used in MoonLight modules. Might be rewriteen as MoonLight Module in the...

(MISSING_IT_THERE)


[grammar] ~40-~40: Ensure spelling is correct
Context: ...nts used in MoonLight modules. Might be rewriteen as MoonLight Module in the future. * ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~44-~44: Ensure spelling is correct
Context: ...* src folder for MoonBase and MoonLight vack-end * MoonLight Nodes: the easiest ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

docs/develop/nodes.md

[style] ~17-~17: In American English, abbreviations like “etc.” require a period.
Context: ...pes (sliders/range, checkboxes, numbers etc). They are added with the addControl() ...

(ETC_PERIOD)


[grammar] ~19-~19: Use a hyphen to join words.
Context: ...e supports. They control when a physical to virtual mapping is recalculated *...

(QB_NEW_EN_HYPHEN)


[grammar] ~19-~19: Use a hyphen to join words.
Context: ...upports. They control when a physical to virtual mapping is recalculated * **...

(QB_NEW_EN_HYPHEN)


[grammar] ~29-~29: Use a hyphen to join words.
Context: ...support different lights e.g a mix of 15 channel lights and 32 channel lights etc...

(QB_NEW_EN_HYPHEN)


[grammar] ~29-~29: Use a hyphen to join words.
Context: ...ts e.g a mix of 15 channel lights and 32 channel lights etc. You could set channe...

(QB_NEW_EN_HYPHEN)


[style] ~31-~31: Consider using polite language here.
Context: ...wheel could be used to really go crazy. Let us know when you have one of these setups 🚨 * Movin...

(INSERT_PLEASE)


[grammar] ~45-~45: Use a hyphen to join words.
Context: ...he LEDs / channels array and the virtual to physical layer mappings. * specif...

(QB_NEW_EN_HYPHEN)


[grammar] ~45-~45: Use a hyphen to join words.
Context: ...LEDs / channels array and the virtual to physical layer mappings. * specify w...

(QB_NEW_EN_HYPHEN)


[style] ~62-~62: In American English, abbreviations like “etc.” require a period.
Context: ...tPan, setTilt, setZoom etc. Also getRGB etc functions exists. ## Drivers ### Init...

(ETC_PERIOD)

docs/develop/standardsguidelines.md

[style] ~12-~12: Consider changing the order of words to improve your wording.
Context: ...ould contain compilable code and tested to not crash the system at minimal and support...

(TO_NOT_VB)


[uncategorized] ~38-~38: The official name of this software platform is spelled with a capital “H”.
Context: ...btle and unexpected consequences on our github repository. > For example, we regularly...

(GITHUB)


[style] ~52-~52: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ... descriptions into English. AI bots are very good at understanding language. However, alw...

(EN_WEAK_ADJECTIVE)


[grammar] ~55-~55: Use a hyphen to join words.
Context: ... sure you understand exactly what the AI generated code does * best practice: a...

(QB_NEW_EN_HYPHEN)


[grammar] ~58-~58: Use a hyphen to join words.
Context: ...nts for correctness * always review AI generated source code * If the AI has ...

(QB_NEW_EN_HYPHEN)


[grammar] ~67-~67: Use a hyphen to join words.
Context: ...and Prettier for front-end files. Right click Format Document on each file you e...

(QB_NEW_EN_HYPHEN)


[style] ~71-~71: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... what it does, including its arguments. Comments are also formatted using Clang-format a...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.18.1)
docs/moonlight/drivers.md

43-43: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


44-44: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

docs/develop/nodes.md

19-19: Images should have alternate text (alt text)

(MD045, no-alt-text)


26-26: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


62-62: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


78-78: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


79-79: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


80-80: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

docs/develop/standardsguidelines.md

5-5: Images should have alternate text (alt text)

(MD045, no-alt-text)


17-17: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


43-43: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


44-44: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


52-52: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


53-53: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


56-56: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


57-57: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


59-59: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


72-72: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


73-73: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


75-75: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


76-76: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


77-77: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


79-79: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


80-80: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


82-82: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


84-84: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


85-85: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


86-86: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


87-87: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


89-89: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


90-90: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


91-91: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


92-92: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

docs/moonbase/inputoutput.md

18-18: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


19-19: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


20-20: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


21-21: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


25-25: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


32-32: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


33-33: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


34-34: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


37-37: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


38-38: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


39-39: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


42-42: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


43-43: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (15)
docs/develop/sveltekit.md (1)

67-67: Terminology standardization is correct and consistent with documentation.

Line 67 updates link text labels from "Front End"/"Back End" to "Front-end"/"Back-end". Web search confirms that the MoonLight documentation pages use this hyphenated terminology consistently across /sveltekit/, /structure/, and development pages. This change aligns with the broader project terminology standardization efforts.

misc/livescripts/E_PanTilt.sc (1)

12-12: The 3-parameter checkbox signature is correct and consistent with codebase patterns.

All checkbox controls in the codebase use exactly this signature: addControl(&var, "name", "checkbox"). The implementation on line 12 aligns with established patterns across the project (see L_panels.sc, M_MoonLight.h, etc.), confirming this is the appropriate API for checkbox controls. No changes needed.

src/MoonBase/Char.h (5)

22-28: LGTM!

Constructors are well-designed with safe string handling via strlcpy. The converting constructor template allows seamless size conversions with automatic truncation.


30-53: LGTM!

Assignment operators correctly handle null checks for JSON types and use safe string copy operations throughout.


69-92: LGTM!

Concatenation operators follow correct C++ idioms - non-mutating operator+ returns a copy while operator+= modifies in place. Buffer size for int conversion is adequate for 32-bit integers.


122-128: LGTM!

The format method safely uses vsnprintf which handles buffer limits correctly.


156-162: LGTM!

The non-member operator+ enables natural "prefix" + charObj syntax with safe string operations.

docs/gettingstarted/overview.md (1)

25-25: Sitemap wording / hyphenation looks consistent.

The updated “Front-end / Back-end” wording matches the rest of the docs; no further changes needed.

docs/develop/installation.md (1)

41-41: Installation doc changes read clearly.

The updated note about developing in branches from main and the front-end regeneration explanation are clear and accurate; no changes required.

Also applies to: 113-118

platformio.ini (1)

36-36: Config and dependency tweaks look reasonable; please confirm on target boards.

  • The “Parallel LED Driver” wording and APP_DATE bump are consistent with the rest of the PR.
  • The HP_ALL_DRIVERS I2SClocklessLedDriver hash change should be verified on the usual test boards to confirm the intended SK6812 behavior and that no regressions were introduced.

Also applies to: 59-59, 215-216

src/MoonBase/NodeManager.h (1)

78-112: NodeManager UI/control wiring refactor looks consistent.

  • setupDefinition(const JsonArray& controls) now uses addControl/addControlValue to define nodes and nested controls, matching the new control model in Nodes.h.
  • Passing a concrete JsonObject control into nodeClass->updateControl(control) and nodeClass->onUpdate(updatedItem.oldValue, control) in the nodes[i].controls[j].value branch is a good improvement over index-only updates and keeps node logic localized.

Please just make sure all NodeManager subclasses override addNodes(const JsonObject& control) and addNode(..., const JsonArray& controls) with the new signatures.

Also applies to: 253-268

docs/moonlight/drivers.md (1)

11-46: Driver documentation accurately updated.

The terminology shift to "Parallel LED Driver" and the hardware peripheral details (I2S for ESP32-D0, LCD_CAM for ESP32-S3, ParLIO for ESP32-P4) are accurate and provide useful platform-specific guidance.

src/MoonLight/Modules/ModuleLightsControl.h (3)

28-29: LGTM: Consistent pin initialization.

The rename from pinRelaisBrightness to pinRelayBrightness (fixing the spelling) and the addition of pinToggleOnOff both use UINT8_MAX for consistent invalid-pin initialization.


72-89: LGTM: Pin configuration properly initialized.

The readPins() function correctly:

  • Resets pins to UINT8_MAX on each update
  • Configures INPUT_PULLUP for the button (line 83)
  • Handles the renamed pin_Relay_Brightness enum

92-137: LGTM: Clean migration to addControl API.

The setupDefinition rewrite replaces manual JSON construction with the cleaner addControl()/addControlValue() pattern, improving maintainability and consistency with the rest of the codebase.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/MoonBase/Char.h (1)

102-110: Add missing end < begin check in substring().

As noted in the previous review, the bounds check is incomplete. When end < begin, line 107 computes end - begin + 1 which underflows (both are uint16_t), producing a very large size value. While strlcpy remains memory-safe due to its destination buffer limit, the method returns incorrect results.

Apply this diff to add the missing check:

   Char<N> substring(uint16_t begin, uint16_t end = sizeof(s) - 1) {
     Char<N> sub;
-    if (begin >= sizeof(s) || end >= sizeof(s))
+    if (begin >= sizeof(s) || end >= sizeof(s) || end < begin)
       sub = "";
     else {
       strlcpy(sub.s, s + begin, end - begin + 1);
     }
     return sub;
   }
🧹 Nitpick comments (2)
src/MoonBase/Char.h (1)

61-68: Remove commented-out code.

The commented operator+ implementations should be removed to keep the codebase clean.

Apply this diff:

-  // Char& operator+(const char* rhs) {
-  //   strlcat(s, rhs, sizeof(s));
-  //   return *this;
-  // }
-  // Char& operator+(const String& rhs) {
-  //   strlcat(s, rhs.c_str(), sizeof(s));
-  //   return *this;
-  // }
interface/src/lib/components/moonbase/MultiRow.svelte (1)

51-60: Fragile dependency on derived filteredItems state.

The reordering logic accesses filteredItems[pos].originalIndex (line 55), coupling the callback to the current derived state. While Svelte's derived values are recalculated when dependencies change and stable during synchronous reads, this pattern is fragile if the filter or data changes during the drag operation. Additionally, originalIndex is included in the reorderedItems type signature but never destructured or used, which is confusing.

Consider one of these approaches for clarity and robustness:

Option 1: Build a stable index map before the loop

 function handleReorder(reorderedItems: { item: any; originalIndex: number }[]) {
   console.log('handleReorder', property.name, reorderedItems);
   const full = [...data[property.name]];
+  // Capture the current filteredItems mapping before any potential state changes
+  const indexMap = filteredItems.map(f => f.originalIndex);
   reorderedItems.forEach(({ item }, pos) => {
-    const originalIndex = filteredItems[pos].originalIndex;
+    const originalIndex = indexMap[pos];
     full[originalIndex] = item;
   });
   data[property.name] = full;
   onChange();
 }

Option 2: Pass the mapping from DraggableList if possible
If DraggableList can preserve metadata from the original items prop, have it return the full structure so you can destructure directly:

-reorderedItems.forEach(({ item }, pos) => {
-  const originalIndex = filteredItems[pos].originalIndex;
+reorderedItems.forEach(({ item, originalIndex }, pos) => {
+  const targetIndex = filteredItems[pos].originalIndex;
+  full[targetIndex] = item;

(This assumes reorderedItems includes the stable mapping you need.)

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77646f0 and 70b883b.

📒 Files selected for processing (6)
  • docs/develop/development.md (4 hunks)
  • docs/develop/installation.md (3 hunks)
  • docs/develop/standardsguidelines.md (1 hunks)
  • interface/src/lib/components/moonbase/MultiRow.svelte (5 hunks)
  • src/MoonBase/Char.h (1 hunks)
  • src/MoonBase/Utilities.h (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/develop/installation.md
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/interface/**/*.{svelte,ts,tsx,js,json} : Modify frontend code in the `interface/` directory using SvelteKit

Applied to files:

  • docs/develop/development.md
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Assume the Vite development server (`npm run dev`) is running during frontend development

Applied to files:

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

Applied to files:

  • docs/develop/development.md
  • src/MoonBase/Utilities.h
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/lib/framework/**/*.{h,cpp} : Leverage the framework services in `lib/framework/` for WiFi management, MQTT, NTP, security, firmware updates, and system utilities

Applied to files:

  • src/MoonBase/Utilities.h
📚 Learning: 2025-11-29T16:17:30.619Z
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: misc/parking/GEMINI.md:0-0
Timestamp: 2025-11-29T16:17:30.619Z
Learning: Applies to misc/parking/src/**/*.{h,cpp} : Use ArduinoJson for JSON serialization and deserialization in C++ backend code

Applied to files:

  • src/MoonBase/Utilities.h
🧬 Code graph analysis (1)
src/MoonBase/Char.h (1)
src/MoonBase/Module.h (3)
  • void (150-166)
  • void (194-194)
  • void (195-195)
🪛 Clang (14.0.6)
src/MoonBase/Char.h

[error] 14-14: 'Arduino.h' file not found

(clang-diagnostic-error)

🪛 LanguageTool
docs/develop/development.md

[style] ~40-~40: To form a complete sentence, be sure to include a subject.
Context: ...w components used in MoonLight modules. Might be rewritten as a MoonLight Module in t...

(MISSING_IT_THERE)


[style] ~93-~93: Consider using a different verb to strengthen your wording.
Context: ...nning on your computer. This very much speeds up UI development, as no flashing to the E...

(SPEED_UP_ACCELERATE)

docs/develop/standardsguidelines.md

[style] ~52-~52: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ... descriptions into English. AI bots are very good at understanding language. However, alw...

(EN_WEAK_ADJECTIVE)


[style] ~71-~71: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... what it does, including its arguments. Comments are also formatted using Clang-format a...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.18.1)
docs/develop/development.md

19-19: Images should have alternate text (alt text)

(MD045, no-alt-text)


26-26: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

docs/develop/standardsguidelines.md

41-41: Bare URL used

(MD034, no-bare-urls)


55-55: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


57-57: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


58-58: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


59-59: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (15)
docs/develop/standardsguidelines.md (3)

6-12: Develop principles section is well-structured and aligned with PR objectives.

The principles clearly codify the architectural direction (one firmware per board, documentation-with-code, upstream sync minimization, main-branch controls, modular src/lib/interface separation). These standards support the broader refactors evident in the PR.


14-23: PR description guidance is comprehensive and actionable.

The section clearly sets expectations (avoid minimal descriptions), provides concrete examples of good coverage (what to achieve, how it works, testing, known limitations, areas for help), and reinforces the policy that documentation should accompany code. This aligns well with the PR workflow discipline.


44-60: AI-assisted code guidance is clear and sets appropriate expectations.

The section appropriately acknowledges that AI-generated code is acceptable, reinforces the need for author understanding and verification, and establishes concrete best practices (comments for larger AI sections, review translations, verify refactors don't lose context). The emphasis on understanding and verification is constructive.

docs/develop/development.md (5)

11-11: Link to new standards guide improves navigation and reinforces contributor expectations.

The addition of a clear link to the Standards and Guidelines document at the top of the workflow section is a good architectural choice. It promotes the standardized practices before diving into step-by-step instructions.

Please verify that the link https://moonmodules.org/MoonLight/develop/standardsguidelines/ is correct and accessible in the published documentation. The relative path in the markdown should resolve correctly in the final documentation build.


17-17: Branch-from-main clarification is helpful and aligns with develop principles.

Adding the explicit instruction "Only branch from the main branch!" reinforces the policy documented in the new Standards and Guidelines and reduces ambiguity for contributors.


23-23: Terminology standardization (Front-end/Back-end) and grammar corrections are consistent and clear.

The changes to use hyphenated "Front-end (UI)" and "Back-end (Server)" improve consistency with the new Standards and Guidelines document, and the grammar corrections ("are also made", "Might be rewritten as a MoonLight Module") align with feedback from prior reviews. The overall structure and readability are improved.

Also applies to: 36-36, 40-44


53-53: Formatting instruction is now clearer and more actionable.

The rephrased instruction to "(right-click Format Document on each page you change)" is more direct and actionable than the previous version, improving clarity for contributors unfamiliar with IDE shortcuts.


70-70: Terminology and phrasing refinements improve consistency and readability.

Corrections to "Additional info", updates to use "front-end" and "back-end" consistently, and rephrasing "Saving a UI file is enough to see the results" (removing the excessive exclamation marks and improving flow) align the document with the new Standards and Guidelines and enhance professional tone.

Also applies to: 91-93

src/MoonBase/Utilities.h (3)

18-18: Char utilities split into Char.h—include looks fine

Including Char.h here keeps existing Utilities.h consumers working after moving the Char utilities out; no issues from this change alone.


26-76: Compile-time log-level gating changes behavior of log arguments

The new CORE_DEBUG_LEVEL-based #if blocks mean that when the level is below the threshold, the EXT_LOG* macros expand to no-ops and their arguments are not evaluated at all. That’s usually desirable, but it is a behavior change vs always-compiling calls, so it’s worth double‑checking that no EXT_LOG* call sites rely on side effects in their arguments (e.g., function calls doing work beyond formatting).


269-270: fastDiv255 inline helper is fine as a header utility

Changing to a single inline definition in the header (with an unsigned argument and shift) is safe and keeps the fast path available everywhere without linkage issues.

src/MoonBase/Char.h (4)

1-16: LGTM!

Header, licensing, and includes are appropriate for an Arduino utility header.


17-53: LGTM!

Constructors and assignment operators are well-designed with safe string handling via strlcpy. Null checks for JSON types and template support for different-sized Char instances are appropriate.


69-92: LGTM!

Concatenation operators are correctly implemented. The operator+ returns by value (non-mutating), while operator+= modifies in place. All operations use strlcat with proper bounds, and integer formatting uses bounded snprintf.


112-200: LGTM!

The remaining utility methods demonstrate safe string handling:

  • Bounded functions (strnlen, strnstr, vsnprintf, strftime) prevent buffer overflows
  • The split() method cleverly preserves the original string by saving and restoring state
  • Non-member operator+ enables natural "string" + Char<N> syntax
  • Utility functions include appropriate null checks

* Only branch from the main branch! Press Branches, press New Branch, give it a name e.g. background-script and press Create new Branch, click on background-script

<img width="90" src="https://github.com/user-attachments/assets/588d0854-bac1-4b70-8931-ba6db4c94248" />
<img width="90" src="https://github.com/user-attachments/assets/588d0854-bac1-4b70-8931-ba6db4c94248" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add alt text to image for accessibility.

The image on line 19 is missing alt text, which is required for accessibility and is flagged by markdownlint.

Apply this diff:

-    <img width="90" src="https://github.com/user-attachments/assets/588d0854-bac1-4b70-8931-ba6db4c94248" />
+    <img width="90" alt="Branch creation interface showing 'New Branch' button and 'background-script' branch name" src="https://github.com/user-attachments/assets/588d0854-bac1-4b70-8931-ba6db4c94248" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img width="90" src="https://github.com/user-attachments/assets/588d0854-bac1-4b70-8931-ba6db4c94248" />
<img width="90" alt="Branch creation interface showing 'New Branch' button and 'background-script' branch name" src="https://github.com/user-attachments/assets/588d0854-bac1-4b70-8931-ba6db4c94248" />
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

19-19: Images should have alternate text (alt text)

(MD045, no-alt-text)

🤖 Prompt for AI Agents
In docs/develop/development.md around line 19, the inline image tag lacks alt
text which fails markdownlint and reduces accessibility; update the <img>
element to include a concise, descriptive alt attribute (e.g., alt="brief
description of image") that conveys the image's purpose/context, ensuring the
src remains unchanged and the alt text accurately describes the image for screen
readers.

bool operator==(const Char& rhs) const { return strcmp(s, rhs.s) == 0; }
bool operator!=(const char* rhs) const { return strcmp(s, rhs) != 0; }

char operator[](const uint16_t indexV) const { return s[indexV]; }
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add bounds check to prevent out-of-bounds access.

The index operator accesses s[indexV] without validating that indexV < N. This can cause out-of-bounds reads and undefined behavior.

Apply this diff to add bounds checking:

-  char operator[](const uint16_t indexV) const { return s[indexV]; }
+  char operator[](const uint16_t indexV) const { 
+    return (indexV < sizeof(s)) ? s[indexV] : '\0'; 
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
char operator[](const uint16_t indexV) const { return s[indexV]; }
char operator[](const uint16_t indexV) const {
return (indexV < sizeof(s)) ? s[indexV] : '\0';
}
🤖 Prompt for AI Agents
In src/MoonBase/Char.h around line 100, the const index operator accesses
s[indexV] without validating indexV < N; add a bounds check that if indexV >= N
throws a std::out_of_range (with a clear message including the invalid index and
N) to prevent undefined behavior, otherwise return s[indexV]; ensure <stdexcept>
is included if not already.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/MoonBase/Char.h (1)

93-100: substring() still has off‑by‑one truncation for single‑character ranges.

strlcpy(sub.s, s + begin, end - begin + 1); allocates space only for end - begin characters plus the NUL terminator. For substring(0, 0), the size argument becomes 1, so strlcpy copies 0 characters and you get an empty string instead of a single character. The new end < begin guard fixes the underflow, but the truncation bug remains.

You can compute the intended length and pass len + 1 as the buffer size (clamped to the capacity):

-  Char<N> substring(uint16_t begin, uint16_t end = sizeof(s) - 1) {
-    Char<N> sub;
-    if (begin >= sizeof(s) || end >= sizeof(s) || end < begin)
-      sub = "";
-    else {
-      strlcpy(sub.s, s + begin, end - begin + 1);
-    }
-    return sub;
-  }
+  Char<N> substring(uint16_t begin, uint16_t end = sizeof(s) - 1) {
+    Char<N> sub;
+    if (begin >= sizeof(s) || end >= sizeof(s) || end < begin) {
+      sub = "";
+    } else {
+      size_t len = static_cast<size_t>(end) - static_cast<size_t>(begin) + 1;  // characters to copy
+      if (len >= sizeof(sub.s)) {
+        len = sizeof(sub.s) - 1;  // leave room for terminator
+      }
+      strlcpy(sub.s, s + begin, len + 1);
+    }
+    return sub;
+  }

It would be good to have tests for substring(0, 0), a multi‑char range (e.g. substring(1, 3)), and out‑of‑range inputs.

🧹 Nitpick comments (4)
docs/develop/standardsguidelines.md (2)

52-52: Consider a stronger descriptor than "very good".

Line 52 uses "very good" which is somewhat weak phrasing. Consider replacing with a more direct descriptor such as "excellent," "skilled," or "adept" to strengthen the statement.


69-72: Reduce repetition of "Comments" at the start of consecutive sentences.

Lines 71–72 begin consecutive sentences with "Comments," which breaks up the flow. Consider rewording one of these sentences to vary the opening. For example:

-Comments are used for technical documentation, always document your changes, each function must have a comment describing what it does, including its arguments. Comments are also formatted using Clang-format and Prettier.
+Comments are used for technical documentation, always document your changes, and each function must have a comment describing what it does, including its arguments. The formatting of comments follows Clang-format and Prettier standards.
src/MoonBase/Char.h (2)

19-21: Add a compile-time guard to prevent zero-capacity Char<0>.

Char with N == 0 would produce an invalid buffer type. It’s cheap and robust to enforce a positive capacity at compile time.

You can add a static assertion inside the struct:

 template <size_t N>
 struct Char {
+  static_assert(N > 0, "Char capacity N must be greater than 0");
   char s[N] = "";

121-132: split() uses strtok, which is non‑reentrant and globally stateful.

Using strtok on s is fine in a single‑threaded, controlled context, but it relies on global state and will interfere with any other strtok usage in the process. It also means split() is not re‑entrant if called from nested contexts.

If you anticipate concurrent or nested use, consider one of:

  • Switching to strtok_r (or equivalent re‑entrant variant) if available on your target.
  • Or implementing a small manual tokenizer that walks s with strchr / pointer arithmetic, avoiding global strtok state entirely.

The current implementation is acceptable for many Arduino setups but worth revisiting if this utility becomes widely reused.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70b883b and ec63333.

📒 Files selected for processing (3)
  • docs/develop/standardsguidelines.md (1 hunks)
  • interface/src/lib/components/moonbase/MultiRow.svelte (5 hunks)
  • src/MoonBase/Char.h (1 hunks)
🧰 Additional context used
🪛 Clang (14.0.6)
src/MoonBase/Char.h

[error] 14-14: 'Arduino.h' file not found

(clang-diagnostic-error)

🪛 LanguageTool
docs/develop/standardsguidelines.md

[style] ~41-~41: Try moving the adverb to make the sentence clearer.
Context: ... very useful tips and tricks here: [How to properly submit a PR](https://github.com/wled-dev/WLED/wiki/...

(SPLIT_INFINITIVE)


[style] ~52-~52: As an alternative to the over-used intensifier ‘very’, consider replacing this phrase.
Context: ... descriptions into English. AI bots are very good at understanding language. However, alw...

(EN_WEAK_ADJECTIVE)


[style] ~71-~71: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... what it does, including its arguments. Comments are also formatted using Clang-format a...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
docs/develop/standardsguidelines.md (1)

54-60: Verify list indentation meets markdown linting standards.

The past review flagged list indentation issues on lines 55–59 (expected 0, actual 2). The current code shows these items at column 0 without leading spaces. To ensure this aligns with markdownlint expectations, please verify the rendered markdown list structure is correct and the indentation warnings no longer appear.

interface/src/lib/components/moonbase/MultiRow.svelte (4)

51-62: LGTM! Reordering logic correctly preserves filtered item positions.

The indexMap approach correctly handles reordering of filtered items by mapping the new order to the original array positions. The shallow copy on line 53 is safe since line 58 assigns entire item references rather than mutating properties.


102-136: LGTM! Filtering correctly preserves original indices.

The derived state correctly returns items wrapped with their original indices in both filtered and unfiltered cases, maintaining consistency with the handleReorder signature. The slice to the first 3 properties (line 118) matches the display logic (line 207).


157-157: LGTM! CRUD checks now use consistent optional chaining.

All CRUD permission checks have been updated to use optional chaining for both the crud property access and the includes() method call. This fully addresses the null-safety concerns raised in previous reviews.

Also applies to: 198-198, 203-203, 240-240


194-199: LGTM! DraggableList props correctly wired.

The dragDisabled logic correctly enables dragging when the sort permission ('s') is present or when CRUD is undefined. The items and onReorder props align with the new signature changes.

Comment on lines +24 to +36
Char() = default; // Keep default constructor
Char(const char* str) { strlcpy(s, str, sizeof(s)); } // Constructor to allow initialization from string literal
template <size_t M>
Char(const Char<M>& other) { // Converting constructor from different-sized Char
strlcpy(s, other.c_str(), sizeof(s));
}

// assign operators
Char& operator=(const char* rhs) {
// if (strlen(rhs) >= N) EXT_LOGW("Char", "Truncating '%s' from %d to %d chars", rhs, strlen(rhs), N - 1);
strlcpy(s, rhs, sizeof(s));
return *this;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check if there's documentation or usage of strlcpy in the repo
rg -i "strlcpy" --type h --type cpp -B 2 -A 2 | head -50

Repository: MoonModules/MoonLight

Length of output: 2656


🏁 Script executed:

# Search for calls to the Char constructor and operator= with nullptr or optional values
rg "Char\s*\(" -t cpp -t h -A 1 | head -100

Repository: MoonModules/MoonLight

Length of output: 513


🏁 Script executed:

# Look for any null safety checks in the codebase
rg "str\s*\?" --type cpp --type h | head -30

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Search for assignments or constructions of Char type
rg "Char\s*\w*\s*=.*;" -t cpp -A 0 | head -40

Repository: MoonModules/MoonLight

Length of output: 191


🏁 Script executed:

# Look for JSON or config parsing that might return nullptr
rg "optional|nullptr|null|NULL" -t cpp --type h -B 2 -A 2 | grep -i "char\|str" | head -50

Repository: MoonModules/MoonLight

Length of output: 5347


🏁 Script executed:

# Check if there are any defensive patterns already in use elsewhere
rg "\?\s*:\s*\"\"|str\s*\?\|safe" -t cpp -t h | head -30

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Search for function signatures that accept optional strings
rg "const char\*.*=\s*nullptr|optional.*char" -t cpp -t h -B 2 | head -50

Repository: MoonModules/MoonLight

Length of output: 1622


🏁 Script executed:

# Check strlcpy man page or any documentation in the repo
man strlcpy 2>/dev/null | head -100 || echo "Man page not available, checking documentation"

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Search for Char instantiations or assignments from JSON/optional sources
rg "Char.*=.*json|Char.*=.*optional|Char.*\[\].*getString" -t cpp -t h -B 2 -A 2 | head -60

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Look for where Char might be assigned from config or external data
rg "setValue|setString|fromJson" -t cpp -A 3 | grep -i "char" | head -40

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Check if there's a pattern of nullptr checks before strlcpy elsewhere
rg "if.*\?" -t cpp -B 1 -A 1 | grep -B 1 -A 1 "strcpy\|strncpy\|strlcpy" | head -30

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Check what string functions are available and how they're used
rg "include.*string|include.*cstring" -t cpp -t h | head -20

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Look at the Char.h file more completely to understand its context
cat -n src/MoonBase/Char.h | head -80

Repository: MoonModules/MoonLight

Length of output: 3253


🏁 Script executed:

# Check if strlcat also has the same vulnerability
rg "strlcat" -t cpp -t h -B 2 -A 2 src/MoonBase/Char.h

Repository: MoonModules/MoonLight

Length of output: 694


Defensively handle null const char* inputs to avoid crash.

The Char(const char* str) constructor and operator=(const char* rhs) call strlcpy directly on the input pointer without null checking. If a caller passes nullptr, this will dereference address 0 and crash. Similarly, operator+=(const char* rhs) calls strlcat without null protection. Notably, the class already guards against null inputs in operator=(const JsonVariant) and operator=(const JsonString) (lines 37–48), but the base const char* paths remain unguarded—creating an inconsistency.

Normalize null pointers to an empty string in all three locations:

-  Char(const char* str) { strlcpy(s, str, sizeof(s)); }
+  Char(const char* str) { 
+    const char* safe = str ? str : "";
+    strlcpy(s, safe, sizeof(s));
+  }

   Char& operator=(const char* rhs) {
-    strlcpy(s, rhs, sizeof(s));
+    const char* safe = rhs ? rhs : "";
+    strlcpy(s, safe, sizeof(s));
     return *this;
   }

   Char& operator+=(const char* rhs) {
-    strlcat(s, rhs, sizeof(s));
+    const char* safe = rhs ? rhs : "";
+    strlcat(s, safe, sizeof(s));
     return *this;
   }

This keeps the API consistent with the existing null-handling in the JSON variant operators and prevents crashes from nullable pointer sources.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/MoonBase/Char.h around lines 24 to 36, the Char(const char*),
operator=(const char*), and related operator+=(const char*) paths call
strlcpy/strlcat directly on the incoming pointer and will crash if nullptr;
update these functions to defensively normalize a null const char* to an empty
string before calling strlcpy/strlcat (use the same null-check pattern as
operator=(const JsonVariant)/operator=(const JsonString) — e.g. treat input ==
nullptr ? "" : input), then proceed with strlcpy/strlcat and return *this.

@ewowi ewowi merged commit 42e6ec1 into main Dec 4, 2025
2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 6, 2025
@ewowi ewowi deleted the board-preset-tuning branch December 7, 2025 22:47
This was referenced Dec 8, 2025
@coderabbitai coderabbitai bot mentioned this pull request Jan 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants