Skip to content

UDP port seggregation#172

Merged
ewowi merged 5 commits intomainfrom
udp-port-seggregation
May 4, 2026
Merged

UDP port seggregation#172
ewowi merged 5 commits intomainfrom
udp-port-seggregation

Conversation

@ewowi
Copy link
Copy Markdown
Collaborator

@ewowi ewowi commented May 4, 2026

Summary by CodeRabbit

  • Documentation
    • Added a full developer reference for the local‑network discovery/control protocol and updated site navigation.
    • Clarified WLED coexistence and discovery limitations for WLED devices; added links to developer UDP protocol reference.
  • New Features
    • Redesigned Devices view with a detailed device table, click‑throughs, editable light controls, and clarified "Last Sync".
  • Behavior
    • Group sync refined to avoid loop/false‑match propagation using hostname‑boundary rules; discovery and control now use separate network channels.
  • Chores
    • Updated embedded build metadata.

ModuleDevices:
- Replace UDPMessage.rommel[6] (all-zeros) with proper UDPWLEDHeader (44 bytes)
  matching WLED/StarLight packet format (token=255, id=1, ip, name, type, version)
- static_assert(sizeof(UDPWLEDHeader)==44) enforces layout at build time
- lightsOn encoded in header.type bit 7, matching WLED convention
- Add UDPControlMessage for port 65507 (MoonLight-only, WLED never sees this)
- Move ALL control (broadcast + unicast) to port 65507; port 65506 carries
  discovery packets only — WLED can no longer receive MoonLight control commands
- receiveUDP: size-based dispatch — 44 bytes = WLED, sizeof(UDPMessage) = MoonLight
- Add updateDevicesWLED() for 44-byte WLED packets (name/ip/lightsOn available)
- Track deviceControlUDPConnected separately; guard sendUDP(true) on it
- partOfGroup comment: corrected "dots" to "hyphens"

Docs:
- Add docs/develop/devices.md: UDP protocol reference for developers
- Update docs/moonbase/devices.md: end-user description including WLED coexistence
- Add develop/devices.md to mkdocs.yml nav
- Replace stale ModuleDevices section in docs/develop/network.md with pointer
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@ewowi has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minute and 5 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 97eeb573-fd88-44e3-b575-9dde2362d1f2

📥 Commits

Reviewing files that changed from the base of the PR and between d2971be and 42e4f00.

📒 Files selected for processing (4)
  • docs/develop/devices.md
  • docs/moonbase/devices.md
  • docs/moonbase/overview.md
  • docs/network/sta.md

Walkthrough

Adds a dual-socket UDP protocol for ModuleDevices: WLED-compatible discovery on UDP/65506 and MoonLight-only control on UDP/65507. Introduces packed wire structs (UDPWLEDHeader, revised UDPMessage, UDPControlMessage), exact-size packet dispatch per socket, group/unicast control semantics with loop prevention, UI→control wiring, docs, and nav updates.

Changes

ModuleDevices — UDP dual-socket protocol & device-table sync

