Skip to content

audioreactive: add UDP broadcast as an alternative sync transport - #5863

Open
efranceschi wants to merge 2 commits into
wled:mainfrom
efranceschi:audiosync-broadcast-transport
Open

efranceschi wants to merge 2 commits into
wled:mainfrom
efranceschi:audiosync-broadcast-transport

Conversation

@efranceschi

@efranceschi efranceschi commented Sep 21, 2026 •

Copy link
Copy Markdown

audioreactive: add UDP broadcast as an alternative sync transport

Problem

Sound sync only sends over multicast (239.0.0.1), which depends on the network keeping our
group membership alive. Access points that run IGMP snooping without an IGMP querier never ask
members to renew, so the membership expires after the default ~260 s group membership interval
and the AP stops forwarding the group. Nothing looks wrong in WLED - the socket is still open,
the info page still says "receive mode" - but no audio arrives until the node reboots.

Same symptom as #4408, closed as stale. Reproducible on demand here.

Change

A Transport dropdown in the AudioReactive sync settings lets the sender use UDP broadcast
instead of multicast. Broadcast has no group membership, so IGMP snooping does not affect it.

int success;
if (audioSyncTransport == 1) {
  // same way the state notifier builds its broadcast address (udp.cpp), so this also
  // works on boards using Ethernet instead of WiFi
  IPAddress broadcastIp = ~uint32_t(WLEDNetwork.subnetMask()) | uint32_t(WLEDNetwork.gatewayIP());
  success = fftUdp.beginPacket(broadcastIp, audioSyncPort);
} else {
  success = fftUdp.beginMulticastPacket(); // returns 0 in case of error
}
  • Default unchanged: transport = 0 is multicast.
  • Only the sender changes - receivers bind to the sync port, so they accept broadcast as is.
    Verified with four receivers on stock 16.0.1.
  • uint8_t rather than bool so a third transport can be added later without a config
    migration, as suggested in Adding ESPNOW connection to sync multiple hardware with sound reactive. #5637. Any value other than 1 falls back to multicast.
  • 22 added lines, 1 changed, all in usermods/audioreactive/.

This is not a new pattern for WLED: the state notifier already broadcasts, and builds its
address the same way. On this network colour sync keeps working while sound sync dies - same
devices, different transport.

Testing

5x ESP32 DevKit, INMP441 mic on the sender, TP-Link Deco mesh (4 APs), receivers on stock
16.0.1 while the sender runs this branch.

Before: every receiver stops ~260 s after joining the group and never recovers. A 6 minute
tcpdump -i en0 igmp recorded zero IGMP packets - the mesh never queries.

After (broadcast): ran for 9 hours overnight, polling all four receivers every 2 minutes.
Not once did a receiver report idle - the transport was never pruned - and the sender ran the
whole period without a reboot, with stable heap.

Also verified: multicast still works with transport = 0 (664 packets in 15 s), the setting
survives a reboot and saves from the Usermods page, and esp32dev, esp32_wrover,
esp32s3dev_8MB_qspi and esp32c3dev all build clean.

Not tested: ESP8266 receivers, AP mode, and networks that rate limit broadcast - which is
why this is an option rather than a new default.

Related

AI disclosure

Written with AI assistance, reviewed line by line and tested on my own hardware.

Summary by CodeRabbit

  • New Features

    • Added a configurable audio synchronization transport option.
    • Audio synchronization can use multicast or network broadcast.
    • Added a Usermod Settings dropdown for selecting the preferred transport.
    • The selected transport is saved and restored automatically.
    • Broadcast-based audio synchronization supports Wi-Fi and Ethernet connections.
  • Bug Fixes

    • Improved network broadcast handling for audio synchronization and device notifications.

Multicast sound sync depends on the network keeping the group membership
alive. Access points that enable IGMP snooping without providing an IGMP
querier stop forwarding the group once the membership times out (~260s),
which silently breaks sound sync for every receiver until they reboot.

Add a 'Transport' setting to the AudioReactive sync section that lets the
sending node use UDP broadcast instead of multicast. Broadcast does not
rely on group membership, so it is not affected by that behaviour.

Default is unchanged (multicast). Receivers need no change: their socket
is bound to the sync port, so it accepts broadcast packets as well.

The setting is a uint8_t rather than a bool so further transports can be
added later without a config migration, as suggested in wled#5637.

Written with AI assistance; every line reviewed and tested on hardware.
@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: wled/WLED/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ec463503-a090-4f30-924a-7fdd6d3a2fb9

📥 Commits

Reviewing files that changed from the base of the PR and between d3dabd0 and 8de2671.

📒 Files selected for processing (4)
  • usermods/audioreactive/audio_reactive.cpp
  • wled00/src/dependencies/network/Network.cpp
  • wled00/src/dependencies/network/Network.h
  • wled00/udp.cpp

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


Walkthrough

The AudioReactive usermod adds a persisted multicast or broadcast setting for UDP sound synchronization. The network layer provides a shared broadcast address for audio sync and UDP notifications.

Changes

Audio sync transport

Layer / File(s) Summary
Network broadcast address
wled00/src/dependencies/network/Network.h, wled00/src/dependencies/network/Network.cpp, wled00/udp.cpp
Adds broadcastIP() and uses it for UDP notification destinations.
Audio transport configuration and transmission
usermods/audioreactive/audio_reactive.cpp
Stores, restores, and exposes sync.transport. transmitAudioData() selects multicast or broadcast transmission.

Priority: ⬇️ Low

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AudioReactive
  participant Config
  participant WLEDNetwork
  participant UDPReceiver
  User->>AudioReactive: Select multicast or broadcast
  AudioReactive->>Config: Store sync.transport
  Config-->>AudioReactive: Load sync.transport
  AudioReactive->>WLEDNetwork: Request broadcastIP() when broadcast is selected
  WLEDNetwork-->>AudioReactive: Return broadcast address
  AudioReactive->>UDPReceiver: Send audio sync packet
Loading

Suggested reviewers: dedehai

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 and concisely describes the primary change: adding UDP broadcast as an alternative synchronization transport for AudioReactive.
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.
  • Fix all pre-merge checks with AI

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.

Comment thread usermods/audioreactive/audio_reactive.cpp Outdated
Comment thread usermods/audioreactive/audio_reactive.cpp Outdated
@softhack007

Copy link
Copy Markdown
Member

