Skip to content

(WIP) esp32: always flush WiFiUDP rx buffer on early exit (fixes #5929) - #5932

Open
softhack007 wants to merge 1 commit into
mainfrom
fix_wifiUDP_rxbuffer
Open

softhack007 wants to merge 1 commit into
mainfrom
fix_wifiUDP_rxbuffer

Conversation

@softhack007

@softhack007 softhack007 commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

see analysis in #5929

needs testing, especially to confirm that the "broadcast ignore" timeout for UDP SYNC is always sufficient to prevent message floods. Reason: the bug also had a side-effect of making the sync master (esp32) ignore all future sync broadcasts after the first self-adressed broadcast was received.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 296eec3 and fc83802.

📒 Files selected for processing: 3
  • wled00/fcn_declare.h
  • wled00/ntp.cpp
  • wled00/udp.cpp

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout: 41
  • GitHub Check: wled_build / Build Environments (esp32s3dev_16MB_opi_hub75)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp32S3_wroom2)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full)
  • GitHub Check: wled_build / Build Environments (esp32p4_16MB)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_4MB_qspi_hub75)
  • GitHub Check: wled_build / Build Environments (esp32c3dev)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_none)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32c5dev)
  • GitHub Check: wled_build / Build Environments (esp32c6dev_4MB)
  • GitHub Check: wled_build / Build Environments (esp32dev_debug)
  • GitHub Check: wled_build / Build Environments (nodemcuv2)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_qspi)
  • GitHub Check: wled_build / Build Environments (esp32dev_hub75_forum_pinout)
  • GitHub Check: wled_build / Build Environments (esp32dev_V4)
  • GitHub Check: wled_build / Build Environments (esp32c5dev_8MB_qspi)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_16MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32_wrover)
  • GitHub Check: wled_build / Build Environments (esp32c3dev_qio)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_compat)
  • GitHub Check: wled_build / Build Environments (esp32_eth_V4)
  • GitHub Check: wled_build / Build Environments (lolin_s2_mini)
  • GitHub Check: wled_build / Build Environments (esp32dev)
  • GitHub Check: wled_build / Build Environments (esp32_eth)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Environments (esp8266_2m)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_compat)
  • GitHub Check: wled_build / Test cdata.js
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (c-cpp)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (actions)
  • GitHub Check: wled_build / Build Environments (esp32c3dev)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp32c5dev)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp32dev_debug)
🧰 Additional context used
📚 Code guidelines: 4
docs/hardening.instructions.md — configured
.github/agent-build.instructions.md — auto-discovered
AGENTS.md — configured
docs/cpp.instructions.md — configured
📓 Path-based instructions: 5
Apply the WLED security hardening rules from docs/hardening.instructions.md, and consult docs/securecode.instructions.md when more details are needed for actionable recommendations.

⚙️ CodeRabbit configuration file

Files:

  • wled00/fcn_declare.h
  • wled00/udp.cpp
  • wled00/ntp.cpp
Follow the C++ coding conventions documented in docs/cpp.instructions.md and the general project guidelines in AGENTS.md and .github/copilot-instructions.md.

⚙️ CodeRabbit configuration file

Files:

  • wled00/fcn_declare.h
  • wled00/udp.cpp
  • wled00/ntp.cpp
Applicability reference: AGENTS.md: # AGENTS.md — WLED AI Coding Agent & AI Code Review Reference See also: `.github/copilot-instructions.md`, `.github/agent-build.instructions.md`, `docs/cpp.instructions.md`, `docs/web.in...

📄 CodeRabbit inference engine (docs/hardening.instructions.md)

Files:

  • wled00/fcn_declare.h
  • wled00/udp.cpp
  • wled00/ntp.cpp
Applicability reference: AGENTS.md: # AGENTS.md — WLED AI Coding Agent & AI Code Review Reference See also: `.github/copilot-instructions.md`, `.github/agent-build.instructions.md`, `docs/cpp.instructions.md`, `docs/web.instructions.md`, `d...

📄 CodeRabbit inference engine (.github/agent-build.instructions.md)

