-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
less restrictive DDP header acceptance, add safety checks to all protocols #5547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3be525c
c9c337a
5fe9806
16c59d4
017c583
2749883
8255f94
e17983e
446b2cd
dedafc4
8a5e7e4
e5463fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai please check if WLED handling of sequence numbers is compatible with DDP protocol specs. Accepting an "outdated" packet may be acceptable, but a wrong rejection should be avoided, even when more than 15 packets are needed to transmit a full "frame". Also check against the real implementation of known DDP senders like LedFx, hyperion, xlights, OpenRGB, FPP.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me research the DDP protocol spec and how known senders handle sequence numbers.
DDP Sequence Number SpecThe DDP spec (byte 1) defines Key point: the sequence number is per-packet (not per-frame), and wraps in the ring 1→2→…→15→1. WLED's Rejection LogicWLED's current code (after this PR): int sn = p->sequenceNum & 0xF; // 0 = disabled
if (sn) {
if (lastPushSeq > 5) {
if (sn > (lastPushSeq - 5) && sn < lastPushSeq) return; // reject window: 5 slots before last push
} else {
if (sn > (10 + lastPushSeq) || sn < lastPushSeq) return;
}
}
Where This Breaks DownThe 5-slot window hardcodes an assumption of ≤5 packets per frame. Consider a large installation with, e.g., 5760 LEDs (12 packets of 480 RGB pixels @ 1440 bytes each, a common xLights packet size):
This means for any installation needing roughly >10 packets per frame (with sequence numbers enabled), current-frame packets will be wrongly rejected. OpenRGB's DDP implementation uses sequence numbers from 1–15 per spec, and xLights defaults to 1440 bytes per DDP packet, which is 480 RGB pixels — meaning only ~7–8 packets are needed before wrapping issues arise for mid-size installs. Known Sender Behaviors
Senders that set Summary of Issues
Recommendations
🧠 Learnings used
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @softhack007 I looked at this code in detail, it is not perfect but rejecting "late packets" seems reasonable. if large setups are needed, just disable this option in the sync settings (it is disabled by default), it is not possible to have both, late packet rejection AND a large setup as the sequence number for DDP is limited to 1-15 and to check for late packets, a max window of 7 would be possible.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @DedeHai @coderabbitai I was actually wondering if we could make improvements for #5412 while working on the code. The DDP spec reads like they wanted to support packet re-transmit within one "push frame", so the sequence counter could be used to identify duplicates (in a history window of 15 packets max). I think dups (same frame) are not a problem in WLED, because they simply cause a re-painting of some pixels. You're right that rejecting late packets - from the previous "push frame" - might be useful in some setups - especially with high latency / unstable WiFi and with lower pixel counts. My fear is that users will simply enable "Skip out-of-sequence packets" (which makes sense for other protocols like Art-Net), and later complain that a bigger setup always misses pixel updates when sequence checks are enabled. I'm not sure atm where is the "breaking limit" (matrix size) when sequence checking will start to mis-behave. But as this is a legacy "buggy feature" I could live with a use-as-is decision 🤔, so we still support the previous "proper use" cases. What do you think?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain
Exact Breaking-Point CalculationThe rejection window always excludes exactly 4 sequence numbers as "late from previous frame":
So: 15 total − 1 (zero/disabled) − 4 (rejected window) = 10 safe data-packet slots + 1 for the push packet itself = 11 packets max per frame without false-rejection. At xLights' default 1440 bytes/packet:
For typical WLED ESP32 setups that max out around 1,000–2,500 LEDs, this is usually fine. The problematic zone is large matrix setups (2,000–8,000+ LEDs with RGBW), which is exactly the target use case for Can We Improve It?Several options with different trade-offs:
The fundamental constraint is the DDP spec's 4-bit sequence number (1-15 ring). No algorithm can safely distinguish "late from previous frame" from "early in new frame" with only 15 values and an unbounded number of packets per frame. RecommendationGiven that:
I'd suggest accepting use-as-is for this PR but adding a comment near the rejection logic that documents the limit, so future contributors understand why the feature can't safely be extended to large setups: // Note: with a 15-value (1–15) DDP sequence ring and a 4-slot rejection window,
// frames requiring more than ~10 packets may experience false rejections.
// "Skip out-of-sequence packets" (e131SkipOutOfSequence) is disabled by default;
// for large installations it should remain disabled.
if (e131SkipOutOfSequence && lastPushSeq) {This way the behavior is documented, and users/maintainers know the safe operating envelope without any code risk from changing the window algorithm. The 🏁 Script executed: Length of output: 0 🌐 Web query: 💡 Result: GitHub issue wled/WLED
DDP itself is the UDP protocol WLED listens for on port 4048. [2] Sources: [1] [2]
To summarize the math concisely:
For this PR, I'd suggest the pragmatic "use-as-is + document the limit" approach, since:
A targeted comment referencing //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)
// Note: the 4-bit DDP sequence ring (1-15) limits safe use of this feature to frames requiring
// ≤10 DDP packets (~5280 RGB / ~3960 RGBW LEDs at 1440 bytes/packet). See issue `#5412`.
// For larger installations, keep "Skip out-of-sequence packets" (e131SkipOutOfSequence) disabled.
if (e131SkipOutOfSequence && lastPushSeq) {This keeps the PR focused on the safety fixes it targets, and creates a clear breadcrumb for whoever addresses 🧠 Learnings used
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, so with RGB (not RGBW which is totally uncommon in large displays, and not supported for HUB75), false rejections start above ~ 82x64 pixels.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not perfect and the fix is to uncheck that option, its more a question of documentation. Until looking into this code for this PR I did not even know that option existed :) What I am most concerned about in this PR is rejecting valid packets in non DDP protocols, which may be the case if the rabbit is correct in its statement above on artnet packets.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so if we can not verify that to be working in a real setup, I'd rather comment out those packet size checks for now as this PR will block 16.0 release until done.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, good point. As testing is very hard to do, especially we don't know much about all the possible senders - maybe this PR should strictly focus on "prevent array bounds violations" and on DDP. Which means, any clamping that is not driven by array/buffer bounds should be commented out. |
||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| 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,32 +93,41 @@ 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; | ||||||||||||||||||||||||||||||||||||||||||
| int seq = 0, mde = REALTIME_MODE_E131; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||
|
DedeHai marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.