Skip to content

Conversation

DedeHai
Copy link
Collaborator

@DedeHai DedeHai commented Oct 10, 2025

  • Enabling DDP over WebSocket: this allows for html pages to stream data to the LEDs much faster than through the JSON API
  • Moved the duplicate function to establish a WS connection from the live-view htm files to common.js

uses only 100bytes of additional flash and is a feature I could make use of in the matrix tool.
Also the new custom palette editor has a "preview" feature that currently uses the JSON API to do a live-preview on the LEDs. Using DDP instead would make it much faster.
I ran some experiments doing video streaming from a ESP-Hosted webpage to the LEDs and this also did work quite well.

Summary by CodeRabbit

  • New Features

    • Live View adds binary WebSocket protocol support and a reusable WebSocket connector for simpler integration.
  • Performance

    • LED data is sent in chunked frames to reduce overhead and improve update latency.
  • Compatibility

    • Unified and more consistent WebSocket initialization and message handling across views.
  • Bug Fixes

    • Stronger packet validation and bounds checks to prevent malformed binary data issues.
  • Refactor

    • Consolidated WebSocket setup into a shared client script for reliability.

- Enabling DDP over WebSocket: this allows for UI or html tools to stream data to the LEDs much faster than through the JSON API.
- Moved the duplicate function to establish a WS connection from the live-view htm files to common.js
Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

Walkthrough

Adds client WebSocket helpers (connectWs, sendDDP) and updates liveview pages to use them; extends server WebSocket handling to accept binary DDP frames (dispatching to E1.31/Art‑Net handlers) and adds bounds checks when parsing E1.31/DDP payloads.

Changes

Cohort / File(s) Summary
Web client WS utilities
wled00/data/common.js
Added connectWs(onOpen) to reuse/create a top-level WebSocket (ensures location globals, handles reverse-proxy/base paths) and sendDDP(ws, start, len, colors) to send RGB data as framed DDP-style packets with chunking, last-frame flag, readiness and error checks.
Liveview pages WS init refactor
wled00/data/liveview.htm, wled00/data/liveviewws2D.htm
Added common.js include and replaced inline WebSocket initialization with connectWs callback usage; removed manual URL construction and explicit binaryType assignments; adjusted message handling to use the event parameter (e.data) while preserving payload processing.
Server WS binary DDP handling
wled00/ws.cpp
Added binary protocol constants and WS_BINARY handling: reads a protocol selector byte, dispatches to E1.31/Art‑Net handlers, parses DDP headers (including optional timecode), validates lengths, and forwards valid DDP payloads to existing handleE131Packet.
E1.31/DDP parsing bounds checks
wled00/e131.cpp
Introduced uint16_t dataLen local variable, replaced direct htons(...) usage, and added bounds-check logic computing numLeds/maxDataIndex to reject packets where parsed indexes exceed the declared data length.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • blazoncek
  • willmmiles

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 pull request title "Adding DDP over WS, moving duplicate WS-connection to common.js" accurately reflects the main changes in the changeset. The title directly addresses the two primary objectives: (1) DDP support over WebSocket is evident in the new sendDDP() function, the binary protocol handling in ws.cpp, and the bounds-checking in e131.cpp, and (2) consolidating duplicate WebSocket connection logic is demonstrated by the new connectWs() function in common.js and its adoption in the two liveview HTML files. The title is specific, concise, and clearly communicates what a reviewer should expect from scanning the changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
Contributor

@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 (2)
wled00/data/liveviewws2D.htm (1)

36-36: Pre-existing bug: Parameter name mismatch in message handler.

The event listener parameter is named e, but line 36 references event.data instead of e.data. While this may work in some browsers due to the global event object, it's not standard and could fail in strict mode or other environments.

Apply this diff to fix the parameter reference:

-            let leds = new Uint8Array(event.data);
+            let leds = new Uint8Array(e.data);

Note: This bug exists in the original code, not introduced by this PR.

wled00/data/liveview.htm (1)

72-72: Pre-existing bug: Parameter name mismatch in message handler.

Same issue as in liveviewws2D.htm: the event listener parameter is e, but the code references event.data instead of e.data.

Apply this diff:

-            let leds = new Uint8Array(event.data);
+            let leds = new Uint8Array(e.data);

Note: This is a pre-existing bug, not introduced by this PR.

🧹 Nitpick comments (2)
wled00/ws.cpp (1)

74-83: Consider adding DDP header validation.

The current implementation validates packet length but does not verify the DDP magic number or version fields. While the length checks prevent buffer overflows, adding validation of bytes 0-3 would improve robustness and help reject malformed packets early.