Files:

  • wled00/fcn_declare.h
  • wled00/udp.cpp
  • wled00/ntp.cpp
Source excerpt: Follow the existing style in the file you are editing Source excerpt: Avoid unexplained "magic numbers".

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • wled00/fcn_declare.h
  • wled00/udp.cpp
  • wled00/ntp.cpp
🧠 Learnings: 1
📓 Common learnings
Learnt from: softhack007
Repo: wled/WLED PR: 5599
File: usermods/audioreactive/audio_reactive.cpp:1303-1308
Timestamp: 2026-05-11T16:00:17.885Z
Learning: In arduino-esp32 v3.x (ESP-IDF 5.x), `WiFiUDP::flush()` is deprecated and does nothing for RX draining. The correct method to clear/drain the UDP receive buffer is `WiFiUDP::clear()`, introduced in arduino-esp32 v3.0. The `NetworkUdp.h` header marks `flush()` with `[[deprecated("Use clear() instead.")]]`. Code that needs to drain UDP RX across both arduino-esp32 v2.x (ESP-IDF <5) and v3.x (ESP-IDF >=5) should use `#if ESP_IDF_VERSION_MAJOR < 5` / `fftUdp.flush()` / `#else` / `fftUdp.clear()` / `#endif`. Do not flag uses of `WiFiUDP::clear()` under ESP-IDF >=5 as non-existent — it is the correct, documented replacement for `flush()`.
🔇 Additional comments: 7
wled00/fcn_declare.h: 1

317-329: LGTM!

wled00/ntp.cpp: 2

207-207: LGTM!

Also applies to: 209-209

312-318: LGTM!

wled00/udp.cpp: 4

490-490: LGTM!

Also applies to: 493-493

501-502: LGTM!

522-522: LGTM!

531-531: LGTM!

Walkthrough

The change adds platform-specific UDP receive-buffer cleanup for NTP handling and drains notification packets that are oversized, disabled, or self-originated before returning.

Changes

UDP receive-buffer cleanup

Layer / File(s) Summary
NTP receive-buffer cleanup
wled00/fcn_declare.h, wled00/ntp.cpp
A helper clears the ESP32 UDP receive buffer with clear() on ESP-IDF 5 or newer and flush() on older versions. NTP handling applies buffer cleanup before draining queued packets and after reading an oversized response.
Notification packet draining
wled00/udp.cpp
Notification handling drains oversized packets and packets rejected because of Hyperion reception settings, packet size, disabled node-list notifications, or self-originated data.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related issues

  • wled/WLED#5929 describes the self-originated UDP SYNC early return and its effect on later notification reception, which matches the packet-draining change.

Suggested labels: bug

Suggested reviewers: dedehai

Merge Risk: ⚪ Minimal · up to fc838

No actionable merge-blocking risk remains in the reviewed UDP cleanup paths.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly identifies the main change: clearing or flushing the ESP32 WiFiUDP receive buffer on early exit to address issue #5929. The wording is concise and directly related to the changes.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@softhack007

Copy link
Copy Markdown
Member Author

@coderabbitai please create a testplan for this change, also covering scenarios where message multiplication from multiple masters on the same subnet must be prevented.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr diff 5932 --repo wled/WLED
gh issue view 5929 --repo wled/WLED --json title,body
ast-grep outline wled00/udp.cpp --items all
ast-grep outline wled00/ntp.cpp --items all
rg -n -C5 'udpDropRXBuffer|CALL_MODE_NOTIFICATION|notificationSentTime|notificationCount|UDP_IN_MAXSIZE' wled00/udp.cpp wled00/ntp.cpp wled00/fcn_declare.h

Length of output: 19641


🏁 Script executed:

#!/bin/bash
sed -n '20,58p' wled00/udp.cpp
sed -n '175,206p' wled00/udp.cpp
sed -n '529,605p' wled00/udp.cpp
sed -n '197,254p' wled00/ntp.cpp
sed -n '255,354p' wled00/ntp.cpp
rg -n -C5 'void stateUpdated\(|notify\(callMode|udpNumRetries|NTP_PACKET_SIZE' wled00 --glob '*.{cpp,h}' | head -180

