Adding DDP over WS, moving duplicate WS-connection to common.js (#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
This commit is contained in:
@@ -116,3 +116,62 @@ function uploadFile(fileObj, name) {
|
|||||||
fileObj.value = '';
|
fileObj.value = '';
|
||||||
return false;
|
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; }
|
||||||
|
try { top.window.ws = ws; } catch (e) {} // store in parent for reuse
|
||||||
|
return ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send LED colors to ESP using WebSocket and DDP protocol (RGB)
|
||||||
|
// 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) {
|
||||||
|
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
|
||||||
|
//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;
|
||||||
|
// 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; // DDP pixel offset in bytes
|
||||||
|
let dLen = cnt * 3;
|
||||||
|
let cOff = i * 3; // offset in color buffer
|
||||||
|
let 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 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[10] = dLen & 255;
|
||||||
|
pkt.set(colors.subarray(cOff, cOff + dLen), 11);
|
||||||
|
if(i + cnt >= len) {
|
||||||
|
pkt[1] = 0x41; //if this is last packet, set the "push" flag to render the frame
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ws.send(pkt.buffer);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var d = document;
|
|
||||||
var ws;
|
var ws;
|
||||||
var tmout = null;
|
var tmout = null;
|
||||||
var c;
|
var c;
|
||||||
@@ -62,32 +62,14 @@
|
|||||||
if (window.location.href.indexOf("?ws") == -1) {update(); return;}
|
if (window.location.href.indexOf("?ws") == -1) {update(); return;}
|
||||||
|
|
||||||
// Initialize WebSocket connection
|
// Initialize WebSocket connection
|
||||||
try {
|
ws = connectWs(function () {
|
||||||
ws = top.window.ws;
|
//console.info("Peek WS open");
|
||||||
} catch (e) {}
|
ws.send('{"lv":true}');
|
||||||
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.addEventListener('message', (e) => {
|
ws.addEventListener('message', (e) => {
|
||||||
try {
|
try {
|
||||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
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'
|
if (leds[0] != 76) return; //'L'
|
||||||
// leds[1] = 1: 1D; leds[1] = 2: 1D/2D (leds[2]=w, leds[3]=h)
|
// 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]})`);
|
draw(leds[1]==2 ? 4 : 2, 3, leds, (a,i) => `rgb(${a[i]},${a[i+1]},${a[i+2]})`);
|
||||||
@@ -102,4 +84,4 @@
|
|||||||
<body onload="S()">
|
<body onload="S()">
|
||||||
<canvas id="canv"></canvas>
|
<canvas id="canv"></canvas>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="common.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="canv"></canvas>
|
<canvas id="canv"></canvas>
|
||||||
@@ -26,30 +27,13 @@
|
|||||||
var ctx = c.getContext('2d');
|
var ctx = c.getContext('2d');
|
||||||
if (ctx) { // Access the rendering context
|
if (ctx) { // Access the rendering context
|
||||||
// use parent WS or open new
|
// use parent WS or open new
|
||||||
var ws;
|
var ws = connectWs(()=>{
|
||||||
try {
|
ws.send('{"lv":true}');
|
||||||
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";
|
|
||||||
ws.addEventListener('message',(e)=>{
|
ws.addEventListener('message',(e)=>{
|
||||||
try {
|
try {
|
||||||
if (toString.call(e.data) === '[object ArrayBuffer]') {
|
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
|
if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp
|
||||||
let mW = leds[2]; // matrix width
|
let mW = leds[2]; // matrix width
|
||||||
let mH = leds[3]; // matrix height
|
let mH = leds[3]; // matrix height
|
||||||
|
|||||||
@@ -30,11 +30,19 @@ void handleDDPPacket(e131_packet_t* p) {
|
|||||||
|
|
||||||
uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed;
|
uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed;
|
||||||
start += DMXAddress / 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;
|
uint8_t* data = p->data;
|
||||||
unsigned c = 0;
|
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
|
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
|
if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet
|
||||||
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP);
|
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
#ifdef WLED_ENABLE_WEBSOCKETS
|
#ifdef WLED_ENABLE_WEBSOCKETS
|
||||||
|
|
||||||
|
// define some constants for binary protocols, dont use defines but C++ style constexpr
|
||||||
|
constexpr uint8_t BINARY_PROTOCOL_GENERIC = 0xFF; // generic / auto detect NOT IMPLEMENTED
|
||||||
|
constexpr uint8_t BINARY_PROTOCOL_E131 = P_E131; // = 0, untested!
|
||||||
|
constexpr uint8_t BINARY_PROTOCOL_ARTNET = P_ARTNET; // = 1, untested!
|
||||||
|
constexpr uint8_t BINARY_PROTOCOL_DDP = P_DDP; // = 2
|
||||||
|
|
||||||
uint16_t wsLiveClientId = 0;
|
uint16_t wsLiveClientId = 0;
|
||||||
unsigned long wsLastLiveTime = 0;
|
unsigned long wsLastLiveTime = 0;
|
||||||
//uint8_t* wsFrameBuffer = nullptr;
|
//uint8_t* wsFrameBuffer = nullptr;
|
||||||
@@ -25,7 +31,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
|
|||||||
// data packet
|
// data packet
|
||||||
AwsFrameInfo * info = (AwsFrameInfo*)arg;
|
AwsFrameInfo * info = (AwsFrameInfo*)arg;
|
||||||
if(info->final && info->index == 0 && info->len == len){
|
if(info->final && info->index == 0 && info->len == len){
|
||||||
// the whole message is in a single frame and we got all of its data (max. 1450 bytes)
|
// the whole message is in a single frame and we got all of its data (max. 1428 bytes / ESP8266: 528 bytes)
|
||||||
if(info->opcode == WS_TEXT)
|
if(info->opcode == WS_TEXT)
|
||||||
{
|
{
|
||||||
if (len > 0 && len < 10 && data[0] == 'p') {
|
if (len > 0 && len < 10 && data[0] == 'p') {
|
||||||
@@ -71,8 +77,29 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
|
|||||||
// force broadcast in 500ms after updating client
|
// force broadcast in 500ms after updating client
|
||||||
//lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this
|
//lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this
|
||||||
}
|
}
|
||||||
|
}else if (info->opcode == WS_BINARY) {
|
||||||
|
// first byte determines protocol. Note: since e131_packet_t is "packed", the compiler handles alignment issues
|
||||||
|
//DEBUG_PRINTF_P(PSTR("WS binary message: len %u, byte0: %u\n"), len, data[0]);
|
||||||
|
int offset = 1; // offset to skip protocol byte
|
||||||
|
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; // 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
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);
|
||||||
//message is comprised of multiple frames or the frame is split into multiple packets
|
//message is comprised of multiple frames or the frame is split into multiple packets
|
||||||
//if(info->index == 0){
|
//if(info->index == 0){
|
||||||
//if (!wsFrameBuffer && len < 4096) wsFrameBuffer = new uint8_t[4096];
|
//if (!wsFrameBuffer && len < 4096) wsFrameBuffer = new uint8_t[4096];
|
||||||
|
|||||||
Reference in New Issue
Block a user