From 015e2e2a2e755b9d549d0d40fb9111d13eb9cd5c Mon Sep 17 00:00:00 2001 From: kelvtech-co-uk Date: Thu, 21 May 2026 23:49:41 +0000 Subject: [PATCH 1/5] Add independent Ethernet/WiFi IP config and primary network interface selection - Add separate static IP/gateway/subnet configuration for Ethernet interface independent of WiFi static IP configuration - Add primary network interface selection to control which interface lwIP uses for outbound connections (MQTT, NTP, internet access) - Implement setPrimaryNetworkInterface() using netif_set_default() to set lwIP default netif based on user selection - Keep both interfaces active simultaneously when Ethernet connects - Show both interface IPs in UI when both are active - Update WiFi & Network settings UI with Ethernet IP config section and Primary Network Interface selector - Ethernet IP config section hidden when Ethernet Type is None Resolves: ethernet static IP not applying independently of WiFi config Related: #5247 Known limitations: - mDNS resolves to primary interface IP only; per-interface mDNS hostname registration is a candidate for follow-on work - Dual interface operation has been validated on boards using ETH_CLOCK_GPIO0_IN (external clock source). Boards using ETH_CLOCK_GPIO17_OUT may experience instability when WiFi and Ethernet operate simultaneously due to ESP32 PLL instability described in #4703. A future enhancement could detect the board clock mode and conditionally disable WiFi on affected boards, preserving existing single-interface behaviour for those users. --- wled00/cfg.cpp | 32 +++++++++- wled00/data/settings_wifi.htm | 46 ++++++++++++-- wled00/fcn_declare.h | 7 +++ wled00/network.cpp | 112 +++++++++++++++++++++++++++++++--- wled00/set.cpp | 19 ++++++ wled00/wled.cpp | 39 +++++++++++- wled00/wled.h | 12 ++++ wled00/xml.cpp | 52 +++++++++++++--- 8 files changed, 296 insertions(+), 23 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 2e458e7da9..4e9d021e10 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -135,7 +135,22 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { #ifdef WLED_USE_ETHERNET JsonObject ethernet = doc[F("eth")]; CJSON(ethernetType, ethernet["type"]); - // NOTE: Ethernet configuration takes priority over other use of pins + // AI: deserialize ethernet static IP configuration. + // Each address is stored as a 4-element JSON array, one byte per octet, + // matching the same pattern used for WiFi static IP (nw.ins[n].ip/gw/sn). + JsonArray eth_ip = ethernet[F("eip")]; + JsonArray eth_gw = ethernet[F("egw")]; + JsonArray eth_sn = ethernet[F("esn")]; + if (!eth_ip.isNull()) { + for (size_t i = 0; i < 4; i++) { + CJSON(ethStaticIP[i], eth_ip[i]); + CJSON(ethStaticGW[i], eth_gw[i]); + CJSON(ethStaticSN[i], eth_sn[i]); + } + } + // AI: deserialize primary network interface selection. + // false = WiFi is default gateway (default), true = Ethernet is default gateway. + CJSON(ethPrimaryInterface, ethernet[F("epi")]); initEthernet(); #endif @@ -935,6 +950,21 @@ void serializeConfig(JsonObject root) { break; } } + // AI: serialize ethernet static IP configuration. + // Stored as 4-element JSON arrays, one byte per octet, consistent + // with the WiFi static IP serialisation pattern in nw.ins[n].ip/gw/sn. + // Only written when an ethernet board type is configured. + JsonArray eth_ip = ethernet.createNestedArray(F("eip")); + JsonArray eth_gw = ethernet.createNestedArray(F("egw")); + JsonArray eth_sn = ethernet.createNestedArray(F("esn")); + for (size_t i = 0; i < 4; i++) { + eth_ip.add(ethStaticIP[i]); + eth_gw.add(ethStaticGW[i]); + eth_sn.add(ethStaticSN[i]); + } + // AI: serialize primary network interface selection. + // false = WiFi is primary, true = Ethernet is primary. + ethernet[F("epi")] = ethPrimaryInterface; #endif JsonObject hw = root.createNestedObject(F("hw")); diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm index e187f887fb..1b78e19a16 100644 --- a/wled00/data/settings_wifi.htm +++ b/wled00/data/settings_wifi.htm @@ -133,12 +133,14 @@ Identity:

