Skip to content

Conversation

@ewowi
Copy link
Collaborator

@ewowi ewowi commented Jan 24, 2026

Summary by CodeRabbit

  • New Features

    • Device status now exposes version, uptime, brightness, palette, and preset; UI reflects these fields.
    • UDP broadcasts now include full device status for improved visibility.
  • Refactor

    • Devices and lights control are more tightly integrated, enabling real‑time status propagation.
    • Update cadence improved for more timely device status updates.

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

backend
=======
- main: moduleLightsControl added as argument to ModuleDevices
- Devices: extend UDPMessage with version, uptime, brightness, palette, preset, add LightsControl->addUpdateHandler sending UDP message when something changes, updateDevices: send new vars
@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

Walkthrough

ModuleDevices was extended to carry and broadcast lights state (version, uptime, brightness, palette, preset), its constructor now accepts a ModuleLightsControl* dependency, UDP read/write and device JSON update paths were updated, and main loop timing/initialization adjusted to wire the new dependency.

Changes

Cohort / File(s) Summary
Device State & API
src/MoonBase/Modules/ModuleDevices.h
Added fields to UDPMessage (version, uptime, brightness, palette, preset); added Module* _moduleLightsControl; changed constructor to accept lights module; changed updateDevices signature; updated read/write UDP serialization and device JSON population to include new fields; added UI definition entries for new fields.
Dependency Injection & Looping
src/main.cpp
Construct ModuleLightsControl before ModuleDevices; updated ModuleDevices constructor call to pass &moduleLightsControl; replaced 1s ticker with 20ms ticker for moduleDevices.loop20ms() and kept 1s/10s tickers for other loops.
Minor Code Comment
src/MoonLight/Modules/ModuleLightsControl.h
Added explanatory comment before moduleIO.read(...) in readPins() (no behavioral change).
CI / Tooling Config
.coderabbit.yaml
Restructured review/chat configuration: added early_access, new reviews block, tools.github-checks.timeout_ms, and chat.auto_reply settings; removed previous flat review/context keys.

Sequence Diagram

sequenceDiagram
    participant MC as ModuleLightsControl
    participant MD as ModuleDevices
    participant UDP as UDP Broadcast
    participant DJ as Device JSON / Devices Map

    MC->>MD: notify lights update (brightness,palette,preset)
    activate MD
    MD->>MD: build UDPMessage (hostname,version,uptime,brightness,palette,preset)
    MD->>UDP: broadcast UDPMessage
    deactivate MD

    UDP->>MD: incoming UDPMessage (from others)
    activate MD
    MD->>DJ: updateDevices(UDPMessage, IPAddress)
    DJ->>DJ: create/update device JSON (name,version,uptime,brightness,palette,preset)
    deactivate MD
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped and hummed a tiny tune,