For example, verify the DDP header magic bytes before forwarding:

 }else if (info->opcode == WS_BINARY) {
   // interpreted binary data using DDP protocol
   if (len < 10) return; // DDP header is 10 bytes
+  // Validate DDP magic number (0x41 = 'A', typically followed by version/flags)
+  if (data[0] != 0x41) return;
   size_t ddpDataLen = (data[8] << 8) | data[9]; // data length in bytes from DDP header
   if (len < (10 + ddpDataLen)) return; // not enough data, prevent out of bounds read
   // could be a valid DDP packet, forward to handler
   IPAddress clientIP = client->remoteIP();
   e131_packet_t *p = (e131_packet_t*)data;
   handleE131Packet(p, clientIP, P_DDP);
 }
wled00/data/common.js (1)

147-169: Consider adding error handling and return value propagation.

The sendDDP function correctly chunks data and constructs DDP headers, but could be more robust:

  1. The function returns true on success but doesn't catch exceptions from ws.send(), which could throw if the WebSocket is closing.
  2. WebSocket send operations can fail silently when the buffer is full.

Consider wrapping the send operation in a try-catch:

     pkt.set(colors.subarray(cOff, cOff + dLen), 10);
 
-    ws.send(pkt.buffer);
+    try {
+      ws.send(pkt.buffer);
+    } catch (err) {
+      console.error("DDP send failed:", err);
+      return false;
+    }
   }
   return true;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f1f986 and 4090af3.

📒 Files selected for processing (4)
  • wled00/data/common.js (1 hunks)
  • wled00/data/liveview.htm (3 hunks)
  • wled00/data/liveviewws2D.htm (2 hunks)
  • wled00/ws.cpp (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
wled00/data/**/*.{htm,html,css,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use tabs for indentation in web files (.htm/.html/.css/.js) under wled00/data

Files:

  • wled00/data/common.js
  • wled00/data/liveviewws2D.htm
  • wled00/data/liveview.htm
wled00/data/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

wled00/data/**: When modifying web UI files, run npm run build to regenerate embedded headers before any firmware build
For web UI changes, edit files only under wled00/data (not firmware or generated files)

Files:

  • wled00/data/common.js
  • wled00/data/liveviewws2D.htm
  • wled00/data/liveview.htm
wled00/**/*.cpp

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use 2-space indentation for C++ source files (.cpp)

Files:

  • wled00/ws.cpp
🧬 Code graph analysis (2)
wled00/data/common.js (1)
wled00/data/index.js (13)
  • e (1501-1501)
  • l (223-223)
  • pathn (233-233)
  • paths (234-234)
  • url (1415-1415)
  • i (632-632)
  • i (766-766)
  • i (941-941)
  • i (1473-1473)
  • i (2212-2212)
  • i (2219-2219)
  • i (2236-2236)
  • i (2819-2819)
wled00/ws.cpp (1)
wled00/e131.cpp (2)
  • handleE131Packet (57-120)
  • handleE131Packet (57-57)
⏰ 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). (11)
  • GitHub Check: wled_build / Build Environments (lolin_s2_mini)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp32c3dev)
  • GitHub Check: wled_build / Build Environments (esp32_wrover)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_160)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_16MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32dev)
  • GitHub Check: wled_build / Build Environments (esp32_eth)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_160)
🔇 Additional comments (4)
wled00/data/common.js (2)

119-140: LGTM! WebSocket connection helper is well-designed.

The connectWs function properly handles:

  • Reusing existing parent WebSocket connections
  • Reverse proxy path detection (consistent with existing code in index.js)
  • Binary frame support via arraybuffer type
  • Optional initialization callbacks

153-153: DDP byte offset calculation is correct

The JS off = (start + i) * 3 sends the byte offset (pixel index × 3 bytes), and the server’s htonl(p->channelOffset)/ddpChannelsPerLed conversion aligns with the DDP header interpretation. No changes required.

wled00/data/liveviewws2D.htm (1)

30-32: LGTM! Clean integration with connectWs.

The refactored WebSocket initialization properly uses the new connectWs helper from common.js and sends the liveview message immediately upon connection.

wled00/data/liveview.htm (1)

65-68: LGTM! Consistent refactoring to use connectWs.

The WebSocket initialization has been properly refactored to use the new connectWs helper, making the code more maintainable and eliminating duplication.

Copy link
Collaborator

@blazoncek blazoncek left a comment

Choose a reason for hiding this comment

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

I hope it will be useful.

wled00/ws.cpp Outdated
size_t ddpDataLen = (data[8] << 8) | data[9]; // data length in bytes from DDP header
if (len < (10 + ddpDataLen)) return; // not enough data, prevent out of bounds read
// could be a valid DDP packet, forward to handler
IPAddress clientIP = client->remoteIP();
Copy link
Collaborator

Choose a reason for hiding this comment

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

realtimeIP = client->remoteIP();

wled00/ws.cpp Outdated
// could be a valid DDP packet, forward to handler
IPAddress clientIP = client->remoteIP();
e131_packet_t *p = (e131_packet_t*)data;
handleE131Packet(p, clientIP, P_DDP);
Copy link
Collaborator

Choose a reason for hiding this comment

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

handleDDPPacket((e131_packet_t*)data);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I did that at first, then wanted to support the other protocols and realized they cannot be destinguished reliably just by the data and additional variables over JSON would be needed. Since I only have use for DDP I dropped the others but kept the function call to set the IP, but setting the IP directly and calling handleDDPPacket makes a lot more sense. Thanks for the review.

Copy link
Contributor

@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: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4090af3 and 7cf935c.

📒 Files selected for processing (4)
  • wled00/data/common.js (1 hunks)
  • wled00/data/liveview.htm (3 hunks)
  • wled00/e131.cpp (1 hunks)
  • wled00/ws.cpp (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • wled00/data/common.js
  • wled00/ws.cpp
🧰 Additional context used
📓 Path-based instructions (3)
wled00/**/*.cpp

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use 2-space indentation for C++ source files (.cpp)

Files:

  • wled00/e131.cpp
wled00/data/**/*.{htm,html,css,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use tabs for indentation in web files (.htm/.html/.css/.js) under wled00/data

Files:

  • wled00/data/liveview.htm
wled00/data/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

wled00/data/**: When modifying web UI files, run npm run build to regenerate embedded headers before any firmware build
For web UI changes, edit files only under wled00/data (not firmware or generated files)

Files:

  • wled00/data/liveview.htm
🧠 Learnings (2)
📚 Learning: 2025-10-10T16:33:28.649Z
Learnt from: DedeHai
PR: wled/WLED#4997
File: wled00/ws.cpp:74-83
Timestamp: 2025-10-10T16:33:28.649Z
Learning: In WLED's DDP packet handling (ws.cpp and e131.cpp), malformed packets must be rejected with comprehensive validation: check timecode flag requires ≥14 bytes total, enforce DDP spec limit of 1440 bytes max data length, and validate that loop iterations won't exceed available data by calculating max index (c + (numLeds-1)*channelsPerLed + channelsPerLed - 1) before accessing the data array.

Applied to files:

  • wled00/e131.cpp
📚 Learning: 2025-08-21T00:00:17.196Z
Learnt from: willmmiles
PR: wled/WLED#4859
File: usermods/udp_name_sync/udp_name_sync.cpp:62-70
Timestamp: 2025-08-21T00:00:17.196Z
Learning: In WLED codebase, UDP packet processing code must never rely on null termination being present on the wire and must always validate packet size first. The codebase already has some vulnerabilities where malformed data can crash the software, so avoiding introduction of new ones is a priority.

Applied to files:

  • wled00/e131.cpp
⏰ 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: wled_build / Build Environments (usermods)
🔇 Additional comments (3)
wled00/data/liveview.htm (3)

72-72: LGTM! Correct usage of ArrayBuffer.

The conversion from e.data to Uint8Array is correct, assuming the binaryType verification passes (see previous comment).


69-80: No action required: binaryType is set
common.js sets ws.binaryType = "arraybuffer", so the ArrayBuffer type check in liveview.htm will work as expected.


20-20: common.js verification complete

common.js is present at wled00/data/common.js, exports the connectWs function, and correctly sets ws.binaryType = "arraybuffer". No further action needed.

Copy link
Contributor

@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

🧹 Nitpick comments (4)
wled00/data/liveviewws2D.htm (2)

30-32: Avoid capturing ws in onOpen; prefer passing socket and JSON.stringify.

If connectWs invokes the callback synchronously, ws may still be undefined. Also prefer JSON.stringify to avoid JSON syntax issues.

Consider this, if connectWs can pass the socket:

- var ws = connectWs(()=>{
-   ws.send('{"lv":true}');
- });
+ var ws = connectWs((socket)=>{
+   socket.send(JSON.stringify({ lv: true }));
+ });

If connectWs does not pass the socket, please confirm the callback is always deferred until after ws is assigned (onopen). Based on learnings.


36-36: Ensure binaryType='arraybuffer' and avoid shadowing.

Require connectWs to set ws.binaryType = 'arraybuffer' so e.data is an ArrayBuffer. Also avoid shadowing the outer leds variable for clarity.

-            let leds = new Uint8Array(e.data);
-            if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp
-            let mW = leds[2]; // matrix width
-            let mH = leds[3]; // matrix height
+            const buf = new Uint8Array(e.data);
+            if (buf[0] != 76 || buf[1] != 2 || !ctx) return; //'L', set in ws.cpp
+            let mW = buf[2]; // matrix width
+            let mH = buf[3]; // matrix height
             let pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)
             let lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)
             var i = 4;
             for (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {
-              ctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;
+              ctx.fillStyle = `rgb(${buf[i]},${buf[i+1]},${buf[i+2]})`;
               ctx.beginPath();
               ctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);
               ctx.fill();
               i+=3;
             }
wled00/data/liveview.htm (2)

20-20: Indent with tabs in web UI files.

The new line is indented with spaces. Please switch to tabs for files under wled00/data.

As per coding guidelines.


65-68: Open-callback pattern + JSON formatting

Good: JSON now uses double quotes. To avoid any race with ws assignment and ensure correctness, prefer passing the socket and using JSON.stringify.

-  ws = connectWs(function () {
-    //console.info("Peek WS open");
-    ws.send('{"lv":true}');
-  });
+  ws = connectWs(function (socket) {
+    //console.info("Peek WS open");
+    socket.send(JSON.stringify({ lv: true }));
+  });

If connectWs doesn’t pass the socket, please confirm the callback is guaranteed to run after ws is assigned (onopen).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cf935c and 492d6b2.

📒 Files selected for processing (2)
  • wled00/data/liveview.htm (3 hunks)
  • wled00/data/liveviewws2D.htm (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
wled00/data/**/*.{htm,html,css,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use tabs for indentation in web files (.htm/.html/.css/.js) under wled00/data

Files:

  • wled00/data/liveviewws2D.htm
  • wled00/data/liveview.htm
wled00/data/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

wled00/data/**: When modifying web UI files, run npm run build to regenerate embedded headers before any firmware build
For web UI changes, edit files only under wled00/data (not firmware or generated files)

Files:

  • wled00/data/liveviewws2D.htm
  • wled00/data/liveview.htm
⏰ 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). (2)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (nodemcuv2)
🔇 Additional comments (3)
wled00/data/liveviewws2D.htm (1)

13-13: Good move to centralize WS logic; remember the UI build step.

Including common.js to dedupe WS connection is 👍. Please run npm run build to regenerate embedded headers before firmware build.

As per coding guidelines.

wled00/data/liveview.htm (2)

72-72: Confirm binaryType is set to arraybuffer.

Ensure connectWs sets ws.binaryType = 'arraybuffer'; otherwise e.data may be a Blob and the ArrayBuffer path won’t run.


87-87: No issues.

Closing tag change is fine.

@DedeHai DedeHai requested a review from netmindz October 11, 2025 10:46
Copy link
Member

@netmindz netmindz left a comment

Choose a reason for hiding this comment

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

Good, except for the lack of protocol indication in the websocket message

return ws;
}

// send led colors to ESP using WebSocket and DDP protocol (RGB)
Copy link
Member

Choose a reason for hiding this comment

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

LED

Copy link
Member

Choose a reason for hiding this comment

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

Typo

wled00/ws.cpp Outdated
Comment on lines 74 to 82
}else if (info->opcode == WS_BINARY) {
// interpreted binary data using DDP protocol
if (len < 10) return; // DDP header is 10 bytes
size_t ddpDataLen = (data[8] << 8) | data[9]; // data length in bytes from DDP header
uint8_t flags = data[0];
if ((flags & DDP_TIMECODE_FLAG) ) ddpDataLen += 4; // timecode flag adds 4 bytes to data length
if (len < (10 + ddpDataLen)) return; // not enough data, prevent out of bounds read
// could be a valid DDP packet, forward to handler
handleE131Packet((e131_packet_t*)data, client->remoteIP(), P_DDP);
Copy link
Member

Choose a reason for hiding this comment

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

This is the only bit that makes me twitchy, the assumption that any binary messages are going to be DDP
I would prefer we had a way to indicate what this data is, with DDP being the first (and only) supported type rather than assume that we can "detect" the format and do some magic later when there are more than DDP being send as binary

Copy link
Collaborator Author

@DedeHai DedeHai Oct 17, 2025

Choose a reason for hiding this comment

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

I am no expert in protocol definition. How to solve it?
Detecting protocols by header as it is (crudely and incompletely) implemented is the simplest version. A AR sync packet could be detected that way as well. Adding another "header" or "request" layer on top could do more harm than good: what happens if two clients connect, one sending DDP one sending AR data for example?
edit:
if in the future, custom needs arise for a completely different protocol, it could also use a header OR do a UI config override: if "some special protocol" is used, disable all other protocol support.

if past experience has tought me anything it is not to try to predict the future of data usage ;)

Copy link
Member

Choose a reason for hiding this comment

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

I've just checked the WebSocket protocol docs and you can send a Sec-WebSocket-Protocol http header, which if we used that then you also have a mechanism to know if the server supports what you are trying to send.
That doesn't however resolve the challenge of knowing when you get a binary message, which function to delegate to.
While I take your point about predicting the future is hard, at the same point, writing something in a way that prevents further extension later is bad. I still think it would be worth adding a single byte at the start that indicates what the message type is

Copy link
Member

Choose a reason for hiding this comment

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

i.e the first byte maps to the function, which might itself support multiple messages

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

adding a header byte would work for me and since no applications exist yet that use binary streaming over WS there are no compatibility issues either. should I implement this approach? say something like:
0 = generic (future implementation or auto detect from following bytes?)
1 = DDP
2 - 9 = reserved for other LED streaming protocols
11 = AR packet (not yet supported)

Copy link
Member

Choose a reason for hiding this comment

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

Not quite sure how auto detection would work, but reserving 0 for that sounds a good idea, beyond that do we already have any constant that identifiers DDP?
I would avoid trying to allocate any particular values for future use, the AR example was just the first hypothetical that came to mind, I'm not sure it would be a good idea as latency is more important than guaranteed delivery, so possibly inadvisable, but possibly only option of anyone was to want to try and do something entirely in the browser.
There might be other usermods however that would benefit from high speed TCP rather than hammering the API

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not quite sure how auto detection would work

for DDP or AR packets for example by looking at each packets header and finding unique identifiers, but this was just a thought, no real usecase in mind. I will go ahead and implement a minimal version

Copy link
Member

@netmindz netmindz Oct 18, 2025

Choose a reason for hiding this comment

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

You definitely could do some form of auto detection, my query was more how to ensure proper encapsulation and separation of concerns within the code if we did that.
For now let's just keep to the simple if ID btye indicates DDP, delegate to the relevant function, otherwise error

Copy link
Contributor

@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

🧹 Nitpick comments (1)
wled00/ws.cpp (1)

84-99: Add break statement and default case to switch.

The switch statement has two style issues:

  1. Missing break; after the DDP case (line 99) - while not a bug here, explicit break statements improve code clarity and prevent future errors
  2. Missing default: case - unknown protocol bytes are silently ignored

Apply this diff:

       case BINARY_PROTOCOL_DDP:
         if (len < 10 + offset) return; // DDP header is 10 bytes (+1 protocol byte)
         size_t ddpDataLen = (data[8+offset] << 8) | data[9+offset]; // data length in bytes from DDP header
         uint8_t flags = data[0+offset];
         if ((flags & DDP_TIMECODE_FLAG) ) ddpDataLen += 4; // timecode flag adds 4 bytes to data length
         if (len < (10 + offset + ddpDataLen)) return; // not enough data, prevent out of bounds read
         // could be a valid DDP packet, forward to handler
         handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP);
+        break;
+      default:
+        DEBUG_PRINTF_P(PSTR("WS binary: unsupported protocol %u\n"), data[0]);
+        return;
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8727a8 and 3cc4c8b.

📒 Files selected for processing (2)
  • wled00/data/common.js (1 hunks)
  • wled00/ws.cpp (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • wled00/data/common.js
🧰 Additional context used
📓 Path-based instructions (1)
wled00/**/*.cpp

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use 2-space indentation for C++ source files (.cpp)

Files:

  • wled00/ws.cpp
🧠 Learnings (2)
📚 Learning: 2025-10-10T18:34:06.550Z
Learnt from: DedeHai
PR: wled/WLED#4997
File: wled00/e131.cpp:33-44
Timestamp: 2025-10-10T18:34:06.550Z
Learning: In WLED's DDP packet handling (ws.cpp and e131.cpp), only prevent out-of-bounds memory access rather than enforcing DDP spec compliance. Don't check the 1440-byte spec limit—accept out-of-spec packets assuming correct encoding. The bounds check `maxDataIndex = c + numLeds * ddpChannelsPerLed; if (maxDataIndex > dataLen) reject` is sufficient and already covers the timecode flag case (when c=4) without needing separate validation.

Applied to files:

  • wled00/ws.cpp
📚 Learning: 2025-08-21T00:00:17.196Z
Learnt from: willmmiles
PR: wled/WLED#4859
File: usermods/udp_name_sync/udp_name_sync.cpp:62-70
Timestamp: 2025-08-21T00:00:17.196Z
Learning: In WLED codebase, UDP packet processing code must never rely on null termination being present on the wire and must always validate packet size first. The codebase already has some vulnerabilities where malformed data can crash the software, so avoiding introduction of new ones is a priority.

Applied to files:

  • wled00/ws.cpp
🧬 Code graph analysis (1)
wled00/ws.cpp (1)
wled00/e131.cpp (2)
  • handleE131Packet (65-128)
  • handleE131Packet (65-65)
⏰ 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). (14)
  • GitHub Check: wled_build / Build Environments (esp32_wrover)
  • GitHub Check: wled_build / Build Environments (lolin_s2_mini)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp32_eth)
  • GitHub Check: wled_build / Build Environments (esp32c3dev)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_16MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32dev)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Environments (esp8266_2m)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_160)
  • GitHub Check: wled_build / Build Environments (nodemcuv2)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_160)