similar to the proposed solution in MoonModules#250

If the root cause is confirmed to be "IGMP snooping" by the access point, we should first investigate if there is a way to keep IGMP happy. Adding broadcast as a "transport" option is a useful addition any way.

@softhack007

Copy link
Copy Markdown
Member

@coderabbitai look for technical references that support "IGMP snooping by the access point" as a root cause for UDP multicast receiver-side connection dropout - IPv4 standards and RFCs related to UDP broadcast, plus official documentation from espressif. Are there other explanations for the observed behaviour, especially affecting older "consumer grade" Wi-Fi internet gateways?

@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🤖 web_search executed:

Find authoritative technical sources: (1) RFC 2236 IGMPv2 membership queries, report suppression, and router membership timeout; (2) RFC 1112 IP multicast host membership and IGMP; (3) RFC 919 and RFC 922 IPv4 broadcast and directed broadcast; (4) RFC 2644 directed broadcast forwarding default; (5) Espressif official ESP-IDF documentation for UDP multicast, IGMP, Wi-Fi power save, and AP multicast/broadcast behavior; (6) reliable vendor documentation that IGMP snooping requires an IGMP querier and ages multicast forwarding entries when no querier exists, particularly switches or Wi-Fi access points. Include direct URLs and concise relevant passages.

Length of output: 10229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target=$(fd -a '^audio_reactive\.cpp$' usermods/audioreactive | head -n 1)
if [ -z "$target" ]; then
  echo "audio_reactive.cpp not found" >&2
  exit 1
fi

ast-grep outline "$target" --items all --match 'transmitAudioData|audioSyncTransport|addToConfig|readFromConfig' --view expanded
printf '\n--- Relevant occurrences ---\n'
rg -n -C 12 'audioSyncTransport|transmitAudioData\s*\(' "$target"

Length of output: 9751


@softhack007 The reported timing strongly supports an IGMP-snooping hypothesis, but it does not confirm it.

  • RFC 2236 defines the default IGMPv2 Group Membership Interval as:

    Robustness Variable × Query Interval + Query Response Interval
    

    The default values are 2 × 125 s + 10 s = 260 s. This matches the reported dropout time closely. See RFC 2236, Section 8.

  • RFC 2236 requires a multicast router or querier to send periodic General Queries. Hosts respond with Membership Reports. The router refreshes group state from these reports. If the membership timer expires, it stops forwarding the group on that interface. See RFC 2236, Sections 3, 5, and 8.

  • An IGMP-snooping switch or access point learns receiver ports by inspecting IGMP reports. Its forwarding state can age out without periodic queries and reports. This behavior is documented by:

This sequence is technically credible:

  1. A receiver joins the multicast group.
  2. The access point learns the receiver association.
  3. No querier sends General Queries.
  4. No receiver sends a new membership report.
  5. The access point ages out the multicast forwarding entry at about 260 seconds.
  6. The sender still transmits packets, but the access point no longer sends them to Wi-Fi receivers.

The PR comment should say that this is a possible network cause, not an established root cause, until a capture confirms it. Capture these packets on the LAN and, if possible, on the affected Wi-Fi segment:

  • IGMP General Queries from the gateway or querier.
  • IGMP Membership Reports from WLED receivers.
  • UDP multicast packets before and after the failure.
  • UDP packets at the receiver-side interface after the failure.

A useful confirmation test is to enable an IGMP querier on the relevant VLAN, or disable IGMP snooping temporarily. If multicast remains stable beyond 260 seconds, the hypothesis becomes strong.

Broadcast standards and this implementation

The implementation calculates:

~uint32_t(WLEDNetwork.subnetMask()) | uint32_t(WLEDNetwork.gatewayIP())

This produces the broadcast address for the gateway subnet, assuming the gateway is in the active interface subnet. This is a subnet-directed IPv4 broadcast address, but the sender uses it on its local LAN. It must not depend on router forwarding to reach local receivers.

  • RFC 919 defines IPv4 broadcasting.
  • RFC 922 defines subnet-directed broadcast behavior.
  • RFC 2644 requires routers to block forwarding of network-prefix-directed broadcasts by default.

RFC 2644 does not prevent local-LAN broadcast delivery. It means this transport should not be expected to cross routed subnet or VLAN boundaries. That limitation is appropriate for this sound-sync protocol.

Espressif references

Espressif documents UDP multicast support through lwIP and its multicast socket APIs:

Espressif also documents a separate Wi-Fi delivery risk:

  • In Wi-Fi power-save modes, a station can miss broadcast traffic around DTIM delivery.
  • Espressif recommends esp_wifi_set_ps(WIFI_PS_NONE) when minimum receive latency is required.
  • ESP32 SoftAP does not cache multicast data for power-save stations.

See ESP-IDF Wi-Fi documentation: power-save behavior.

This means UDP broadcast is a workaround for IGMP-state loss, not a universal reliability improvement. Broadcast and multicast both remain vulnerable to Wi-Fi loss and access-point policy.

Other plausible causes on older consumer gateways

Older consumer gateways can also cause the observed behavior through:

  • Broken or incomplete IGMP snooping. The device may age multicast state but not provide a querier.
  • Wireless multicast-to-unicast conversion defects. Some access points convert multicast to per-station unicast and can lose association state or apply incorrect filtering.
  • Multicast-rate policies. Access points often transmit multicast and broadcast at a low basic Wi-Fi rate. A busy or weak Wi-Fi network can then lose packets.
  • Broadcast suppression or rate limiting. The broadcast fallback can fail on gateways that restrict broadcast traffic.
  • Client isolation or guest-network isolation. The gateway can prevent station-to-station traffic, multicast traffic, or broadcast traffic.
  • Wi-Fi power save and DTIM behavior. This can drop multicast and broadcast packets at sleeping stations.
  • Firmware defects in the gateway. Consumer devices can have stale bridge, snooping, or wireless-driver state that clears only after a reconnect or reboot.

The selectable transport is therefore a useful compatibility feature. Multicast should remain the default because it limits traffic to group members. Broadcast provides a practical fallback for local networks where multicast forwarding is unreliable.


✏️ Learnings added
Learnt from: softhack007
URL: https://github.com/wled/WLED/pull/5863

