Skip to content

Commit c4850ae

Browse files
authored
Adding DDP over WS, moving duplicate WS-connection to common.js (wled#4997)
- Enabling DDP over WebSocket: this allows for UI or html tools to stream data to the LEDs much faster than through the JSON API. - first byte of data array is used to determine protocol for future use - Moved the duplicate function to establish a WS connection from the live-view htm files to common.js - add better safety check for DDP: prevent OOB reads of buffer
1 parent ca5debe commit c4850ae

File tree

5 files changed

+108
-48
lines changed

5 files changed

+108
-48
lines changed

wled00/data/common.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,62 @@ function uploadFile(fileObj, name) {
116116
fileObj.value = '';
117117
return false;
118118
}
119+
// connect to WebSocket, use parent WS or open new
120+
function connectWs(onOpen) {
121+
try {
122+
if (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {
123+
if (onOpen) onOpen();
124+
return top.window.ws;
125+
}
126+
} catch (e) {}
127+
128+
getLoc(); // ensure globals (loc, locip, locproto) are up to date
129+
let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws";
130+
let ws = new WebSocket(url);
131+
ws.binaryType = "arraybuffer";
132+
if (onOpen) { ws.onopen = onOpen; }
133+
try { top.window.ws = ws; } catch (e) {} // store in parent for reuse
134+
return ws;
135+
}
136+
137+
// send LED colors to ESP using WebSocket and DDP protocol (RGB)
138+
// ws: WebSocket object
139+
// start: start pixel index
140+
// len: number of pixels to send
141+
// colors: Uint8Array with RGB values (3*len bytes)
142+
function sendDDP(ws, start, len, colors) {
143+
if (!colors || colors.length < len * 3) return false; // not enough color data
144+
let maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels
145+
//let maxDDPpx = 172; // ESP8266: must fit into one WebSocket frame of 528 bytes -> 172 RGB pixels TODO: add support for ESP8266?
146+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
147+
// send in chunks of maxDDPpx
148+
for (let i = 0; i < len; i += maxDDPpx) {
149+
let cnt = Math.min(maxDDPpx, len - i);
150+
let off = (start + i) * 3; // DDP pixel offset in bytes
151+
let dLen = cnt * 3;
152+
let cOff = i * 3; // offset in color buffer
153+
let pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED websocket protocol indicator
154+
pkt[0] = 0x02; // DDP protocol indicator for WLED websocket. Note: below DDP protocol bytes are offset by 1
155+
pkt[1] = 0x40; // flags: 0x40 = no push, 0x41 = push (i.e. render), note: this is DDP protocol byte 0
156+
pkt[2] = 0x00; // reserved
157+
pkt[3] = 0x01; // 1 = RGB (currently only supported mode)
158+
pkt[4] = 0x01; // destination id (not used but 0x01 is default output)
159+
pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset
160+
pkt[6] = (off >> 16) & 255;
161+
pkt[7] = (off >> 8) & 255;
162+
pkt[8] = off & 255;
163+
pkt[9] = (dLen >> 8) & 255; // DDP protocol 8-9 is data length
164+
pkt[10] = dLen & 255;
165+
pkt.set(colors.subarray(cOff, cOff + dLen), 11);
166+
if(i + cnt >= len) {
167+
pkt[1] = 0x41; //if this is last packet, set the "push" flag to render the frame
168+
}
169+
try {
170+
ws.send(pkt.buffer);
171+
} catch (e) {
172+
console.error(e);
173+
return false;
174+
}
175+
}
176+
return true;
177+
}

wled00/data/liveview.htm

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
position: absolute;
1818
}
1919
</style>
20+
<script src="common.js"></script>
2021
<script>
21-
var d = document;
2222
var ws;
2323
var tmout = null;
2424
var c;
@@ -62,32 +62,14 @@
6262
if (window.location.href.indexOf("?ws") == -1) {update(); return;}
6363

6464
// Initialize WebSocket connection
65-
try {
66-
ws = top.window.ws;
67-
} catch (e) {}
68-
if (ws && ws.readyState === WebSocket.OPEN) {
69-
//console.info("Peek uses top WS");
70-
ws.send("{'lv':true}");
71-
} else {
72-
//console.info("Peek WS opening");
73-
let l = window.location;
74-
let pathn = l.pathname;
75-
let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/");
76-
let url = l.origin.replace("http","ws");
77-
if (paths.length > 1) {
78-
url += "/" + paths[0];
79-
}
80-
ws = new WebSocket(url+"/ws");
81-
ws.onopen = function () {
82-
//console.info("Peek WS open");
83-
ws.send("{'lv':true}");
84-
}
85-
}
86-
ws.binaryType = "arraybuffer";
65+
ws = connectWs(function () {
66+
//console.info("Peek WS open");
67+
ws.send('{"lv":true}');
68+
});
8769
ws.addEventListener('message', (e) => {
8870
try {
8971
if (toString.call(e.data) === '[object ArrayBuffer]') {
90-
let leds = new Uint8Array(event.data);
72+
let leds = new Uint8Array(e.data);
9173
if (leds[0] != 76) return; //'L'
9274
// leds[1] = 1: 1D; leds[1] = 2: 1D/2D (leds[2]=w, leds[3]=h)
9375
draw(leds[1]==2 ? 4 : 2, 3, leds, (a,i) => `rgb(${a[i]},${a[i+1]},${a[i+2]})`);
@@ -102,4 +84,4 @@
10284
<body onload="S()">
10385
<canvas id="canv"></canvas>
10486
</body>
105-
</html>
87+
</html>

wled00/data/liveviewws2D.htm

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
margin: 0;
1111
}
1212
</style>
13+
<script src="common.js"></script>
1314
</head>
1415
<body>
1516
<canvas id="canv"></canvas>
@@ -26,30 +27,13 @@
2627
var ctx = c.getContext('2d');
2728
if (ctx) { // Access the rendering context
2829
// use parent WS or open new
29-
var ws;
30-
try {
31-
ws = top.window.ws;
32-
} catch (e) {}
33-
if (ws && ws.readyState === WebSocket.OPEN) {
34-
ws.send("{'lv':true}");
35-
} else {
36-
let l = window.location;
37-
let pathn = l.pathname;
38-
let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/");
39-
let url = l.origin.replace("http","ws");
40-
if (paths.length > 1) {
41-
url += "/" + paths[0];
42-
}
43-
ws = new WebSocket(url+"/ws");
44-
ws.onopen = ()=>{
45-
ws.send("{'lv':true}");
46-
}
47-
}
48-
ws.binaryType = "arraybuffer";
30+
var ws = connectWs(()=>{
31+
ws.send('{"lv":true}');
32+
});
4933
ws.addEventListener('message',(e)=>{
5034
try {
5135
if (toString.call(e.data) === '[object ArrayBuffer]') {
52-
let leds = new Uint8Array(event.data);
36+
let leds = new Uint8Array(e.data);
5337
if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp
5438
let mW = leds[2]; // matrix width
5539
let mH = leds[3]; // matrix height

wled00/e131.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,19 @@ void handleDDPPacket(e131_packet_t* p) {
3030

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

39+
unsigned numLeds = stop - start; // stop >= start is guaranteed
40+
unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array
41+
if (maxDataIndex > dataLen) {
42+
DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting."));
43+
return;
44+
}
45+
3846
if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet
3947
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP);
4048

wled00/ws.cpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
*/
66
#ifdef WLED_ENABLE_WEBSOCKETS
77

8+
// define some constants for binary protocols, dont use defines but C++ style constexpr
9+
constexpr uint8_t BINARY_PROTOCOL_GENERIC = 0xFF; // generic / auto detect NOT IMPLEMENTED
10+
constexpr uint8_t BINARY_PROTOCOL_E131 = P_E131; // = 0, untested!
11+
constexpr uint8_t BINARY_PROTOCOL_ARTNET = P_ARTNET; // = 1, untested!
12+
constexpr uint8_t BINARY_PROTOCOL_DDP = P_DDP; // = 2
13+
814
uint16_t wsLiveClientId = 0;
915
unsigned long wsLastLiveTime = 0;
1016
//uint8_t* wsFrameBuffer = nullptr;
@@ -25,7 +31,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
2531
// data packet
2632
AwsFrameInfo * info = (AwsFrameInfo*)arg;
2733
if(info->final && info->index == 0 && info->len == len){
28-
// the whole message is in a single frame and we got all of its data (max. 1450 bytes)
34+
// the whole message is in a single frame and we got all of its data (max. 1428 bytes / ESP8266: 528 bytes)
2935
if(info->opcode == WS_TEXT)
3036
{
3137
if (len > 0 && len < 10 && data[0] == 'p') {
@@ -71,8 +77,29 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
7177
// force broadcast in 500ms after updating client
7278
//lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this
7379
}
80+
}else if (info->opcode == WS_BINARY) {
81+
// first byte determines protocol. Note: since e131_packet_t is "packed", the compiler handles alignment issues
82+
//DEBUG_PRINTF_P(PSTR("WS binary message: len %u, byte0: %u\n"), len, data[0]);
83+
int offset = 1; // offset to skip protocol byte
84+
switch (data[0]) {
85+
case BINARY_PROTOCOL_E131:
86+
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131);
87+
break;
88+
case BINARY_PROTOCOL_ARTNET:
89+
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET);
90+
break;
91+
case BINARY_PROTOCOL_DDP:
92+
if (len < 10 + offset) return; // DDP header is 10 bytes (+1 protocol byte)
93+
size_t ddpDataLen = (data[8+offset] << 8) | data[9+offset]; // data length in bytes from DDP header
94+
uint8_t flags = data[0+offset];
95+
if ((flags & DDP_TIMECODE_FLAG) ) ddpDataLen += 4; // timecode flag adds 4 bytes to data length
96+
if (len < (10 + offset + ddpDataLen)) return; // not enough data, prevent out of bounds read
97+
// could be a valid DDP packet, forward to handler
98+
handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP);
99+
}
74100
}
75101
} else {
102+
DEBUG_PRINTF_P(PSTR("WS multipart message: final %u index %u len %u total %u\n"), info->final, info->index, len, (uint32_t)info->len);
76103
//message is comprised of multiple frames or the frame is split into multiple packets
77104
//if(info->index == 0){
78105
//if (!wsFrameBuffer && len < 4096) wsFrameBuffer = new uint8_t[4096];

0 commit comments

Comments
 (0)