🔇 Additional comments (4)
wled00/ws.cpp (4)

34-34: LGTM: Improved documentation of frame size limits.

The updated comment clarifies platform-specific WebSocket frame size limits, which is helpful for understanding the constraints.


91-98: LGTM: DDP bounds validation logic is sound.

The bounds checking correctly prevents out-of-bounds memory access:

  • Line 92 ensures minimum header size (10 bytes + protocol byte)
  • Lines 93-95 compute total data length, accounting for optional timecode (4 bytes)
  • Line 96 validates the full packet length before forwarding to the handler

This aligns with the established approach of preventing out-of-bounds access rather than enforcing DDP spec compliance.

Based on learnings.


102-102: LGTM: Enhanced debug logging for multipart messages.

The additional debug information (final, index, len, total) will be helpful for troubleshooting multipart message handling.


8-12: Validate input bounds for E131/ARTNET handlers; add default case for protocol switch.

The switch statement handles binary protocols at lines 84-99. While the DDP case includes bounds validation (lines 92, 96), the E131 and ARTNET cases directly invoke handlers without checking packet size first. Given these protocols are marked untested and per prior learnings to validate packet size before access, verify that handleE131Packet() performs adequate input validation for all three protocol types.

Additionally, the switch lacks a default case to handle unknown protocol bytes—any unrecognized value silently falls through. Add a default case to log unexpected protocols:

switch (data[0]) {
  case BINARY_PROTOCOL_E131:
    handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131);
    break;
  case BINARY_PROTOCOL_ARTNET:
    handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET);
    break;
  case BINARY_PROTOCOL_DDP:
    if (len < 10 + offset) return;
    size_t ddpDataLen = (data[8+offset] << 8) | data[9+offset];
    uint8_t flags = data[0+offset];
    if ((flags & DDP_TIMECODE_FLAG)) ddpDataLen += 4;
    if (len < (10 + offset + ddpDataLen)) return;
    handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP);
    break;
  default:
    DEBUG_PRINTF_P(PSTR("WS binary: unsupported protocol %u\n"), data[0]);
    return;
}

Copy link
Contributor

@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: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc4c8b and 900babd.

📒 Files selected for processing (1)
  • wled00/data/common.js (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
wled00/data/**/*.{htm,html,css,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use tabs for indentation in web files (.htm/.html/.css/.js) under wled00/data

Files:

  • wled00/data/common.js
wled00/data/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

wled00/data/**: When modifying web UI files, run npm run build to regenerate embedded headers before any firmware build
For web UI changes, edit files only under wled00/data (not firmware or generated files)

Files:

  • wled00/data/common.js
🧬 Code graph analysis (1)
wled00/data/common.js (1)
wled00/data/index.js (10)
  • e (1501-1501)
  • url (1415-1415)
  • i (632-632)
  • i (766-766)
  • i (941-941)
  • i (1473-1473)
  • i (2212-2212)
  • i (2219-2219)
  • i (2236-2236)
  • i (2819-2819)
🔇 Additional comments (1)
wled00/data/common.js (1)

136-175: No issues found—server-side DDP packet handling is already correct.

The verification confirms that ws.cpp and e131.cpp correctly handle the packet structure from sendDDP:

  1. ws.cpp (lines 83-98) correctly skips the protocol byte at offset 0 and casts &data[offset] as the 10-byte DDP header (e131_packet_t*).
  2. ws.cpp line 92 validates minimum packet size: if (len < 10 + offset) return; covers the header.
  3. e131.cpp handleDDPPacket (lines 40-44) performs proper bounds checking: if (maxDataIndex > dataLen) return; and correctly handles the timecode flag offset.

The client sends 11 bytes (1 protocol indicator + 10-byte DDP header) followed by RGB data, and the server-side parsing correctly interprets this structure. No changes are needed.

@netmindz
Copy link
Member

Last two rabbit suggestions look fair, but otherwise I think we are good to merge

Copy link
Contributor

@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

🧹 Nitpick comments (4)
wled00/data/common.js (4)

121-135: Handle CONNECTING reuse and clear stale reference on close.

Avoid spawning a second WS while an existing one is CONNECTING; also clear top.window.ws when that socket closes. Prefer addEventListener over onopen to not clobber handlers.

 function connectWs(onOpen) {
 	try {
-		if (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {
-			if (onOpen) onOpen();
-			return top.window.ws;
-		}
+		if (top.window.ws) {
+			if (top.window.ws.readyState === WebSocket.OPEN) {
+				if (onOpen) onOpen();
+				return top.window.ws;
+			}
+			if (top.window.ws.readyState === WebSocket.CONNECTING) {
+				if (onOpen) top.window.ws.addEventListener('open', onOpen, { once:true });
+				return top.window.ws;
+			}
+		}
 	} catch (e) {}
 
 	getLoc(); // ensure globals (loc, locip, locproto) are up to date
-	let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws";
-	let ws = new WebSocket(url);
-	ws.binaryType = "arraybuffer";
-	if (onOpen) { ws.onopen = onOpen; }
+	const url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws";
+	const ws = new WebSocket(url);
+	ws.binaryType = "arraybuffer";
+	if (onOpen) ws.addEventListener('open', onOpen, { once:true });
+	ws.addEventListener('close', () => { try { if (top.window.ws === ws) top.window.ws = null; } catch(e) {} });
 	try { top.window.ws = ws; } catch (e) {} // store in parent for reuse
 	return ws;
 }

142-147: Validate arguments and enforce Uint8Array input.

Guard against negative/zero lengths and wrong buffer types; keep existing length check.

 function sendDDP(ws, start, len, colors) {
-	if (!colors || colors.length < len * 3) return false; // not enough color data
-	let maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels
+	if (!Number.isInteger(start) || start < 0 || !Number.isInteger(len) || len <= 0) return false;
+	if (!(colors instanceof Uint8Array) || colors.length < len * 3) return false; // not enough color data
+	const maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels
 	//let maxDDPpx = 172; // ESP8266: must fit into one WebSocket frame of 528 bytes -> 172 RGB pixels TODO: add support for ESP8266?
 	if (!ws || ws.readyState !== WebSocket.OPEN) return false;

Based on learnings.


154-165: Typos in comments and small const nit.

Fix minor typos; prefer const where values don’t change.

-		let pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED websocket protocol indicator
+		const pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED WebSocket protocol indicator
 		pkt[0] = 0x02; // DDP protocol indicator for WLED websocket. Note: below DDP protocol bytes are offset by 1
 		pkt[1] = 0x40; // flags: 0x40 = no push, 0x41 = push (i.e. render), note: this is DDP protocol byte 0
 		pkt[2] = 0x00; // reserved
 		pkt[3] = 0x01; // 1 = RGB (currently only supported mode)
 		pkt[4] = 0x01; // destination id (not used but 0x01 is default output)
-		pkt[5] = (off >> 24) & 255; // DDP protocl 4-7 is offset
+		pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset
 		pkt[6] = (off >> 16) & 255;
 		pkt[7] = (off >> 8) & 255;
 		pkt[8] = off & 255;
-		pkt[9] = (dLen >> 8) & 255; // DDP protocol 8-9 is data length
+		pkt[9] = (dLen >> 8) & 255; // DDP protocol 8-9 is data length
 		pkt[10] = dLen & 255;

119-177: Remember to rebuild embedded web assets.

After changing files under wled00/data, run: npm run build before firmware build so headers get regenerated. As per coding guidelines.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 900babd and 5e128cb.

📒 Files selected for processing (1)
  • wled00/data/common.js (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
wled00/data/**/*.{htm,html,css,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use tabs for indentation in web files (.htm/.html/.css/.js) under wled00/data

Files:

  • wled00/data/common.js
wled00/data/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

wled00/data/**: When modifying web UI files, run npm run build to regenerate embedded headers before any firmware build
For web UI changes, edit files only under wled00/data (not firmware or generated files)

Files:

  • wled00/data/common.js
🧠 Learnings (1)
📚 Learning: 2025-10-10T18:34:06.550Z
Learnt from: DedeHai
PR: wled/WLED#4997
File: wled00/e131.cpp:33-44
Timestamp: 2025-10-10T18:34:06.550Z
Learning: In WLED's DDP packet handling (ws.cpp and e131.cpp), only prevent out-of-bounds memory access rather than enforcing DDP spec compliance. Don't check the 1440-byte spec limit—accept out-of-spec packets assuming correct encoding. The bounds check `maxDataIndex = c + numLeds * ddpChannelsPerLed; if (maxDataIndex > dataLen) reject` is sufficient and already covers the timecode flag case (when c=4) without needing separate validation.

Applied to files:

  • wled00/data/common.js
🧬 Code graph analysis (1)
wled00/data/common.js (1)
wled00/data/index.js (10)
  • e (1501-1501)
  • url (1415-1415)
  • i (632-632)
  • i (766-766)
  • i (941-941)
  • i (1473-1473)
  • i (2212-2212)
  • i (2219-2219)
  • i (2236-2236)
  • i (2819-2819)
⏰ 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). (17)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32_wrover)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_16MB_opi)
  • GitHub Check: wled_build / Build Environments (lolin_s2_mini)
  • GitHub Check: wled_build / Build Environments (esp32c3dev)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_160)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (esp32_eth)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_compat)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_160)
  • GitHub Check: wled_build / Build Environments (esp32dev)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_160)
  • GitHub Check: wled_build / Build Environments (esp8266_2m)
  • GitHub Check: wled_build / Build Environments (nodemcuv2)

@DedeHai DedeHai merged commit c4850ae into wled:main Oct 21, 2025
20 checks passed
@DedeHai DedeHai deleted the DDP_over_WS branch October 21, 2025 17:42
Copy link
Contributor

@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

🧹 Nitpick comments (1)
wled00/data/common.js (1)

142-177: Bounds validation properly added; consider optional type-safety improvement.

The previous bounds-checking issue has been correctly addressed at line 143. The function now validates sufficient color data before processing.

The DDP packet construction, chunking logic, and error handling are all correct. The implementation properly sets the push flag on the final packet to trigger rendering.

Optional improvement: The validation at line 143 checks colors.length but doesn't verify that colors has a subarray method. While the function documentation specifies a Uint8Array parameter, adding a simple check like typeof colors?.subarray !== 'function' would provide a clearer error message if the wrong type is passed, rather than throwing at line 165.

Optional type-safety improvement
 function sendDDP(ws, start, len, colors) {
-	if (!colors || colors.length < len * 3) return false; // not enough color data
+	if (!colors || typeof colors.subarray !== 'function' || colors.length < len * 3) return false; // not enough color data
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e128cb and 4183e60.

📒 Files selected for processing (1)
  • wled00/data/common.js (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
wled00/data/**/*.{htm,html,css,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use tabs for indentation in web files (.htm/.html/.css/.js) under wled00/data

Files:

  • wled00/data/common.js
wled00/data/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

wled00/data/**: When modifying web UI files, run npm run build to regenerate embedded headers before any firmware build
For web UI changes, edit files only under wled00/data (not firmware or generated files)

Files:

  • wled00/data/common.js
🧠 Learnings (1)
📚 Learning: 2025-10-10T18:34:06.550Z
Learnt from: DedeHai
PR: wled/WLED#4997
File: wled00/e131.cpp:33-44
Timestamp: 2025-10-10T18:34:06.550Z
Learning: In WLED's DDP packet handling (ws.cpp and e131.cpp), only prevent out-of-bounds memory access rather than enforcing DDP spec compliance. Don't check the 1440-byte spec limit—accept out-of-spec packets assuming correct encoding. The bounds check `maxDataIndex = c + numLeds * ddpChannelsPerLed; if (maxDataIndex > dataLen) reject` is sufficient and already covers the timecode flag case (when c=4) without needing separate validation.

Applied to files:

  • wled00/data/common.js
🧬 Code graph analysis (1)
wled00/data/common.js (1)
wled00/data/index.js (10)
  • e (1501-1501)
  • url (1415-1415)
  • i (632-632)
  • i (766-766)
  • i (941-941)
  • i (1473-1473)
  • i (2212-2212)
  • i (2219-2219)
  • i (2236-2236)
  • i (2819-2819)
🔇 Additional comments (1)
wled00/data/common.js (1)

119-135: LGTM! WebSocket reuse properly implemented.

The previous critical issue has been correctly fixed. The WebSocket is now stored in top.window.ws at line 133, enabling proper connection reuse across subsequent calls. The try-catch blocks appropriately handle potential cross-origin access restrictions.

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.

3 participants