brightness, palette, preset — sent by moon;
UDP whispers through the night,
devices wake and glow with light,
constructor linked — the network's in tune. ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Super sync part 1' is vague and generic, using non-descriptive terminology that doesn't convey meaningful information about the changeset. Readers cannot understand the primary change (UDP packet processing optimization, faster device loops, or sync improvements) from this title alone. Consider a more descriptive title such as 'Optimize UDP packet processing with 20ms device loop' or 'Improve sync speed by draining all queued UDP packets' that clearly indicates the main technical objective.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch supersync-part1

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/MoonBase/Modules/ModuleDevices.h`:
- Around line 47-54: The code is assigning the whole JSON object
state.data["preset"] to message.preset (and logging it), which when cast to
uint8_t becomes 0; instead extract the selected preset field (e.g., the numeric
property like "selected" or "value") from the JSON object and assign that to
message.preset, and log that numeric value rather than serializing the whole
object; update the lambda that handles ModuleState (references: ModuleState,
state.data["preset"], message.preset, serializeJson, EXT_LOGD, MB_TAG) to read
the correct subfield and convert it to uint8_t before assignment and logging
(apply the same change to the other occurrences around lines 188-191).
- Line 58: The assignment to message.uptime currently computes time(nullptr) -
millis()/1000 which yields the boot timestamp and can underflow; update the two
locations where message.uptime is set (the message.uptime assignments in
ModuleDevices.h) to assign uptime seconds directly as millis()/1000 if you
intend to store uptime, or if you intended to store the boot epoch rename the
field (e.g., bootTime) and then compute boot time as time(nullptr) -
(millis()/1000); also ensure any consumer code uses the new semantics/field
name.
- Around line 169-175: The UDPMessage struct is partially uninitialized when
packetSize < sizeof(UDPMessage); before memcpy into UDPMessage (symbol: message)
zero-initialize it (e.g., memset(&message, 0, sizeof(message)) or
value-initialize UDPMessage message) so any trailing fields like
version/brightness/palette/preset are deterministic, then perform deviceUDP.read
into buffer and memcpy(&message, buffer, min(packetSize, sizeof(message))).
Ensure you still use ::min(packetSize, sizeof(message)) for the copy length.

Comment on lines 47 to 54
[&](ModuleState& state) {
message.brightness = state.data["brightness"];
message.palette = state.data["palette"];
message.preset = state.data["preset"];
String ddd;
serializeJson(state.data["preset"], ddd);
EXT_LOGD(MB_TAG, "pr %s", ddd.c_str());
},
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

Send the selected preset value, not the preset object.

state.data["preset"] is a JSON object; converting it to uint8_t yields 0. Use the selected preset field instead.

🔧 Suggested fix
-              message.preset = state.data["preset"];
+              auto preset = state.data["preset"]["selected"];
+              if (preset.isNull()) preset = state.data["preset"]["select"];
+              message.preset = preset.as<uint8_t>();
-            message.preset = state.data["preset"];
+            auto preset = state.data["preset"]["selected"];
+            if (preset.isNull()) preset = state.data["preset"]["select"];
+            message.preset = preset.as<uint8_t>();

Also applies to: 188-191

🤖 Prompt for AI Agents
In `@src/MoonBase/Modules/ModuleDevices.h` around lines 47 - 54, The code is
assigning the whole JSON object state.data["preset"] to message.preset (and
logging it), which when cast to uint8_t becomes 0; instead extract the selected
preset field (e.g., the numeric property like "selected" or "value") from the
JSON object and assign that to message.preset, and log that numeric value rather
than serializing the whole object; update the lambda that handles ModuleState
(references: ModuleState, state.data["preset"], message.preset, serializeJson,
EXT_LOGD, MB_TAG) to read the correct subfield and convert it to uint8_t before
assignment and logging (apply the same change to the other occurrences around
lines 188-191).

_moduleName);

EXT_LOGD(MB_TAG, "b: %d pa %d pr %d", message.brightness, message.palette, message.preset);
message.uptime = time(nullptr) - millis() / 1000;
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

Uptime calculation returns boot time (and can underflow).

time(nullptr) - millis()/1000 yields the boot timestamp, not uptime, and can underflow before time is set. If you want uptime seconds, use millis()/1000; if you want boot time, rename the field.

🔧 Suggested fix
-        message.uptime = time(nullptr) - millis() / 1000;
+        message.uptime = millis() / 1000;
-      message.uptime = time(nullptr) - millis() / 1000;
+      message.uptime = millis() / 1000;

Also applies to: 194-194

🤖 Prompt for AI Agents
In `@src/MoonBase/Modules/ModuleDevices.h` at line 58, The assignment to
message.uptime currently computes time(nullptr) - millis()/1000 which yields the
boot timestamp and can underflow; update the two locations where message.uptime
is set (the message.uptime assignments in ModuleDevices.h) to assign uptime
seconds directly as millis()/1000 if you intend to store uptime, or if you
intended to store the boot epoch rename the field (e.g., bootTime) and then
compute boot time as time(nullptr) - (millis()/1000); also ensure any consumer
code uses the new semantics/field name.

@ewowi
Copy link
Collaborator Author

ewowi commented Jan 24, 2026

@coderabbitai, for some reason the package send in _moduleLightsControl->addUpdateHandler arrives very late (in the next 10s update?) in another device, which calls readUDP(), calling updateDevices. The 10s message (mostly... packages missed?)
Why is that? Could it be that loop1s() (every second) misses packages?

@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Find the ModuleDevices implementation file
fd -e cpp -e h "ModuleDevices" --exec cat {}

echo "=== Separator: Looking for loop1s and loop10s implementations ==="

# Find loop1s and loop10s implementations
rg -n -A 20 "void.*loop1s\(\)" --type cpp

echo "=== Separator: Looking for loop10s ==="

rg -n -A 20 "void.*loop10s\(\)" --type cpp

echo "=== Separator: Looking for UDP receive buffer or socket setup ==="

# Look for UDP setup and buffer configuration
rg -n -C 5 "UDP|udp" src/MoonBase/Modules/ModuleDevices.h src/MoonBase/Modules/ModuleDevices.cpp

Length of output: 24698


🏁 Script executed:

#!/bin/bash

# Find where loop1s and loop10s are called from the main loop
rg -n -B 5 -A 5 "loop1s\(\)|loop10s\(\)" --type cpp | head -80

echo "=== Separator: Check NetworkUDP buffer behavior ==="

# Search for NetworkUDP definition or WiFiUDP usage
rg -n "class NetworkUDP|typedef.*NetworkUDP|WiFiUDP" --type cpp --type-add 'h:*.h'

Length of output: 4693


🏁 Script executed:

#!/bin/bash

# Find NetworkUDP definition
rg -n "NetworkUDP" src/ --type-add 'h:*.h' -A 2 -B 2

echo "=== Separator: Check Utilities.h for NetworkUDP ==="

# Check Utilities.h which is included in ModuleDevices.h
cat src/MoonBase/Utilities.h 2>/dev/null || echo "File not found"

echo "=== Separator: Search in all files ==="

fd -e h Utilities.h --exec cat {}

Length of output: 23214


🏁 Script executed:

#!/bin/bash

# Search for NetworkUDP definition or typedef
rg -n "typedef.*NetworkUDP|define.*NetworkUDP|using NetworkUDP" --type-add 'h:*.h'

echo "=== Separator: Search in lib or platformio dependencies ==="

# Check if there's a lib folder with ESP32SvelteKit
find . -name "*.h" -type f | xargs rg -l "NetworkUDP" | head -10

echo "=== Separator: Look for ESP32SvelteKit or ESPFS files ==="

# Find ESPFS.h which is included
fd ESPFS.h --exec cat {} | head -100

Length of output: 766


🏁 Script executed:

#!/bin/bash

# Search more broadly for NetworkUDP or WiFiUDP
rg -n "WiFiUDP|EthernetUDP" --type cpp --type-add 'h:*.h' -B 2 -A 2

echo "=== Separator: Check platformio.ini for dependencies ==="

cat platformio.ini 2>/dev/null | head -50

echo "=== Separator: Look for lib_deps or framework ==="

rg -n "lib_deps|framework.*=" platformio.ini 2>/dev/null | head -20

Length of output: 4475


🏁 Script executed:

#!/bin/bash

# Search for where NetworkUDP might be defined - could be in ESP32SvelteKit library
rg -n "NetworkUDP" -B 5 -A 5 --type-add 'h:*.h'

echo "=== Separator: Check if it's WiFi or Ethernet based ==="

# Look for WiFi and ETH usage in context
rg -n "WiFi\.|ETH\." src/MoonBase/Modules/ModuleDevices.h -C 3

Length of output: 2683


@ewowi, I found the issue! The problem is in the readUDP() function (lines 168-179). It only reads one UDP packet per call, but it's called only once per second in loop1s().

Root Cause

When _moduleLightsControl->addUpdateHandler fires (lines 41-66), it immediately broadcasts a UDP packet. If brightness, palette, or preset change multiple times within a second (or even across a few seconds), multiple UDP packets are sent and queue up in the receiving device's UDP buffer.

However, readUDP() only processes one packet per second:

void readUDP() {
    size_t packetSize = deviceUDP.parsePacket();  // ← Only called ONCE
    if (packetSize >= 38) {
      // ... process one packet
      updateDevices(message, deviceUDP.remoteIP());
    }
}

If 10 packets are queued, it takes 10 seconds to drain them all at 1 packet/second, which explains the "next 10s update" delay you're seeing.

Solution

Drain all available packets in readUDP() by using a while loop:

void readUDP() {
  // Process all available UDP packets in the buffer
  while (size_t packetSize = deviceUDP.parsePacket()) {
    if (packetSize >= 38) {  // WLED has 44, MM had ! 38
      char buffer[packetSize];
      UDPMessage message;
      deviceUDP.read(buffer, packetSize);
      memcpy(&message, buffer, ::min(packetSize, sizeof(message)));
      // EXT_LOGD(ML_TAG, "UDP packet read from %d: %s (%d)", deviceUDP.remoteIP()[3], buffer + 6, packetSize);

      updateDevices(message, deviceUDP.remoteIP());
    }
  }
}

This will process all queued packets immediately instead of one per second, eliminating the delay.

backend
=======
- main: add last20ms and use for devices
- devices: loop20 for direct response, readudp loops over received packets, use preset.selected
@ewowi
Copy link
Collaborator Author

ewowi commented Jan 24, 2026

@coderabbitai, can you review latest commit: ced9a76

@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the specific commit details
git show ced9a76e876ae0ffe6f2952e1d45f804f9aaffef --stat

echo "=== Separator: Full diff of the commit ==="

# Get the full diff of the commit
git show ced9a76e876ae0ffe6f2952e1d45f804f9aaffef

Length of output: 7649


@ewowi, excellent implementation! 🎉 The commit addresses the UDP delay issue perfectly and adds significant improvements. Here's my review:

✅ Core Fix: readUDP() While Loop

Perfect implementation of the suggested fix:

while (size_t packetSize = deviceUDP.parsePacket()) {
  if (packetSize >= 38) {
    // ... process packet
  }
}

This drains all queued packets immediately instead of one per call, eliminating the backlog delay.

🚀 Major Improvement: 20ms Response Time

Renaming loop1s()loop20ms() and calling it every 20ms is brilliant! This improves UDP responsiveness by 50x (from 1000ms to 20ms). Combined with the while-loop, this means:

  • Before: 1 packet/second → 10 packets took 10 seconds
  • After: All packets drained every 20ms → 10 packets processed in ~20ms

📝 Other Improvements

  1. Zero-initialization: UDPMessage message{}; prevents garbage data in uninitialized fields ✓

  2. Correct preset field: Using state.data["preset"]["selected"] instead of state.data["preset"]

  3. Platform abstraction: pal::millis() instead of millis() for better portability ✓

  4. Uptime safety: time(nullptr) ? time(nullptr) - pal::millis() / 1000 : pal::millis() / 1000 handles cases where NTP time isn't set yet ✓

⚠️ Minor Considerations

  1. Stack allocation: char buffer[packetSize]; allocates on stack inside the loop. For typical UDP packets (<1500 bytes), this is fine, but if you ever receive unusually large packets, consider a static buffer or heap allocation.

  2. Loop duration: If packets arrive faster than 20ms intervals, the while-loop could process many packets in one call. This is generally fine, but if you ever see watchdog issues, you could add a packet count limit or taskYIELD() inside the loop.

  3. Nested ticker structure: The 20ms → 1s → 10s nesting works but is unconventional. Consider flattening to independent counters if you add more timing tiers.

Overall, this is a solid fix that should eliminate the 10-second delays completely! The 20ms response time is a huge win for device synchronization.


🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-06T13:45:03.067Z
Learning: For MoonLight on ESP32 at extreme loads (e.g., 49K channels @ 100 fps): Task watchdog can fire when AppDrivers/AppEffects process many nodes without yielding. Even with vTaskDelay(1) at loop end, the node iteration for-loops (PhysicalLayer::loopDrivers line 108-111, PhysicalLayer::loop line 73-75) can take >5 seconds without yielding, starving IDLE task (priority 0). Solution: add taskYIELD() or vTaskDelay(1) inside the node/layer iteration loops to give IDLE task chances to run and reset watchdog. Lowering task priority won't help since IDLE is already at 0 (lowest).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T12:49:07.297Z
Learning: In MoonLight channel reordering architecture: Keep rgbwBufferMapping in driver task (not effect task) because: (1) Driver has spare CPU cycles waiting for DMA/I2S/Art-Net timing; effect task is CPU-bound and starvation-prone. (2) Contiguous RGB writes in effects are cache-friendly; scattered writes at final offsets are cache-hostile for non-RGB orders (GRB, BGR, etc.). (3) forEachLightIndex already handles sparse mapping iteration; moving reordering to effects adds complexity without benefit since drivers must send full frames anyway (can't skip iteration). (4) rgbwBufferMapping does brightness/gamma LUT + white extraction + reordering—moving just reordering creates code duplication. (5) Effects should express color intent (CRGB), not hardware details (offsetRed/Green/Blue). Driver-side transformation maintains separation of concerns and task load balance for scaling to 98K+ pixels.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-08T11:39:52.878Z
Learning: In MoonLight WebSocket monitor: When browser tabs are hidden, browsers throttle JavaScript (including WebSocket onmessage handlers) but keep TCP connections open. Monitor data (48KB frames every 20-300ms) continues arriving at TCP level but isn't processed. This fills ESP32's httpd_ws_send_frame_async() buffers, causing ESP_FAIL. When tab becomes visible, hundreds of queued frames overwhelm the JavaScript main thread, freezing the UI. Solution: use Page Visibility API (client_visibility event) to stop emitting monitor data to hidden clients, preventing buffer overflow and backlog buildup.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:33.598Z
Learning: In MoonLight GameOfLifeEffect (src/MoonLight/Nodes/Effects/E_MoonModules.h): The effect's nested loops (especially lines 257-323) iterate through all cells (e.g., 128×128 = 16,384) with nested neighbor checks (18 per cell = 294,912 operations) without yielding. This starves effectTask (priority 3) when higher-priority tasks like HTTP server (priority 5) run, causing the effect to disappear. UI actions (save, refresh) temporarily show one frame because HTTP completes and briefly releases CPU back to effectTask. Solution: Add taskYIELD() inside outer X loop to prevent task starvation. This is the same root cause as issue `#102` (Starsky glitches with many driver nodes).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR `#84`: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T17:07:33.344Z
Learning: FastLED library does not support direct 4-byte-per-pixel (CRGBW) buffers. It always expects CRGB (3-byte) arrays and internally generates the W channel from RGB using .setRgbw() API. Casting a 4-byte RGBW buffer to CRGB* causes misalignment (W becomes next pixel's R). For MoonLight's FastLED driver with channelsPerLight=4: must either (1) repack RGB in-place before FastLED.show() then restore after, (2) use driver-specific channelsPerLight=3 layout, or (3) allocate temporary RGB-only buffer. Option 1 (in-place repacking) is best for ESP32-D0 memory constraints.

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