Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
32 changes: 31 additions & 1 deletion wled00/cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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")]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
initEthernet();
#endif
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -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"));
Expand Down
46 changes: 42 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,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() {
Expand Down Expand Up @@ -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';
}

</script>
</head>
<body>
Expand All @@ -228,7 +244,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 +263,36 @@ <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>
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><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
112 changes: 104 additions & 8 deletions wled00/network.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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();
}
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 +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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions wled00/set.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
initEthernet();
#endif
}
Expand Down
Loading
Loading