Timestamp: 2026-09-21T15:04:18.234Z
Learning: For `usermods/audioreactive/audio_reactive.cpp`, a UDP multicast receiver dropout near 260 seconds is consistent with the RFC 2236 IGMPv2 default Group Membership Interval (`2 × 125 s + 10 s`). Treat IGMP snooping without an effective querier as a plausible cause until packet capture or network configuration testing confirms it. The `audioSyncTransport` broadcast path is local-subnet-only and can still be affected by Wi-Fi broadcast suppression, rate limiting, or power-save behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@softhack007 softhack007 added connectivity Issue regarding protocols, WiFi connection or availability of interfaces usermod usermod related AI Partly generated by an AI. Make sure that the contributor fully understands the code! labels Sep 21, 2026
@softhack007

softhack007 commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

@coderabbitai

RFC 2236 requires a multicast router or querier to send periodic General Queries.

Is there a way to implement this using the arduino-esp32 WifiUDP class?

Espressif recommends esp_wifi_set_ps(WIFI_PS_NONE) when minimum receive latency is required.

How would we implement this? is there an API in arduino-esp32 WifiUDP?

@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🤖 web_search executed:

Search official Arduino-ESP32 documentation and source references for WiFiUDP multicast/IGMP capabilities, WiFi.setSleep API, and official Espressif ESP-IDF esp_wifi_set_ps(WIFI_PS_NONE) documentation. Confirm whether an ESP32 station can act as an IGMP querier or send IGMP General Queries through Arduino WiFiUDP. Include version constraints and relevant direct URLs.

Length of output: 9861


@softhack007

WiFiUDP cannot implement an IGMP querier.

WiFiUDP sends and receives UDP datagrams only. IGMP uses IP protocol number 2, not UDP. A General Query is normally sent by the multicast router or the elected IGMP querier on the LAN. An ESP32 receiver should not take that role.

beginMulticast() joins a multicast group. The network stack should then send IGMP Membership Reports in response to General Queries. The application does not send these reports with WiFiUDP.

For this problem, use one of these network-side actions:

  1. Enable an IGMP querier on the gateway, switch, or access point VLAN.
  2. Disable IGMP snooping for a test.
  3. Capture IGMP traffic to verify that the ESP32 receives General Queries and responds with Membership Reports.
  4. Keep the broadcast transport as a fallback for networks with broken multicast forwarding.

There is an important Arduino-ESP32 regression risk. The current Arduino-ESP32 issue espressif/arduino-esp32#12408 reports that some recent versions can fail to receive the 224.0.0.1 all-systems multicast traffic needed for IGMP queries. This can prevent normal lwIP IGMP processing even when beginMulticast() succeeds. The issue applies especially to Ethernet multicast filtering, but it is worth testing on the exact Arduino-ESP32 version used by WLED.

For Wi-Fi power save, WiFiUDP has no API. Use the Arduino Wi-Fi API or ESP-IDF API after Wi-Fi starts.

The Arduino-ESP32 wrapper is:

`#include` <WiFi.h>

// Call after the station interface has started.
bool ok = WiFi.setSleep(false);  // Requests WIFI_PS_NONE

The direct ESP-IDF call is:

`#include` <esp_wifi.h>

// Call after the station interface has started.
esp_err_t err = esp_wifi_set_ps(WIFI_PS_NONE);
if (err != ESP_OK) {
  // Handle or log the failure.
}

To restore the normal default mode:

esp_wifi_set_ps(WIFI_PS_MIN_MODEM);

Espressif documents WIFI_PS_MIN_MODEM as the default power-save mode and WIFI_PS_NONE as the no-power-save mode in the esp_wifi_set_ps() API.

For WLED, this should be a global Wi-Fi policy, not an operation inside transmitAudioData(). Repeatedly switching power-save state while packets transmit is not appropriate. If WLED adds an AudioReactive setting, apply it after the Wi-Fi station starts and restore the configured policy when the usermod is disabled.

This setting affects only Wi-Fi station operation. It does not apply to Ethernet receivers. It can increase power consumption, so it should remain opt-in. It can reduce DTIM-related receive latency, but it will not repair missing IGMP queries or access-point multicast filtering.


🧠 Learnings used
Learnt from: softhack007
URL: https://github.com/wled/WLED/pull/5863

Timestamp: 2026-09-21T15:04:18.591Z
Learning: For `usermods/audioreactive/audio_reactive.cpp`, a UDP multicast receiver dropout near 260 seconds is consistent with the RFC 2236 IGMPv2 default Group Membership Interval (`2 × 125 s + 10 s`). Treat IGMP snooping without an effective querier as a plausible cause until packet capture or network configuration testing confirms it. The `audioSyncTransport` broadcast path is local-subnet-only and can still be affected by Wi-Fi broadcast suppression, rate limiting, or power-save behavior.

You are interacting with an AI system.

@softhack007

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@softhack007

Copy link
Copy Markdown
Member

// Call after the station interface has started.
esp_err_t err = esp_wifi_set_ps(WIFI_PS_NONE);

is the current WLED code for "disable Wi-Fi sleep" equivalent to this?

Yes. On ESP32, the current WLED implementation is equivalent in effect.

Short summary: yes it is. Its a strong recommendation to use "disable wifi sleep" whenever sound sync is used => improvement for WLED-Docs.

@softhack007

Copy link
Copy Markdown
Member

Sound sync only sends over multicast (239.0.0.1)

There is an important Arduino-ESP32 regression risk. The current Arduino-ESP32 issue espressif/arduino-esp32#12408 reports that some recent versions can fail to receive the 224.0.0.1 all-systems multicast traffic needed for IGMP queries.

@coderabbitai

  1. check if the tasmota and pioarduino platform versions used in our "V5" builds include the bugfix that allows to receive 224.0.0.1 all-systems multicast traffic
  2. check if the tasmota arduino-esp32 2.0.18 framework may have the same problem

The AudioReactive broadcast transport was computing the broadcast address
inline, which softhack007 rightly called fragile - and checking the core
turned up two different open-coded ways of doing it rather than a pattern
to follow.

Add broadcastIP() to WLEDNetworkClass, next to localIP(), subnetMask() and
gatewayIP(). It returns the directed broadcast of whichever interface is
up, falls back to the limited broadcast while disconnected, and gives us a
single place to revisit for IPv6.

The state notifier in udp.cpp now uses it too, replacing its open-coded
'~subnetMask | gatewayIP'.
@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PlatformIO configuration candidates ---'
fd -a -t f -i 'platformio|platform|package' . | head -n 160