Layer / File(s) Summary
Protocol Definition
src/MoonBase/Modules/ModuleDevices.h
Add packed UDPWLEDHeader (44 bytes), replace old UDPMessage with embedded UDPWLEDHeader (101 bytes), and add UDPControlMessage (80 bytes).
Public State
src/MoonBase/Modules/ModuleDevices.h
Add bool deviceControlUDPConnected and keep distinct ports: discovery 65506, control 65507.
Socket Binding & Lifecycle
src/MoonBase/Modules/ModuleDevices.h
loop10s() binds discovery and control sockets independently with separate connection flags and retry resilience; discovery broadcasts after discovery binds.
Reception & Dispatch
src/MoonBase/Modules/ModuleDevices.h
receiveUDP() routes by exact packet size per port: 44→updateDevicesWLED, 101→updateDevices, 80→processControlMessage; validates token/id for WLED packets and drops other sizes.
Discovery Parsing
src/MoonBase/Modules/ModuleDevices.h
updateDevices() consumes full MoonLight discovery fields; updateDevicesWLED() populates limited WLED-derived fields and zeros unavailable MoonLight-only fields.
Control Processing
src/MoonBase/Modules/ModuleDevices.h
processControlMessage() applies messages addressed to this host or group broadcasts (empty targetName + partOfGroup()), converts to control state, calls _moduleControl->update() with origin "unicast" or "group", and suppresses re-broadcast when originId == "group".
Outbound Send Logic
src/MoonBase/Modules/ModuleDevices.h
sendUDP(true) sends UDPControlMessage on 65507 (group when targetName empty); sendUDP(false) broadcasts UDPMessage on 65506 and updates local device table. onUpdate() serializes UI edits to UDPControlMessage, applies locally if targeting self, otherwise unicasts or group-broadcasts via control socket (requires deviceControlUDPConnected).
Helpers & Conversions
src/MoonBase/Modules/ModuleDevices.h
Add/adjust helpers: infoToHeader, infoToMessage, infoToControlMessage, messageToControlState, controlStateToControlMessage, deviceToControlMessage, and header building to include lights-on bit in header.type.
Grouping Logic
src/MoonBase/Modules/ModuleDevices.h
Tighten partOfGroup() to require hyphen-boundary prefix matches (avoid false prefix matches).
Device Table Finalization
src/MoonBase/Modules/ModuleDevices.h
finaliseDeviceUpdate() filters stale entries (>1 day), sorts by name/IP, and batches UI updates.
Developer Docs
docs/develop/devices.md
New developer reference documenting ports 65506/65507, packet formats (44/101/80 bytes), exact-size dispatch rules, token/id validation, socket lifecycle, control/group semantics, loop prevention, and versioning implications (packageSize/struct-size matching).
User Docs
docs/moonbase/devices.md
Rewrite of Devices panel docs: device table columns, click behavior, “Last Sync”, WLED discovery limitations (no brightness/palette/preset in 44‑byte header), Group sync rules, and WLED coexistence (shared discovery vs MoonLight control port).
Navigation
mkdocs.yml, docs/develop/network.md
Add develop/devices.md to Develop nav and replace inline UDP details in develop/network.md with a link to the new developer page.
Build Flags
firmware/esp32-d0.ini, platformio.ini
Add -mtext-section-literals to [env:esp32-d0-wrover] build_flags; update APP_DATE build flag from 20260411 to 20260504.

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI
    participant MB as ModuleDevices
    participant Sock65506 as UDP Socket (65506)
    participant Sock65507 as UDP Socket (65507)
    participant Remote as Remote Device

    Note over Sock65506,Sock65507: Discovery (65506) & Control (65507) split

    UI->>MB: onUpdate(device, state)
    alt target == local
        MB->>MB: apply locally via _moduleControl->update(origin="unicast")
    else
        MB->>Sock65507: send UDPControlMessage -> targetIP:65507 (unicast) or broadcast (group, targetName="")
    end

    Note over Remote,Sock65507: Group control propagation
    Sock65507->>Remote: UDPControlMessage (group/unicast)
    Remote->>Sock65507: may re-broadcast unless originId == "group"

    Note over Remote,Sock65506: Discovery flow
    Remote->>Sock65506: broadcast UDPWLEDHeader (44) or UDPMessage (101)
    Sock65506->>MB: receiveUDP()
    alt size == 44
        MB->>MB: updateDevicesWLED(header, IP)
    else size == 101
        MB->>MB: updateDevices(message, IP)
    end
    MB->>MB: finaliseDeviceUpdate()
    MB->>UI: update device table
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • MoonModules/MoonLight#165: Modifies ModuleDevices UDP port segregation and control socket (deviceControlUDP / port 65507) and related send/receive splitting.
  • MoonModules/MoonLight#119: Changes to UDP structs and ModuleDevices parsing/send logic; closely related to wire-format updates.

Poem

🐰 Hopping packets by moonlit code and tune,

Discovery on six-five-five-oh-six sings noon,
Control whispers on seven — no echo, no loop,
Hyphens bind groups; unicast finds its troop,
A rabbit cheers: "Sync steady — lights bloom soon!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'UDP port seggregation' directly matches the primary objective of separating UDP traffic across distinct ports (65506 for discovery, 65507 for control), which is the core change throughout the codebase and documentation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch udp-port-seggregation

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Copy Markdown