`; } + // AI: removed "Also used by Ethernet" note from WiFi static IP label + // WiFi and Ethernet now have independent IP configurations var b = `

Network name (SSID${i==0?", empty to not connect":""}):
0?"required":""}>
${encryptionTypeField} Network password:

BSSID (optional):

-Static IP (leave at 0.0.0.0 for DHCP)${i==0?"
Also used by Ethernet":""}:
+Static IP (leave at 0.0.0.0 for DHCP):
...
Static gateway:
...
@@ -166,6 +168,8 @@ function tE() { // keep the hidden input with MAC addresses, only toggle visibility of the list UI gId('rlc').style.display = d.Sf.RE.checked ? 'block' : 'none'; + // AI: also refresh ethernet IP section visibility on page init + toggleEthIP(); } // reset remotes: initialize empty list (called from xml.cpp) function rstR() { @@ -207,6 +211,18 @@ } } + // AI: toggleEthIP() shows or hides the Ethernet static IP configuration + // section based on whether an ethernet board type is selected. + // Called on page init (via tE) and on ethernet type dropdown change. + function toggleEthIP() { + const ethSelect = d.Sf.ETH; + if (!ethSelect) return; + const ethIpSection = gId('ethip'); + if (!ethIpSection) return; + // AI: value "0" means "None" - no ethernet board selected, hide IP config + ethIpSection.style.display = (ethSelect.value !== "0") ? 'block' : 'none'; + } + @@ -228,7 +244,8 @@

Wireless network

Ethernet Type

- @@ -246,7 +263,27 @@

Ethernet Type



-
+ + +

DNS & mDNS

DNS server address:
@@ -254,7 +291,8 @@

DNS & mDNS


mDNS address (leave empty for no mDNS):
http:// .local
- Client IP: Not connected
+ + Active IP(s): Not connected

Configure Access Point

diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 6201a19192..c7f3bbed0d 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -301,6 +301,13 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs //network.cpp bool initEthernet(); // result is informational +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) +bool initEthernet(); +// AI: sets lwIP primary network interface based on ethPrimaryInterface +// by enabling wled to be dual-homed we need to be deterministic about which interface +// wled uses for outgoing connections to minimise asyncronous routing issues. +void setPrimaryNetworkInterface(); +#endif int getSignalQuality(int rssi); void fillMAC2Str(char *str, const uint8_t *mac); void fillStr2MAC(uint8_t *mac, const char *str); diff --git a/wled00/network.cpp b/wled00/network.cpp index e4d3378f0e..560ca60fbe 100644 --- a/wled00/network.cpp +++ b/wled00/network.cpp @@ -4,6 +4,8 @@ #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) +#include "lwip/netif.h" // AI: required for netif_set_default() and netif_list +#include "lwip/tcpip.h" // AI: required for LOCK_TCPIP_CORE/UNLOCK_TCPIP_CORE // The following six pins are neither configurable nor // can they be re-assigned through IOMUX / GPIO matrix. // See https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-ethernet-kit-v1.1.html#ip101gri-phy-interface @@ -274,18 +276,93 @@ bool initEthernet() } // https://github.com/wled/WLED/issues/5247 - if (multiWiFi[0].staticIP != (uint32_t)0x00000000 && multiWiFi[0].staticGW != (uint32_t)0x00000000) { - ETH.config(multiWiFi[0].staticIP, multiWiFi[0].staticGW, multiWiFi[0].staticSN, dnsAddress); + // AI: apply ethernet static IP configuration using the new dedicated + // ethernet IP variables (ethStaticIP, ethStaticGW, ethStaticSN) rather than + // sharing the first WiFi network's static IP config as was previously done. + // ethStaticIP of 0.0.0.0 means use DHCP for ethernet. + // Gateway of 0.0.0.0 is valid — means no default route via ethernet, + // lwIP will only install a subnet route for the ethernet interface. + if ((uint32_t)ethStaticIP != 0x00000000) { + // AI: always pass the configured gateway to ETH.config(). + // Default route selection between interfaces is handled by netif_set_default() + // in setPrimaryNetworkInterface(). Gateway of 0.0.0.0 is explicitly supported + // for users who want ethernet as a stub interface with no onward routing. + ETH.config(ethStaticIP, ethStaticGW, ethStaticSN, dnsAddress); + DEBUG_PRINTF_P(PSTR("initE: Static IP configured. IP=%d.%d.%d.%d GW=%d.%d.%d.%d PNI=%s\n"), + ethStaticIP[0], ethStaticIP[1], ethStaticIP[2], ethStaticIP[3], + ethStaticGW[0], ethStaticGW[1], ethStaticGW[2], ethStaticGW[3], + ethPrimaryInterface ? "ETH" : "WiFi"); } else { + // AI: no static IP configured, use DHCP for ethernet ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); + DEBUG_PRINTLN(F("initE: DHCP configured for ethernet")); } successfullyConfiguredEthernet = true; DEBUG_PRINTLN(F("initE: *** Ethernet successfully configured! ***")); return true; } -#endif +// AI: setPrimaryNetworkInterface() explicitly sets the lwIP primary +// network interface based on the user's ethPrimaryInterface selection. +// Directly tells lwIP which netif to use for outbound traffic, resolving +// asymmetric routing issues where reply packets were routed out the wrong +// interface when both WiFi and Ethernet are active simultaneously. +// Interfaces are identified by name prefix ('en'=ethernet, 'st'=WiFi STA) +// which works correctly for both static IP and DHCP configurations. +// Called from multiple network events to ensure it fires after whichever +// interface comes up last. + +// AI: below section was generated by an AI +void setPrimaryNetworkInterface() { + struct netif *netif_iter; + struct netif *target = nullptr; + struct netif *fallback = nullptr; + + // AI: interface name prefixes in arduino-esp32 IDF V4 (Tasmota platform): + // 'en' = ethernet, 'st' = WiFi STA. Validated on IDF 4.4.8. + const char *targetName = ethPrimaryInterface ? + "en" : + "st"; + + // AI: acquire lwIP TCP/IP core lock before accessing netif_list + // and calling netif_set_default() to avoid thread-safety assertions + LOCK_TCPIP_CORE(); + + for (netif_iter = netif_list; netif_iter != NULL; netif_iter = netif_iter->next) { + if (!netif_is_up(netif_iter) || netif_iter->ip_addr.u_addr.ip4.addr == 0) continue; + const bool isPreferred = (netif_iter->name[0] == targetName[0] && + netif_iter->name[1] == targetName[1]); + if (isPreferred) { + target = netif_iter; + break; + } + if (!fallback) fallback = netif_iter; + } + + // AI: if preferred interface unavailable, fall back to any ready interface + // prevents outbound traffic being pinned to a dead default netif + if (!target && fallback) { + target = fallback; + DEBUG_PRINTLN(F("setPNI: Preferred interface unavailable, using fallback")); + } + + if (target != nullptr) { + netif_set_default(target); + DEBUG_PRINTF_P(PSTR("setPNI: Primary netif set to %c%c%d (%d.%d.%d.%d)\n"), + target->name[0], target->name[1], target->num, + ip4_addr1(&target->ip_addr.u_addr.ip4), + ip4_addr2(&target->ip_addr.u_addr.ip4), + ip4_addr3(&target->ip_addr.u_addr.ip4), + ip4_addr4(&target->ip_addr.u_addr.ip4)); + } else { + DEBUG_PRINTLN(F("setPNI: No ready interface found, will retry on next IP event")); + } + + UNLOCK_TCPIP_CORE(); +} +#endif +// AI: end //by https://github.com/tzapu/WiFiManager/blob/master/WiFiManager.cpp int getSignalQuality(int rssi) @@ -383,6 +460,7 @@ bool isWiFiConfigured() { #define ARDUINO_EVENT_WIFI_SCAN_DONE SYSTEM_EVENT_SCAN_DONE #define ARDUINO_EVENT_ETH_START SYSTEM_EVENT_ETH_START #define ARDUINO_EVENT_ETH_CONNECTED SYSTEM_EVENT_ETH_CONNECTED + #define ARDUINO_EVENT_ETH_GOT_IP SYSTEM_EVENT_ETH_GOT_IP // AI: added for DHCP ethernet IP assignment event #define ARDUINO_EVENT_ETH_DISCONNECTED SYSTEM_EVENT_ETH_DISCONNECTED #endif @@ -431,6 +509,11 @@ void WiFiEvent(WiFiEvent_t event) break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: DEBUG_PRINT(F("WiFi-E: IP address: ")); DEBUG_PRINTLN(Network.localIP()); + // AI: re-evaluate primary network interface when WiFi gets its IP + // handles both static IP and DHCP scenarios for WiFi interface + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) + setPrimaryNetworkInterface(); + #endif break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: // followed by IDLE and SCAN_DONE @@ -465,17 +548,30 @@ void WiFiEvent(WiFiEvent_t event) case ARDUINO_EVENT_ETH_CONNECTED: { DEBUG_PRINTLN(F("ETH-E: Connected")); - if (!apActive) { - WiFi.disconnect(true); // disable WiFi entirely - } - char hostname[64] = {'\0'}; // any "hostname" within a Fully Qualified Domain Name (FQDN) must not exceed 63 characters - getWLEDhostname(hostname, sizeof(hostname), true); // create DNS name based on mDNS name if set, or fall back to standard WLED server name + // AI: WiFi is intentionally kept active when ethernet connects. + // Previously WiFi was disabled here to prevent routing conflicts, but + // with dual-interface support, netif_set_default() handles routing + // preference between interfaces. Disabling WiFi here would defeat the + // purpose of the feature entirely. + char hostname[64] = {'\0'}; + getWLEDhostname(hostname, sizeof(hostname), true); ETH.setHostname(hostname); + // AI: attempt to set default gateway interface on ethernet connect + setPrimaryNetworkInterface(); showWelcomePage = false; break; } + case ARDUINO_EVENT_ETH_GOT_IP: + // AI: ethernet DHCP IP assigned — now safe to set default netif + // this event is the reliable trigger for DHCP ethernet configuration + DEBUG_PRINT(F("ETH-E: Got IP: ")); DEBUG_PRINTLN(ETH.localIP()); + setPrimaryNetworkInterface(); + break; case ARDUINO_EVENT_ETH_DISCONNECTED: DEBUG_PRINTLN(F("ETH-E: Disconnected")); + // AI: re-evaluate primary network interface on ethernet disconnect + // ensures fallback to WiFi if ethernet was the primary interface + setPrimaryNetworkInterface(); // This doesn't really affect ethernet per se, // as it's only configured once. Rather, it // may be necessary to reconnect the WiFi when diff --git a/wled00/set.cpp b/wled00/set.cpp index fb516ac7d6..66eef749a6 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -147,6 +147,25 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) ethernetType = request->arg(F("ETH")).toInt(); + // AI: read ethernet static IP configuration from form POST. + // Each IP address is submitted as four separate octet fields (0-255), + // reassembled here into IPAddress objects matching the same pattern + // used for WiFi static IP fields (IP{n}0-3, GW{n}0-3, SN{n}0-3). + if (request->hasArg(F("EIP0"))) { + for (int i = 0; i < 4; i++) { + char eip[6], egw[6], esn[6]; + snprintf_P(eip, sizeof(eip), PSTR("EIP%d"), i); + snprintf_P(egw, sizeof(egw), PSTR("EGW%d"), i); + snprintf_P(esn, sizeof(esn), PSTR("ESN%d"), i); + ethStaticIP[i] = request->arg(eip).toInt(); + ethStaticGW[i] = request->arg(egw).toInt(); + ethStaticSN[i] = request->arg(esn).toInt(); + } + } + // AI: read primary network interface selection. + // PNI field value 0 = WiFi is primary interface, 1 = Ethernet is primary interface. + // Radio buttons only submit when selected so use hasArg with value check. + ethPrimaryInterface = (request->arg(F("EPI")).toInt() == 1); initEthernet(); #endif } diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 493c687700..a61d633d01 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -715,8 +715,26 @@ void WLED::initConnection() WiFi.setHostname(hostname); #endif - if (multiWiFi[selectedWiFi].staticIP != 0U && multiWiFi[selectedWiFi].staticGW != 0U) { +// AI: below section was generated by an AI ... + if (multiWiFi[selectedWiFi].staticIP != 0U) { + // AI: apply WiFi static IP configuration. + // Always pass the configured gateway to WiFi.config(). + // Default route selection between interfaces is handled by netif_set_default() + // in setPrimaryNetworkInterface(). Gateway of 0.0.0.0 is explicitly supported + // for users who want WiFi as a stub interface with no onward routing. + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) WiFi.config(multiWiFi[selectedWiFi].staticIP, multiWiFi[selectedWiFi].staticGW, multiWiFi[selectedWiFi].staticSN, dnsAddress); + DEBUG_PRINTF_P(PSTR("initC: WiFi static IP. IP=%d.%d.%d.%d GW=%d.%d.%d.%d PNI=%s\n"), + multiWiFi[selectedWiFi].staticIP[0], multiWiFi[selectedWiFi].staticIP[1], + multiWiFi[selectedWiFi].staticIP[2], multiWiFi[selectedWiFi].staticIP[3], + multiWiFi[selectedWiFi].staticGW[0], multiWiFi[selectedWiFi].staticGW[1], + multiWiFi[selectedWiFi].staticGW[2], multiWiFi[selectedWiFi].staticGW[3], + ethPrimaryInterface ? "ETH" : "WiFi"); + #else + // AI: no ethernet support compiled in, use WiFi gateway normally + WiFi.config(multiWiFi[selectedWiFi].staticIP, multiWiFi[selectedWiFi].staticGW, multiWiFi[selectedWiFi].staticSN, dnsAddress); + #endif +// AI: end comments } else { WiFi.config(IPAddress((uint32_t)0), IPAddress((uint32_t)0), IPAddress((uint32_t)0)); } @@ -870,9 +888,19 @@ void WLED::initInterfaces() e131.begin(e131Multicast, e131Port, e131Universe, E131_MAX_UNIVERSE_COUNT); ddp.begin(false, DDP_DEFAULT_PORT); reconnectHue(); + +// AI: below section was generated by an AI ... + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) + // AI: final attempt to set primary network interface after all + // interfaces are initialised. Catches the case where ethernet configured + // synchronously at boot before ETH events fired, meaning setPrimaryNetworkInterface() + // called from ETH_CONNECTED/ETH_GOT_IP events was too early. + setPrimaryNetworkInterface(); + #endif interfacesInited = true; wasConnected = true; } +// AI: end comments void WLED::handleConnection() { @@ -973,7 +1001,16 @@ void WLED::handleConnection() sendImprovStateResponse(0x04); if (improvActive > 1) sendImprovIPRPCResult(ImprovRPCType::Command_Wifi); } + // AI: below section was generated by an AI ... initInterfaces(); + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) + // AI: additional setPrimaryNetworkInterface() call after interfaces + // are fully initialised. ETH events fire before WiFi.onEvent() is registered + // during boot, so earlier calls in the event handler may miss the ethernet + // netif. By this point both interfaces should be up and visible in netif_list. + setPrimaryNetworkInterface(); + #endif + // AI: end comments userConnected(); UsermodManager::connected(); lastMqttReconnectAttempt = 0; // force immediate update diff --git a/wled00/wled.h b/wled00/wled.h index 1a5f1b143e..5ba366e88c 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -390,6 +390,18 @@ WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_19_5dBm); #else WLED_GLOBAL int ethernetType _INIT(WLED_ETH_NONE); // use none for ethernet board type if default not defined #endif +// AI: below section was generated by an AI + // AI: separate static IP configuration for ethernet interface. + // These are independent of the WiFi static IP fields (staticIP/staticGW/staticSN) + // which live in the WiFiConfig struct inside multiWiFi. + // All three default to 0.0.0.0 which means DHCP will be used for ethernet. + WLED_GLOBAL IPAddress ethStaticIP _INIT_N(((0, 0, 0, 0))); + WLED_GLOBAL IPAddress ethStaticGW _INIT_N(((0, 0, 0, 0))); + WLED_GLOBAL IPAddress ethStaticSN _INIT_N(((255, 255, 255, 0))); + // AI: primary network interface selection. + // use for unknown network communications e.g. internet access, ntp, mqtt + WLED_GLOBAL bool ethPrimaryInterface _INIT(false); +// AI: end #endif // LED CONFIG diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 812ef8c207..da55199164 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -131,6 +131,7 @@ static void appendGPIOinfo(Print& settingsScript) settingsScript.print(hardwareTX); // debug output (TX) pin firstPin = false; #endif + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { if (!firstPin) settingsScript.print(','); @@ -288,28 +289,61 @@ void getSettingsJS(byte subPage, Print& settingsScript) settingsScript.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif + // AI: below section was generated by an AI #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - printSetFormValue(settingsScript,PSTR("ETH"),ethernetType); + printSetFormValue(settingsScript,PSTR("ETH"),ethernetType); + // AI: populate ethernet static IP fields with current saved values. + // Each IPAddress is output as four separate octet values matching the + // EIP0-3, EGW0-3, ESN0-3 field naming convention in settings_wifi.htm. + for (int i = 0; i < 4; i++) { + char key[5]; + snprintf_P(key, sizeof(key), PSTR("EIP%d"), i); + printSetFormValue(settingsScript, key, ethStaticIP[i]); + snprintf_P(key, sizeof(key), PSTR("EGW%d"), i); + printSetFormValue(settingsScript, key, ethStaticGW[i]); + snprintf_P(key, sizeof(key), PSTR("ESN%d"), i); + printSetFormValue(settingsScript, key, ethStaticSN[i]); + } + // AI: set the primary network interface radio button. + // EPI value 0 = WiFi , 1 = Ethernet. + // printSetFormValue on a radio button sets the checked state by value match. + printSetFormValue(settingsScript, PSTR("EPI"), ethPrimaryInterface ? 1 : 0); #else - //hide ethernet setting if not compiled in - settingsScript.print(F("gId('ethd').style.display='none';")); + // AI: hide ethernet section entirely if ethernet support not compiled in + settingsScript.print(F("gId('ethd').style.display='none';")); #endif + // AI: end if (Network.isConnected()) //is connected { - char s[32]; + char s[64] = {'\0'}; + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) + // AI: show both interface IPs when both are active so users can + // identify which IP to use from each subnet. mDNS resolves to the + // primary interface IP only. + IPAddress ethIP = ETH.localIP(); + IPAddress wifiIP = WiFi.localIP(); + if (ethernetType != WLED_ETH_NONE && + ethIP != (uint32_t)0 && + wifiIP != (uint32_t)0) { + // both interfaces active + snprintf_P(s, sizeof(s), PSTR("%d.%d.%d.%d (ETH) / %d.%d.%d.%d (WiFi)"), + ethIP[0], ethIP[1], ethIP[2], ethIP[3], + wifiIP[0], wifiIP[1], wifiIP[2], wifiIP[3]); + } else { + IPAddress localIP = Network.localIP(); + sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); + if (Network.isEthernet()) strcat_P(s, PSTR(" (Ethernet)")); + } + #else IPAddress localIP = Network.localIP(); sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); - - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - if (Network.isEthernet()) strcat_P(s ,PSTR(" (Ethernet)")); #endif - printSetClassElementHTML(settingsScript,PSTR("sip"),0,s); + printSetClassElementHTML(settingsScript, PSTR("sip"), 0, s); } else { printSetClassElementHTML(settingsScript,PSTR("sip"),0,(char*)F("Not connected")); } - if (WiFi.softAPIP()[0] != 0) //is active { char s[16]; From 551b5c623b274c54f138c2c0b1322fa612a292cb Mon Sep 17 00:00:00 2001 From: kelvtech-co-uk Date: Thu, 28 May 2026 19:49:46 +0000 Subject: [PATCH 2/5] Fix: replace sprintf with snprintf_P for IP formatting in xml.cpp --- wled00/xml.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/wled00/xml.cpp b/wled00/xml.cpp index da55199164..778bfc3ad0 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -332,12 +332,15 @@ void getSettingsJS(byte subPage, Print& settingsScript) wifiIP[0], wifiIP[1], wifiIP[2], wifiIP[3]); } else { IPAddress localIP = Network.localIP(); - sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); - if (Network.isEthernet()) strcat_P(s, PSTR(" (Ethernet)")); + if (Network.isEthernet()) { + snprintf_P(s, sizeof(s), PSTR("%d.%d.%d.%d (Ethernet)"), localIP[0], localIP[1], localIP[2], localIP[3]); + } else { + snprintf_P(s, sizeof(s), PSTR("%d.%d.%d.%d"), localIP[0], localIP[1], localIP[2], localIP[3]); + } } #else IPAddress localIP = Network.localIP(); - sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); + snprintf_P(s, sizeof(s), PSTR("%d.%d.%d.%d"), localIP[0], localIP[1], localIP[2], localIP[3]); #endif printSetClassElementHTML(settingsScript, PSTR("sip"), 0, s); } else From e949871a4463c424c6e889c61181da026a270b13 Mon Sep 17 00:00:00 2001 From: kelvtech-co-uk Date: Thu, 28 May 2026 19:55:54 +0000 Subject: [PATCH 3/5] Fix: add ARDUINO_ARCH_ESP32 guard around ESP32-only ethernet symbols in cfg.cpp --- wled00/cfg.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 4e9d021e10..0b61f35374 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -135,9 +135,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { #ifdef WLED_USE_ETHERNET JsonObject ethernet = doc[F("eth")]; CJSON(ethernetType, ethernet["type"]); + #if defined(ARDUINO_ARCH_ESP32) // AI: deserialize ethernet static IP configuration. - // Each address is stored as a 4-element JSON array, one byte per octet, - // matching the same pattern used for WiFi static IP (nw.ins[n].ip/gw/sn). JsonArray eth_ip = ethernet[F("eip")]; JsonArray eth_gw = ethernet[F("egw")]; JsonArray eth_sn = ethernet[F("esn")]; @@ -149,8 +148,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } } // AI: deserialize primary network interface selection. - // false = WiFi is default gateway (default), true = Ethernet is default gateway. CJSON(ethPrimaryInterface, ethernet[F("epi")]); + #endif initEthernet(); #endif From 353126cfc81edfe3adfeb83f27802ff1bf6bcba3 Mon Sep 17 00:00:00 2001 From: kelvtech-co-uk Date: Thu, 28 May 2026 20:29:42 +0000 Subject: [PATCH 4/5] Fix: apply ethernet IP config changes immediately on save without reboot --- wled00/set.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/wled00/set.cpp b/wled00/set.cpp index 66eef749a6..b95bd265bb 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -145,6 +145,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } #endif +// AI: below section was generated by an AI #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) ethernetType = request->arg(F("ETH")).toInt(); // AI: read ethernet static IP configuration from form POST. @@ -166,8 +167,34 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // PNI field value 0 = WiFi is primary interface, 1 = Ethernet is primary interface. // Radio buttons only submit when selected so use hasArg with value check. ethPrimaryInterface = (request->arg(F("EPI")).toInt() == 1); + // AI: apply ethernet IP config changes immediately without reboot, + // bringing ethernet IP configuration to parity with WiFi live-update + // behaviour. Only calls ETH.config() if values actually changed, + // avoiding unnecessary network disruption on save. + // initEthernet() still called for first-time hardware init only. + if (ethernetType != WLED_ETH_NONE) { + IPAddress currentIP = ETH.localIP(); + IPAddress currentGW = ETH.gatewayIP(); + IPAddress currentSN = ETH.subnetMask(); + bool ethIPChanged = ( + (uint32_t)ethStaticIP != (uint32_t)currentIP || + (uint32_t)ethStaticGW != (uint32_t)currentGW || + (uint32_t)ethStaticSN != (uint32_t)currentSN + ); + if (ethIPChanged) { + if ((uint32_t)ethStaticIP != 0) { + ETH.config(ethStaticIP, ethStaticGW, ethStaticSN, dnsAddress); + DEBUG_PRINTLN(F("ETH: IP config updated from settings")); + } else { + ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); + DEBUG_PRINTLN(F("ETH: Switched to DHCP from settings")); + } + setPrimaryNetworkInterface(); + } + } initEthernet(); #endif + // AI: end } //LED SETTINGS From 1661874b36c552351aa15c3123175cb5224400e1 Mon Sep 17 00:00:00 2001 From: kelvtech-co-uk Date: Fri, 29 May 2026 12:19:07 +0000 Subject: [PATCH 5/5] Fix: call setPrimaryNetworkInterface() when EPI radio changes without IP change --- wled00/set.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wled00/set.cpp b/wled00/set.cpp index b95bd265bb..092778bb80 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -166,7 +166,9 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // AI: read primary network interface selection. // PNI field value 0 = WiFi is primary interface, 1 = Ethernet is primary interface. // Radio buttons only submit when selected so use hasArg with value check. + bool prevPrimaryInterface = ethPrimaryInterface; ethPrimaryInterface = (request->arg(F("EPI")).toInt() == 1); + bool ethPNIChanged = (ethPrimaryInterface != prevPrimaryInterface); // AI: apply ethernet IP config changes immediately without reboot, // bringing ethernet IP configuration to parity with WiFi live-update // behaviour. Only calls ETH.config() if values actually changed, @@ -189,6 +191,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); DEBUG_PRINTLN(F("ETH: Switched to DHCP from settings")); } + } + if (ethIPChanged || ethPNIChanged) { setPrimaryNetworkInterface(); } }