From 1c7bc1b022151334192638efde2146cf32c87c52 Mon Sep 17 00:00:00 2001 From: Philip Lykov Date: Thu, 26 Feb 2026 14:53:42 +0200 Subject: [PATCH 1/5] Fix RAK4631 Ethernet gateway API connection loss after W5100S brownout PoE power instability can brownout the W5100S while the nRF52 MCU keeps running, causing all chip registers (MAC, IP, sockets) to revert to defaults. The firmware had no mechanism to detect or recover from this. Changes: - Detect W5100S chip reset by periodically verifying MAC address register in reconnectETH(); on mismatch, perform full hardware reset and re-initialize Ethernet interface and services - Add deInitApiServer() for clean API server teardown during recovery - Add ~APIServerPort destructor to prevent memory leaks - Switch nRF52 from EthernetServer::available() to accept() to prevent the same connected client from being repeatedly re-reported - Add proactive dead-connection cleanup in APIServerPort::runOnce() - Add 15-minute TCP idle timeout to close half-open connections that consume limited W5100S hardware sockets Fixes meshtastic/firmware#6970 Made-with: Cursor --- src/mesh/api/ServerAPI.cpp | 17 +++++++++- src/mesh/api/ServerAPI.h | 3 ++ src/mesh/api/ethServerAPI.cpp | 9 ++++++ src/mesh/api/ethServerAPI.h | 1 + src/mesh/eth/ethClient.cpp | 58 +++++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/mesh/api/ServerAPI.cpp b/src/mesh/api/ServerAPI.cpp index 1a506421c8d..d8f079a4119 100644 --- a/src/mesh/api/ServerAPI.cpp +++ b/src/mesh/api/ServerAPI.cpp @@ -1,7 +1,10 @@ #include "ServerAPI.h" +#include "Throttle.h" #include "configuration.h" #include +#define TCP_IDLE_TIMEOUT_MS (15 * 60 * 1000UL) + template ServerAPI::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client) { @@ -28,6 +31,12 @@ template bool ServerAPI::checkIsConnected() template int32_t ServerAPI::runOnce() { if (client.connected()) { + if (lastContactMsec > 0 && !Throttle::isWithinTimespanMs(lastContactMsec, TCP_IDLE_TIMEOUT_MS)) { + LOG_WARN("TCP connection timeout, no data for %lu ms", (unsigned long)TCP_IDLE_TIMEOUT_MS); + close(); + enabled = false; + return 0; + } return StreamAPI::runOncePart(); } else { LOG_INFO("Client dropped connection, suspend API service"); @@ -45,13 +54,19 @@ template void APIServerPort::init() template int32_t APIServerPort::runOnce() { + if (openAPI && !openAPI->isClientConnected()) { + LOG_INFO("Cleaning up disconnected TCP API client"); + delete openAPI; + openAPI = nullptr; + } + #ifdef ARCH_ESP32 #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) auto client = U::accept(); #else auto client = U::available(); #endif -#elif defined(ARCH_RP2040) +#elif defined(ARCH_RP2040) || defined(ARCH_NRF52) auto client = U::accept(); #else auto client = U::available(); diff --git a/src/mesh/api/ServerAPI.h b/src/mesh/api/ServerAPI.h index 111314476b0..3f8c5b726df 100644 --- a/src/mesh/api/ServerAPI.h +++ b/src/mesh/api/ServerAPI.h @@ -21,6 +21,8 @@ template class ServerAPI : public StreamAPI, private concurrency::OSTh /// override close to also shutdown the TCP link virtual void close(); + bool isClientConnected() { return client.connected(); } + protected: /// We override this method to prevent publishing EVENT_SERIAL_CONNECTED/DISCONNECTED for wifi links (we want the board to /// stay in the POWERED state to prevent disabling wifi) @@ -50,6 +52,7 @@ template class APIServerPort : public U, private concurrency: public: explicit APIServerPort(int port); + ~APIServerPort() { delete openAPI; } void init(); diff --git a/src/mesh/api/ethServerAPI.cpp b/src/mesh/api/ethServerAPI.cpp index 10ff06df23a..43ed74cf811 100644 --- a/src/mesh/api/ethServerAPI.cpp +++ b/src/mesh/api/ethServerAPI.cpp @@ -17,6 +17,15 @@ void initApiServer(int port) } } +void deInitApiServer() +{ + if (apiPort) { + LOG_INFO("Deinit API server"); + delete apiPort; + apiPort = nullptr; + } +} + ethServerAPI::ethServerAPI(EthernetClient &_client) : ServerAPI(_client) { LOG_INFO("Incoming ethernet connection"); diff --git a/src/mesh/api/ethServerAPI.h b/src/mesh/api/ethServerAPI.h index c616c87be7d..8f81ee6ffff 100644 --- a/src/mesh/api/ethServerAPI.h +++ b/src/mesh/api/ethServerAPI.h @@ -24,4 +24,5 @@ class ethServerPort : public APIServerPort }; void initApiServer(int port = SERVER_API_DEFAULT_PORT); +void deInitApiServer(); #endif diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index a811ec16cd5..e823bb46d19 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -32,6 +32,64 @@ static Periodic *ethEvent; static int32_t reconnectETH() { if (config.network.eth_enabled) { + + // Detect W5100S chip reset by verifying the MAC address register. + // PoE power instability can brownout the W5100S while the MCU keeps running, + // causing all chip registers (MAC, IP, sockets) to revert to defaults. + uint8_t currentMac[6]; + Ethernet.MACAddress(currentMac); + + uint8_t expectedMac[6]; + getMacAddr(expectedMac); + expectedMac[0] &= 0xfe; + + if (memcmp(currentMac, expectedMac, 6) != 0) { + LOG_WARN("W5100S MAC mismatch (chip reset detected), reinitializing Ethernet"); + + syslog.disable(); +#if !MESHTASTIC_EXCLUDE_SOCKETAPI + deInitApiServer(); +#endif + +#ifdef PIN_ETHERNET_RESET + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); + delay(100); + digitalWrite(PIN_ETHERNET_RESET, HIGH); + delay(100); +#endif + +#ifdef RAK11310 + ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); + ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); + ETH_SPI_PORT.setRX(PIN_SPI0_MISO); + ETH_SPI_PORT.begin(); +#endif + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + int status = 0; + if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { + status = Ethernet.begin(expectedMac); + } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { + Ethernet.begin(expectedMac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, + config.network.ipv4_config.gateway, config.network.ipv4_config.subnet); + status = 1; + } + + if (status == 0) { + LOG_ERROR("Ethernet re-initialization failed, will retry"); + return 5000; + } + + LOG_INFO("Ethernet reinitialized - IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], + Ethernet.localIP()[2], Ethernet.localIP()[3]); + + ethStartupComplete = false; +#ifndef DISABLE_NTP + ntp_renew = 0; +#endif + } + Ethernet.maintain(); if (!ethStartupComplete) { // Start web server From 477276c806502abfb5351cbe6ce4f50c443d59b1 Mon Sep 17 00:00:00 2001 From: Philip Lykov Date: Thu, 26 Feb 2026 15:49:27 +0200 Subject: [PATCH 2/5] Log actual elapsed idle time instead of constant timeout value Address Copilot review comment: log millis() - lastContactMsec to show the real time since last client activity, rather than always logging the TCP_IDLE_TIMEOUT_MS constant. Made-with: Cursor --- src/mesh/api/ServerAPI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/api/ServerAPI.cpp b/src/mesh/api/ServerAPI.cpp index d8f079a4119..4c2913ca138 100644 --- a/src/mesh/api/ServerAPI.cpp +++ b/src/mesh/api/ServerAPI.cpp @@ -32,7 +32,7 @@ template int32_t ServerAPI::runOnce() { if (client.connected()) { if (lastContactMsec > 0 && !Throttle::isWithinTimespanMs(lastContactMsec, TCP_IDLE_TIMEOUT_MS)) { - LOG_WARN("TCP connection timeout, no data for %lu ms", (unsigned long)TCP_IDLE_TIMEOUT_MS); + LOG_WARN("TCP connection timeout, no data for %lu ms", (unsigned long)(millis() - lastContactMsec)); close(); enabled = false; return 0; From 1a7439dbb768434e782674728a0cb1a4f83107ee Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 27 Feb 2026 05:34:58 -0600 Subject: [PATCH 3/5] Update src/mesh/api/ServerAPI.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/mesh/api/ServerAPI.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/mesh/api/ServerAPI.h b/src/mesh/api/ServerAPI.h index 3f8c5b726df..e833b0aa834 100644 --- a/src/mesh/api/ServerAPI.h +++ b/src/mesh/api/ServerAPI.h @@ -52,7 +52,14 @@ template class APIServerPort : public U, private concurrency: public: explicit APIServerPort(int port); - ~APIServerPort() { delete openAPI; } + ~APIServerPort() + { + if (openAPI != nullptr) + { + delete openAPI; + openAPI = nullptr; + } + } void init(); From 41d07578e9167367541754c46d0d7466e218c9d3 Mon Sep 17 00:00:00 2001 From: Philip Lykov Date: Wed, 18 Mar 2026 09:41:51 +0200 Subject: [PATCH 4/5] Stop UDP multicast handler during W5100S brownout recovery After a W5100S chip brownout, the udpHandler isRunning flag stays true while the underlying socket is dead. Without calling stop(), the subsequent start() no-ops and multicast is silently broken after recovery. Made-with: Cursor --- src/mesh/eth/ethClient.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index e823bb46d19..542492f46e1 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -50,6 +50,11 @@ static int32_t reconnectETH() #if !MESHTASTIC_EXCLUDE_SOCKETAPI deInitApiServer(); #endif +#if HAS_UDP_MULTICAST + if (udpHandler) { + udpHandler->stop(); + } +#endif #ifdef PIN_ETHERNET_RESET pinMode(PIN_ETHERNET_RESET, OUTPUT); From d396b0a7061019951963bacd4ffdfdf80f2cb02a Mon Sep 17 00:00:00 2001 From: Philip Lykov Date: Thu, 19 Mar 2026 08:54:21 +0200 Subject: [PATCH 5/5] Address Copilot review: recovery flags and timeout constant Move ethStartupComplete and ntp_renew reset to immediately after service teardown, before Ethernet.begin(). Previously, if DHCP failed the early return left ethStartupComplete=true, preventing service re-initialization on subsequent retries. Replace #define TCP_IDLE_TIMEOUT_MS with static constexpr uint32_t for type safety and better C++ practice. Made-with: Cursor --- src/mesh/api/ServerAPI.cpp | 2 +- src/mesh/eth/ethClient.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mesh/api/ServerAPI.cpp b/src/mesh/api/ServerAPI.cpp index 281224d4543..f3e7854caee 100644 --- a/src/mesh/api/ServerAPI.cpp +++ b/src/mesh/api/ServerAPI.cpp @@ -3,7 +3,7 @@ #include "configuration.h" #include -#define TCP_IDLE_TIMEOUT_MS (15 * 60 * 1000UL) +static constexpr uint32_t TCP_IDLE_TIMEOUT_MS = 15 * 60 * 1000UL; template ServerAPI::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client) diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 542492f46e1..80741810a36 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -56,6 +56,11 @@ static int32_t reconnectETH() } #endif + ethStartupComplete = false; +#ifndef DISABLE_NTP + ntp_renew = 0; +#endif + #ifdef PIN_ETHERNET_RESET pinMode(PIN_ETHERNET_RESET, OUTPUT); digitalWrite(PIN_ETHERNET_RESET, LOW); @@ -88,11 +93,6 @@ static int32_t reconnectETH() LOG_INFO("Ethernet reinitialized - IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3]); - - ethStartupComplete = false; -#ifndef DISABLE_NTP - ntp_renew = 0; -#endif } Ethernet.maintain();