From 3c9582c23563c963491da7061ba00841d333a1b3 Mon Sep 17 00:00:00 2001 From: "Andrew J.Swan" Date: Tue, 18 Aug 2026 15:00:30 +0300 Subject: [PATCH] Fix DDP fragmentation: add multi-packet handling and data offset support - Implement DDP data offset handling in DDPComponent::process_ to support fragmented multi-packet UDP streams. - Update virtual signature of LightEffect::process_ to accept LED offset parameter. - Refactor DDPAddressableLightEffect to map color payloads using incoming packet offset and optimize LED index calculation by replacing division with increment. - Fix single bulb DDPLightEffect to drop non-zero offset packets to prevent memory corruption in chained setups. - Add hardware state clearing to DDPAddressableLightEffect::start() to prevent erratic LED flashing on effect activation. --- components/ddp/ddp.cpp | 23 ++++++------ .../ddp/ddp_addressable_light_effect.cpp | 35 +++++++++++++------ components/ddp/ddp_addressable_light_effect.h | 2 +- components/ddp/ddp_light_effect.cpp | 7 +++- components/ddp/ddp_light_effect.h | 2 +- components/ddp/ddp_light_effect_base.h | 2 +- 6 files changed, 45 insertions(+), 26 deletions(-) diff --git a/components/ddp/ddp.cpp b/components/ddp/ddp.cpp index c9e8657..f505ca3 100644 --- a/components/ddp/ddp.cpp +++ b/components/ddp/ddp.cpp @@ -59,18 +59,19 @@ bool DDPComponent::process_(const uint8_t *payload, uint16_t size) { return false; } - // ignore packet if data offset != [00 00 00 00]. This likely means the device is receiving a DDP packet not meant for it. - // There may be a better way to handle this header field. One user was receiving packets with non-zero data offset that were - // screwing up the light effect (flickering). - if (payload[4] || payload[5] || payload[6] || payload[7]) { - ESP_LOGE(TAG, "Ignoring DDP Packet with non-zero data offset."); - return false; - } + // Extract 32-bit data offset from header bytes 4-7 (Big Endian) + uint32_t data_offset = ((uint32_t)payload[4] << 24) | + ((uint32_t)payload[5] << 16) | + ((uint32_t)payload[6] << 8) | + (uint32_t)payload[7]; + + // Convert byte offset to LED index (3 bytes per RGB pixel) + uint16_t led_offset = data_offset / 3; ESP_LOGV(TAG, - "DDP packet received (size=%d): - %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x [%02x %02x %02x]", - size, payload[0], payload[1], payload[2], payload[3], payload[4], payload[5], payload[6], payload[7], payload[8], - payload[9], payload[10], payload[11], payload[12]); + "DDP packet received (size=%d, led_offset=%u): - %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x [%02x %02x %02x]", + size, led_offset, payload[0], payload[1], payload[2], payload[3], payload[4], payload[5], payload[6], payload[7], + payload[8], payload[9], payload[10], payload[11], payload[12]); // first 10 bytes are the header, so consider them used from the get-go // if timecode field is used, takes up an additional 4 bytes of header. @@ -85,7 +86,7 @@ bool DDPComponent::process_(const uint8_t *payload, uint16_t size) { if (used >= size) { return false; } - uint16_t new_used = light_effect->process_(payload, size, used); + uint16_t new_used = light_effect->process_(payload, size, used, led_offset); if (new_used == 0) { return false; } else { diff --git a/components/ddp/ddp_addressable_light_effect.cpp b/components/ddp/ddp_addressable_light_effect.cpp index 73229fb..475dfd7 100644 --- a/components/ddp/ddp_addressable_light_effect.cpp +++ b/components/ddp/ddp_addressable_light_effect.cpp @@ -38,7 +38,6 @@ void DDPAddressableLightEffect::start() { // not automatically active just because enabled this->set_effect_active_(this->get_addressable_(), false); - } void DDPAddressableLightEffect::stop() { @@ -95,7 +94,7 @@ void DDPAddressableLightEffect::apply(light::AddressableLight &it, const Color & } -uint16_t DDPAddressableLightEffect::process_(const uint8_t *payload, uint16_t size, uint16_t used) { +uint16_t DDPAddressableLightEffect::process_(const uint8_t *payload, uint16_t size, uint16_t used, uint16_t offset) { // disable gamma on first received packet, not just based on effect being enabled. // that way home assistant light can still be used as normal when DDP packets are not @@ -117,12 +116,18 @@ uint16_t DDPAddressableLightEffect::process_(const uint8_t *payload, uint16_t si // effect is active once a ddp packet is received. this->set_effect_active_(it, true); + // Calculate remaining pixels from the offset position to prevent overflow + int remaining_leds = 0; + if (offset < it->size()) { + remaining_leds = it->size() - offset; + } - uint16_t num_pixels = std::min(it->size(), (size - used) / 3); + // Limit processed pixels by remaining strip space and packet data size + uint16_t num_pixels = std::min(remaining_leds, (size - used) / 3); if ( num_pixels < 1 ) { return 0; } - ESP_LOGV(TAG, "Applying DDP data for '%s' (size: %d - used: %d - num_pixels: %d)", get_name(), size, used, num_pixels); + ESP_LOGV(TAG, "Applying DDP data for '%s' (size: %d - used: %d - num_pixels: %d - offset: %d)", get_name(), size, used, num_pixels, offset); // will be multiplied by RGB values in scale_* scaling modes float multiplier = 1.0f; @@ -131,11 +136,11 @@ uint16_t DDPAddressableLightEffect::process_(const uint8_t *payload, uint16_t si // max out brightness in all but multiply mode, in which brightness is used. switch (this->scaling_mode_) { case DDP_SCALE_PACKET: - multiplier = this->scan_packet_and_return_multiplier_(payload,10,size); + multiplier = this->scan_packet_and_return_multiplier_(payload, 10, size); set_max_brightness_(); break; case DDP_SCALE_STRIP: - multiplier = this->scan_packet_and_return_multiplier_(payload, used, used + (num_pixels*3)); + multiplier = this->scan_packet_and_return_multiplier_(payload, used, used + (num_pixels * 3)); set_max_brightness_(); break; case DDP_NO_SCALING: // no scaling requires brightness maxed so that ddp values will be displayed raw. @@ -145,13 +150,19 @@ uint16_t DDPAddressableLightEffect::process_(const uint8_t *payload, uint16_t si break; // Multiply mode is default ESPHome behavior, no need to do anything to handle it. } + uint16_t target_led = offset; + // loop through all pixels being displayed now. - for (uint16_t i = used; i < used+(num_pixels*3); i+=3) { + for (uint16_t i = used; i < used + (num_pixels * 3); i += 3) { + if (target_led >= it->size()) { + break; + } + // get RGB value of current pixel. uint8_t red = payload[i]; - uint8_t green = payload[i+1]; - uint8_t blue = payload[i+2]; + uint8_t green = payload[i + 1]; + uint8_t blue = payload[i + 2]; // set multiplier for this pixel if in pixel scaling mode if ( this->scaling_mode_ == DDP_SCALE_PIXEL ) { @@ -183,12 +194,14 @@ uint16_t DDPAddressableLightEffect::process_(const uint8_t *payload, uint16_t si } // assign pixel color; clear white channel for RGBW strips - auto output = (*it)[(i-used)/3]; + auto output = (*it)[target_led]; output.set_rgbw(red, green, blue, 0); + + target_led++; } it->schedule_show(); - return (num_pixels*3); + return (num_pixels * 3); } float DDPAddressableLightEffect::scan_packet_and_return_multiplier_(const uint8_t *payload, uint16_t start, uint16_t end) { diff --git a/components/ddp/ddp_addressable_light_effect.h b/components/ddp/ddp_addressable_light_effect.h index 15f2610..5936fca 100644 --- a/components/ddp/ddp_addressable_light_effect.h +++ b/components/ddp/ddp_addressable_light_effect.h @@ -31,7 +31,7 @@ class DDPAddressableLightEffect : public DDPLightEffectBase, public light::Addre #endif protected: - uint16_t process_(const uint8_t *payload, uint16_t size, uint16_t used) override; + uint16_t process_(const uint8_t *payload, uint16_t size, uint16_t used, uint16_t offset = 0) override; float scan_packet_and_return_multiplier_(const uint8_t *payload, uint16_t start, uint16_t end); float multiplier_from_max_val_(uint8_t max_val); diff --git a/components/ddp/ddp_light_effect.cpp b/components/ddp/ddp_light_effect.cpp index f2585b6..78878ef 100644 --- a/components/ddp/ddp_light_effect.cpp +++ b/components/ddp/ddp_light_effect.cpp @@ -71,7 +71,12 @@ void DDPLightEffect::apply() { } -uint16_t DDPLightEffect::process_(const uint8_t *payload, uint16_t size, uint16_t used) { +uint16_t DDPLightEffect::process_(const uint8_t *payload, uint16_t size, uint16_t used, uint16_t offset) { + + // Ignore fragments meant for subsequent devices when operating in single bulb mode + if (offset > 0) { + return (size - used); + } // at least for now, we require 3 bytes of data (r, g, b). // If there aren't 3 unused bytes, return 0 to indicate error. diff --git a/components/ddp/ddp_light_effect.h b/components/ddp/ddp_light_effect.h index a9bd881..cf2b02d 100644 --- a/components/ddp/ddp_light_effect.h +++ b/components/ddp/ddp_light_effect.h @@ -21,7 +21,7 @@ class DDPLightEffect : public DDPLightEffectBase, public light::LightEffect { void apply() override; protected: - uint16_t process_(const uint8_t *payload, uint16_t size, uint16_t used) override; + uint16_t process_(const uint8_t *payload, uint16_t size, uint16_t used, uint16_t offset = 0) override; }; } // namespace ddp diff --git a/components/ddp/ddp_light_effect_base.h b/components/ddp/ddp_light_effect_base.h index 0ec7e3d..1500241 100644 --- a/components/ddp/ddp_light_effect_base.h +++ b/components/ddp/ddp_light_effect_base.h @@ -46,7 +46,7 @@ class DDPLightEffectBase { DDPScalingMode scaling_mode_{DDP_NO_SCALING}; - virtual uint16_t process_(const uint8_t *payload, uint16_t size, uint16_t used) = 0; + virtual uint16_t process_(const uint8_t *payload, uint16_t size, uint16_t used, uint16_t offset = 0) = 0; friend class DDPComponent; };