Length of output: 21236


@softhack007 Here is a hardware test plan for #5932. It covers receive-buffer recovery and message multiplication with multiple masters.

Status: Source inspection only. These tests have not been run.

1. Setup

Use one isolated subnet with:

  • Three WLED nodes, A, B, and C.
  • A test host for packet injection and packet capture.
  • An access point that permits broadcast traffic between clients.

Test these firmware variants:

Variant Purpose
ESP32, ESP-IDF below 5 Exercise flush()
ESP32, ESP-IDF 5 or later Exercise clear()
ESP8266 Confirm that the no-op helper preserves reception

Run the main reproduction on the parent revision and the PR revision. Use the same hardware and configuration.

Configure A and B to send and receive sync notifications. Configure C as receive-only. Use matching sync groups. Disable playlists, timers, MQTT automation, and other sources of state changes during packet-count tests. Disable ESP-NOW to isolate UDP.

Record the actual ports. The source defaults are:

  • Primary notifier: 21324
  • Supplemental notifier: 65506
  • Raw RGB: 19446

Capture traffic at the access point or another location that sees all node transmissions. A capture on an ordinary Wi-Fi client might not see all traffic.

2. Expected anti-loop behavior

The supplied UDP-sync learning agrees with the inspected code:

  • parseNotifyPacket() calls stateUpdated(CALL_MODE_NOTIFICATION).
  • stateUpdated() excludes that mode from notify().
  • notify() also rejects that mode.
  • Notifications received less than 1000 ms after the node's last transmission are ignored.

The timeout is not the only protection against multiplication. A received notification must not cause a new sync transmission, even after the timeout expires.

Each retry updates notificationSentTime. Measure the 1000 ms window from the last transmission, not the original state change.

Node-info announcements can occur after brightness changes. Count those separately from sync notifications.

3. Receive-buffer recovery

For each case, send the rejected packet first. Then send a known-good packet on the same socket, without a reboot or reconnect. Wait until any sync suppression window has expired before checking sync acceptance.

ID Test Pass criteria
RX-1 Change A locally. Confirm that A receives and rejects its own broadcast. Then send a distinct change from B. Repeat 100 times. A continues to accept B's changes. No receive stall occurs.
RX-2 Send a 1473-byte datagram to the primary notifier. Then send a valid sync packet. Repeat on the supplemental notifier. Each socket processes the following valid packet.
RX-3 Test notifier datagrams at 1471, 1472, and 1473 bytes. Use protocol-valid payloads for accepted sizes. The size guard permits up to 1472 bytes and rejects larger packets. Reception continues.
RX-4 Disable direct reception. Send raw RGB traffic. Stop the sender, enable direct reception, and send a distinct valid frame. Disabled traffic does not change LEDs. The new frame works without a restart.
RX-5 With direct reception enabled, send raw RGB datagrams of 1, 2, and 1473 bytes. Follow each with a valid frame. Also test valid frames of 3 and 1470 bytes. Invalid sizes do not stall reception. Valid frames render correctly.
RX-6 Disable the node list. Send valid node-info packets, then send valid sync traffic on the supplemental socket. Re-enable the node list and send fresh node info. Ignored node info does not block later packets. Fresh node info appears when enabled.
RX-7 If the network delivers self-originated supplemental broadcasts, exercise that rejection path. Then send peer node info. The node ignores its own entry and still accepts peer entries.
RX-8 Queue a rejected packet followed by several distinct valid packets. Repeat on each affected socket. Cleanup does not leave the socket stalled or discard all following packets. Keep load low enough to avoid queue overflow.

For RX-1 and RX-7, verify that the self-originated packet reaches the node. A test without self-delivery does not exercise the relevant guard.

4. Multiple-master tests

Start each packet-count test after boot traffic and previous retries have stopped. First use udpNumRetries = 0. Then repeat with retries set to 1 and 3.

For an isolated local change with r retries, the expected sender count is:

1 original sync notification + r retries

Receiving nodes must add zero sync transmissions. Count packets by source IP and protocol, not only by destination port.