printf '%s\n' '--- V5 environment definitions and platform references ---'
rg -n -i -C 4 '^\[env:.*v5|v5|tasmota|pioarduino|platform(_packages)?\s*=|framework-arduinoespressif32' \
  -g 'platformio*.ini' -g '*.ini' -g '*.json' -g '*.yml' -g '*.yaml' .

printf '%s\n' '--- Git submodules (if any) ---'
git submodule status || true

printf '%s\n' '--- Current repository revision ---'
git rev-parse HEAD
git remote -v | head -n 4

Length of output: 75396


🤖 web_search executed:

GitHub espressif/arduino-esp32 issue 12408 224.0.0.1 IGMP all-systems multicast fix pull request commit release version

Length of output: 13121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT

inspect_platform() {
  local name="$1"
  local url="$2"
  local archive="$workdir/$name.zip"
  local dir="$workdir/$name"

  echo "--- $name: download and package metadata ---"
  curl --fail --location --silent --show-error "$url" --output "$archive"
  unzip -q "$archive" -d "$dir"

  echo "Archive SHA-256:"
  sha256sum "$archive"

  echo "Relevant package declarations:"
  rg -n -i -C 3 \
    'framework-arduinoespressif32|arduino-esp32|version|url|package' \
    "$dir" -g 'platform.json' -g 'package.json' -g 'package.py' -g '*.json' \
    | head -n 220 || true

  echo "Embedded Ethernet fix symbols, if framework source is present:"
  rg -n -C 3 \
    'receiveAllMulticast|addMulticastFilter|ETH_CMD_S_ALL_MULTICAST' \
    "$dir" | head -n 160 || true
}

inspect_platform \
  tasmota \
  https://github.com/tasmota/platform-espressif32/releases/download/2026.05.50/platform-espressif32.zip

inspect_platform \
  pioarduino \
  https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip

echo "--- Upstream Arduino-ESP32 issue, PR, releases, and fix commit ancestry ---"
gh api repos/espressif/arduino-esp32/issues/12408 \
  --jq '{number, state, created_at, closed_at, body, pull_request}'
gh api repos/espressif/arduino-esp32/pulls/12409 \
  --jq '{number, state, merged_at, merge_commit_sha, base:{ref:.base.ref,sha:.base.sha}, body}'
merge_sha=$(gh api repos/espressif/arduino-esp32/pulls/12409 --jq '.merge_commit_sha')
echo "merge_sha=$merge_sha"
gh api "repos/espressif/arduino-esp32/commits/$merge_sha/branches-where-head" --paginate \
  --jq '.[] | {name}'
gh api repos/espressif/arduino-esp32/releases --paginate \
  --jq '.[] | select(.published_at >= "2026-02-28T00:00:00Z") | {tag_name, published_at, target_commitish, prerelease}' \
  | head -n 80

Length of output: 54530


🏁 Script executed:

#!/bin/bash
set -euo pipefail

workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT

inspect_framework() {
  local name="$1"
  local url="$2"
  local archive="$workdir/$name.tar.xz"
  local dir="$workdir/$name"

  echo "--- $name ---"
  curl --fail --location --silent --show-error "$url" --output "$archive"
  echo "SHA-256: $(sha256sum "$archive")"
  mkdir -p "$dir"
  tar -xJf "$archive" -C "$dir"

  echo "Package metadata:"
  fd -a -t f '^package\.json$' "$dir" -x sh -c 'echo "### $1"; cat "$1"' sh {} \; || true

  echo "Ethernet multicast filter implementation:"
  rg -n -C 8 \
    'receiveAllMulticast|addMulticastFilter|removeMulticastFilter|ETH_CMD_S_ALL_MULTICAST' \
    "$dir" -g 'ETH.cpp' -g 'ETH.h' || true
}

inspect_framework \
  tasmota_3_3_8_260506 \
  https://github.com/tasmota/arduino-esp32/releases/download/v3.3.8.260506/framework-arduinoespressif32.tar.xz

inspect_framework \
  official_3_3_9 \
  https://github.com/espressif/arduino-esp32/releases/download/3.3.9/esp32-core-3.3.9.tar.xz

echo "--- Official Arduino-ESP32 2.0.18 tag ---"
git clone --quiet --filter=blob:none --no-checkout https://github.com/espressif/arduino-esp32.git "$workdir/arduino"
git -C "$workdir/arduino" sparse-checkout set --no-cone libraries/Ethernet/src/ETH.cpp libraries/Ethernet/src/ETH.h package/package.json
git -C "$workdir/arduino" checkout --quiet 2.0.18

echo "Relevant 2.0.18 Ethernet initialization and multicast APIs:"
rg -n -C 10 \
  'begin\s*\(|receiveAllMulticast|addMulticastFilter|ETH_CMD_S_ALL_MULTICAST|esp_eth_ioctl' \
  "$workdir/arduino/libraries/Ethernet/src/ETH.cpp" \
  "$workdir/arduino/libraries/Ethernet/src/ETH.h" || true

Length of output: 2294


🏁 Script executed:

#!/bin/bash
set -euo pipefail

workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT

inspect_framework() {
  local name="$1"
  local url="$2"
  local archive="$workdir/$name.tar.xz"
  local dir="$workdir/$name"

  echo "--- $name ---"
  curl --fail --location --silent --show-error "$url" --output "$archive"
  echo "SHA-256: $(sha256sum "$archive")"
  mkdir -p "$dir"
  python3 - "$archive" "$dir" <<'PY'
import sys
import tarfile

archive, destination = sys.argv[1:]
with tarfile.open(archive, mode="r:xz") as tar:
    tar.extractall(destination, filter="data")
PY

  echo "Package metadata:"
  fd -a -t f '^package\.json$' "$dir" -x sh -c 'echo "### $1"; cat "$1"' sh {} \; || true

  echo "Ethernet multicast filter implementation:"
  rg -n -C 8 \
    'receiveAllMulticast|addMulticastFilter|removeMulticastFilter|ETH_CMD_S_ALL_MULTICAST' \
    "$dir" -g 'ETH.cpp' -g 'ETH.h' || true
}

inspect_framework \
  tasmota_3_3_8_260506 \
  https://github.com/tasmota/arduino-esp32/releases/download/v3.3.8.260506/framework-arduinoespressif32.tar.xz

inspect_framework \
  official_3_3_9 \
  https://github.com/espressif/arduino-esp32/releases/download/3.3.9/esp32-core-3.3.9.tar.xz

echo "--- Official Arduino-ESP32 2.0.18 tag ---"
git clone --quiet --filter=blob:none --no-checkout https://github.com/espressif/arduino-esp32.git "$workdir/arduino"
git -C "$workdir/arduino" sparse-checkout set --no-cone libraries/Ethernet/src/ETH.cpp libraries/Ethernet/src/ETH.h
git -C "$workdir/arduino" checkout --quiet 2.0.18

echo "Relevant 2.0.18 Ethernet initialization and multicast APIs:"
rg -n -C 10 \
  'begin\s*\(|receiveAllMulticast|addMulticastFilter|ETH_CMD_S_ALL_MULTICAST|esp_eth_ioctl' \
  "$workdir/arduino/libraries/Ethernet/src/ETH.cpp" \
  "$workdir/arduino/libraries/Ethernet/src/ETH.h" || true

Length of output: 28586


🏁 Script executed:

#!/bin/bash
set -euo pipefail

workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT

platform_url='https://github.com/tasmota/platform-espressif32/releases/download/2024.06.00/platform-espressif32.zip'
platform_zip="$workdir/platform.zip"
platform_dir="$workdir/platform"
curl --fail --location --silent --show-error "$platform_url" --output "$platform_zip"
unzip -q "$platform_zip" -d "$platform_dir"

platform_json=$(fd -a -t f '^platform\.json$' "$platform_dir" | head -n 1)
echo '--- Tasmota platform 2024.06.00 framework declaration ---'
python3 - "$platform_json" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as f:
    p = json.load(f)
print(p["packages"]["framework-arduinoespressif32"]["version"])
print(p["packages"]["framework-espidf"]["version"])
PY

framework_url=$(python3 - "$platform_json" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as f:
    print(json.load(f)["packages"]["framework-arduinoespressif32"]["version"])
PY
)

framework_archive="$workdir/framework.tar.xz"
framework_dir="$workdir/framework"
curl --fail --location --silent --show-error "$framework_url" --output "$framework_archive"
echo "Framework SHA-256: $(sha256sum "$framework_archive")"
mkdir -p "$framework_dir"
python3 - "$framework_archive" "$framework_dir" <<'PY'
import sys
import tarfile
with tarfile.open(sys.argv[1], mode="r:xz") as tar:
    tar.extractall(sys.argv[2], filter="data")
PY

echo '--- Package metadata ---'
fd -a -t f '^package\.json$' "$framework_dir" -x sh -c 'echo "### $1"; cat "$1"' sh {} \;

echo '--- Ethernet multicast-related implementation ---'
rg -n -C 10 \
  'receiveAllMulticast|addMulticastFilter|ETH_CMD_S_ALL_MULTICAST|ETH_CMD_[A-Z_]*MULTICAST|esp_eth_ioctl|esp_eth_start' \
  "$framework_dir" -g 'ETH.cpp' -g 'ETH.h' || true

Length of output: 3997


🏁 Script executed:

#!/bin/bash
set -euo pipefail

workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT

framework_url='https://github.com/tasmota/arduino-esp32/releases/download/2.0.18/framework-arduinoespressif32.zip'
framework_archive="$workdir/framework.zip"
framework_dir="$workdir/framework"
curl --fail --location --silent --show-error "$framework_url" --output "$framework_archive"
echo "Framework SHA-256: $(sha256sum "$framework_archive")"
mkdir -p "$framework_dir"
python3 - "$framework_archive" "$framework_dir" <<'PY'
import shutil
import sys
shutil.unpack_archive(sys.argv[1], sys.argv[2])
PY

echo '--- Package metadata ---'
fd -a -t f '^package\.json$' "$framework_dir" -x sh -c 'echo "### $1"; cat "$1"' sh {} \;

echo '--- Ethernet initialization and multicast-related implementation ---'
rg -n -C 10 \
  'receiveAllMulticast|addMulticastFilter|ETH_CMD_S_ALL_MULTICAST|ETH_CMD_[A-Z_]*MULTICAST|esp_eth_ioctl|esp_eth_start' \
  "$framework_dir" -g 'ETH.cpp' -g 'ETH.h' || true

echo '--- Ethernet begin implementations ---'
rg -n -C 20 'bool ETHClass::begin' "$framework_dir" -g 'ETH.cpp' || true

Length of output: 16617


@softhack007 I checked the exact framework archives referenced by platformio.ini.

1. V5 builds

Both V5 platform choices include the fix for Arduino-ESP32 issue #12408.

WLED V5 platform Framework Result
Tasmota 2026.05.50 Tasmota Arduino-ESP32 3.3.8+sha.f2a3fa2b, dated 2026-05-06 Fixed
pioarduino 55.03.39 Official Arduino-ESP32 3.3.9 Fixed

In both framework archives, libraries/Ethernet/src/ETH.cpp calls:

receiveAllMulticast(true);

after Ethernet initialization. The call is enabled for ESP-IDF 5.5.0 and later. It configures the Ethernet MAC to receive all multicast frames, including 224.0.0.1. This is the upstream repair from PR #12409.

The Tasmota V5 package contains this call at Lines 469 and 945. The pioarduino Arduino-ESP32 3.3.9 package contains it at Lines 467 and 943.

2. Tasmota Arduino-ESP32 2.0.18

The Tasmota V4 platform uses:

  • Arduino-ESP32 2.0.18+sha.8ba0dc0
  • ESP-IDF 4.4.8
  • Package date: 2024-06-29

This framework does not contain receiveAllMulticast(), addMulticastFilter(), or ETH_CMD_S_ALL_MULTICAST.

That is expected. The reported regression started in Arduino-ESP32 3.3.6 with the newer ESP-IDF 5.5 Ethernet behavior. The upstream fix is also conditionally compiled only for ESP-IDF 5.5.0 and later.

Therefore, the specific #12408 failure mode does not apply to Tasmota Arduino-ESP32 2.0.18. It predates the regression.

The upstream report is also Ethernet-specific. It used an ESP32 with a LAN8720 PHY and the ETH_DEF interface. It does not show a corresponding Wi-Fi receive-filter failure. Other multicast or IGMP failures can still occur on either framework due to network equipment or Wi-Fi behavior.


