diff --git a/wled00/data/common.js b/wled00/data/common.js index e6cea4d526..61a6e4bd05 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -200,7 +200,7 @@ function sendDDP(ws, start, len, colors) { 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[2] = 0x00; // upper nibble is reserved, lower nibble is sequence number, if set to 0 no sequence checking is done (if enabled) pkt[3] = 0x0B; // RGB, 8bit per channel pkt[4] = 0x01; // destination id (not used but 0x01 is default output) pkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 6846e5124e..9da9c15009 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -5,7 +5,7 @@ #define MAX_CHANNELS_PER_UNIVERSE 512 // forward declarations -static void handleDDPPacket(e131_packet_t* p); +static void handleDDPPacket(e131_packet_t* p, size_t packetLen); static void handleArtnetPollReply(IPAddress ipAddress); static void prepareArtnetPollReply(ArtPollReply *reply); static void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress); @@ -17,20 +17,31 @@ static void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16 //DDP protocol support, called by handleE131Packet //handles RGB data only -static void handleDDPPacket(e131_packet_t* p) { +static void handleDDPPacket(e131_packet_t* p, size_t packetLen) { static bool ddpSeenPush = false; // have we seen a push yet? int lastPushSeq = e131LastSequenceNumber[0]; + if (packetLen < DDP_HEADER_LEN) return; // too short to safely read any DDP header fields + // reject unsupported color data types (only RGB and RGBW are supported) - uint8_t maskedType = p->dataType & 0x3F; // mask out custom and reserved flags, only type bits are relevant - if (maskedType != DDP_TYPE_RGB24 && maskedType != DDP_TYPE_RGBW32) return; + //uint8_t maskedType = p->dataType & 0x3F; // mask out custom and reserved flags, only type bits are relevant + //if (maskedType != DDP_TYPE_RGB24 && maskedType != DDP_TYPE_RGBW32) return; + + // note: for maximum compatibility we do not reject unknonw or malformed data types but simply default to RGB24 and check there is enough data available in the packet to do so + // also we assume 8bit per channel and currently do not support other bit depths + + // reject control, status and config packets (not implemented) + if (p->destination == DDP_ID_CONTROL || p->destination == DDP_ID_STATUS || p->destination == DDP_ID_CONFIG) return; + + // reject query and response packets (not implemented) + if (p->flags & (DDP_FLAGS_QUERY | DDP_FLAGS_REPLY)) return; - // reject status and config packets (not implemented) - if (p->destination == DDP_ID_STATUS || p->destination == DDP_ID_CONFIG) return; + bool push = p->flags & DDP_FLAGS_PUSH; // push flag means "render now" + if (!push && (p->flags & DDP_FLAGS_STORAGE)) return; // reject "from storage" flag but still let the push flag pass if set along with it - //reject late packets belonging to previous frame (assuming 4 packets max. before push) + //reject late packets belonging to previous frame (assuming 4 packets max. before push, if more are used and packets are very late, they are still accepted) if (e131SkipOutOfSequence && lastPushSeq) { - int sn = p->sequenceNum & 0xF; + int sn = p->sequenceNum & 0xF; // sequence number is 4 bits, 1-15, 0 means unused if (sn) { if (lastPushSeq > 5) { if (sn > (lastPushSeq -5) && sn < lastPushSeq) return; @@ -40,7 +51,8 @@ static void handleDDPPacket(e131_packet_t* p) { } } - unsigned ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel) + unsigned ddpChannelsPerLed = 3; // default to RGB + if ((p->dataType & 0b00111000)>>3 == 0b011) ddpChannelsPerLed = 4; // RGBW data type (see DDP protocol definition) uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed; start += DMXAddress / ddpChannelsPerLed; @@ -50,8 +62,14 @@ static void handleDDPPacket(e131_packet_t* p) { unsigned c = 0; if (p->flags & DDP_FLAGS_TIME) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later + // ensure the received packet is at least as long as the header claims + if (packetLen < DDP_HEADER_LEN + c + dataLen) { + DEBUG_PRINTLN(F("DDP packet incomplete")); + return; + } + unsigned numLeds = stop - start; // stop >= start is guaranteed - unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array + unsigned maxDataIndex = numLeds * ddpChannelsPerLed; // validate bounds before accessing data array if (maxDataIndex > dataLen) { DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting.")); return; @@ -66,7 +84,6 @@ static void handleDDPPacket(e131_packet_t* p) { } } - bool push = p->flags & DDP_FLAGS_PUSH; ddpSeenPush |= push; if (!ddpSeenPush || push) { // if we've never seen a push, or this is one, render display e131NewData = true; @@ -76,7 +93,7 @@ static void handleDDPPacket(e131_packet_t* p) { } //E1.31 and Art-Net protocol support -void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ +void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol, size_t packetLen){ int uni = 0, dmxChannels = 0; uint8_t* e131_data = nullptr; @@ -84,24 +101,33 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (protocol == P_ARTNET) { + if (packetLen < 10) return; // need at least art_opcode (offset 8, 2 bytes) if (p->art_opcode == ARTNET_OPCODE_OPPOLL) { handleArtnetPollReply(clientIP); return; } + if (packetLen < 18) return; // need art_length (offset 16, 2 bytes) for DMX data uni = p->art_universe; dmxChannels = htons(p->art_length); + const int artNetMaxData = (packetLen >= 18) ? (int)(packetLen - 18) : 0; // art_data at offset 18; clamp so e131_data[dmxChannels] stays in bounds + if (dmxChannels > artNetMaxData) dmxChannels = artNetMaxData; + if (dmxChannels > MAX_CHANNELS_PER_UNIVERSE) dmxChannels = MAX_CHANNELS_PER_UNIVERSE; e131_data = p->art_data; seq = p->art_sequence_number; mde = REALTIME_MODE_ARTNET; } else if (protocol == P_E131) { + if (packetLen < 126) return; // need up to property_values[0] (offset 125) and property_value_count (offset 123) // Ignore PREVIEW data (E1.31: 6.2.6) if ((p->options & 0x80) != 0) return; - dmxChannels = htons(p->property_value_count) - 1; + dmxChannels = htons(p->property_value_count) - 1; // on malformed packets, this can become negative, checked below // DMX level data is zero start code. Ignore everything else. (E1.11: 8.5) - if (dmxChannels == 0 || p->property_values[0] != 0) return; + if (dmxChannels <= 0 || p->property_values[0] != 0) return; uni = htons(p->universe); e131_data = p->property_values; seq = p->sequence_number; + const int e131MaxData = (packetLen > 126) ? (int)(packetLen - 126) : 0; // property_values at offset 125; clamp so e131_data[dmxChannels] stays in bounds + if (dmxChannels > e131MaxData) dmxChannels = e131MaxData; + if (dmxChannels > MAX_CHANNELS_PER_UNIVERSE) dmxChannels = MAX_CHANNELS_PER_UNIVERSE; if (e131Priority != 0) { if (p->priority < e131Priority ) return; // track highest priority & skip all lower priorities @@ -110,15 +136,17 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } } else { //DDP realtimeIP = clientIP; - handleDDPPacket(p); + handleDDPPacket(p, packetLen); return; } #ifdef WLED_ENABLE_DMX // does not act on out-of-order packets yet if (e131ProxyUniverse > 0 && uni == e131ProxyUniverse) { + // Art-Net: art_data is 0-indexed (channel 1 at index 0) + // E1.31: property_values[0] is start code, (channel 1 at index 1) for (uint16_t i = 1; i <= dmxChannels; i++) - dmx.write(i, e131_data[i]); + dmx.write(i, mde == REALTIME_MODE_ARTNET ? e131_data[i-1] : e131_data[i]); dmx.update(); } #endif diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index ffb2c1202f..e86d76c65f 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -100,7 +100,7 @@ void initDMXInput(); void handleDMXInput(); //e131.cpp -void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol); +void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol, size_t packetLen); void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses); // void handleArtnetPollReply(IPAddress ipAddress); // local function, only used in e131.cpp // void prepareArtnetPollReply(ArtPollReply* reply); // local function, only used in e131.cpp diff --git a/wled00/src/dependencies/e131/ESPAsyncE131.cpp b/wled00/src/dependencies/e131/ESPAsyncE131.cpp index 75d6b8dc29..a1c9cad86b 100644 --- a/wled00/src/dependencies/e131/ESPAsyncE131.cpp +++ b/wled00/src/dependencies/e131/ESPAsyncE131.cpp @@ -21,10 +21,10 @@ #include "../network/Network.h" #include -// E1.17 ACN Packet Identifier +// E1.17 ACN Packet Identifier "ASC-E1.17" const byte ESPAsyncE131::ACN_ID[12] = { 0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00 }; -// Art-Net Packet Identifier +// Art-Net Packet Identifier "Art-Net" const byte ESPAsyncE131::ART_ID[8] = { 0x41, 0x72, 0x74, 0x2d, 0x4e, 0x65, 0x74, 0x00 }; // Constructor @@ -99,36 +99,43 @@ bool ESPAsyncE131::initMulticast(uint16_t port, uint16_t universe, uint8_t n) { void ESPAsyncE131::parsePacket(AsyncUDPPacket _packet) { bool error = false; - uint8_t protocol = P_E131; + uint8_t protocol = P_ARTNET; + const size_t pktLen = _packet.length(); e131_packet_t *sbuff = reinterpret_cast(_packet.data()); - - //E1.31 packet identifier ("ACS-E1.17") - if (memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id))) - protocol = P_ARTNET; - - if (protocol == P_ARTNET) { - if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id))) - error = true; //not "Art-Net" - if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL) - error = true; //not a DMX or poll packet - } else { //E1.31 error handling - if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT) - error = true; - if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME) - error = true; - if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP) - error = true; - if (sbuff->property_values[0] != 0) - error = true; - } - + + // E1.31 packet identifier (ACN_ID = "ASC-E1.17"), need at least 16 bytes to safely read acn_id (offset 4, length 12). + if (pktLen >= 16) { + if (!memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id))) + protocol = P_E131; + } + + if (protocol == P_ARTNET) { + if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id))) + error = true; //not ART_ID = "Art-Net" + if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL) + error = true; //not a DMX or poll packet + } else { //E1.31 error handling + if (pktLen < 126) { // need up to property_values[0] at offset 125 + error = true; + } else { + if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT) + error = true; + if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME) + error = true; + if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP) + error = true; + if (sbuff->property_values[0] != 0) + error = true; + } + } + if (error && _packet.localPort() == DDP_DEFAULT_PORT) { //DDP packet error = false; protocol = P_DDP; } if (!error) { - _callback(sbuff, _packet.remoteIP(), protocol); + _callback(sbuff, _packet.remoteIP(), protocol, pktLen); } } \ No newline at end of file diff --git a/wled00/src/dependencies/e131/ESPAsyncE131.h b/wled00/src/dependencies/e131/ESPAsyncE131.h index e0ddccfd67..e61f080de3 100644 --- a/wled00/src/dependencies/e131/ESPAsyncE131.h +++ b/wled00/src/dependencies/e131/ESPAsyncE131.h @@ -55,19 +55,24 @@ typedef struct ip_addr ip4_addr_t; #define DDP_FLAGS_VER 0xc0 // version mask #define DDP_FLAGS_VER1 0x40 // version=1 #define DDP_FLAGS_PUSH 0x01 -#define DDP_FLAGS_QUERY 0x02 -#define DDP_FLAGS_REPLY 0x04 -#define DDP_FLAGS_STORAGE 0x08 +#define DDP_FLAGS_QUERY 0x02 // unsupported - used by XLights for auto-discovery +#define DDP_FLAGS_REPLY 0x04 // unsupported - response packet from another display +#define DDP_FLAGS_STORAGE 0x08 // unsupported - show data from a storage unit instead of from packet data field. Data field defines storage unit (by name, number, URL or whatever mechanism wanted). #define DDP_FLAGS_TIME 0x10 #define DDP_CHANNELS_PER_PACKET 1440 // 480 leds #define DDP_TYPE_RGB24 0x0B // 00 001 011 (RGB , 8 bits per channel, 3 channels) #define DDP_TYPE_RGBW32 0x1B // 00 011 011 (RGBW, 8 bits per channel, 4 channels) +#define DDP_TYPE_LEGACY 0x01 // 00 000 001 legacy RGB 8bit definition +#define DDP_TYPE_UNDEF 0x00 // type and bit depth undefined -#define DDP_ID_DISPLAY 1 -#define DDP_ID_CONFIG 250 -#define DDP_ID_STATUS 251 +// DDP Source or Destination ID (header byte 3) +#define DDP_ID_DISPLAY 1 // default output device +#define DDP_ID_CONTROL 246 // JSON control (not implemented) +#define DDP_ID_CONFIG 250 // JSON config (not implemented) +#define DDP_ID_STATUS 251 // JSON status (not implemented) +#define DDP_ID_ALL 255 // all devices #define ARTNET_OPCODE_OPDMX 0x5000 #define ARTNET_OPCODE_OPPOLL 0x2000 @@ -212,7 +217,7 @@ typedef union { } ArtPollReply; // new packet callback -typedef void (*e131_packet_callback_function) (e131_packet_t* p, IPAddress clientIP, byte protocol); +typedef void (*e131_packet_callback_function) (e131_packet_t* p, IPAddress clientIP, byte protocol, size_t packetLen); class ESPAsyncE131 { private: @@ -267,4 +272,4 @@ class E131Priority { } }; -#endif // ESPASYNCE131_H_ \ No newline at end of file +#endif // ESPASYNCE131_H_ diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 728f19e909..ed8bc6c8f3 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -804,6 +804,7 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const // write the header /*0*/ddpUdp.write(flags); + // TODO: sequence number should be 1-15 as 0 means "unused", it has no bad consequences other than out of sequence packet may be accepted /*1*/ddpUdp.write(sequenceNumber++ & 0x0F); // sequence may be unnecessary unless we are sending twice (as requested in Sync settings) /*2*/ddpUdp.write(isRGBW ? DDP_TYPE_RGBW32 : DDP_TYPE_RGB24); /*3*/ddpUdp.write(DDP_ID_DISPLAY); diff --git a/wled00/ws.cpp b/wled00/ws.cpp index a73bc297ec..6e9038c101 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -87,19 +87,13 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp if (!data || len < offset+1) return; // catch invalid / single-byte payload switch (data[0]) { case BINARY_PROTOCOL_E131: - handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131); + handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131, len - offset); break; case BINARY_PROTOCOL_ARTNET: - handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET); + handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET, len - offset); 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_FLAGS_TIME) ) 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); + handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP, len - offset); } } } else {