ID Scenario Pass criteria
MM-1 Make one local change on A. Leave B and C untouched. A sends only its configured packet count. B and C update but send no sync notifications.
MM-2 After all retries and suppression windows expire, make one local change on B. Alternate A and B for 100 cycles. Each master can receive after sending. Counts stay bounded.
MM-3 Trigger one local change on A and one on B within 10–50 ms. Stop all input. Traffic contains only the two original notification sequences and their retries. No echo chain develops.
MM-4 After A's final transmission, inject one valid peer notification at approximately 900 ms, then at 1100 ms. Repeat near the boundary with device-side timing. The first is ignored. The second is accepted. Neither causes A to transmit a new sync notification.
MM-5 Delay or replay a valid captured sync notification until more than 1000 ms after every receiver's last transmission. Repeat several times. Receivers may apply the state, but none re-broadcasts the notification. This tests anti-loop behavior outside the timeout.
MM-6 Enable send and receive on C too. Trigger changes on all three masters, both together and at staggered times. Packet counts scale with local changes and configured retries, not with the number of receivers.
MM-7 Send controlled local changes from A and B for five minutes. Include intervals below 250 ms, below 1000 ms, and above 1000 ms. Then stop all input. No growing notification rate appears. After pending retries finish, sync traffic stops for at least 10 seconds.
MM-8 Repeat MM-1 through MM-5 with mixed ESP32 and ESP8266 nodes. Swap the master roles. The same no-rebroadcast rule holds across platforms.

Do not require a deterministic final color after simultaneous conflicting changes. The suppression window can leave masters with different states. After the window expires, send one new change from a selected master and confirm that the other nodes receive it.

For MM-7, do not apply the isolated-event packet-count formula. New local changes can replace an unfinished retry sequence.

5. NTP regression tests

Use a controlled NTP server. Keep time sync pending when testing recovery.

ID Test Pass criteria
NTP-1 Return a valid 48-byte response. Time synchronizes normally.
NTP-2 Return valid responses with trailing bytes, such as 49, 68, and 96 bytes. WLED processes the standard fields. Trailing bytes do not stall later reception.
NTP-3 Return an invalid response longer than 48 bytes. Then return a valid 48-byte response while sync remains pending. WLED rejects the invalid response and processes the following valid response.
NTP-4 Return short responses, such as 1 and 47 bytes. Then return a valid response. Short responses do not stall the socket.
NTP-5 Queue stale responses before a new request. Include a retained unread response if test instrumentation permits. The pre-request cleanup completes. WLED sends a new request and accepts its response.
NTP-6 Repeat request, timeout, and recovery cycles with both a numeric server address and a DNS name. NTP continues to recover without a reboot.

6. Soak test and acceptance criteria

Run a one-hour mixed workload. Include multi-master changes, rejected datagrams, valid raw RGB frames, and NTP responses. Keep injection rates bounded so that queue overload does not hide the result.

Record:

  • Firmware revision, board, Arduino core, and ESP-IDF version.
  • Node configuration and retry count.
  • Packet captures with test markers.
  • Receive failures, resets, watchdog events, and UI responsiveness.
  • Free heap and largest free block, where available.

Accept the change when:

  • Every rejected-packet case permits later valid traffic on the same socket.
  • Received sync notifications never create additional sync notifications.
  • Traffic stops after local inputs and pending retries stop.
  • ESP8266 behavior remains unchanged.
  • NTP continues to synchronize.
  • No sustained heap decline or receive stall appears.

The original receive stall does not by itself prove a heap leak. Report heap behavior separately from receive recovery.


🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-05-18T21:27:01.781Z
Learning: In WLED's UDP sync notification system, receiving a sync packet never causes the receiving node to re-emit. parseNotifyPacket() calls stateUpdated(CALL_MODE_NOTIFICATION), which is explicitly excluded from triggering notify() re-broadcast. Three independent anti-loop protections exist: (1) CALL_MODE_NOTIFICATION hits `default: return` in notify()'s switch, (2) 1000ms suppression window (notificationSentTime guard in parseNotifyPacket), (3) self-IP rejection.

You are interacting with an AI system.