🧠 Learnings used
Learnt from: softhack007
URL: https://github.com/wled/WLED/pull/5863

Timestamp: 2026-09-21T15:04:18.591Z
Learning: For `usermods/audioreactive/audio_reactive.cpp`, a UDP multicast receiver dropout near 260 seconds is consistent with the RFC 2236 IGMPv2 default Group Membership Interval (`2 × 125 s + 10 s`). Treat IGMP snooping without an effective querier as a plausible cause until packet capture or network configuration testing confirms it. The `audioSyncTransport` broadcast path is local-subnet-only and can still be affected by Wi-Fi broadcast suppression, rate limiting, or power-save behavior.

You are interacting with an AI system.

if (ip[0] == 0) {
return IPAddress(255, 255, 255, 255); // not connected yet: limited broadcast
}
return IPAddress(uint32_t(ip) | ~uint32_t(subnetMask()));

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.

🤔 not sure this is the right solution. @netmindz @willmmiles what do you think?

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.

@coderabbitai other network features in the WLED core - especially UDP sync and real-time protocols - also include broadcast support. How do these modules find the right "broadcast" IP address to use?

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== WLEDNetwork broadcastIP declarations and call sites =='
rg -n -C 4 '\bbroadcastIP\s*\(' wled00 usermods --glob '*.{cpp,h,hpp,ino}' || true

printf '%s\n' '== Existing directed/limited broadcast address construction and broadcast send destinations =='
rg -n -i -C 5 'broadcast|255\s*,\s*255\s*,\s*255\s*,\s*255|subnetMask\s*\(|gatewayIP\s*\(' wled00 --glob '*.{cpp,h,hpp,ino}' || true

printf '%s\n' '== UDP notifier and real-time protocol send paths =='
rg -n -i -C 4 'notify|realtime|E1\.31|Art-?Net|DDP|TPM2' wled00 --glob '*.{cpp,h,hpp,ino}' || true

Length of output: 169880


@softhack007, the core does not use one common broadcast-address method in all modules.

  • WLED UDP sync notifications now send to WLEDNetwork.broadcastIP() in wled00/udp.cpp at Line 196. This produces the directed broadcast address from the active interface IP and subnet mask.
  • Node discovery sends to the fixed limited broadcast address 255.255.255.255 in sendSysInfoUDP() at wled00/udp.cpp Lines 730-733.
  • Network LED buses for DDP and Art-Net do not discover a broadcast address. BusNetwork stores a configured _client IP address and passes it to realtimeBroadcast(). The DDP and Art-Net send paths use that address directly. A user can configure a broadcast address as the destination.
  • E1.31, DDP, Art-Net, TPM2.NET, and Hyperion are primarily receiver paths in WLED. They bind and receive packets. They do not select a broadcast destination.
  • Art-Net poll replies go back to the poll sender’s IP address. They are not broadcast.

So, before this change, there was no consistent core helper for directed IPv4 broadcast. The new WLEDNetwork.broadcastIP() centralizes the correct calculation for the active Wi-Fi or Ethernet interface. It also replaces the state notifier’s previous inline calculation. The fixed 255.255.255.255 node-discovery behavior remains separate by design.

You are interacting with an AI system.

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.

@coderabbitai broadcast for the protocols you listed already works in the main branch.
a) is the proposed addition correct from UDP and IP protocol point of view?
b) is the suggested addition consistent with other protocols that already support broadcast?

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== PR checkout: broadcast-related sender/address code =='
rg -n -i -C 5 'broadcast|INADDR_BROADCAST|255\s*,\s*255\s*,\s*255\s*,\s*255|subnetMask\s*\(|gatewayIP\s*\(' \
  wled00 usermods \
  --glob '*.{cpp,h,hpp,ino}' \
  -g '!**/html_*.h' || true

printf '%s\n' '== GitHub main: candidate files and relevant lines =='
for path in \
  wled00/udp.cpp \
  wled00/src/dependencies/network/Network.cpp \
  wled00/src/dependencies/network/Network.h \
  wled00/bus_manager.cpp \
  wled00/e131.cpp \
  wled00/src/dependencies/network/Network.h
do
  printf '\n-- main:%s --\n' "$path"
  gh api "repos/wled/WLED/contents/$path?ref=main" --jq .content 2>/dev/null \
    | base64 -d 2>/dev/null \
    | rg -n -i -C 5 'broadcast|INADDR_BROADCAST|255\s*,\s*255\s*,\s*255\s*,\s*255|subnetMask\s*\(|gatewayIP\s*\(' || true
done

printf '%s\n' '== GitHub main: locate likely broadcast send code =='
gh api 'search/code?q=repo:wled/WLED+%28broadcast+OR+INADDR_BROADCAST%29' --paginate \
  --jq '.items[] | [.path, .html_url] | `@tsv`' 2>/dev/null || true

Length of output: 57817


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== PR checkout: broadcast-related sender/address code =='
rg -n -i -C 5 'broadcast|INADDR_BROADCAST|255\s*,\s*255\s*,\s*255\s*,\s*255|subnetMask\s*\(|gatewayIP\s*\(' \
  wled00 usermods \
  --glob '*.{cpp,h,hpp,ino}' \
  -g '!**/html_*.h' || true

printf '%s\n' '== GitHub main: candidate files and relevant lines =='
for path in \
  wled00/udp.cpp \
  wled00/src/dependencies/network/Network.cpp \
  wled00/src/dependencies/network/Network.h \
  wled00/bus_manager.cpp \
  wled00/e131.cpp
do
  printf '\n-- main:%s --\n' "$path"
  gh api "repos/wled/WLED/contents/$path?ref=main" --jq .content 2>/dev/null \
    | base64 -d 2>/dev/null \
    | rg -n -i -C 5 'broadcast|INADDR_BROADCAST|255\s*,\s*255\s*,\s*255\s*,\s*255|subnetMask\s*\(|gatewayIP\s*\(' || true
done

printf '%s\n' '== GitHub main: locate likely broadcast send code =='
gh api 'search/code?q=repo:wled/WLED+%28broadcast+OR+INADDR_BROADCAST%29' --paginate \
  --jq '.items[] | [.path, .html_url] | `@tsv`' 2>/dev/null || true

Length of output: 57495


🏁 Script executed:

#!/bin/bash
set -euo pipefail
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