@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/Modules/ModuleDevices.h (2)

149-170: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Decouple the control socket lifecycle from the discovery socket.

deviceControlUDP.begin() is only retried while deviceUDPConnected is false, and loop20ms() stops polling entirely when 65506 is down. That means one transient bind failure on 65507, or any ongoing failure on 65506, can disable MoonLight control even though the other socket is still usable.

Suggested change
   void loop20ms() override {
     Module::loop20ms();
 
     if (!networkIsConnected()) return;
-
-    if (!deviceUDPConnected) return;
+    if (!deviceUDPConnected && !deviceControlUDPConnected) return;
 
     receiveUDP();  // and updateDevices
   }
 
   void loop10s() override {
     if (!networkIsConnected()) return;
 
     if (!deviceUDPConnected) {
       deviceUDPConnected = deviceUDP.begin(deviceUDPPort);
-      deviceControlUDPConnected = deviceControlUDP.begin(deviceControlUDPPort);  // Initialize control UDP on separate port
+    }
+    if (!deviceControlUDPConnected) {
+      deviceControlUDPConnected = deviceControlUDP.begin(deviceControlUDPPort);  // Initialize control UDP on separate port
       EXT_LOGD(MB_TAG, "deviceUDP:%d p:%d, controlUDP:%d p:%d",
                deviceUDPConnected, deviceUDPPort, deviceControlUDPConnected, deviceControlUDPPort);
       if (!deviceControlUDPConnected)
         EXT_LOGW(MB_TAG, "Failed to bind control UDP on port %d", deviceControlUDPPort);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonBase/Modules/ModuleDevices.h` around lines 149 - 170, The control UDP
socket lifecycle is currently tied to deviceUDPConnected causing control to stop
if the discovery socket or its bind (deviceUDP.begin/deviceUDPConnected) fails;
update loop10s() and loop20ms() so deviceControlUDP.begin(deviceControlUDPPort)
is attempted and retried independently of deviceUDPConnected, use
deviceControlUDPConnected as its own flag, and ensure loop20ms() still
polls/receives from the control socket (and/or calls the control-specific
receive path) even when deviceUDPConnected is false; keep existing logs
(EXT_LOGD/EXT_LOGW) but reflect independent bind attempts for
deviceControlUDPConnected and do not gate sendUDP/receive of control traffic
behind deviceUDPConnected.

464-473: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require a hyphen boundary when matching groups.

device.startsWith(base.substring(0, pos)) will treat kitchen-1 as part of the same group as kitchenette-2, stage-1 as matching stagecoach-3, etc. Grouping here is hyphen-delimited, so the prefix match needs to respect that boundary.

Suggested change
-    return device.startsWith(base.substring(0, pos));
+    String prefix = base.substring(0, pos);
+    return device == prefix || device.startsWith(prefix + "-");
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonBase/Modules/ModuleDevices.h` around lines 464 - 473, The current
suffix-group check uses device.startsWith(base.substring(0, pos)) which can
falsely match names like "kitchenette-2"; update the final condition to enforce
a hyphen boundary at the split point: after finding pos (index of the hyphen in
base) require that device starts with base.substring(0, pos) AND that device has
a hyphen at the same position (e.g., device.length() > pos and
device.charAt(pos) == '-') so only true hyphen-delimited group members match;
keep the existing exact-match return (base == device) path unchanged.
🧹 Nitpick comments (1)
src/MoonBase/Modules/ModuleDevices.h (1)

41-61: ⚡ Quick win

Add compile-time size checks for the full wire packets.

receiveUDP() dispatches by exact sizeof(UDPMessage) / sizeof(UDPControlMessage), and the docs now call out fixed sizes. Right now only UDPWLEDHeader is guarded, so any future change to Char<32> or packing will break interoperability silently.

Suggested change
 struct UDPMessage {
   UDPWLEDHeader header; // 44 bytes — WLED-compatible
   Char<32> versionStr;  // human-readable version string
   char build[16];       // build date string
   uint32_t uptime;
   uint16_t packageSize; // sizeof(UDPMessage) — used by receiver to validate packet type
   uint8_t brightness;
   uint8_t palette;
   uint8_t preset;
 } __attribute__((packed));  // Force no padding
+static_assert(sizeof(UDPMessage) == 101, "UDPMessage must be exactly 101 bytes");
 
 // MoonLight-only control message sent exclusively on port 65507.
 // WLED does not listen on port 65507, so it never receives these packets.
 struct UDPControlMessage {
   UDPWLEDHeader header; // 44 bytes: sender identification
   char targetName[32];  // unicast: receiver hostname; group broadcast: empty string
   uint8_t brightness;
   uint8_t lightsOn;
   uint8_t palette;
   uint8_t preset;
 } __attribute__((packed));  // Force no padding
+static_assert(sizeof(UDPControlMessage) == 80, "UDPControlMessage must be exactly 80 bytes");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonBase/Modules/ModuleDevices.h` around lines 41 - 61, Add compile-time
size assertions for the full wire structs by inserting static_assert checks
immediately after the UDPMessage and UDPControlMessage definitions that verify
sizeof(UDPMessage) == 101 and sizeof(UDPControlMessage) == 80 (these totals come
from UDPWLEDHeader == 44 plus the declared field sizes), and provide clear
diagnostic messages so any accidental change to Char<32>, packing, or field
sizes fails at compile time; reference the UDPMessage and UDPControlMessage
symbols when adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/develop/devices.md`:
- Around line 24-32: Update the three fenced code blocks that show the packet
layout to include a language identifier so markdownlint MD040 is satisfied: add
"text" after each opening triple-backtick for the block starting with "byte 0:  
token   — must be 255 (WLED validates this)" and the two blocks beginning with
"[UDPWLEDHeader — 44 bytes]". Ensure you change only the opening fence (``` ->
```text) for those blocks so rendering is unchanged.

In `@src/MoonBase/Modules/ModuleDevices.h`:
- Around line 293-328: Network-sourced fixed-size string fields must be
NUL-terminated before any strcmp/logging/JSON use: after reading packets in
receiveUDP (after deviceUDP.read / deviceControlUDP.read), explicitly set the
final byte to '\0' on any char arrays like UDPWLEDHeader::name,
UDPMessage::targetName (and any other fixed-size char buffers in UDPWLEDHeader,
UDPMessage, UDPControlMessage) so they cannot run past the buffer when used by
updateDevicesWLED, updateDevices, processControlMessage or logging. Ensure you
apply this for both discovery (UDPWLEDHeader/header and UDPMessage/message) and
control (UDPControlMessage/msg) reads immediately after read returns. />

---

Outside diff comments:
In `@src/MoonBase/Modules/ModuleDevices.h`:
- Around line 149-170: The control UDP socket lifecycle is currently tied to
deviceUDPConnected causing control to stop if the discovery socket or its bind
(deviceUDP.begin/deviceUDPConnected) fails; update loop10s() and loop20ms() so
deviceControlUDP.begin(deviceControlUDPPort) is attempted and retried
independently of deviceUDPConnected, use deviceControlUDPConnected as its own
flag, and ensure loop20ms() still polls/receives from the control socket (and/or
calls the control-specific receive path) even when deviceUDPConnected is false;
keep existing logs (EXT_LOGD/EXT_LOGW) but reflect independent bind attempts for
deviceControlUDPConnected and do not gate sendUDP/receive of control traffic
behind deviceUDPConnected.
- Around line 464-473: The current suffix-group check uses
device.startsWith(base.substring(0, pos)) which can falsely match names like
"kitchenette-2"; update the final condition to enforce a hyphen boundary at the
split point: after finding pos (index of the hyphen in base) require that device
starts with base.substring(0, pos) AND that device has a hyphen at the same
position (e.g., device.length() > pos and device.charAt(pos) == '-') so only
true hyphen-delimited group members match; keep the existing exact-match return
(base == device) path unchanged.

---

Nitpick comments:
In `@src/MoonBase/Modules/ModuleDevices.h`:
- Around line 41-61: Add compile-time size assertions for the full wire structs
by inserting static_assert checks immediately after the UDPMessage and
UDPControlMessage definitions that verify sizeof(UDPMessage) == 101 and
sizeof(UDPControlMessage) == 80 (these totals come from UDPWLEDHeader == 44 plus
the declared field sizes), and provide clear diagnostic messages so any
accidental change to Char<32>, packing, or field sizes fails at compile time;
reference the UDPMessage and UDPControlMessage symbols when adding these
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 19c69f48-24e8-4883-b44c-46138ebbdccd

📥 Commits

Reviewing files that changed from the base of the PR and between 9a8b914 and 73847bc.

📒 Files selected for processing (5)
  • docs/develop/devices.md
  • docs/develop/network.md
  • docs/moonbase/devices.md
  • mkdocs.yml
  • src/MoonBase/Modules/ModuleDevices.h

Comment thread docs/develop/devices.md Outdated
Comment thread src/MoonBase/Modules/ModuleDevices.h
… match, static_asserts, doc fences

- receiveUDP(): NUL-terminate all char arrays after udp.read() to guard against non-terminated network data
- loop10s/loop20ms: bind discovery and control sockets independently; poll control even when discovery is unbound
- partOfGroup(): require device.charAt(pos)=='-' to prevent "kitchenette" matching the "kitchen" group
- Add static_assert for UDPMessage (101 bytes) and UDPControlMessage (80 bytes)
- docs/develop/devices.md: add `text` language identifier to three fenced code blocks
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/MoonBase/Modules/ModuleDevices.h`:
- Around line 295-299: Unicast controls are being treated as module-origin group
updates because update(...) is called with _moduleName as the origin, which
causes addUpdateHandler to treat it as a remote-origin and call sendUDP(true);
change the origin argument so group broadcasts still use "group" but
unicast-controlled updates use a non-module origin (e.g. "unicast" or empty
string) instead of _moduleName: modify the call to
_moduleControl->update(newState, ModuleState::update, isGroupBroadcast ? "group"
: "unicast") so addUpdateHandler won't re-broadcast unicast as group; verify
related logic in addUpdateHandler/sendUDP still interprets "group" as
suppressing rebroadcast.
- Around line 281-283: The pointer stored in myName comes from a temporary
String returned by esp32sveltekit.getSystemHostname(), leaving myName dangling
when later used (e.g., in strcmp); fix this by capturing the returned String in
a persistent local variable (e.g., myNameStr) before taking c_str(), so myName
points into a String that remains alive for the strcmp/usage; update the code
around senderName, myName and getSystemHostname() in ModuleDevices.h
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8be2e0c1-033b-4334-955b-91d9116573c3

📥 Commits

Reviewing files that changed from the base of the PR and between 73847bc and 792be90.

📒 Files selected for processing (2)
  • docs/develop/devices.md
  • src/MoonBase/Modules/ModuleDevices.h
✅ Files skipped from review due to trivial changes (1)
  • docs/develop/devices.md

Comment thread src/MoonBase/Modules/ModuleDevices.h
Comment thread src/MoonBase/Modules/ModuleDevices.h
…teral pool, and status broadcast

- processControlMessage: store getSystemHostname() in String (not const char*) to fix dangling pointer that prevented unicast control from applying
- addUpdateHandler: always call sendUDP(false) so remote device tables update immediately on any control change; suppress sendUDP(true) for both "group" and "unicast" origins
- Apply received unicast control with origin "unicast" (not _moduleName) so addUpdateHandler skips group re-broadcast
- UDPMessage::versionStr: char[32] instead of Char<32> (POD, avoids GCC packed-struct warning); update all call sites to strlcpy/direct index
- onUpdate() remote path: guard on deviceControlUDPConnected and log beginPacket failures
- receiveUDP() control port: guard on deviceControlUDPConnected before parsePacket loop
- esp32-d0-wrover: add -mtext-section-literals to fix l32r literal target out of range linker error
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
src/MoonBase/Modules/ModuleDevices.h (1)

128-135: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat self-targeted row edits as unicast, not module-origin group control.

When targetIP == activeIP, this calls _moduleControl->update(..., _moduleName). The handler on Lines 79-84 treats any origin other than "group"/"unicast" as a local control change and calls sendUDP(true), so editing the local device row fans out to the whole group while the remote-row path stays targeted. Using "unicast" here keeps the self-target path consistent with the network unicast path and avoids accidental group propagation.

Suggested fix
-        _moduleControl->update(newState, ModuleState::update, _moduleName);  // Do not add server in the originID as that blocks updates, see execOnUpdate
+        _moduleControl->update(newState, ModuleState::update, "unicast");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonBase/Modules/ModuleDevices.h` around lines 128 - 135, When handling
self-targeted UDP control (where targetIP == activeIP), don't pass the module's
name as the origin to _moduleControl->update (which causes the handler to treat
it as a local/group-origin change); instead use the unicast origin identifier so
the update is treated as a unicast/network-targeted change. Locate the block
that calls messageToControlState(...) and _moduleControl->update(newState,
ModuleState::update, _moduleName) and change the origin argument to the
equivalent "unicast" origin string used elsewhere so the handler will not call
sendUDP(true) and will keep the self-targeted edit as unicast.
🧹 Nitpick comments (1)
firmware/esp32-d0.ini (1)

53-59: ⚡ Quick win

Uncomment and update the platform version to ensure toolchain stability.

The recommendation to pin the platform is already noted in line 48, but line 49 remains commented. Since -mtext-section-literals is a compiler-specific Xtensa flag and now required for livescripts, uncomment the platform pin and update it to espressif32@6.13.0 (or verify against the latest stable release) to prevent toolchain drift and ensure reproducible builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@firmware/esp32-d0.ini` around lines 53 - 59, Uncomment and set the Platform
pin to a fixed espressif32 toolchain version to avoid toolchain drift: locate
the existing commented platform pin near the build_flags block (the same env
that uses -mtext-section-literals and build_flags) and replace the commented
line with platform = espressif32@6.13.0 (or the latest verified stable), then
save so the -mtext-section-literals flag and livescripts build_flags use a
reproducible toolchain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/MoonBase/Modules/ModuleDevices.h`:
- Around line 128-135: When handling self-targeted UDP control (where targetIP
== activeIP), don't pass the module's name as the origin to
_moduleControl->update (which causes the handler to treat it as a
local/group-origin change); instead use the unicast origin identifier so the
update is treated as a unicast/network-targeted change. Locate the block that
calls messageToControlState(...) and _moduleControl->update(newState,
ModuleState::update, _moduleName) and change the origin argument to the
equivalent "unicast" origin string used elsewhere so the handler will not call
sendUDP(true) and will keep the self-targeted edit as unicast.

---

Nitpick comments:
In `@firmware/esp32-d0.ini`:
- Around line 53-59: Uncomment and set the Platform pin to a fixed espressif32
toolchain version to avoid toolchain drift: locate the existing commented
platform pin near the build_flags block (the same env that uses
-mtext-section-literals and build_flags) and replace the commented line with
platform = espressif32@6.13.0 (or the latest verified stable), then save so the
-mtext-section-literals flag and livescripts build_flags use a reproducible
toolchain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 57f56c83-40f3-41cf-9e9f-300409358fc8

📥 Commits

Reviewing files that changed from the base of the PR and between 792be90 and 6b23182.

📒 Files selected for processing (2)
  • firmware/esp32-d0.ini
  • src/MoonBase/Modules/ModuleDevices.h

…oc updates

- onUpdate(): detect same-group vs different-group before sending control
  - same group → group broadcast (empty targetName, one packet reaches all members)
  - different group → unicast to target; target re-broadcasts to its own group
- addUpdateHandler: only "group" origin suppresses sendUDP(true); "unicast" now
  allows re-broadcast so cross-group unicast recipients propagate to their group
- docs/develop/devices.md: rewrite control flow section (three-case dispatch,
  processControlMessage dangling-pointer fix note, updated loop prevention table,
  corrected socket lifecycle description, char[] versionStr in packet layout)
- docs/moonbase/devices.md: update group sync "how it works" for same/cross-group
- platformio.ini: bump APP_DATE to 20260504
@ewowi
Copy link
Copy Markdown
Collaborator Author

ewowi commented May 4, 2026

@coderabbitai, I am about to merge this PR. Please produce three outputs:

  1. PR review — in-depth review of all commits: a concise summary of what changed
    and why, a merge recommendation, and a prioritised list of follow-up actions.
    For the most urgent items (blockers or high-risk changes), include a ready-to-paste
    prompt that a Claude Code agent can execute immediately before merge.

  2. End-user docs prompt — a ready-to-paste prompt for AI agents to update
    /docs. Rules: only describe usage implications (what changed for the user);
    no internals, no code, no architecture; check existing pages before adding —
    update in place rather than duplicating; keep additions compact and user-friendly.

  3. Developer docs prompt — a ready-to-paste prompt for AI agents to update
    /docs/develop. Rules: target contributors, not end users; be concise — if the
    detail is already in the code or commit messages, do not repeat it; focus on
    decisions, patterns, and guidance that are not obvious from reading the source.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Tip

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

@ewowi: Here are your three requested outputs.


1 · PR Review

Summary of changes

Area What changed Why
Protocol Split UDP traffic onto two ports: 65506 (shared discovery, WLED-compatible) and 65507 (MoonLight control only) Prevent MoonLight control packets from confusing WLED devices and vice-versa
Packet layout New packed structs — UDPWLEDHeader (44 B), UDPMessage (101 B), UDPControlMessage (80 B) Enables WLED coexistence and exact-size dispatch
Receive loop Dispatches by exact packet size; port 65506 accepts 44 B (WLED) or 101 B (MoonLight); port 65507 accepts 80 B only Eliminates ambiguity without a type field
Group sync Hyphen-boundary hostname prefix matching; originId = "group" suppresses re-broadcast loops Tighter than the old prefix match (e.g. "kitchen" no longer matches "kitchenette")
Device table Split into updateDevices / updateDevicesWLED + shared finaliseDeviceUpdate Cleaner separation of WLED vs MoonLight state
Build -mtext-section-literals added for esp32-d0-wrover; APP_DATE bumped to 20260504 Fixes literal-pool out-of-range crash caused by livescripts
Docs New docs/develop/devices.md; docs/moonbase/devices.md rewritten; docs/develop/network.md trimmed Keeps user and developer docs in sync with protocol

Merge recommendation

Recommend merge — the segregation design is sound and the documentation is comprehensive. One blocker-level risk to verify before merging:

Breaking protocol change: Any node running the old firmware (previous UDPMessage size) will silently drop packets from nodes running the new firmware because the receive loop rejects unexpected sizes. Mixed-version networks will lose discovery/sync until all nodes are updated.

This is acceptable if you are doing a coordinated OTA update across the fleet. If partial rollout is expected, consider keeping the old size as a fallback branch.

Prioritised follow-up actions

# Priority Item
1 🔴 High Verify __attribute__((packed)) is applied to all three structs (UDPWLEDHeader, UDPMessage, UDPControlMessage) — misalignment on non-x86 targets causes silent parse failures
2 🔴 High Add a static assert (static_assert(sizeof(UDPMessage) == 101, ...)) and matching asserts for the other two structs — catches ABI drift at compile time
3 🟠 Medium Confirm dual-socket bind retry logic handles the case where the OS has not yet released port 65507 after a crash/reset
4 🟠 Medium originId = "group" loop prevention: confirm this sentinel is never a valid real device name (could be enforced by hostname validation)
5 🟡 Low partOfGroup hyphen-boundary fix — add a unit/integration test to prevent regression
6 🟡 Low WLED discovery packets show brightness/palette/preset as 0 — UI warning in the device table is documented but consider a tooltip or greyed-out style

Ready-to-paste Claude Code prompt (blockers 1 & 2)

You are reviewing src/MoonBase/Modules/ModuleDevices.h in the MoonModules/MoonLight repository.

Tasks:
1. Confirm that UDPWLEDHeader, UDPMessage, and UDPControlMessage are all declared with
   __attribute__((packed)) (or the equivalent pragma pack). If any struct is missing it,
   add the attribute.

2. Immediately after each struct definition, add a static_assert verifying the exact
   expected byte size:
     static_assert(sizeof(UDPWLEDHeader)    == 44,  "UDPWLEDHeader size mismatch");
     static_assert(sizeof(UDPMessage)       == 101, "UDPMessage size mismatch");
     static_assert(sizeof(UDPControlMessage)== 80,  "UDPControlMessage size mismatch");

3. If the actual compile-time sizes differ from the expected values above, report the
   discrepancy and DO NOT silently change the expected values — flag them for human review.

4. Search for any other location in the file where packet sizes are hardcoded as integer
   literals (e.g. 44, 101, 80) and replace them with sizeof() expressions or named
   constants to avoid future drift.

Do not change any logic. Produce a unified diff.

2 · End-user docs prompt

You are updating the MoonLight end-user documentation in /docs (excluding /docs/develop).

Context: PR `#172` introduced UDP port segregation between MoonLight and WLED devices.

Instructions:
- Read docs/moonbase/devices.md before making any changes.
- Do NOT duplicate content; update existing sections in place.
- Do NOT mention UDP ports, packet sizes, structs, or any internal architecture.
- Keep additions compact and user-friendly.

Changes to reflect (user-visible only):
1. WLED devices discovered on the network will appear in the Device table, but their
   Brightness, Palette, and Preset columns will show 0 because WLED discovery packets
   do not include those values. Ensure this limitation is clearly surfaced (a warning
   or note near the WLED coexistence section).

2. Group sync now uses stricter hostname matching: a device is in a group only if its
   hostname matches the group prefix up to a hyphen boundary. Example: "living-room-1"
   and "living-room-2" are in the same group; "living" and "livingroom-1" are NOT.
   Update the Group sync section if this rule is not already stated precisely.

3. The Device table's "Last Sync" column shows the time since the last received update
   from that device. Confirm this is documented and add it if missing.

4. Check all other pages under /docs for any references to device sync, group sync, or
   WLED coexistence and update them to be consistent with the above.

After each change state which file and section was updated and why.

3 · Developer docs prompt

You are updating the MoonLight contributor/developer documentation in /docs/develop.

Context: PR `#172` refactored ModuleDevices UDP networking (port segregation, new packet
structs, group-sync loop prevention). The primary reference page is docs/develop/devices.md
which was added in this PR.

Instructions:
- Read docs/develop/devices.md and docs/develop/network.md before making any changes.
- Do NOT repeat information already present in devices.md.
- Do NOT describe end-user workflows; focus on decisions, constraints, and pitfalls for
  contributors.
- Be concise — link to existing pages rather than duplicating them.

Add or update the following (only if not already covered):

1. **Struct size contract**: Explain that the receive loop dispatches on exact
   sizeof() values and that changing any field in UDPWLEDHeader, UDPMessage, or
   UDPControlMessage is a breaking protocol change. Document the current sizes
   (44 / 101 / 80 bytes) and the static_assert guard pattern that must accompany
   any struct change.

2. **Mixed-version network behaviour**: Document that nodes running different firmware
   versions will silently ignore each other's packets (no error, no fallback). State
   that coordinated OTA is required when struct sizes change.

3. **originId loop-prevention sentinel**: Document that sending a group broadcast sets
   originId to the string "group", and that receivers skip re-broadcast when they see
   this value. Note that "group" must never be a valid hostname prefix.

4. **partOfGroup contract**: Document the hyphen-boundary rule and the regression risk
   if the comparison logic changes. Suggest a unit test covering edge cases
   (e.g. "kitchen" vs "kitchenette", "a-b" vs "a-bc").

5. **Socket lifecycle**: If not already in devices.md, note the retry-bind pattern for
   port 65507 and the deviceControlUDPConnected guard that prevents sends before the
   socket is ready.

After each change state which file and section was updated and why.

@ewowi ewowi merged commit 5175b6f into main May 4, 2026
39 checks passed
@ewowi ewowi deleted the udp-port-seggregation branch May 4, 2026 21:29
@coderabbitai coderabbitai Bot mentioned this pull request May 5, 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.

1 participant