@softhack007 softhack007 mentioned this pull request Oct 23, 2026
1 task done
@softhack007
softhack007 requested review from Aircoookie and a balanced review from Copilot October 23, 2026 22:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

ESP32 multi-device testing is still needed to confirm continued reception and effective UDP SYNC flood suppression.

No open issues 🎉

What changed in this PR

This PR addresses #5929 by clearing unread ESP32 UDP receive buffers that can block subsequent packets, while preserving ESP8266 behavior.

Changes:

  • Adds a platform-aware helper using clear() or flush() on ESP32.
  • Clears rejected packets in notification and realtime UDP handling.
  • Clears retained NTP data before requests and after partially read responses.
File Description
wled00/​udp.cpp Discards receive buffers when rejecting UDP packets.
wled00/​ntp.cpp Clears retained NTP buffers and unread response bytes.
wled00/​fcn_declare.h Defines the ESP32-specific receive-buffer cleanup helper.

Review effort: Balanced

@Aircoookie Aircoookie left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is a very valuable fix, though with initial testing I have been so far unable to reproduce an unpatched ESP32 sync master (both with 16.0.1 and main) entering a blocked state where it ignores incoming sync packets (and potentially leak memory)

Comment thread wled00/ntp.cpp
#ifdef ARDUINO_ARCH_ESP32 // I had problems using udp.flush() on 8266
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
while (ntpUdp.parsePacket() > 0) ntpUdp.clear(); // flush() is deprecated in arduino-esp32 3.x.y
ntpUdp.clear(); while (ntpUdp.parsePacket() > 0) ntpUdp.clear(); // flush() is deprecated in arduino-esp32 3.x.y

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for adding the unconditional clear() for clearing the ntpUdp socket? Logically, I don't understand it fully - the second one as part of the parse loop should suffice?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the intent here is to flush any unread packets prior to sending a new NTP poll.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, unread (rejected or partially read) packets cause parsePacket()==0 so the while loop won't trigger. Calling clear() is always allowed even when there is no packet waiting.

I could change it into a do { clear(); } while parsePacket() > 0 to make the code a bit clearer.

Comment thread wled00/ntp.cpp
#else
if (cb > NTP_PACKET_SIZE) ntpUdp.flush();
#endif
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be if (cb > NTP_PACKET_SIZE) udpDropRXBuffer(ntpUdp);. In fact you could even do this without any #ifdefs as dead code elimination will remove it on ESP8266.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, ifdef soup is everywhere ... I'll change it

Comment thread wled00/udp.cpp
Comment on lines 489 to +490
size_t packetSize = notifierUdp.parsePacket();
if (packetSize > UDP_IN_MAXSIZE) udpDropRXBuffer(notifierUdp); // too big

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be good to wrap this pattern up in another little helper function. It might even be worth going all the way to a full read-to-buffer-if-ready call -- it'd clean up the logic here quite a bit, I think.

size_t udpRead(AsyncUDP& udp_socket, uint8_t* buf, size_t buf_size) {
   size_t result = udp_socket.parsePacket();
   if (result > buf_size) {
       udpDropRXBuffer(udp_socket);  // discard oversize packet
       return 0;
   }
   if (result > 0) {
       udp_socket.read(buf, buf_size);
   }
   return result;
}

@softhack007

softhack007 commented Oct 24, 2026 •

Copy link
Copy Markdown
Member Author

@Aircoookie thanks, the main precondition for the blocked state is that your router allows broadcast-to-self, I think modern routers can filter this out. We had bug reports where all esp32 in the network act as sync masters, and stopped reacting on sync messages for example after being controlled by HyperHDR or FFP.

The main idea of the fix is from wled-mm, however from a time when WiFiUDP used to leak memory when there was no "read" or "flush". We were hunting for leaks at the time, and adding "flush()" to every early return stabilised it. So the basic idea works, similar code is running in WLED-MM for some time already.

I'm just not sure about the message flooding scenario, that requires several esp32 to multiply sync messages to each other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UDP SYNC broadcast: master device can block itself from receiving any new messages until reboot (ESP8266 not affected)

4 participants