diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 2e458e7da9..0b61f35374 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -135,7 +135,21 @@ 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 + #if defined(ARDUINO_ARCH_ESP32) + // AI: deserialize ethernet static IP configuration. + 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. + CJSON(ethPrimaryInterface, ethernet[F("epi")]); + #endif initEthernet(); #endif @@ -935,6 +949,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..def3bd553c 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,10 @@ 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(); + // AI: update primary network interface warning + updatePNIWarning(); } // reset remotes: initialize empty list (called from xml.cpp) function rstR() { @@ -207,6 +213,33 @@ } } + // 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'; + } + // AI: updatePNIWarning() compares the configured primary network interface + // (EPI radio button selection) against the currently active interface + // (served via pnia class element from setPrimaryNetworkInterface() runtime state). + // Shows an orange warning when fallback is active — i.e. the configured + // preferred interface is unavailable and WLED is using the other interface instead. + // Called on page load and when EPI radio buttons change. + function updatePNIWarning() { + var epi = document.querySelector('input[name="EPI"]:checked'); + var configured = epi ? (epi.value === "1" ? "Ethernet" : "WiFi") : "WiFi"; + var pnia = document.querySelector('.pnia'); + var active = pnia ? pnia.textContent : "-"; + var warning = gId('pniw'); + if (warning) warning.style.display = + (active !== "-" && active !== configured) ? "block" : "none"; + } + @@ -228,7 +261,8 @@

Wireless network

Ethernet Type

- @@ -246,7 +280,31 @@

Ethernet Type



-
+ +

DNS & mDNS

DNS server address:
@@ -254,7 +312,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 606fa65d9e..588520927f 100644 --- a/wled00/network.cpp +++ b/wled00/network.cpp @@ -4,6 +4,10 @@ #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 +#include "esp_netif.h" // AI: required for esp_netif_next() and esp_netif_get_desc() +#include "esp_netif_net_stack.h" // AI: required for esp_netif_get_netif_impl() // 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 +278,98 @@ 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: below section was generated by an AI +// AI: setPrimaryNetworkInterface() sets the lwIP default network interface +// based on the user's ethPrimaryInterface selection. +// Uses esp-netif API (esp_netif_next / esp_netif_get_desc) to identify +// interfaces by their official description strings ("sta" for WiFi STA, +// "eth" for Ethernet) rather than fragile lwIP netif name prefixes. +// Description strings are official esp-netif defaults defined in +// esp_netif_defaults.h — stable across IDF versions. +// Falls back to any available interface if the preferred one is not ready. +void setPrimaryNetworkInterface() { + const char *targetDesc = ethPrimaryInterface ? "eth" : "sta"; + + struct netif *target = nullptr; + struct netif *fallback = nullptr; + const char *selectedDesc = nullptr; + const char *fallbackDesc = nullptr; + + // AI: acquire lwIP TCP/IP core lock before calling netif_set_default() + LOCK_TCPIP_CORE(); + + // AI: iterate esp_netif handles using official API, identify interface + // type via description string, get lwIP netif pointer via impl handle + // TODO: migrate to esp_netif_next_unsafe() when IDF 5.x is minimum target + // esp_netif_next() is deprecated in IDF v5.x in favour of the unsafe variant + // which requires holding esp_netif's own list lock separately from LOCK_TCPIP_CORE + esp_netif_t *esp_netif = esp_netif_next(NULL); + while (esp_netif != NULL) { + const char *desc = esp_netif_get_desc(esp_netif); + struct netif *netif_impl = (struct netif *)esp_netif_get_netif_impl(esp_netif); + if (netif_impl && netif_is_up(netif_impl) && + netif_impl->ip_addr.u_addr.ip4.addr != 0) { + if (desc && strcmp(desc, targetDesc) == 0) { + target = netif_impl; + selectedDesc = desc; + break; + } + if (!fallback) { fallback = netif_impl; fallbackDesc = desc; } + } + esp_netif = esp_netif_next(esp_netif); + } + + // AI: fall back to any ready interface if preferred is unavailable + // prevents outbound traffic being pinned to a dead default netif + if (!target && fallback) { + target = fallback; + selectedDesc = fallbackDesc; + DEBUG_PRINTLN(F("setPNI: Preferred interface unavailable, using fallback")); + } + + if (target != nullptr) { + netif_set_default(target); + DEBUG_PRINTF_P(PSTR("setPNI: Primary netif set via desc='%s' (%d.%d.%d.%d)\n"), + selectedDesc ? selectedDesc : "unknown", + 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 +467,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 +516,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 @@ -481,17 +571,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..092778bb80 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -145,10 +145,60 @@ 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. + // 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. + 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, + // 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")); + } + } + if (ethIPChanged || ethPNIChanged) { + setPrimaryNetworkInterface(); + } + } initEthernet(); #endif + // AI: end } //LED SETTINGS diff --git a/wled00/wled.cpp b/wled00/wled.cpp index e91fcca8f5..b40acb9b29 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -718,8 +718,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)); } @@ -876,9 +894,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() { @@ -979,7 +1007,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..57a2710825 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -1,5 +1,14 @@ #include "wled.h" #include "wled_ethernet.h" +// AI: required for real-time default netif query in settings page response +// esp_netif_next/esp_netif_get_desc identify interface by description string +// esp_netif_get_netif_impl bridges esp-netif handle to lwIP struct netif* +// netif_default is the lwIP global pointer to the current default interface +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) + #include "esp_netif.h" // AI: required for esp_netif_next() and esp_netif_get_desc() + #include "esp_netif_net_stack.h" // AI: required for esp_netif_get_netif_impl() + #include "lwip/netif.h" // AI: required for netif_default +#endif /* * Sending XML status files to client @@ -131,6 +140,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 +298,84 @@ 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); + // AI: serve active primary interface state by checking actual lwIP default + // netif at request time rather than cached variable, ensuring accuracy + // even when setPrimaryNetworkInterface() hasn't been called this session + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) + struct netif *defaultNetif = netif_default; + bool ethIsActive = false; + if (defaultNetif != nullptr) { + esp_netif_t *esp_netif = esp_netif_next(NULL); + while (esp_netif != NULL) { + if (esp_netif_get_netif_impl(esp_netif) == defaultNetif) { + const char *desc = esp_netif_get_desc(esp_netif); + ethIsActive = (desc && strcmp(desc, "eth") == 0); + break; + } + esp_netif = esp_netif_next(esp_netif); + } + } + printSetClassElementHTML(settingsScript, PSTR("pnia"), 0, + ethIsActive ? (char*)"Ethernet" : (char*)"WiFi"); + #endif #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]; - IPAddress localIP = Network.localIP(); - sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); - + char s[64] = {'\0'}; #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - if (Network.isEthernet()) strcat_P(s ,PSTR(" (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(); + 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(); + 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); + 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];