diff --git a/wled00/data/common.js b/wled00/data/common.js index 5a98b4fe1f..6e72428d56 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -116,3 +116,62 @@ 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; } + 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; +} diff --git a/wled00/data/liveview.htm b/wled00/data/liveview.htm index 8c10ba9624..6f54e06c4d 100644 --- a/wled00/data/liveview.htm +++ b/wled00/data/liveview.htm @@ -17,8 +17,8 @@ position: absolute; } + @@ -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 diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 4d7c7b666c..4309bc9ffd 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -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); diff --git a/wled00/ws.cpp b/wled00/ws.cpp index 3a97459fee..6a02247203 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -5,6 +5,12 @@ */ #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; unsigned long wsLastLiveTime = 0; //uint8_t* wsFrameBuffer = nullptr; @@ -25,7 +31,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp // data packet AwsFrameInfo * info = (AwsFrameInfo*)arg; 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 (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 //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 { + 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 //if(info->index == 0){ //if (!wsFrameBuffer && len < 4096) wsFrameBuffer = new uint8_t[4096];