for path in wled00/udp.cpp wled00/src/dependencies/network/Network.cpp wled00/bus_manager.cpp wled00/e131.cpp; do
  out="$tmpdir/$(basename "$path")"
  if gh api "repos/wled/WLED/contents/$path?ref=main" --jq .content | base64 -d > "$out"; then
    echo "===== main/$path ====="
    rg -n -i -C 6 'broadcast|255\.255\.255\.255|255\s*,\s*255\s*,\s*255\s*,\s*255|subnetMask\s*\(|gatewayIP\s*\(' "$out" || true
  fi
done

echo '===== PR checkout: exact prior state-notifier code context ====='
sed -n '175,210p' wled00/udp.cpp

Length of output: 9694


@softhack007, the inspection confirms that the proposed calculation is both protocol-correct and already used by an existing WLED UDP sender.

  • main uses ~subnetMask() | gatewayIP() for core UDP state notifications in wled00/udp.cpp at Line 196.
  • usermods/udp_name_sync/udp_name_sync.cpp already uses localIP() | ~subnetMask() at Line 39.
  • The PR moves the state notifier to WLEDNetwork.broadcastIP() and uses the same localIP() | ~subnetMask() calculation.

For a normal configuration, where the gateway is inside the active interface subnet, these expressions produce the same directed broadcast address. For example, both produce 192.168.1.255 on 192.168.1.0/24.

The localIP() form is the better source for this helper. A directed broadcast address derives from the active interface network, not from the default gateway. It also works when the interface has no gateway, and it aligns with the existing udp_name_sync usermod.

The fixed 255.255.255.255 address in sendSysInfoUDP() is intentionally different. It is a limited broadcast for node discovery. The network LED buses also support broadcast because BusNetwork forwards its configured _client destination to realtimeBroadcast(). They do not calculate a destination address internally.

Therefore, this PR is consistent with existing WLED behavior:

  1. It preserves node discovery’s limited broadcast.
  2. It preserves configured broadcast destinations for network LED buses.
  3. It centralizes the directed-broadcast calculation already used by UDP name sync.
  4. It replaces the core notifier’s gateway-based variant with the more direct interface-based variant.

You are interacting with an AI system.

@softhack007 softhack007 Sep 21, 2026 •

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.

@efranceschi OK, this looks reasonable from my perspective, especially as you have already done some confidence testing with your code. But I would like to hear the opinion of other maintainers (@DedeHai @willmmiles @netmindz) before we add it to the WLED core.

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'm happy to take this addition. Moving this calculation to our network utils library so it can be used consistently is a good idea, and localIP() is strictly superior to gatewayIP() for this purpose.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks. One detail that matters for the case @softhack007 was asking about: when there is no station connection - AP mode, or STA down - localIP() is 0.0.0.0, so broadcastIP() returns the limited broadcast 255.255.255.255 instead. The old expression produced ~mask | 0 = 0.0.0.255 there, which goes nowhere.

Comment thread wled00/udp.cpp
DEBUG_PRINTLN(F("UDP sending packet."));
IPAddress broadcastIp = ~uint32_t(WLEDNetwork.subnetMask()) | uint32_t(WLEDNetwork.gatewayIP());
notifierUdp.beginPacket(broadcastIp, udpPort);
notifierUdp.beginPacket(WLEDNetwork.broadcastIP(), udpPort);

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.

@coderabbitai please suggest a testcase to validate this change (common case = same behaviour, special case = behaviour improvement)

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.

@softhack007, use these two tests for notify().

  1. Common case: retain the current directed-broadcast behavior

    Configure a connected interface with:

    • Local IP: 192.168.10.42
    • Subnet mask: 255.255.255.0
    • Gateway: 192.168.10.1
    • udpConnected = true

    Trigger notify() with an enabled notification mode.

    Verify that notifierUdp.beginPacket() receives:

    destination = 192.168.10.255
    port = udpPort
    

    This validates that the new WLEDNetwork.broadcastIP() call preserves the previous result on a normal /24 network.

  2. Special case: no gateway, but a valid local interface address

    Configure a connected interface with:

    • Local IP: 192.168.10.42
    • Subnet mask: 255.255.255.0
    • Gateway: 0.0.0.0
    • udpConnected = true

    Trigger notify() again.

    Verify that notifierUdp.beginPacket() receives:

    destination = 192.168.10.255
    port = udpPort
    

    The previous expression would produce 0.0.0.255 because it uses gatewayIP(). The new implementation must derive the directed broadcast address from localIP() and subnetMask(). This is the behavior improvement.

A useful additional assertion is a non-/24 subnet, for example 192.168.10.42 with mask 255.255.252.0. The expected destination is 192.168.11.255. This confirms that the mask calculation is not limited to /24 networks.

You are interacting with an AI system.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ran both cases on hardware. Sender: ESP32 running this branch, four stock 16.0.1 receivers, colour sync measured by polling each receiver after every change.

Common case (local IP 192.168.68.95, mask 255.255.255.0, gateway 192.168.68.1): 12/12 changes delivered to all four receivers, before and after the change. Same behaviour, as expected - both expressions yield 192.168.68.255 here.

Special case as suggested (gateway 0.0.0.0 with a static IP): not reachable through WLED's own config. I set it up expecting the old expression to break, and it did not - 12/12 again. The reason is wled.cpp:794:

if (uint32_t(multiWiFi[selectedWiFi].staticIP) != 0U && uint32_t(multiWiFi[selectedWiFi].staticGW) != 0U) {
  WiFi.config(...);
}

A static IP without a gateway is silently ignored and the node falls back to DHCP, so gatewayIP() never actually returns zero that way.

Where the two do diverge is when there is no station connection at all - AP mode, or the STA interface down - because gatewayIP() then returns INADDR_NONE:

old expression WLEDNetwork.broadcastIP()
STA up 192.168.68.255 192.168.68.255
no STA (AP mode) ~mask | 0 = 0.0.0.255 255.255.255.255

So the regression test is "same address while connected", which I verified, and the improvement shows up in AP mode. I did not exercise that end to end, since it would mean moving the receivers onto the sender's own AP.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@softhack007

This comment was marked as outdated.

@coderabbitai

This comment was marked as outdated.

@softhack007

Copy link
Copy Markdown
Member

@coderabbitai

This comment was marked as resolved.

@softhack007

Copy link
Copy Markdown
Member

