Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion wled00/cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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")]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#endif
initEthernet();
#endif
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -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"));
Expand Down
67 changes: 63 additions & 4 deletions wled00/data/settings_wifi.htm
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,14 @@
Identity:<br><input type="text" id="EI${i}" name="EI${i}" maxlength="64" value="${ident}"><br>
</div>`;
}
// AI: removed "Also used by Ethernet" note from WiFi static IP label
// WiFi and Ethernet now have independent IP configurations
var b = `<div id="net${i}"><hr class="sml">
Network name (SSID${i==0?", empty to not connect":""}):<br><input type="text" id="CS${i}" name="CS${i}" maxlength="32" value="${ssid}" ${i>0?"required":""}><br>
${encryptionTypeField}
Network password:<br><input type="password" name="PW${i}" maxlength="64" value="${pass}"><br>
BSSID (optional):<br><input type="text" id="BS${i}" name="BS${i}" maxlength="12" value="${bssid}"><br>
Static IP (leave at 0.0.0.0 for DHCP)${i==0?"<br>Also used by Ethernet":""}:<br>
Static IP (leave at 0.0.0.0 for DHCP):<br>
<input name="IP${i}0" type="number" class="s" min="0" max="255" value="${ip&0xFF}" required>.<input name="IP${i}1" type="number" class="s" min="0" max="255" value="${(ip>>8)&0xFF}" required>.<input name="IP${i}2" type="number" class="s" min="0" max="255" value="${(ip>>16)&0xFF}" required>.<input name="IP${i}3" type="number" class="s" min="0" max="255" value="${(ip>>24)&0xFF}" required><br>
Static gateway:<br>
<input name="GW${i}0" type="number" class="s" min="0" max="255" value="${gw&0xFF}" required>.<input name="GW${i}1" type="number" class="s" min="0" max="255" value="${(gw>>8)&0xFF}" required>.<input name="GW${i}2" type="number" class="s" min="0" max="255" value="${(gw>>16)&0xFF}" required>.<input name="GW${i}3" type="number" class="s" min="0" max="255" value="${(gw>>24)&0xFF}" required><br>
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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";
}

</script>
</head>
<body>
Expand All @@ -228,7 +261,8 @@ <h3>Wireless network</h3>
</div>
<div class="sec" id="ethd">
<h3>Ethernet Type</h3>
<select name="ETH">
<!-- AI: added onchange="toggleEthIP()" to show/hide ethernet IP section -->
<select name="ETH" onchange="toggleEthIP()">
<option value="0">None</option>
<option value="6">IoTorero/ESP32Deux/RGB2Go</option>
<option value="9">ABC! WLED V43 & compatible</option>
Expand All @@ -246,15 +280,40 @@ <h3>Ethernet Type</h3>
<option value="1">WT32-ETH01</option>
<option value="13">Gledopto</option>
</select><br><br>
</div>
<!-- AI: below section was generated by an AI -->
<div id="ethip" style="display:none;">
<h3>Ethernet IP Configuration</h3>
Static IP (leave at 0.0.0.0 for DHCP):<br>
<input name="EIP0" type="number" class="s" min="0" max="255" value="0" required>.<input name="EIP1" type="number" class="s" min="0" max="255" value="0" required>.<input name="EIP2" type="number" class="s" min="0" max="255" value="0" required>.<input name="EIP3" type="number" class="s" min="0" max="255" value="0" required><br>
Static gateway (leave at 0.0.0.0 for no gateway):<br>
<input name="EGW0" type="number" class="s" min="0" max="255" value="0" required>.<input name="EGW1" type="number" class="s" min="0" max="255" value="0" required>.<input name="EGW2" type="number" class="s" min="0" max="255" value="0" required>.<input name="EGW3" type="number" class="s" min="0" max="255" value="0" required><br>
Static subnet mask:<br>
<input name="ESN0" type="number" class="s" min="0" max="255" value="255" required>.<input name="ESN1" type="number" class="s" min="0" max="255" value="255" required>.<input name="ESN2" type="number" class="s" min="0" max="255" value="255" required>.<input name="ESN3" type="number" class="s" min="0" max="255" value="0" required><br><br>

<hr class="sml">

<!-- AI: primary network interface selector shown when ethernet board is selected -->
<h3>Primary Network Interface</h3>
<i style="font-size:0.85em;color:#aaa;display:block;margin-top:-25px;">(Only applicable when both interfaces have a gateway configured.)</i>
Select the primary network WLED should use for internet and for access to network services like NTP, MQTT, etc.<br>
<input type="radio" name="EPI" value="0" checked> WiFi
&nbsp;&lt;--&gt;&nbsp;
<input type="radio" name="EPI" value="1" > Ethernet<br>
<i>Active: <span class="pnia">-</span></i><br>
<div id="pniw" style="display:none;color:orange;">
&#9888; Configured interface unavailable &mdash; fallback active<br><br>
<!-- AI: end -->
</div>
</div>
<div class="sec">
<h3>DNS & mDNS</h3>
DNS server address:<br>
<input name="D0" type="number" class="s" min="0" max="255" required>.<input name="D1" type="number" class="s" min="0" max="255" required>.<input name="D2" type="number" class="s" min="0" max="255" required>.<input name="D3" type="number" class="s" min="0" max="255" required><br>
<br>
mDNS address (leave empty for no mDNS):<br>
http:// <input type="text" name="CM" maxlength="32"> .local<br>
Client IP: <span class="sip"> Not connected </span> <br>
<!-- AI: updated label to reflect dual interface operation -->
Active IP(s): <span class="sip"> Not connected </span>
</div>
<div class="sec">
<h3>Configure Access Point</h3>
Expand Down
7 changes: 7 additions & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
119 changes: 111 additions & 8 deletions wled00/network.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#endif
// AI: end

//by https://github.com/tzapu/WiFiManager/blob/master/WiFiManager.cpp
int getSignalQuality(int rssi)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading