Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions wled00/data/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,52 @@ function uploadFile(fileObj, name) {
fileObj.value = '';
return false;
}
// connect to WebSocket, use parent WS or open new
function connectWs(onOpen) {
try {
if (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {
if (onOpen) onOpen();
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; }
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

// ws: WebSocket object
// start: start pixel index
// len: number of pixels to send
// colors: Uint8Array with RGB values (3*len bytes)
function sendDDP(ws, start, len, colors) {
let maxDDPpx = 480; // must fit into one WebSocket frame of 1450 bytes, DDP header is 10 bytes -> 480 RGB pixels
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
// send in chunks of maxDDPpx
for (let i = 0; i < len; i += maxDDPpx) {
let cnt = Math.min(maxDDPpx, len - i);
let off = (start + i) * 3;
let dLen = cnt * 3;
let cOff = i * 3;
let pkt = new Uint8Array(10 + dLen);
pkt[0] = 0x41; pkt[1] = 0x01; pkt[2] = 0x01; pkt[3] = 0;
pkt[4] = (off >> 24) & 255;
pkt[5] = (off >> 16) & 255;
pkt[6] = (off >> 8) & 255;
pkt[7] = off & 255;
pkt[8] = (dLen >> 8) & 255;
pkt[9] = dLen & 255;
pkt.set(colors.subarray(cOff, cOff + dLen), 10);
try {
ws.send(pkt.buffer);
} catch (e) {
console.error(e);
return false;
}
}
return true;
}
32 changes: 7 additions & 25 deletions wled00/data/liveview.htm
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
position: absolute;
}
</style>
<script src="common.js"></script>
<script>
var d = document;
var ws;
var tmout = null;
var c;
Expand Down Expand Up @@ -62,32 +62,14 @@
if (window.location.href.indexOf("?ws") == -1) {update(); return;}

// Initialize WebSocket connection
try {
ws = top.window.ws;
} catch (e) {}
if (ws && ws.readyState === WebSocket.OPEN) {
//console.info("Peek uses top WS");
ws.send("{'lv':true}");
} else {
//console.info("Peek WS opening");
let l = window.location;
let pathn = l.pathname;
let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/");
let url = l.origin.replace("http","ws");
if (paths.length > 1) {
url += "/" + paths[0];
}
ws = new WebSocket(url+"/ws");
ws.onopen = function () {
//console.info("Peek WS open");
ws.send("{'lv':true}");
}
}
ws.binaryType = "arraybuffer";
ws = connectWs(function () {
//console.info("Peek WS open");
ws.send('{"lv":true}');
});
ws.addEventListener('message', (e) => {
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
let leds = new Uint8Array(event.data);
let leds = new Uint8Array(e.data);
if (leds[0] != 76) return; //'L'
// leds[1] = 1: 1D; leds[1] = 2: 1D/2D (leds[2]=w, leds[3]=h)
draw(leds[1]==2 ? 4 : 2, 3, leds, (a,i) => `rgb(${a[i]},${a[i+1]},${a[i+2]})`);
Expand All @@ -102,4 +84,4 @@
<body onload="S()">
<canvas id="canv"></canvas>
</body>
</html>
</html>
26 changes: 5 additions & 21 deletions wled00/data/liveviewws2D.htm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
margin: 0;
}
</style>
<script src="common.js"></script>
</head>
<body>
<canvas id="canv"></canvas>
Expand All @@ -26,30 +27,13 @@
var ctx = c.getContext('2d');
if (ctx) { // Access the rendering context
// use parent WS or open new
var ws;
try {
ws = top.window.ws;
} catch (e) {}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send("{'lv':true}");
} else {
let l = window.location;
let pathn = l.pathname;
let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/");
let url = l.origin.replace("http","ws");
if (paths.length > 1) {
url += "/" + paths[0];
}
ws = new WebSocket(url+"/ws");
ws.onopen = ()=>{
ws.send("{'lv':true}");
}
}
ws.binaryType = "arraybuffer";
var ws = connectWs(()=>{
ws.send('{"lv":true}');
});
ws.addEventListener('message',(e)=>{
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
let leds = new Uint8Array(event.data);
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
Expand Down
10 changes: 9 additions & 1 deletion wled00/e131.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@ void handleDDPPacket(e131_packet_t* p) {

uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed;
start += DMXAddress / ddpChannelsPerLed;
unsigned stop = start + htons(p->dataLen) / ddpChannelsPerLed;
uint16_t dataLen = htons(p->dataLen);
unsigned stop = start + dataLen / ddpChannelsPerLed;
uint8_t* data = p->data;
unsigned c = 0;
if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later

unsigned numLeds = stop - start; // stop >= start is guaranteed
unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array
if (maxDataIndex > dataLen) {
DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting."));
return;
}

if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP);

Expand Down
9 changes: 9 additions & 0 deletions wled00/ws.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
// force broadcast in 500ms after updating client
//lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this
}
}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

}
} else {
//message is comprised of multiple frames or the frame is split into multiple packets
Expand Down