Replacing Line 1560 with fftUdp.begin(audioSyncPort) would remove the multicast group membership and break multicast reception.

@coderabbitai I think this was the intention of the PR: either use multicast, or use broadcast. Otherwise there would be no difference on the receiver side, and the "transport" option would only affect behaviour of the sender.

@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

target=$(fd -a -t f '^audio_reactive\.cpp$' usermods | head -n 1)
if [ -z "$target" ]; then
  echo "audio_reactive.cpp not found" >&2
  exit 1
fi

echo "--- relevant symbol outline ---"
ast-grep outline "$target" --items all --match 'audioSyncTransport|beginMulticast|transmitAudioData|readFromConfig|addToConfig' --view expanded

echo "--- transport, socket, receive, and config contexts ---"
rg -n -C 14 'audioSyncTransport|beginMulticast|fftUdp\.begin\s*\(|parsePacket\s*\(|remoteIP\s*\(|transmitAudioData\s*\(|readFromConfig\s*\(|addToConfig\s*\(' "$target"

echo "--- PR files and diff ---"
git diff --find-renames origin/main...HEAD -- "$target" 2>/dev/null || git diff --find-renames HEAD^...HEAD -- "$target"

Length of output: 23798


@softhack007 The shell output confirms the correction.

audioSyncTransport currently affects only transmitAudioData() at Line 1267. The receiver always calls fftUdp.beginMulticast(...) at Line 1560.

For the intended mutually exclusive transport mode, connected() also needs to use audioSyncTransport:

if (audioSyncTransport == 1) {
  udpSyncConnected = fftUdp.begin(audioSyncPort);
} else {
`#ifdef` ARDUINO_ARCH_ESP32
  udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort);
`#else`
  udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort);
`#endif`
}

This change ensures that broadcast-mode receivers do not join the multicast group. It also makes the setting a true transport selection on both sender and receiver.

The setting takes effect when connected() runs. A configuration update must reconnect the UDP socket, or users must reboot or reconnect Wi-Fi after they change the transport setting.


🧠 Learnings used
Learnt from: softhack007
URL: https://github.com/wled/WLED/pull/5863

Timestamp: 2026-09-21T15:04:18.591Z
Learning: For `usermods/audioreactive/audio_reactive.cpp`, a UDP multicast receiver dropout near 260 seconds is consistent with the RFC 2236 IGMPv2 default Group Membership Interval (`2 × 125 s + 10 s`). Treat IGMP snooping without an effective querier as a plausible cause until packet capture or network configuration testing confirms it. The `audioSyncTransport` broadcast path is local-subnet-only and can still be affected by Wi-Fi broadcast suppression, rate limiting, or power-save behavior.

You are interacting with an AI system.

@softhack007

softhack007 commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Line 1560 does not need a broadcast-specific change.
For Arduino-ESP32, WiFiUDP::beginMulticast() first binds the socket to all local IPv4 addresses on audioSyncPort. The socket can therefore receive all transports: multicast, directed-broadcast, or direct connections (no broadcast).

@netmindz this means the receiver can actually handle everything (the UDP port is always used correctly). Maybe we should keep this behaviour - its fail-safe and users only need to set the transport option on the sender side. What do you think?

@efranceschi

Copy link
Copy Markdown
Author

Confirmed the root cause, with a test rather than an inference.

The network has no IGMP querier. I ran an IGMPv2 General Query from a laptop on the same subnet. The detail that matters for anyone reproducing this: the query is ignored unless it carries the IP Router Alert option (RFC 2113) - my first attempt without it changed nothing at all.

With a proper query every 30 s, receivers that had already been pruned came back on their own, without rebooting:

13:56 - 13:59   all four idle          (no querier)
14:02           all four receiving     (querier running)
14:03 - 14:11   still receiving        (~450 s, no reboots)

Reception went from 50% of samples to 98%, purely by adding queries to the network.

That also settles the regression question. They only recovered because they answered the query, so on this firmware (16.0.1, core 4.4.8.240628) the IGMP responder works and espressif/arduino-esp32#12408 does not explain the behaviour here. Still worth checking separately on V5 builds, where the core is 3.x based.

On power save: all five nodes already run with wifi.sleep = false, so WIFI_PS_NONE is not what is missing in this case - though I agree it belongs in the sound sync docs.

On keeping IGMP happy from the firmware side: the usermod joins once in connected(), and connectUDPSoundSync() returns early while udpSyncConnected is true, so a receiver never re-announces itself. Re-joining every 120 s fixed it here: 495 s of uninterrupted reception, no idle sample at all, while unmodified receivers on the same network stayed dead. It is fftUdp.stop() plus connected(), so sampling and FFT are untouched - the objection raised against the workaround in #4408. Happy to open it as a separate PR if you want it.

@softhack007

Copy link
Copy Markdown
Member

Re-joining every 120 s fixed it here: 495 s of uninterrupted reception, no idle sample at all, while unmodified receivers on the same network stayed dead. It is fftUdp.stop() plus connected(), so sampling and FFT are untouched - the objection raised against the workaround in #4408. Happy to open it as a separate PR if you want it.

@efranceschi not necessary to add a PR for this - we will soon migrate to the out-of-tree audioreactive (WLED-MM based). The out-of-tree AR already has reconnection logic.

@softhack007

Copy link
Copy Markdown
Member

I ran an IGMPv2 General Query from a laptop on the same subnet.

@efranceschi "I" = your AI coding agent? I'm just asking because we usually prefer to talk directly, instead of remotely driving an AI session...

Comment thread usermods/audioreactive/audio_reactive.cpp
@netmindz

Copy link
Copy Markdown
Member

@softhack007 We are duplicating trying to address issues we have already addressed in the MM version.

AC is missing reconnection code and i have also added broadcast support to work around various multicast issues with some networks

@DedeHai

DedeHai commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

@softhack007 I agree with @netmindz, if any changes from this PR can be used in upstream MM AR, they should be added there. AC AR is currently in a limbo state, I could merge my two pending PR's then update the code to use the ADC manager just to serve as a reference for the changes to port them to MM later.

@netmindz netmindz 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.

Not an issue with the code as such, just duplication, so don't then want issue of breaking change when we swap

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

Labels

AI Partly generated by an AI. Make sure that the contributor fully understands the code! connectivity Issue regarding protocols, WiFi connection or availability of interfaces usermod usermod related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants