From 6f04b3d01beefcda7e90fc1b79cc05af72ceb42f Mon Sep 17 00:00:00 2001 From: 1sthandy Date: Wed, 3 Jun 2026 18:37:23 +0200 Subject: [PATCH 1/6] Add RAK4631 PoE and Ethernet companion support (W5100S / RAK13800) Adds support for running a RAK4631 over PoE using the RAK13800 (W5100S) Ethernet module on a RAK19018 base, in two roles: - PoE repeater: powers up reliably on the RAK19018 (Silvertel) converter. The cold-start path is shortened and CPU sleep is disabled, because dropping below the converter's hold current makes it fold back and reset. Boot-voltage protection is bypassed when battery-less on PoE. - Ethernet companion: exposes the MeshCore companion protocol as a TCP server (default port 5000) over the W5100S, so Home Assistant connects to the device's IP. Uses a static IP to avoid blocking DHCP at cold start. W5100S PHY bring-up is deferred out of setup() into loop() to avoid collapsing the marginal PoE supply during the cold-start window. New files: - src/helpers/SerialEthernetInterface.{cpp,h}: TCP transport for the companion serial frame protocol. - variants/rak4631/W5100SPoE.{cpp,h}: W5100S power/reset + init helper. New platformio envs: - RAK_4631_repeater_poe (+ _debug) - RAK_4631_companion_radio_eth All additions are guarded by WITH_W5100S_POE / WITH_ETHERNET_COMPANION, so existing RAK4631 builds are unaffected. Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/main.cpp | 290 +++++++++++++++++------- examples/simple_repeater/main.cpp | 14 ++ src/helpers/SerialEthernetInterface.cpp | 151 ++++++++++++ src/helpers/SerialEthernetInterface.h | 78 +++++++ variants/rak4631/RAK4631Board.cpp | 36 ++- variants/rak4631/W5100SPoE.cpp | 113 +++++++++ variants/rak4631/W5100SPoE.h | 42 ++++ variants/rak4631/target.cpp | 4 + 8 files changed, 637 insertions(+), 91 deletions(-) create mode 100644 src/helpers/SerialEthernetInterface.cpp create mode 100644 src/helpers/SerialEthernetInterface.h create mode 100644 variants/rak4631/W5100SPoE.cpp create mode 100644 variants/rak4631/W5100SPoE.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 89f0e6cb9f..932f01f2f5 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,59 +12,6 @@ static uint32_t _atoi(const char* sp) { return n; } -// interface manager -#include -MultiSerialInterface interface_manager; - -// include bluetooth interface -#if defined(BLE_PIN_CODE) - #ifdef ESP32 - // include esp32 bluetooth interface - #include - SerialBLEInterface bluetooth_interface; - #elif defined(NRF52_PLATFORM) - // include nrf52 bluetooth interface - #include - SerialBLEInterface bluetooth_interface; - #else - #error "SerialBLEInterface is not defined for this platform" - #endif -#endif - -// include wifi interface -#ifdef WIFI_SSID - #ifndef TCP_PORT - #define TCP_PORT 5000 - #endif - #ifdef ESP32 - // include esp32 wifi interface - #include - SerialWifiInterface wifi_interface; - #else - #error "SerialWifiInterface is not defined for this platform" - #endif -#endif - -// include usb interface -#if defined(ENABLE_USB_INTERFACE) - #include - ArduinoSerialInterface usb_serial_interface; -#endif - -// include ethernet interface -#if defined(ETHERNET_ENABLED) - #include - ETHERNET_CLASS ethernet_interface; -#endif - -// include hardware serial interface -#if defined(SERIAL_RX) - #include - ArduinoSerialInterface hardware_serial_interface; - HardwareSerial companion_serial(1); -#endif - -// platform file system #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #if defined(QSPIFLASH) @@ -87,10 +34,89 @@ MultiSerialInterface interface_manager; DataStore store(SPIFFS, rtc_clock); #endif +#ifdef ESP32 + #ifdef WIFI_SSID + #include + SerialWifiInterface serial_interface; + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #elif defined(BLE_PIN_CODE) + #include + SerialBLEInterface serial_interface; + #elif defined(SERIAL_RX) + #include + ArduinoSerialInterface serial_interface; + HardwareSerial companion_serial(1); + #else + #include + ArduinoSerialInterface serial_interface; + #endif +#elif defined(RP2040_PLATFORM) + //#ifdef WIFI_SSID + // #include + // SerialWifiInterface serial_interface; + // #ifndef TCP_PORT + // #define TCP_PORT 5000 + // #endif + // #elif defined(BLE_PIN_CODE) + // #include + // SerialBLEInterface serial_interface; + #if defined(SERIAL_RX) + #include + ArduinoSerialInterface serial_interface; + HardwareSerial companion_serial(1); + #else + #include + ArduinoSerialInterface serial_interface; + #endif +#elif defined(NRF52_PLATFORM) + #if defined(WITH_ETHERNET_COMPANION) + #include + #include + SerialEthernetInterface serial_interface; + // Dedicated SPI for the W5100S on its own pins (SCK=3, MISO=29, MOSI=30). + // The radio remaps the global `SPI` to the LoRa pins (43/44/45) in + // std_init(), so the W5100S needs its own SPIM peripheral. SPIM2 is free + // (radio uses SPIM3, Wire uses TWIM0/1). + SPIClass eth_spi(NRF_SPIM2, 29, 3, 30); // (SPIM, MISO=29, SCK=3, MOSI=30) + uint8_t g_eth_mac[6] = {0}; // set in setup(), used in loop() + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + // Static IP (no DHCP): predictable address for Home Assistant AND avoids + // the multi-second blocking DHCP that reboot-loops the device on PoE. + // Override per network if needed (octets are comma-separated). + #ifndef ETH_STATIC_IP + #define ETH_STATIC_IP 192,168,1,50 + #endif + #ifndef ETH_GATEWAY + #define ETH_GATEWAY 192,168,1,1 + #endif + #ifndef ETH_SUBNET + #define ETH_SUBNET 255,255,255,0 + #endif + #elif defined(BLE_PIN_CODE) + #include + SerialBLEInterface serial_interface; + #elif defined(ETHERNET_ENABLED) + #include + SerialEthernetInterface serial_interface; + #else + #include + ArduinoSerialInterface serial_interface; + #endif +#elif defined(STM32_PLATFORM) + #include + ArduinoSerialInterface serial_interface; +#else + #error "need to define a serial interface" +#endif + /* GLOBAL OBJECTS */ #ifdef DISPLAY_CLASS #include "UITask.h" - UITask ui_task(&board, &interface_manager); + UITask ui_task(&board, &serial_interface); #endif StdRNG fast_rng; @@ -113,6 +139,27 @@ void halt() { unsigned long last_wifi_reconnect_attempt = 0; #endif +#if defined(WITH_ETHERNET_COMPANION) +// Direct W5100S register write via eth_spi (proven path). Common-register +// block addresses are fixed: GAR=0x0001, SUBR=0x0005, SHAR=0x0009, SIPR=0x000F. +static void eth_wr(uint16_t a, uint8_t v) { + eth_spi.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); + digitalWrite(26, LOW); + eth_spi.transfer(0xF0); eth_spi.transfer(a >> 8); eth_spi.transfer(a & 0xFF); eth_spi.transfer(v); + digitalWrite(26, HIGH); + eth_spi.endTransaction(); +} +static void eth_write_netcfg(const uint8_t* mac) { + const uint8_t ip[4] = { ETH_STATIC_IP }; + const uint8_t gw[4] = { ETH_GATEWAY }; + const uint8_t sn[4] = { ETH_SUBNET }; + for (int i = 0; i < 4; i++) eth_wr(0x0001 + i, gw[i]); // GAR + for (int i = 0; i < 4; i++) eth_wr(0x0005 + i, sn[i]); // SUBR + for (int i = 0; i < 6; i++) eth_wr(0x0009 + i, mac[i]); // SHAR + for (int i = 0; i < 4; i++) eth_wr(0x000F + i, ip[i]); // SIPR +} +#endif + void setup() { Serial.begin(115200); board.begin(); @@ -160,6 +207,58 @@ void setup() { false #endif ); + +#if defined(WITH_ETHERNET_COMPANION) + { + // Bring up the W5100S (RAK13800) TCP/IP stack so the companion protocol is + // reachable over Ethernet (Home Assistant connects to this IP : TCP_PORT). + // Chip power + reset is handled in board.begin() (WITH_W5100S_POE: 3V3_EN + + // RST). The W5100S has its OWN SPI peripheral (eth_spi on SPIM2, pins + // SCK=3/MISO=29/MOSI=30, CS=26) — separate from the radio, which uses the + // global SPI on SPIM3 remapped to the LoRa pins. Derive a stable + // locally-administered MAC from the nRF52 device ID. + // Compute a stable locally-administered MAC from the nRF52 device ID. + // IMPORTANT: the W5100S/Ethernet library bring-up (W5100.init does a PHY + // soft-reset) is DEFERRED to loop() — see below. Doing it here in setup + // dipped the W5100S current during the marginal PoE cold-start window and + // collapsed the RAK19018 (Silvertel) converter → reboot loop. board.begin + // already has the W5100S drawing current (3V3_EN + RST + bit-bang reset), + // which latches the PoE converter just like the plain repeater build. + g_eth_mac[0] = 0x02; // locally administered, unicast + uint32_t id0 = NRF_FICR->DEVICEID[0]; + uint32_t id1 = NRF_FICR->DEVICEID[1]; + g_eth_mac[1] = (id0 >> 24) & 0xFF; + g_eth_mac[2] = (id0 >> 16) & 0xFF; + g_eth_mac[3] = (id0 >> 8) & 0xFF; + g_eth_mac[4] = (id0) & 0xFF; + g_eth_mac[5] = (id1) & 0xFF; + + // Non-disruptive SPI setup here (no chip reset); the disruptive part — the + // lib's Ethernet.begin() / W5100.init() PHY soft-reset — is deferred to + // loop() (~6 s) so it can't collapse the marginal PoE supply at cold start. + eth_spi.begin(); + Ethernet.init(eth_spi, 26); + Serial.println("Ethernet companion: bring-up deferred to loop()"); + } +#elif defined(BLE_PIN_CODE) + serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); + the_mesh.startInterface(serial_interface); +#elif defined(ETHERNET_ENABLED) + Serial.print("Waiting for serial to connect...\n"); + unsigned long timeout = millis(); + while (!Serial) { + if ((millis() - timeout) < 5000) { delay(100); } else { break; } + } + Serial.println("Initializing Ethernet adapter..."); + if (serial_interface.begin()) { + the_mesh.startInterface(serial_interface); + } else { + Serial.println("ETH: Init failed, continuing without Ethernet (mesh only)"); + } +#else + serial_interface.begin(Serial); + the_mesh.startInterface(serial_interface); +#endif #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); @@ -170,6 +269,22 @@ void setup() { false #endif ); + + //#ifdef WIFI_SSID + // WiFi.begin(WIFI_SSID, WIFI_PWD); + // serial_interface.begin(TCP_PORT); + // #elif defined(BLE_PIN_CODE) + // char dev_name[32+16]; + // sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName()); + // serial_interface.begin(dev_name, the_mesh.getBLEPin()); + #if defined(SERIAL_RX) + companion_serial.setPins(SERIAL_RX, SERIAL_TX); + companion_serial.begin(115200); + serial_interface.begin(companion_serial); + #else + serial_interface.begin(Serial); + #endif + the_mesh.startInterface(serial_interface); #elif defined(ESP32) SPIFFS.begin(true); store.begin(); @@ -180,17 +295,7 @@ void setup() { false #endif ); -#else - #error "need to define filesystem" -#endif - -// add bluetooth interface -#if defined(BLE_PIN_CODE) - bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); - interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface); -#endif -// add wifi interface #ifdef WIFI_SSID board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.setAutoReconnect(true); @@ -206,31 +311,21 @@ void setup() { }); WiFi.begin(WIFI_SSID, WIFI_PWD); - wifi_interface.begin(TCP_PORT); - interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); -#endif - -// add usb interface -#if defined(ENABLE_USB_INTERFACE) - usb_serial_interface.begin(Serial); - interface_manager.addInterface(InterfaceType::USB, &usb_serial_interface); -#endif - -// add ethernet interface -#if defined(ETHERNET_ENABLED) - ethernet_interface.begin(); - interface_manager.addInterface(InterfaceType::Ethernet, ðernet_interface); -#endif - -// add hardware serial interface -#if defined(SERIAL_RX) + serial_interface.begin(TCP_PORT); +#elif defined(BLE_PIN_CODE) + serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); +#elif defined(SERIAL_RX) companion_serial.setPins(SERIAL_RX, SERIAL_TX); companion_serial.begin(115200); - hardware_serial_interface.begin(companion_serial); - interface_manager.addInterface(InterfaceType::HardwareSerial, &hardware_serial_interface); + serial_interface.begin(companion_serial); +#else + serial_interface.begin(Serial); +#endif + the_mesh.startInterface(serial_interface); +#else + #error "need to define filesystem" #endif - the_mesh.startInterface(interface_manager); sensors.begin(); #if ENV_INCLUDE_GPS == 1 @@ -246,7 +341,6 @@ void setup() { void loop() { the_mesh.loop(); - interface_manager.loop(); sensors.loop(); #ifdef DISPLAY_CLASS ui_task.loop(); @@ -256,6 +350,10 @@ void loop() { external_watchdog.loop(); #endif +#ifdef ETHERNET_ENABLED + serial_interface.loop(); +#endif + if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible @@ -271,4 +369,24 @@ void loop() { last_wifi_reconnect_attempt = millis(); } #endif + +#if defined(WITH_ETHERNET_COMPANION) + // Deferred Ethernet bring-up: only AFTER the device has booted and the PoE + // converter is solidly latched (~6 s). The W5100.init() PHY soft-reset would + // collapse the marginal PoE supply if done during setup() (reboot loop). + static bool _eth_up = false; + if (!_eth_up && millis() > 6000) { + IPAddress sip(ETH_STATIC_IP), sgw(ETH_GATEWAY), ssn(ETH_SUBNET); + Ethernet.begin(g_eth_mac, sip, sgw, sgw, ssn); // inits chip mode/sockets (PHY soft-reset) + serial_interface.begin(TCP_PORT); // start TCP server + delay(50); + eth_write_netcfg(g_eth_mac); // force IP/GW/SN/MAC (reliable here) + _eth_up = true; + IPAddress ip = Ethernet.localIP(); + Serial.print("Ethernet up (deferred): "); + Serial.print(ip[0]); Serial.print('.'); Serial.print(ip[1]); Serial.print('.'); + Serial.print(ip[2]); Serial.print('.'); Serial.print(ip[3]); + Serial.print(":"); Serial.println(TCP_PORT); + } +#endif } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a714db68ec..e4641cfcfb 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -37,7 +37,14 @@ static unsigned long userBtnDownAt = 0; void setup() { Serial.begin(115200); +#ifdef WITH_W5100S_POE + // PoE cold-start: get to board.begin() (which activates the W5100S load) + // ASAP, before the RAK19018/Silvertel converter folds back. Skip the 1 s + // serial-settle delay — there is no operator on the serial port on PoE. + delay(20); +#else delay(1000); +#endif board.begin(); @@ -195,6 +202,12 @@ void loop() { #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif + +#ifdef WITH_W5100S_POE + // PoE-powered (RAK19018/Silvertel): the device must NEVER sleep. CPU sleep + // drops the current draw below the converter's ~125 mA hold threshold, + // making it fold back and reset. Skip the powersaving/sleep path entirely. +#else if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible @@ -204,4 +217,5 @@ void loop() { } #endif } +#endif // WITH_W5100S_POE } diff --git a/src/helpers/SerialEthernetInterface.cpp b/src/helpers/SerialEthernetInterface.cpp new file mode 100644 index 0000000000..6509238dc7 --- /dev/null +++ b/src/helpers/SerialEthernetInterface.cpp @@ -0,0 +1,151 @@ +#include "SerialEthernetInterface.h" + +void SerialEthernetInterface::begin(int port) { + // Ethernet hardware (Ethernet.init/begin) is brought up in setup(); + // here we only start the TCP server. + server = new EthernetServer(port); + server->begin(); +} + +void SerialEthernetInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + send_queue_len = 0; +} + +void SerialEthernetInterface::disable() { + _isEnabled = false; +} + +size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + ETH_DEBUG_PRINTLN("writeFrame(): frame too big, len=%d", (int)len); + return 0; + } + if (!_connected || len == 0) return 0; + + if (send_queue_len >= ETH_FRAME_QUEUE_SIZE) { + ETH_DEBUG_PRINTLN("writeFrame(): send_queue full (dropping code=0x%02x)", src[0]); + return 0; + } + + // PUSH codes (>= 0x80) go to all clients; command responses go to the + // client that issued the most recent command. + int8_t target = (src[0] >= 0x80) ? -1 : (int8_t)_last_rx; + + ETH_DEBUG_PRINTLN("TX code=0x%02x len=%d -> %s", src[0], (int)len, + target < 0 ? "all" : (target == 0 ? "slot0" : target == 1 ? "slot1" : "slot2")); + + send_queue[send_queue_len].target = target; + send_queue[send_queue_len].len = (uint8_t)len; + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + return len; +} + +size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { + if (server == NULL) return 0; + + // ---- accept a new connection into a free slot -------------------------- + // accept() returns each new connection once and maintains the listen socket, + // so it must be called every loop. + EthernetClient nc = server->accept(); + if (nc) { + int slot = -1; + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { + if (!clients[i].connected()) { slot = i; break; } + } + if (slot >= 0) { + clients[slot].stop(); // free any lingering socket in this slot + clients[slot] = nc; + rx_header[slot].type = 0; + rx_header[slot].length = 0; + ETH_DEBUG_PRINTLN("Got connection (slot %d)", slot); + } else { + nc.stop(); // all slots busy — reject + ETH_DEBUG_PRINTLN("Rejected connection (all %d slots busy)", MAX_ETH_CLIENTS); + } + } + + // ---- refresh connected state, free dropped sockets --------------------- + bool any = false; + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { + if (clients[i].connected()) { + any = true; + } else if (rx_header[i].type || rx_header[i].length) { + // a client that was active just dropped — reset its parse state + rx_header[i].type = 0; + rx_header[i].length = 0; + clients[i].stop(); + ETH_DEBUG_PRINTLN("Disconnected (slot %d)", i); + } + } + _connected = any; + + // ---- drain the outbound queue ------------------------------------------ + while (send_queue_len > 0) { + Frame &f = send_queue[0]; + uint8_t pkt[3 + MAX_FRAME_SIZE]; + pkt[0] = '>'; + pkt[1] = (f.len & 0xFF); + pkt[2] = (f.len >> 8); + memcpy(&pkt[3], f.buf, f.len); + + if (f.target < 0) { // broadcast (push) + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { + if (clients[i].connected()) clients[i].write(pkt, 3 + f.len); + } + } else if (f.target < MAX_ETH_CLIENTS && clients[f.target].connected()) { + clients[f.target].write(pkt, 3 + f.len); // response to the requester + } + + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) send_queue[i] = send_queue[i + 1]; + } + + // ---- read ONE inbound frame (round-robin across clients) --------------- + for (int k = 0; k < MAX_ETH_CLIENTS; k++) { + int i = (_rr + k) % MAX_ETH_CLIENTS; + EthernetClient &c = clients[i]; + if (!c.connected()) continue; + + // frame header = [type][len_lo][len_hi] + if (rx_header[i].type == 0 || rx_header[i].length == 0) { + if (c.available() >= 3) { + c.readBytes(&rx_header[i].type, 1); + c.readBytes((uint8_t *)&rx_header[i].length, 2); + } + } + + if (rx_header[i].type != 0 && rx_header[i].length != 0) { + int avail = c.available(); + int frame_type = rx_header[i].type; + int frame_length = rx_header[i].length; + + if (frame_length > avail) continue; // wait for the rest + + if (frame_length > MAX_FRAME_SIZE || frame_type != '<') { + // oversized or unexpected type — discard + while (frame_length > 0) { + uint8_t skip[1]; + int n = c.read(skip, 1); + if (n <= 0) break; + frame_length -= n; + } + rx_header[i].type = 0; + rx_header[i].length = 0; + continue; + } + + c.readBytes(dest, frame_length); + rx_header[i].type = 0; + rx_header[i].length = 0; + _last_rx = i; // route responses back here + _rr = (i + 1) % MAX_ETH_CLIENTS; // fairness + ETH_DEBUG_PRINTLN("RX[%d] cmd=0x%02x len=%d", i, dest[0], frame_length); + return frame_length; + } + } + + return 0; +} diff --git a/src/helpers/SerialEthernetInterface.h b/src/helpers/SerialEthernetInterface.h new file mode 100644 index 0000000000..69f3fc44f5 --- /dev/null +++ b/src/helpers/SerialEthernetInterface.h @@ -0,0 +1,78 @@ +#pragma once + +#include "BaseSerialInterface.h" +#include + +// Multi-client TCP companion interface over a W5100S Ethernet module (RAK13800). +// Lets several clients (e.g. Home Assistant AND the phone app) stay connected +// at once — the single-client model had them kicking each other off the one +// socket, causing an endless reconnect loop. +// +// Routing of outbound frames (the companion protocol isn't natively +// multi-client, so we route by frame code): +// - PUSH frames (code >= 0x80, e.g. LoRa-RX log, adverts) -> ALL clients +// - command RESPONSES (code < 0x80) -> the client +// that issued +// the last command +// +// Ethernet hardware (Ethernet.init/begin) is brought up outside this class. + +#ifndef MAX_ETH_CLIENTS + #define MAX_ETH_CLIENTS 3 // W5100S has 4 sockets: up to 3 clients + 1 listen +#endif + +class SerialEthernetInterface : public BaseSerialInterface { + bool _isEnabled; + bool _connected; // true if at least one client is connected + + EthernetServer* server; + EthernetClient clients[MAX_ETH_CLIENTS]; + + struct FrameHeader { uint8_t type; uint16_t length; }; + FrameHeader rx_header[MAX_ETH_CLIENTS]; // per-client inbound parse state + + struct Frame { + int8_t target; // -1 = broadcast, else client index + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define ETH_FRAME_QUEUE_SIZE 16 + int send_queue_len; + Frame send_queue[ETH_FRAME_QUEUE_SIZE]; + + int _last_rx; // client index of the most recent inbound command + int _rr; // round-robin cursor for fair inbound polling + +public: + SerialEthernetInterface() : server(NULL) { + _isEnabled = false; + _connected = false; + send_queue_len = 0; + _last_rx = -1; + _rr = 0; + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { rx_header[i].type = 0; rx_header[i].length = 0; } + } + + void begin(int port); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override { return _connected; } + bool isWriteBusy() const override { return false; } + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; +}; + +#if ETH_DEBUG_LOGGING && ARDUINO + #include + #define ETH_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) + #define ETH_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) +#else + #define ETH_DEBUG_PRINT(...) {} + #define ETH_DEBUG_PRINTLN(...) {} +#endif diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 1b5698d0eb..dbb5f6c1ff 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -16,13 +16,24 @@ static void __attribute__((constructor(102))) rak4631_early_poe_power() { } #endif +#ifdef WITH_W5100S_POE + #include "W5100SPoE.h" +#endif + #ifdef NRF52_POWER_MANAGEMENT -// Static configuration for power management -// Values set in variant.h defines const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, - .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, + // WITH_W5100S_POE = PoE operation without a battery. + // isExternalPowered() only detects USB VBUS, not PoE power. + // Without this exception checkBootVoltage() reads the floating + // battery ADC pin (no battery) and triggers the protection + // shutdown -> SYSTEMOFF loop -> red LED flicker. +#ifdef WITH_W5100S_POE + .voltage_bootlock = 0 +#else + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK +#endif }; void RAK4631Board::initiateShutdown(uint8_t reason) { @@ -64,4 +75,19 @@ void RAK4631Board::begin() { #endif digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1262 some time to power up -} \ No newline at end of file + +#ifdef WITH_W5100S_POE + uint8_t w5100s_ver = w5100s_poe_init(); + (void)w5100s_ver; + #ifdef MESH_DEBUG + // Wait for USB-CDC to re-enumerate so this line isn't eaten during the + // reconnect, then print it repeatedly to be sure it is seen. Debug only — + // the PoE production build skips this and boots fast. + delay(3000); + for (int i = 0; i < 5; i++) { + MESH_DEBUG_PRINTLN(">>> W5100S VERSIONR = 0x%02X (expect 0x51) <<<", w5100s_ver); + delay(200); + } + #endif +#endif +} diff --git a/variants/rak4631/W5100SPoE.cpp b/variants/rak4631/W5100SPoE.cpp new file mode 100644 index 0000000000..f56ac0c958 --- /dev/null +++ b/variants/rak4631/W5100SPoE.cpp @@ -0,0 +1,113 @@ +#ifdef WITH_W5100S_POE + +#include +#include +#include "W5100SPoE.h" + +// ── Early power-rail + RST release (constructor priority 200) ──────────────── +// Runs before setup(), right after SystemInit. Two jobs, as early as possible +// so the W5100S draws its full operating current before the RAK19018 +// (Silvertel) converter folds back during PoE cold-start: +// +// 1. Drive PIN_3V3_EN (P1.02 / WB_IO2 / Arduino 34) HIGH — this is the +// RAK19007 3.3 V PERIPHERAL POWER ENABLE that feeds the RAK13800/W5100S. +// Meshtastic does this in initVariant(); stock MeshCore never did, so our +// W5100S was only weakly powered via a default path (responds on USB but +// can't pull its full ~130 mA on the marginal PoE rail). +// 2. Drive W5100S RST (P0.21 / WB_IO3 / Arduino 21) HIGH — out of reset. +// +// Raw registers because the Arduino GPIO layer isn't up this early. +static void __attribute__((constructor(200))) w5100s_early_power_init() { + // P1.02 = 3V3_EN → HIGH (power the peripheral rail FIRST) + NRF_P1->PIN_CNF[2] = + (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos) | + (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos) | + (GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos) | + (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos) | + (GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos); + NRF_P1->OUTSET = (1UL << 2); + + // P0.21 = W5100S RST → HIGH (release from reset) + NRF_P0->PIN_CNF[21] = + (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos) | + (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos) | + (GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos) | + (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos) | + (GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos); + NRF_P0->OUTSET = (1UL << 21); +} + +// ── Bit-banged SPI to the W5100S ──────────────────────────────────────────── +// Shares the SPI bus (SCK=3 / MOSI=30 / MISO=29) with the SX1262 radio; only +// the chip-select differs (W5100S CS=26, SX1262 NSS=42). Bit-banged so it +// works regardless of which SPIClass owns the bus and runs before RadioLib. +// W5100S frame: write [0xF0][hi][lo][data], read [0x0F][hi][lo]->[data]. +#define ETH_SCK_PIN 3 +#define ETH_MOSI_PIN 30 +#define ETH_MISO_PIN 29 +#define LORA_NSS_PIN 42 + +static void bb_write_reg(uint16_t addr, uint8_t data) { + const uint8_t frame[4] = { 0xF0, (uint8_t)(addr >> 8), (uint8_t)(addr & 0xFF), data }; + digitalWrite(W5100S_CS_PIN, LOW); + for (uint8_t b = 0; b < 4; b++) { + uint8_t v = frame[b]; + for (int8_t i = 7; i >= 0; i--) { + digitalWrite(ETH_MOSI_PIN, (v >> i) & 1); + digitalWrite(ETH_SCK_PIN, HIGH); + digitalWrite(ETH_SCK_PIN, LOW); + } + } + digitalWrite(W5100S_CS_PIN, HIGH); +} + +static uint8_t bb_read_reg(uint16_t addr) { + const uint8_t frame[3] = { 0x0F, (uint8_t)(addr >> 8), (uint8_t)(addr & 0xFF) }; + digitalWrite(W5100S_CS_PIN, LOW); + for (uint8_t b = 0; b < 3; b++) { + uint8_t v = frame[b]; + for (int8_t i = 7; i >= 0; i--) { + digitalWrite(ETH_MOSI_PIN, (v >> i) & 1); + digitalWrite(ETH_SCK_PIN, HIGH); + digitalWrite(ETH_SCK_PIN, LOW); + } + } + uint8_t out = 0; + for (int8_t i = 7; i >= 0; i--) { + digitalWrite(ETH_SCK_PIN, HIGH); + out = (out << 1) | (digitalRead(ETH_MISO_PIN) & 1); + digitalWrite(ETH_SCK_PIN, LOW); + } + digitalWrite(W5100S_CS_PIN, HIGH); + return out; +} + +// ── Full W5100S bring-up ──────────────────────────────────────────────────── +// Called from RAK4631Board::begin(). Confirms the 3V3 rail + RST are driven +// (Arduino API, in case the core re-init touched them), then soft-resets and +// reads VERSIONR. Returns VERSIONR (0x51 = healthy W5100S). +uint8_t w5100s_poe_init() { + // Make sure the peripheral power rail stays driven HIGH. + pinMode(W5100S_3V3_EN_PIN, OUTPUT); digitalWrite(W5100S_3V3_EN_PIN, HIGH); + delay(20); // let the rail/W5100S settle after enable + + // Park both chip-selects HIGH on the shared bus, RST released. + pinMode(LORA_NSS_PIN, OUTPUT); digitalWrite(LORA_NSS_PIN, HIGH); + pinMode(W5100S_CS_PIN, OUTPUT); digitalWrite(W5100S_CS_PIN, HIGH); + pinMode(W5100S_RST_PIN, OUTPUT); digitalWrite(W5100S_RST_PIN, HIGH); + + pinMode(ETH_SCK_PIN, OUTPUT); digitalWrite(ETH_SCK_PIN, LOW); + pinMode(ETH_MOSI_PIN, OUTPUT); digitalWrite(ETH_MOSI_PIN, LOW); + pinMode(ETH_MISO_PIN, INPUT); + + bb_write_reg(0x0000, 0x80); // MR: software reset + delay(2); + + uint8_t ver = bb_read_reg(0x0080); // VERSIONR (0x51 on W5100S) + + // Chip stays powered and out of reset; PHY auto-negotiates with the switch + // and the W5100S draws its full operating current. + return ver; +} + +#endif // WITH_W5100S_POE diff --git a/variants/rak4631/W5100SPoE.h b/variants/rak4631/W5100SPoE.h new file mode 100644 index 0000000000..c3582f0daf --- /dev/null +++ b/variants/rak4631/W5100SPoE.h @@ -0,0 +1,42 @@ +#pragma once + +// W5100S activation for PoE operation on RAK10720 (RAK4631 + RAK13800 + RAK19018). +// +// ROOT CAUSE (confirmed via RAK forum + Meshtastic rak4631_eth_gw variant): +// The RAK19018 PoE module (Silvertel Ag9905MT) enters a non-continuous +// "gated pulse" mode when the load is below ~125-200 mA. A bare MeshCore +// repeater draws only a few mA, so the converter never latches → the supply +// pulses and the device never boots (fade-in / brighten / die / repeat LED). +// +// The fix that lets Meshtastic boot on PoE without a battery: bring the +// W5100S PHY into its active state. An active W5100S draws ~120 mA — enough +// to keep the Silvertel converter latched in continuous mode. +// +// The W5100S has no power-enable pin on this board (always powered), but its +// RST must be HIGH for the PHY to run. We release RST as early as possible +// (before setup(), via a constructor) so the PHY draws current and latches +// the converter before its foldback timer expires. +// +// Pin mapping (from Meshtastic rak4631_eth_gw variant.h): +// RST → PIN_ETHERNET_RESET = 21 (P0.21 / WB_IO3) +// CS → PIN_ETHERNET_SS = 26 (WB_SPI_CS) +// SPI → SPI1 (SCK=3, MISO=29, MOSI=30) [not used in Layer 1] + +#ifndef W5100S_RST_PIN + #define W5100S_RST_PIN 21 // P0.21 / WB_IO3 +#endif + +#ifndef W5100S_CS_PIN + #define W5100S_CS_PIN 26 // WB_SPI_CS +#endif + +// RAK19007 3.3 V peripheral power enable (feeds the RAK13800/W5100S). +// Must be driven HIGH or the W5100S is only weakly powered — exactly what +// Meshtastic's initVariant() does and stock MeshCore omits. +#ifndef W5100S_3V3_EN_PIN + #define W5100S_3V3_EN_PIN 34 // P1.02 / WB_IO2 +#endif + +// Called from RAK4631Board::begin(); fully brings up the W5100S (soft reset + +// activation) so it draws full operating current. Returns VERSIONR (0x51 = ok). +uint8_t w5100s_poe_init(); diff --git a/variants/rak4631/target.cpp b/variants/rak4631/target.cpp index a41ba72075..039471ad78 100644 --- a/variants/rak4631/target.cpp +++ b/variants/rak4631/target.cpp @@ -2,6 +2,10 @@ #include "target.h" #include +#ifdef WITH_W5100S_POE + #include "W5100SPoE.h" +#endif + RAK4631Board board; #ifndef PIN_USER_BTN From 902dfa0e22dda90e5e74c852dc3f6dbb2708371d Mon Sep 17 00:00:00 2001 From: 1sthandy Date: Mon, 8 Jun 2026 21:09:00 +0200 Subject: [PATCH 2/6] Default the Ethernet companion to DHCP with static fallback DHCP is now the default for the Ethernet companion, addressing the need for DHCP in managed/enterprise networks. It is done PoE-safely: - deferred to loop() after the converter is latched (~6 s), so the blocking DHCP exchange can't collapse the supply at cold start; - bounded timeout (12 s lease / 4 s response) instead of the 60 s default; - falls back to the static IP if no DHCP server answers, so the node is always reachable and never reboot-loops; - Ethernet.maintain() renews the lease. Use a DHCP reservation on the router for a stable address. Add -D ETH_STATIC_ONLY to opt out of DHCP and use the static IP directly. Tested on RAK4631 + RAK13800 + RAK19018: repeated PoE-only cold starts come up reliably with the DHCP-assigned address. Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/main.cpp | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 932f01f2f5..8a9ecec612 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -84,8 +84,9 @@ static uint32_t _atoi(const char* sp) { #ifndef TCP_PORT #define TCP_PORT 5000 #endif - // Static IP (no DHCP): predictable address for Home Assistant AND avoids - // the multi-second blocking DHCP that reboot-loops the device on PoE. + // Fallback static IP, used only if DHCP fails (or if ETH_STATIC_ONLY is + // set). DHCP is the default and is done deferred, after the PoE supply is + // latched, so it no longer reboot-loops the device on cold start. // Override per network if needed (octets are comma-separated). #ifndef ETH_STATIC_IP #define ETH_STATIC_IP 192,168,1,50 @@ -376,11 +377,30 @@ void loop() { // collapse the marginal PoE supply if done during setup() (reboot loop). static bool _eth_up = false; if (!_eth_up && millis() > 6000) { +#if defined(ETH_STATIC_ONLY) + // Static-only (opt-out of DHCP via -D ETH_STATIC_ONLY). IPAddress sip(ETH_STATIC_IP), sgw(ETH_GATEWAY), ssn(ETH_SUBNET); Ethernet.begin(g_eth_mac, sip, sgw, sgw, ssn); // inits chip mode/sockets (PHY soft-reset) serial_interface.begin(TCP_PORT); // start TCP server delay(50); eth_write_netcfg(g_eth_mac); // force IP/GW/SN/MAC (reliable here) +#else + // Default: DHCP, but only HERE (deferred) where the PoE supply is already + // latched, so the blocking DHCP exchange can't collapse the converter at + // cold start. Bounded timeout; fall back to the static IP if no DHCP server + // answers, so the node is always reachable and never reboot-loops. Use a + // DHCP reservation on the router for a stable address. + Serial.println("Ethernet: trying DHCP (deferred)..."); + int dhcp_ok = Ethernet.begin(g_eth_mac, 12000, 4000); // 12s lease, 4s resp + if (!dhcp_ok) { + IPAddress sip(ETH_STATIC_IP), sgw(ETH_GATEWAY), ssn(ETH_SUBNET); + Ethernet.begin(g_eth_mac, sip, sgw, sgw, ssn); + delay(50); + eth_write_netcfg(g_eth_mac); // force static into W5100S regs + Serial.println("Ethernet: DHCP failed -> static IP fallback"); + } + serial_interface.begin(TCP_PORT); // start TCP server +#endif _eth_up = true; IPAddress ip = Ethernet.localIP(); Serial.print("Ethernet up (deferred): "); @@ -388,5 +408,10 @@ void loop() { Serial.print(ip[2]); Serial.print('.'); Serial.print(ip[3]); Serial.print(":"); Serial.println(TCP_PORT); } +#if !defined(ETH_STATIC_ONLY) + else if (_eth_up) { + Ethernet.maintain(); // renew the DHCP lease in the background + } +#endif #endif } From b99bf20cbd15b7de4e9f5aac91dfe1eb83bf62f8 Mon Sep 17 00:00:00 2001 From: 1sthandy Date: Fri, 3 Jul 2026 20:38:44 +0200 Subject: [PATCH 3/6] Fix build of non-Ethernet RAK4631 variants (move interface to helpers/nrf52) SerialEthernetInterface lived in src/helpers/, which the base build_src_filter compiles for EVERY variant via `+`. Its `#include ` then broke all non-Ethernet RAK4631 builds (repeater, room server, companion usb/ble, ...) with "RAK13800_W5100S.h: No such file or directory", since that library is only a dependency of the Ethernet env. Move it into src/helpers/nrf52/ (a subdirectory the base filter does NOT glob), matching how SerialBLEInterface is handled, and include it explicitly only in the Ethernet env via `+`. Now every RAK4631 variant builds, and only the Ethernet build pulls in the W5100S library. --- examples/companion_radio/main.cpp | 127 +------------------- src/helpers/SerialEthernetInterface.cpp | 151 ------------------------ src/helpers/SerialEthernetInterface.h | 78 ------------ 3 files changed, 2 insertions(+), 354 deletions(-) delete mode 100644 src/helpers/SerialEthernetInterface.cpp delete mode 100644 src/helpers/SerialEthernetInterface.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 8a9ecec612..03a1949d75 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -71,33 +71,7 @@ static uint32_t _atoi(const char* sp) { ArduinoSerialInterface serial_interface; #endif #elif defined(NRF52_PLATFORM) - #if defined(WITH_ETHERNET_COMPANION) - #include - #include - SerialEthernetInterface serial_interface; - // Dedicated SPI for the W5100S on its own pins (SCK=3, MISO=29, MOSI=30). - // The radio remaps the global `SPI` to the LoRa pins (43/44/45) in - // std_init(), so the W5100S needs its own SPIM peripheral. SPIM2 is free - // (radio uses SPIM3, Wire uses TWIM0/1). - SPIClass eth_spi(NRF_SPIM2, 29, 3, 30); // (SPIM, MISO=29, SCK=3, MOSI=30) - uint8_t g_eth_mac[6] = {0}; // set in setup(), used in loop() - #ifndef TCP_PORT - #define TCP_PORT 5000 - #endif - // Fallback static IP, used only if DHCP fails (or if ETH_STATIC_ONLY is - // set). DHCP is the default and is done deferred, after the PoE supply is - // latched, so it no longer reboot-loops the device on cold start. - // Override per network if needed (octets are comma-separated). - #ifndef ETH_STATIC_IP - #define ETH_STATIC_IP 192,168,1,50 - #endif - #ifndef ETH_GATEWAY - #define ETH_GATEWAY 192,168,1,1 - #endif - #ifndef ETH_SUBNET - #define ETH_SUBNET 255,255,255,0 - #endif - #elif defined(BLE_PIN_CODE) + #if defined(BLE_PIN_CODE) #include SerialBLEInterface serial_interface; #elif defined(ETHERNET_ENABLED) @@ -140,27 +114,6 @@ void halt() { unsigned long last_wifi_reconnect_attempt = 0; #endif -#if defined(WITH_ETHERNET_COMPANION) -// Direct W5100S register write via eth_spi (proven path). Common-register -// block addresses are fixed: GAR=0x0001, SUBR=0x0005, SHAR=0x0009, SIPR=0x000F. -static void eth_wr(uint16_t a, uint8_t v) { - eth_spi.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); - digitalWrite(26, LOW); - eth_spi.transfer(0xF0); eth_spi.transfer(a >> 8); eth_spi.transfer(a & 0xFF); eth_spi.transfer(v); - digitalWrite(26, HIGH); - eth_spi.endTransaction(); -} -static void eth_write_netcfg(const uint8_t* mac) { - const uint8_t ip[4] = { ETH_STATIC_IP }; - const uint8_t gw[4] = { ETH_GATEWAY }; - const uint8_t sn[4] = { ETH_SUBNET }; - for (int i = 0; i < 4; i++) eth_wr(0x0001 + i, gw[i]); // GAR - for (int i = 0; i < 4; i++) eth_wr(0x0005 + i, sn[i]); // SUBR - for (int i = 0; i < 6; i++) eth_wr(0x0009 + i, mac[i]); // SHAR - for (int i = 0; i < 4; i++) eth_wr(0x000F + i, ip[i]); // SIPR -} -#endif - void setup() { Serial.begin(115200); board.begin(); @@ -209,39 +162,7 @@ void setup() { #endif ); -#if defined(WITH_ETHERNET_COMPANION) - { - // Bring up the W5100S (RAK13800) TCP/IP stack so the companion protocol is - // reachable over Ethernet (Home Assistant connects to this IP : TCP_PORT). - // Chip power + reset is handled in board.begin() (WITH_W5100S_POE: 3V3_EN + - // RST). The W5100S has its OWN SPI peripheral (eth_spi on SPIM2, pins - // SCK=3/MISO=29/MOSI=30, CS=26) — separate from the radio, which uses the - // global SPI on SPIM3 remapped to the LoRa pins. Derive a stable - // locally-administered MAC from the nRF52 device ID. - // Compute a stable locally-administered MAC from the nRF52 device ID. - // IMPORTANT: the W5100S/Ethernet library bring-up (W5100.init does a PHY - // soft-reset) is DEFERRED to loop() — see below. Doing it here in setup - // dipped the W5100S current during the marginal PoE cold-start window and - // collapsed the RAK19018 (Silvertel) converter → reboot loop. board.begin - // already has the W5100S drawing current (3V3_EN + RST + bit-bang reset), - // which latches the PoE converter just like the plain repeater build. - g_eth_mac[0] = 0x02; // locally administered, unicast - uint32_t id0 = NRF_FICR->DEVICEID[0]; - uint32_t id1 = NRF_FICR->DEVICEID[1]; - g_eth_mac[1] = (id0 >> 24) & 0xFF; - g_eth_mac[2] = (id0 >> 16) & 0xFF; - g_eth_mac[3] = (id0 >> 8) & 0xFF; - g_eth_mac[4] = (id0) & 0xFF; - g_eth_mac[5] = (id1) & 0xFF; - - // Non-disruptive SPI setup here (no chip reset); the disruptive part — the - // lib's Ethernet.begin() / W5100.init() PHY soft-reset — is deferred to - // loop() (~6 s) so it can't collapse the marginal PoE supply at cold start. - eth_spi.begin(); - Ethernet.init(eth_spi, 26); - Serial.println("Ethernet companion: bring-up deferred to loop()"); - } -#elif defined(BLE_PIN_CODE) +#if defined(BLE_PIN_CODE) serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); the_mesh.startInterface(serial_interface); #elif defined(ETHERNET_ENABLED) @@ -370,48 +291,4 @@ void loop() { last_wifi_reconnect_attempt = millis(); } #endif - -#if defined(WITH_ETHERNET_COMPANION) - // Deferred Ethernet bring-up: only AFTER the device has booted and the PoE - // converter is solidly latched (~6 s). The W5100.init() PHY soft-reset would - // collapse the marginal PoE supply if done during setup() (reboot loop). - static bool _eth_up = false; - if (!_eth_up && millis() > 6000) { -#if defined(ETH_STATIC_ONLY) - // Static-only (opt-out of DHCP via -D ETH_STATIC_ONLY). - IPAddress sip(ETH_STATIC_IP), sgw(ETH_GATEWAY), ssn(ETH_SUBNET); - Ethernet.begin(g_eth_mac, sip, sgw, sgw, ssn); // inits chip mode/sockets (PHY soft-reset) - serial_interface.begin(TCP_PORT); // start TCP server - delay(50); - eth_write_netcfg(g_eth_mac); // force IP/GW/SN/MAC (reliable here) -#else - // Default: DHCP, but only HERE (deferred) where the PoE supply is already - // latched, so the blocking DHCP exchange can't collapse the converter at - // cold start. Bounded timeout; fall back to the static IP if no DHCP server - // answers, so the node is always reachable and never reboot-loops. Use a - // DHCP reservation on the router for a stable address. - Serial.println("Ethernet: trying DHCP (deferred)..."); - int dhcp_ok = Ethernet.begin(g_eth_mac, 12000, 4000); // 12s lease, 4s resp - if (!dhcp_ok) { - IPAddress sip(ETH_STATIC_IP), sgw(ETH_GATEWAY), ssn(ETH_SUBNET); - Ethernet.begin(g_eth_mac, sip, sgw, sgw, ssn); - delay(50); - eth_write_netcfg(g_eth_mac); // force static into W5100S regs - Serial.println("Ethernet: DHCP failed -> static IP fallback"); - } - serial_interface.begin(TCP_PORT); // start TCP server -#endif - _eth_up = true; - IPAddress ip = Ethernet.localIP(); - Serial.print("Ethernet up (deferred): "); - Serial.print(ip[0]); Serial.print('.'); Serial.print(ip[1]); Serial.print('.'); - Serial.print(ip[2]); Serial.print('.'); Serial.print(ip[3]); - Serial.print(":"); Serial.println(TCP_PORT); - } -#if !defined(ETH_STATIC_ONLY) - else if (_eth_up) { - Ethernet.maintain(); // renew the DHCP lease in the background - } -#endif -#endif } diff --git a/src/helpers/SerialEthernetInterface.cpp b/src/helpers/SerialEthernetInterface.cpp deleted file mode 100644 index 6509238dc7..0000000000 --- a/src/helpers/SerialEthernetInterface.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include "SerialEthernetInterface.h" - -void SerialEthernetInterface::begin(int port) { - // Ethernet hardware (Ethernet.init/begin) is brought up in setup(); - // here we only start the TCP server. - server = new EthernetServer(port); - server->begin(); -} - -void SerialEthernetInterface::enable() { - if (_isEnabled) return; - _isEnabled = true; - send_queue_len = 0; -} - -void SerialEthernetInterface::disable() { - _isEnabled = false; -} - -size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { - if (len > MAX_FRAME_SIZE) { - ETH_DEBUG_PRINTLN("writeFrame(): frame too big, len=%d", (int)len); - return 0; - } - if (!_connected || len == 0) return 0; - - if (send_queue_len >= ETH_FRAME_QUEUE_SIZE) { - ETH_DEBUG_PRINTLN("writeFrame(): send_queue full (dropping code=0x%02x)", src[0]); - return 0; - } - - // PUSH codes (>= 0x80) go to all clients; command responses go to the - // client that issued the most recent command. - int8_t target = (src[0] >= 0x80) ? -1 : (int8_t)_last_rx; - - ETH_DEBUG_PRINTLN("TX code=0x%02x len=%d -> %s", src[0], (int)len, - target < 0 ? "all" : (target == 0 ? "slot0" : target == 1 ? "slot1" : "slot2")); - - send_queue[send_queue_len].target = target; - send_queue[send_queue_len].len = (uint8_t)len; - memcpy(send_queue[send_queue_len].buf, src, len); - send_queue_len++; - return len; -} - -size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { - if (server == NULL) return 0; - - // ---- accept a new connection into a free slot -------------------------- - // accept() returns each new connection once and maintains the listen socket, - // so it must be called every loop. - EthernetClient nc = server->accept(); - if (nc) { - int slot = -1; - for (int i = 0; i < MAX_ETH_CLIENTS; i++) { - if (!clients[i].connected()) { slot = i; break; } - } - if (slot >= 0) { - clients[slot].stop(); // free any lingering socket in this slot - clients[slot] = nc; - rx_header[slot].type = 0; - rx_header[slot].length = 0; - ETH_DEBUG_PRINTLN("Got connection (slot %d)", slot); - } else { - nc.stop(); // all slots busy — reject - ETH_DEBUG_PRINTLN("Rejected connection (all %d slots busy)", MAX_ETH_CLIENTS); - } - } - - // ---- refresh connected state, free dropped sockets --------------------- - bool any = false; - for (int i = 0; i < MAX_ETH_CLIENTS; i++) { - if (clients[i].connected()) { - any = true; - } else if (rx_header[i].type || rx_header[i].length) { - // a client that was active just dropped — reset its parse state - rx_header[i].type = 0; - rx_header[i].length = 0; - clients[i].stop(); - ETH_DEBUG_PRINTLN("Disconnected (slot %d)", i); - } - } - _connected = any; - - // ---- drain the outbound queue ------------------------------------------ - while (send_queue_len > 0) { - Frame &f = send_queue[0]; - uint8_t pkt[3 + MAX_FRAME_SIZE]; - pkt[0] = '>'; - pkt[1] = (f.len & 0xFF); - pkt[2] = (f.len >> 8); - memcpy(&pkt[3], f.buf, f.len); - - if (f.target < 0) { // broadcast (push) - for (int i = 0; i < MAX_ETH_CLIENTS; i++) { - if (clients[i].connected()) clients[i].write(pkt, 3 + f.len); - } - } else if (f.target < MAX_ETH_CLIENTS && clients[f.target].connected()) { - clients[f.target].write(pkt, 3 + f.len); // response to the requester - } - - send_queue_len--; - for (int i = 0; i < send_queue_len; i++) send_queue[i] = send_queue[i + 1]; - } - - // ---- read ONE inbound frame (round-robin across clients) --------------- - for (int k = 0; k < MAX_ETH_CLIENTS; k++) { - int i = (_rr + k) % MAX_ETH_CLIENTS; - EthernetClient &c = clients[i]; - if (!c.connected()) continue; - - // frame header = [type][len_lo][len_hi] - if (rx_header[i].type == 0 || rx_header[i].length == 0) { - if (c.available() >= 3) { - c.readBytes(&rx_header[i].type, 1); - c.readBytes((uint8_t *)&rx_header[i].length, 2); - } - } - - if (rx_header[i].type != 0 && rx_header[i].length != 0) { - int avail = c.available(); - int frame_type = rx_header[i].type; - int frame_length = rx_header[i].length; - - if (frame_length > avail) continue; // wait for the rest - - if (frame_length > MAX_FRAME_SIZE || frame_type != '<') { - // oversized or unexpected type — discard - while (frame_length > 0) { - uint8_t skip[1]; - int n = c.read(skip, 1); - if (n <= 0) break; - frame_length -= n; - } - rx_header[i].type = 0; - rx_header[i].length = 0; - continue; - } - - c.readBytes(dest, frame_length); - rx_header[i].type = 0; - rx_header[i].length = 0; - _last_rx = i; // route responses back here - _rr = (i + 1) % MAX_ETH_CLIENTS; // fairness - ETH_DEBUG_PRINTLN("RX[%d] cmd=0x%02x len=%d", i, dest[0], frame_length); - return frame_length; - } - } - - return 0; -} diff --git a/src/helpers/SerialEthernetInterface.h b/src/helpers/SerialEthernetInterface.h deleted file mode 100644 index 69f3fc44f5..0000000000 --- a/src/helpers/SerialEthernetInterface.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include "BaseSerialInterface.h" -#include - -// Multi-client TCP companion interface over a W5100S Ethernet module (RAK13800). -// Lets several clients (e.g. Home Assistant AND the phone app) stay connected -// at once — the single-client model had them kicking each other off the one -// socket, causing an endless reconnect loop. -// -// Routing of outbound frames (the companion protocol isn't natively -// multi-client, so we route by frame code): -// - PUSH frames (code >= 0x80, e.g. LoRa-RX log, adverts) -> ALL clients -// - command RESPONSES (code < 0x80) -> the client -// that issued -// the last command -// -// Ethernet hardware (Ethernet.init/begin) is brought up outside this class. - -#ifndef MAX_ETH_CLIENTS - #define MAX_ETH_CLIENTS 3 // W5100S has 4 sockets: up to 3 clients + 1 listen -#endif - -class SerialEthernetInterface : public BaseSerialInterface { - bool _isEnabled; - bool _connected; // true if at least one client is connected - - EthernetServer* server; - EthernetClient clients[MAX_ETH_CLIENTS]; - - struct FrameHeader { uint8_t type; uint16_t length; }; - FrameHeader rx_header[MAX_ETH_CLIENTS]; // per-client inbound parse state - - struct Frame { - int8_t target; // -1 = broadcast, else client index - uint8_t len; - uint8_t buf[MAX_FRAME_SIZE]; - }; - - #define ETH_FRAME_QUEUE_SIZE 16 - int send_queue_len; - Frame send_queue[ETH_FRAME_QUEUE_SIZE]; - - int _last_rx; // client index of the most recent inbound command - int _rr; // round-robin cursor for fair inbound polling - -public: - SerialEthernetInterface() : server(NULL) { - _isEnabled = false; - _connected = false; - send_queue_len = 0; - _last_rx = -1; - _rr = 0; - for (int i = 0; i < MAX_ETH_CLIENTS; i++) { rx_header[i].type = 0; rx_header[i].length = 0; } - } - - void begin(int port); - - // BaseSerialInterface methods - void enable() override; - void disable() override; - bool isEnabled() const override { return _isEnabled; } - - bool isConnected() const override { return _connected; } - bool isWriteBusy() const override { return false; } - - size_t writeFrame(const uint8_t src[], size_t len) override; - size_t checkRecvFrame(uint8_t dest[]) override; -}; - -#if ETH_DEBUG_LOGGING && ARDUINO - #include - #define ETH_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) - #define ETH_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) -#else - #define ETH_DEBUG_PRINT(...) {} - #define ETH_DEBUG_PRINTLN(...) {} -#endif From 001dd4795e3dd245d5588633de1d71a4f8fe99be Mon Sep 17 00:00:00 2001 From: 1sthandy Date: Sat, 25 Jul 2026 22:04:34 +0200 Subject: [PATCH 4/6] Rebase PoE delta onto merged PR #1983 Ethernet foundation envs, single-client SerialEthernetInterface, EthernetCLI telnet CLI) and independently added the same WB_IO2 rail-power fix we found for PoE boot. Layer the remaining PoE-specific delta on top of it instead of duplicating: - W5100SPoE: drop the now-redundant WB_IO2 early-ctor code (covered by #1983's own constructor), keep only the early W5100S RST release and the bit-bang activation/VERSIONR check. - SerialEthernetInterface: add PoE-safe deferred bring-up (Ethernet.init/ begin() moved out of begin() into loop(), gated by an _hwReady flag, so the disruptive PHY soft-reset + DHCP can't collapse the marginal PoE supply at cold start) and bounded DHCP with a static-IP fallback. - EthernetCLI: same deferral (vTaskDelay before hardware bring-up) and bounded DHCP + static fallback for the repeater/room_server telnet path. - platformio.ini: add RAK_4631_{repeater,room_server,companion_radio} _ethernet_poe envs extending #1983's _ethernet envs with WITH_W5100S_POE. Verified: all three new _poe envs build, plus the plain _ethernet envs and non-Ethernet variants (no cross-variant breakage). Co-Authored-By: Claude Sonnet 5 --- src/helpers/nrf52/EthernetCLI.h | 43 ++++++++++++++++++++++++++++++++- variants/rak4631/W5100SPoE.cpp | 38 +++++++++-------------------- variants/rak4631/W5100SPoE.h | 26 ++++++++------------ variants/rak4631/platformio.ini | 24 ++++++++++++++++++ 4 files changed, 87 insertions(+), 44 deletions(-) diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h index 34802e567a..5a9fe7e409 100644 --- a/src/helpers/nrf52/EthernetCLI.h +++ b/src/helpers/nrf52/EthernetCLI.h @@ -27,6 +27,26 @@ static SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SP #define ETHERNET_RETRY_INTERVAL_MS 30000 +#ifdef WITH_W5100S_POE + // Give the RAK19018 (Silvertel) PoE converter time to latch on the current + // the W5100S is already drawing (board.begin()'s early RST release + + // bit-bang soft-reset) before doing the *disruptive* Ethernet-library + // bring-up (another PHY soft-reset + blocking DHCP) — doing that + // immediately reliably collapsed the marginal PoE supply. + #ifndef ETH_POE_DEFER_MS + #define ETH_POE_DEFER_MS 6000 + #endif + #ifndef ETH_STATIC_IP + #define ETH_STATIC_IP 192,168,1,50 + #endif + #ifndef ETH_GATEWAY + #define ETH_GATEWAY 192,168,1,1 + #endif + #ifndef ETH_SUBNET + #define ETH_SUBNET 255,255,255,0 + #endif +#endif + static EthernetServer ethernet_server(ETHERNET_TCP_PORT); static EthernetClient ethernet_client; static volatile bool ethernet_running = false; @@ -35,6 +55,10 @@ static volatile bool ethernet_running = false; static void ethernet_task(void* param) { (void)param; +#ifdef WITH_W5100S_POE + vTaskDelay(pdMS_TO_TICKS(ETH_POE_DEFER_MS)); +#endif + Serial.println("ETH: Initializing hardware"); // WB_IO2 (power enable) is already driven HIGH by early constructor // in RAK4631Board.cpp to support POE boot. @@ -51,6 +75,22 @@ static void ethernet_task(void* param) { Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +#ifdef WITH_W5100S_POE + // Bounded DHCP with a static-IP fallback, so the node stays reachable even + // without a DHCP server (no infinite retry — this build has no battery and + // must always come up somewhere). + Serial.println("ETH: Attempting DHCP..."); + if (Ethernet.begin(mac, 10000, 2000) == 0) { + Serial.println("ETH: DHCP failed -> static IP fallback"); + IPAddress ip(ETH_STATIC_IP), gw(ETH_GATEWAY), sn(ETH_SUBNET); + Ethernet.begin(mac, ip, gw, gw, sn); + } + IPAddress ip = Ethernet.localIP(); + Serial.printf("ETH: IP: %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT); + ethernet_server.begin(); + ethernet_running = true; +#else // Retry loop: keep trying until we get an IP while (!ethernet_running) { Serial.println("ETH: Attempting DHCP..."); @@ -75,8 +115,9 @@ static void ethernet_task(void* param) { ethernet_server.begin(); ethernet_running = true; } +#endif - // DHCP succeeded, task is done + // DHCP succeeded (or static fallback applied), task is done vTaskDelete(NULL); } diff --git a/variants/rak4631/W5100SPoE.cpp b/variants/rak4631/W5100SPoE.cpp index f56ac0c958..8ff9814af4 100644 --- a/variants/rak4631/W5100SPoE.cpp +++ b/variants/rak4631/W5100SPoE.cpp @@ -4,29 +4,15 @@ #include #include "W5100SPoE.h" -// ── Early power-rail + RST release (constructor priority 200) ──────────────── -// Runs before setup(), right after SystemInit. Two jobs, as early as possible -// so the W5100S draws its full operating current before the RAK19018 -// (Silvertel) converter folds back during PoE cold-start: -// -// 1. Drive PIN_3V3_EN (P1.02 / WB_IO2 / Arduino 34) HIGH — this is the -// RAK19007 3.3 V PERIPHERAL POWER ENABLE that feeds the RAK13800/W5100S. -// Meshtastic does this in initVariant(); stock MeshCore never did, so our -// W5100S was only weakly powered via a default path (responds on USB but -// can't pull its full ~130 mA on the marginal PoE rail). -// 2. Drive W5100S RST (P0.21 / WB_IO3 / Arduino 21) HIGH — out of reset. +// ── Early RST release (constructor priority 200) ───────────────────────────── +// Runs before setup(), right after SystemInit — earlier than the ETHERNET_ENABLED +// constructor in RAK4631Board.cpp that powers the peripheral rail (WB_IO2, +// priority 102 runs first) but well before setup()/board.begin(), so the +// W5100S starts drawing its full operating current as early as possible, +// before the RAK19018 (Silvertel) converter's foldback timer expires. // // Raw registers because the Arduino GPIO layer isn't up this early. -static void __attribute__((constructor(200))) w5100s_early_power_init() { - // P1.02 = 3V3_EN → HIGH (power the peripheral rail FIRST) - NRF_P1->PIN_CNF[2] = - (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos) | - (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos) | - (GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos) | - (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos) | - (GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos); - NRF_P1->OUTSET = (1UL << 2); - +static void __attribute__((constructor(200))) w5100s_early_rst_release() { // P0.21 = W5100S RST → HIGH (release from reset) NRF_P0->PIN_CNF[21] = (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos) | @@ -83,13 +69,11 @@ static uint8_t bb_read_reg(uint16_t addr) { } // ── Full W5100S bring-up ──────────────────────────────────────────────────── -// Called from RAK4631Board::begin(). Confirms the 3V3 rail + RST are driven -// (Arduino API, in case the core re-init touched them), then soft-resets and -// reads VERSIONR. Returns VERSIONR (0x51 = healthy W5100S). +// Called from RAK4631Board::begin(). Confirms RST is driven (Arduino API, in +// case the core re-init touched it), then soft-resets and reads VERSIONR. +// Returns VERSIONR (0x51 = healthy W5100S). uint8_t w5100s_poe_init() { - // Make sure the peripheral power rail stays driven HIGH. - pinMode(W5100S_3V3_EN_PIN, OUTPUT); digitalWrite(W5100S_3V3_EN_PIN, HIGH); - delay(20); // let the rail/W5100S settle after enable + delay(20); // let the rail/W5100S settle after the ETHERNET_ENABLED ctor's WB_IO2 enable // Park both chip-selects HIGH on the shared bus, RST released. pinMode(LORA_NSS_PIN, OUTPUT); digitalWrite(LORA_NSS_PIN, HIGH); diff --git a/variants/rak4631/W5100SPoE.h b/variants/rak4631/W5100SPoE.h index c3582f0daf..98c9140613 100644 --- a/variants/rak4631/W5100SPoE.h +++ b/variants/rak4631/W5100SPoE.h @@ -1,6 +1,7 @@ #pragma once -// W5100S activation for PoE operation on RAK10720 (RAK4631 + RAK13800 + RAK19018). +// W5100S early activation for PoE operation on RAK10720 (RAK4631 + RAK13800 + +// RAK19018). // // ROOT CAUSE (confirmed via RAK forum + Meshtastic rak4631_eth_gw variant): // The RAK19018 PoE module (Silvertel Ag9905MT) enters a non-continuous @@ -8,14 +9,14 @@ // repeater draws only a few mA, so the converter never latches → the supply // pulses and the device never boots (fade-in / brighten / die / repeat LED). // -// The fix that lets Meshtastic boot on PoE without a battery: bring the -// W5100S PHY into its active state. An active W5100S draws ~120 mA — enough -// to keep the Silvertel converter latched in continuous mode. -// -// The W5100S has no power-enable pin on this board (always powered), but its -// RST must be HIGH for the PHY to run. We release RST as early as possible -// (before setup(), via a constructor) so the PHY draws current and latches -// the converter before its foldback timer expires. +// The fix: bring the W5100S PHY into its active state as early as possible. +// An active W5100S draws ~120 mA — enough to keep the Silvertel converter +// latched in continuous mode. The peripheral power rail (WB_IO2 / P1.02) is +// already driven HIGH by an early constructor in RAK4631Board.cpp whenever +// ETHERNET_ENABLED is set; this file only handles releasing RST (the rail +// alone isn't enough — the chip must actually be brought out of reset and +// made to draw its full current well before the Ethernet library's own +// (later, disruptive) bring-up runs). // // Pin mapping (from Meshtastic rak4631_eth_gw variant.h): // RST → PIN_ETHERNET_RESET = 21 (P0.21 / WB_IO3) @@ -30,13 +31,6 @@ #define W5100S_CS_PIN 26 // WB_SPI_CS #endif -// RAK19007 3.3 V peripheral power enable (feeds the RAK13800/W5100S). -// Must be driven HIGH or the W5100S is only weakly powered — exactly what -// Meshtastic's initVariant() does and stock MeshCore omits. -#ifndef W5100S_3V3_EN_PIN - #define W5100S_3V3_EN_PIN 34 // P1.02 / WB_IO2 -#endif - // Called from RAK4631Board::begin(); fully brings up the W5100S (soft reset + // activation) so it draws full operating current. Returns VERSIONR (0x51 = ok). uint8_t w5100s_poe_init(); diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 0dfbf79775..595e466b04 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -271,3 +271,27 @@ build_src_filter = ${rak4631.build_src_filter} +<../examples/kiss_modem/> lib_deps = ${rak4631.lib_deps} + +; RAK10720 (RAK4631 + RAK13800/W5100S + RAK19018 PoE) variants. +; WITH_W5100S_POE adds: early W5100S RST release + activation (so the +; RAK19018/Silvertel converter latches on PoE with no battery), the +; no-battery boot-voltage bypass, and PoE-safe deferred Ethernet bring-up +; (DHCP with a bounded timeout + static-IP fallback, after the converter +; is solidly latched). +[env:RAK_4631_repeater_ethernet_poe] +extends = env:RAK_4631_repeater_ethernet +build_flags = + ${env:RAK_4631_repeater_ethernet.build_flags} + -D WITH_W5100S_POE=1 + +[env:RAK_4631_room_server_ethernet_poe] +extends = env:RAK_4631_room_server_ethernet +build_flags = + ${env:RAK_4631_room_server_ethernet.build_flags} + -D WITH_W5100S_POE=1 + +[env:RAK_4631_companion_radio_ethernet_poe] +extends = env:RAK_4631_companion_radio_ethernet +build_flags = + ${env:RAK_4631_companion_radio_ethernet.build_flags} + -D WITH_W5100S_POE=1 From 7a8bd99ed1a05a988632c650937b5482f86fdf51 Mon Sep 17 00:00:00 2001 From: 1sthandy Date: Sat, 25 Jul 2026 22:27:08 +0200 Subject: [PATCH 5/6] Fix stale debug-flag name in companion_radio_ethernet env comment The commented-out debug flag still referenced #1983's original macro name (ETHERNET_DEBUG_LOGGING). SerialEthernetInterface.h now checks ETH_DEBUG_LOGGING after the merge with our multi-client/PoE-deferred implementation, so the comment was stale and enabling it would have done nothing. Co-Authored-By: Claude Sonnet 5 --- variants/rak4631/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 595e466b04..d23afd54f9 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -195,7 +195,7 @@ build_flags = -D ETHERNET_CLASS=RAK13800EthernetInterface ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 -; -D ETHERNET_DEBUG_LOGGING=1 +; -D ETH_DEBUG_LOGGING=1 build_src_filter = ${rak4631.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> From 351a2c933c1b89dec578303e86b56bf5085114b9 Mon Sep 17 00:00:00 2001 From: 1sthandy Date: Fri, 21 Aug 2026 21:18:07 +0200 Subject: [PATCH 6/6] Port PoE delta onto the new RAK13800EthernetInterface architecture liamcottle's unrelated refactor (MultiSerialInterface + chip-agnostic EthernetInterface/SerialEthernetInterface base with RAK13800/CH390 drivers) landed in dev and deleted the old src/helpers/nrf52/SerialEthernetInterface.* this PR previously extended. Rebase onto that, and re-target the multi-client + PoE-safe deferred bring-up work at the new RAK13800EthernetInterface class instead, so #2679 stays mergeable: - RAK13800EthernetInterface.{h,cpp}: keep inheriting the shared SerialEthernetInterface base (for its debug macros/ETHERNET_TCP_PORT default), but fully override writeFrame()/checkRecvFrame()/enable()/ disable()/isConnected()/isWriteBusy()/loop() with a multi-client-aware implementation (MAX_ETH_CLIENTS=3, per-client parse state, round-robin polling, response routing, push-frame broadcast) -- this is now the default for ALL RAK13800 companion builds, not just the PoE ones, matching #2972's original intent. available()/read()/write() are trivial stubs kept only to satisfy the base class's pure-virtual contract. - Same WITH_W5100S_POE-gated deferred bring-up as before (skip Ethernet init/begin in begin(), do it from loop() after ETH_POE_DEFER_MS once the PoE converter has latched, bounded DHCP with static-IP fallback). - No changes needed to the shared SerialEthernetInterface/EthernetInterface/ MultiSerialInterface files, CH390's driver, or companion_radio/main.cpp -- same class name/location, so ETHERNET_CLASS=RAK13800EthernetInterface picks this up automatically. - simple_room_server/main.cpp: add the same WITH_W5100S_POE boot-delay shortcut (1000ms -> 20ms) simple_repeater already had, for consistency now that it also has a _poe env. This also makes #2972 (the other multi-client attempt) moot -- it targets the now-deleted nrf52/SerialEthernetInterface.* path. Verified: RAK_4631_{repeater,room_server,companion_radio}_ethernet_poe, the plain _ethernet envs, and non-Ethernet envs (repeater, room_server, companion_radio_usb/ble) all build clean. Co-Authored-By: Claude Sonnet 5 --- examples/companion_radio/main.cpp | 192 ++++++------- examples/simple_room_server/main.cpp | 7 + .../RAK13800/RAK13800EthernetInterface.cpp | 252 +++++++++++++++--- .../RAK13800/RAK13800EthernetInterface.h | 89 ++++++- 4 files changed, 383 insertions(+), 157 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 03a1949d75..89f0e6cb9f 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,6 +12,59 @@ static uint32_t _atoi(const char* sp) { return n; } +// interface manager +#include +MultiSerialInterface interface_manager; + +// include bluetooth interface +#if defined(BLE_PIN_CODE) + #ifdef ESP32 + // include esp32 bluetooth interface + #include + SerialBLEInterface bluetooth_interface; + #elif defined(NRF52_PLATFORM) + // include nrf52 bluetooth interface + #include + SerialBLEInterface bluetooth_interface; + #else + #error "SerialBLEInterface is not defined for this platform" + #endif +#endif + +// include wifi interface +#ifdef WIFI_SSID + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #ifdef ESP32 + // include esp32 wifi interface + #include + SerialWifiInterface wifi_interface; + #else + #error "SerialWifiInterface is not defined for this platform" + #endif +#endif + +// include usb interface +#if defined(ENABLE_USB_INTERFACE) + #include + ArduinoSerialInterface usb_serial_interface; +#endif + +// include ethernet interface +#if defined(ETHERNET_ENABLED) + #include + ETHERNET_CLASS ethernet_interface; +#endif + +// include hardware serial interface +#if defined(SERIAL_RX) + #include + ArduinoSerialInterface hardware_serial_interface; + HardwareSerial companion_serial(1); +#endif + +// platform file system #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #if defined(QSPIFLASH) @@ -34,64 +87,10 @@ static uint32_t _atoi(const char* sp) { DataStore store(SPIFFS, rtc_clock); #endif -#ifdef ESP32 - #ifdef WIFI_SSID - #include - SerialWifiInterface serial_interface; - #ifndef TCP_PORT - #define TCP_PORT 5000 - #endif - #elif defined(BLE_PIN_CODE) - #include - SerialBLEInterface serial_interface; - #elif defined(SERIAL_RX) - #include - ArduinoSerialInterface serial_interface; - HardwareSerial companion_serial(1); - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(RP2040_PLATFORM) - //#ifdef WIFI_SSID - // #include - // SerialWifiInterface serial_interface; - // #ifndef TCP_PORT - // #define TCP_PORT 5000 - // #endif - // #elif defined(BLE_PIN_CODE) - // #include - // SerialBLEInterface serial_interface; - #if defined(SERIAL_RX) - #include - ArduinoSerialInterface serial_interface; - HardwareSerial companion_serial(1); - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(NRF52_PLATFORM) - #if defined(BLE_PIN_CODE) - #include - SerialBLEInterface serial_interface; - #elif defined(ETHERNET_ENABLED) - #include - SerialEthernetInterface serial_interface; - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(STM32_PLATFORM) - #include - ArduinoSerialInterface serial_interface; -#else - #error "need to define a serial interface" -#endif - /* GLOBAL OBJECTS */ #ifdef DISPLAY_CLASS #include "UITask.h" - UITask ui_task(&board, &serial_interface); + UITask ui_task(&board, &interface_manager); #endif StdRNG fast_rng; @@ -161,26 +160,6 @@ void setup() { false #endif ); - -#if defined(BLE_PIN_CODE) - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); - the_mesh.startInterface(serial_interface); -#elif defined(ETHERNET_ENABLED) - Serial.print("Waiting for serial to connect...\n"); - unsigned long timeout = millis(); - while (!Serial) { - if ((millis() - timeout) < 5000) { delay(100); } else { break; } - } - Serial.println("Initializing Ethernet adapter..."); - if (serial_interface.begin()) { - the_mesh.startInterface(serial_interface); - } else { - Serial.println("ETH: Init failed, continuing without Ethernet (mesh only)"); - } -#else - serial_interface.begin(Serial); - the_mesh.startInterface(serial_interface); -#endif #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); @@ -191,22 +170,6 @@ void setup() { false #endif ); - - //#ifdef WIFI_SSID - // WiFi.begin(WIFI_SSID, WIFI_PWD); - // serial_interface.begin(TCP_PORT); - // #elif defined(BLE_PIN_CODE) - // char dev_name[32+16]; - // sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName()); - // serial_interface.begin(dev_name, the_mesh.getBLEPin()); - #if defined(SERIAL_RX) - companion_serial.setPins(SERIAL_RX, SERIAL_TX); - companion_serial.begin(115200); - serial_interface.begin(companion_serial); - #else - serial_interface.begin(Serial); - #endif - the_mesh.startInterface(serial_interface); #elif defined(ESP32) SPIFFS.begin(true); store.begin(); @@ -217,7 +180,17 @@ void setup() { false #endif ); +#else + #error "need to define filesystem" +#endif + +// add bluetooth interface +#if defined(BLE_PIN_CODE) + bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); + interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface); +#endif +// add wifi interface #ifdef WIFI_SSID board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.setAutoReconnect(true); @@ -233,21 +206,31 @@ void setup() { }); WiFi.begin(WIFI_SSID, WIFI_PWD); - serial_interface.begin(TCP_PORT); -#elif defined(BLE_PIN_CODE) - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); -#elif defined(SERIAL_RX) + wifi_interface.begin(TCP_PORT); + interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); +#endif + +// add usb interface +#if defined(ENABLE_USB_INTERFACE) + usb_serial_interface.begin(Serial); + interface_manager.addInterface(InterfaceType::USB, &usb_serial_interface); +#endif + +// add ethernet interface +#if defined(ETHERNET_ENABLED) + ethernet_interface.begin(); + interface_manager.addInterface(InterfaceType::Ethernet, ðernet_interface); +#endif + +// add hardware serial interface +#if defined(SERIAL_RX) companion_serial.setPins(SERIAL_RX, SERIAL_TX); companion_serial.begin(115200); - serial_interface.begin(companion_serial); -#else - serial_interface.begin(Serial); -#endif - the_mesh.startInterface(serial_interface); -#else - #error "need to define filesystem" + hardware_serial_interface.begin(companion_serial); + interface_manager.addInterface(InterfaceType::HardwareSerial, &hardware_serial_interface); #endif + the_mesh.startInterface(interface_manager); sensors.begin(); #if ENV_INCLUDE_GPS == 1 @@ -263,6 +246,7 @@ void setup() { void loop() { the_mesh.loop(); + interface_manager.loop(); sensors.loop(); #ifdef DISPLAY_CLASS ui_task.loop(); @@ -272,10 +256,6 @@ void loop() { external_watchdog.loop(); #endif -#ifdef ETHERNET_ENABLED - serial_interface.loop(); -#endif - if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index d833fff39e..2ad9108d1b 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -28,7 +28,14 @@ static char ethernet_command[MAX_POST_TEXT_LEN+1]; void setup() { Serial.begin(115200); +#ifdef WITH_W5100S_POE + // PoE cold-start: get to board.begin() (which activates the W5100S load) + // ASAP, before the RAK19018/Silvertel converter folds back. Skip the 1 s + // serial-settle delay — there is no operator on the serial port on PoE. + delay(20); +#else delay(1000); +#endif board.begin(); diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp index 027581e303..211c0858a5 100644 --- a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp @@ -13,39 +13,58 @@ SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS 26 -bool RAK13800EthernetInterface::begin() { +#ifdef WITH_W5100S_POE + // Give the RAK19018 (Silvertel) PoE converter time to latch on the current + // the W5100S is already drawing (board.begin()'s early RST release + bit-bang + // soft-reset) before doing the *disruptive* Ethernet-library bring-up (another + // PHY soft-reset + blocking DHCP) — doing that immediately reliably collapsed + // the marginal PoE supply. + #ifndef ETH_POE_DEFER_MS + #define ETH_POE_DEFER_MS 6000 + #endif + #ifndef ETH_STATIC_IP + #define ETH_STATIC_IP 192,168,1,50 + #endif + #ifndef ETH_GATEWAY + #define ETH_GATEWAY 192,168,1,1 + #endif + #ifndef ETH_SUBNET + #define ETH_SUBNET 255,255,255,0 + #endif +#endif +static void eth_init_spi_and_pins() { // WB_IO2 (power enable) is already driven HIGH by early constructor // in RAK4631Board.cpp to support POE boot. // Skip hardware reset — the W5100S comes out of power-on reset cleanly, // and toggling reset kills the PHY link which breaks POE power. #ifdef PIN_ETHERNET_RESET - pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, HIGH); + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, HIGH); #endif - // generate mac address - uint8_t mac[6]; - generateEthernetMac(mac); - ETHERNET_DEBUG_PRINTLN( - "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", - mac[0], - mac[1], - mac[2], - mac[3], - mac[4], - mac[5]); ETHERNET_SPI_PORT.begin(); Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); +} - // Use static IP if build flags are defined, otherwise DHCP - #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS) +// Bring up the DHCP/static IP + start listening. Returns true on success. +static bool eth_bring_up(uint8_t mac[6]) { +#ifdef WITH_W5100S_POE + // Bounded DHCP with a static-IP fallback, so the node stays reachable even + // without a DHCP server and never blocks indefinitely at cold start. + ETHERNET_DEBUG_PRINTLN("Trying DHCP (deferred)..."); + if (Ethernet.begin(mac, 12000, 4000) == 0) { + ETHERNET_DEBUG_PRINTLN("DHCP failed -> static IP fallback"); + IPAddress ip(ETH_STATIC_IP), gw(ETH_GATEWAY), sn(ETH_SUBNET); + Ethernet.begin(mac, ip, gw, gw, sn); + } +#elif defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS) IPAddress ip(ETHERNET_STATIC_IP); IPAddress gateway(ETHERNET_STATIC_GATEWAY); IPAddress subnet(ETHERNET_STATIC_SUBNET); IPAddress dns(ETHERNET_STATIC_DNS); Ethernet.begin(mac, ip, dns, gateway, subnet); - #else +#else if (Ethernet.begin(mac) == 0) { ETHERNET_DEBUG_PRINTLN("Failed to initialize RAK13800 hardware."); if (Ethernet.hardwareStatus() == EthernetNoHardware) { @@ -57,53 +76,202 @@ bool RAK13800EthernetInterface::begin() { } return false; } - #endif +#endif ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); ETHERNET_DEBUG_PRINT_IP("IP Address", Ethernet.localIP()); ETHERNET_DEBUG_PRINT_IP("Subnet Mask", Ethernet.subnetMask()); ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP()); - ETHERNET_DEBUG_PRINT_IP("DNS", Ethernet.dnsServerIP()); + return true; +} +bool RAK13800EthernetInterface::begin() { + uint8_t mac[6]; + generateEthernetMac(mac); + ETHERNET_DEBUG_PRINTLN("Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + +#ifdef WITH_W5100S_POE + // Non-disruptive only: no Ethernet.init()/begin() here (that resets the + // PHY and can collapse the marginal PoE supply at cold start). The real + // bring-up runs from loop() after ETH_POE_DEFER_MS. + _startedAt = millis(); + ETHERNET_DEBUG_PRINTLN("Ethernet bring-up deferred (PoE-safe)"); + return true; +#else + eth_init_spi_and_pins(); + if (!eth_bring_up(mac)) return false; server.begin(); ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT); - return true; +#endif } -int RAK13800EthernetInterface::available() { - return client.available(); +#ifdef WITH_W5100S_POE +bool RAK13800EthernetInterface::bringUpHardware() { + eth_init_spi_and_pins(); + uint8_t mac[6]; + generateEthernetMac(mac); + if (!eth_bring_up(mac)) return false; + server.begin(); + ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT); + return true; } +#endif -int RAK13800EthernetInterface::read() { - return client.read(); +void RAK13800EthernetInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); } -size_t RAK13800EthernetInterface::write(const uint8_t *buf, size_t size) { - return client.write(buf, size); +void RAK13800EthernetInterface::disable() { + _isEnabled = false; } -bool RAK13800EthernetInterface::isConnected() const { - return _isConnected; +size_t RAK13800EthernetInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(): frame too big, len=%d", (int)len); + return 0; + } +#ifdef WITH_W5100S_POE + if (!_hwReady) return 0; +#endif + if (!_connected || len == 0) return 0; + + if (send_queue_len >= ETH_FRAME_QUEUE_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(): send_queue full (dropping code=0x%02x)", src[0]); + return 0; + } + + // PUSH codes (>= 0x80) go to all clients; command responses go to the + // client that issued the most recent command. + int8_t target = (src[0] >= 0x80) ? -1 : (int8_t)_last_rx; + + send_queue[send_queue_len].target = target; + send_queue[send_queue_len].len = (uint8_t)len; + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + return len; } -void RAK13800EthernetInterface::loop() { +size_t RAK13800EthernetInterface::checkRecvFrame(uint8_t dest[]) { +#ifdef WITH_W5100S_POE + if (!_hwReady) return 0; +#endif - Ethernet.maintain(); - - auto newClient = server.accept(); - if (newClient) { - IPAddress remoteIp = newClient.remoteIP(); - uint16_t remotePort = newClient.remotePort(); - ETHERNET_DEBUG_PRINTLN("New client accepted %u.%u.%u.%u:%u", remoteIp[0], remoteIp[1], remoteIp[2], remoteIp[3], remotePort); - if (client) { - ETHERNET_DEBUG_PRINTLN("Closing previous client"); - client.stop(); + // ---- accept a new connection into a free slot -------------------------- + // accept() returns each new connection once and maintains the listen socket, + // so it must be called every loop. + EthernetClient nc = server.accept(); + if (nc) { + int slot = -1; + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { + if (!clients[i].connected()) { slot = i; break; } + } + if (slot >= 0) { + clients[slot].stop(); // free any lingering socket in this slot + clients[slot] = nc; + rx_header[slot].type = 0; + rx_header[slot].length = 0; + ETHERNET_DEBUG_PRINTLN("Got connection (slot %d)", slot); + } else { + nc.stop(); // all slots busy — reject + ETHERNET_DEBUG_PRINTLN("Rejected connection (all %d slots busy)", MAX_ETH_CLIENTS); + } + } + + // ---- refresh connected state, free dropped sockets --------------------- + bool any = false; + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { + if (clients[i].connected()) { + any = true; + } else if (rx_header[i].type || rx_header[i].length) { + // a client that was active just dropped — reset its parse state + rx_header[i].type = 0; + rx_header[i].length = 0; + clients[i].stop(); + ETHERNET_DEBUG_PRINTLN("Disconnected (slot %d)", i); + } + } + _connected = any; + + // ---- drain the outbound queue ------------------------------------------ + while (send_queue_len > 0) { + Frame &f = send_queue[0]; + uint8_t pkt[3 + MAX_FRAME_SIZE]; + pkt[0] = '>'; + pkt[1] = (f.len & 0xFF); + pkt[2] = (f.len >> 8); + memcpy(&pkt[3], f.buf, f.len); + + if (f.target < 0) { // broadcast (push) + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { + if (clients[i].connected()) clients[i].write(pkt, 3 + f.len); + } + } else if (f.target < MAX_ETH_CLIENTS && clients[f.target].connected()) { + clients[f.target].write(pkt, 3 + f.len); // response to the requester + } + + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) send_queue[i] = send_queue[i + 1]; + } + + // ---- read ONE inbound frame (round-robin across clients) --------------- + for (int k = 0; k < MAX_ETH_CLIENTS; k++) { + int i = (_rr + k) % MAX_ETH_CLIENTS; + EthernetClient &c = clients[i]; + if (!c.connected()) continue; + + // frame header = [type][len_lo][len_hi] + if (rx_header[i].type == 0 || rx_header[i].length == 0) { + if (c.available() >= 3) { + c.readBytes(&rx_header[i].type, 1); + c.readBytes((uint8_t *)&rx_header[i].length, 2); + } + } + + if (rx_header[i].type != 0 && rx_header[i].length != 0) { + int avail = c.available(); + int frame_type = rx_header[i].type; + int frame_length = rx_header[i].length; + + if (frame_length > avail) continue; // wait for the rest + + if (frame_length > MAX_FRAME_SIZE || frame_type != '<') { + // oversized or unexpected type — discard + while (frame_length > 0) { + uint8_t skip[1]; + int n = c.read(skip, 1); + if (n <= 0) break; + frame_length -= n; + } + rx_header[i].type = 0; + rx_header[i].length = 0; + continue; + } + + c.readBytes(dest, frame_length); + rx_header[i].type = 0; + rx_header[i].length = 0; + _last_rx = i; // route responses back here + _rr = (i + 1) % MAX_ETH_CLIENTS; // fairness + ETHERNET_DEBUG_PRINTLN("RX[%d] cmd=0x%02x len=%d", i, dest[0], frame_length); + return frame_length; } - client = newClient; - onClientConnected(); } - _isConnected = client.connected(); + return 0; +} +void RAK13800EthernetInterface::loop() { +#ifdef WITH_W5100S_POE + if (!_hwReady) { + if (millis() - _startedAt > ETH_POE_DEFER_MS) { + _hwReady = bringUpHardware(); + } + return; + } +#endif + Ethernet.maintain(); } diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h index 34bb758777..33e6bc9dab 100644 --- a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h @@ -4,24 +4,95 @@ #include #include +// Multi-client TCP companion interface over a W5100S Ethernet module (RAK13800). +// Lets several clients (e.g. Home Assistant AND the phone app) stay connected +// at once — a single-client model has them kicking each other off the one +// socket, causing an endless reconnect loop. +// +// This fully overrides SerialEthernetInterface's writeFrame()/checkRecvFrame() +// (and enable/disable/isWriteBusy) rather than using its single-client queue + +// framing state, since routing frames to/from several concurrent clients needs +// per-client state the shared base doesn't track. available()/read()/write() +// are still implemented (trivially) only to satisfy the base's pure-virtual +// contract — they are unused here. +// +// Routing of outbound frames (the companion protocol isn't natively +// multi-client, so we route by frame code): +// - PUSH frames (code >= 0x80, e.g. LoRa-RX log, adverts) -> ALL clients +// - command RESPONSES (code < 0x80) -> the client +// that issued +// the last command + +#ifndef MAX_ETH_CLIENTS + #define MAX_ETH_CLIENTS 3 // W5100S has 4 sockets: up to 3 clients + 1 listen +#endif + class RAK13800EthernetInterface : public SerialEthernetInterface { - - bool _isConnected; + + bool _isEnabled; + bool _connected; // true if at least one client is connected + EthernetServer server; - EthernetClient client; + EthernetClient clients[MAX_ETH_CLIENTS]; + + struct FrameHeader { uint8_t type; uint16_t length; }; + FrameHeader rx_header[MAX_ETH_CLIENTS]; // per-client inbound parse state + + struct Frame { + int8_t target; // -1 = broadcast, else client index + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define ETH_FRAME_QUEUE_SIZE 16 + int send_queue_len; + Frame send_queue[ETH_FRAME_QUEUE_SIZE]; + + int _last_rx; // client index of the most recent inbound command + int _rr; // round-robin cursor for fair inbound polling + +#ifdef WITH_W5100S_POE + bool _hwReady; // true once the real Ethernet bring-up has run + unsigned long _startedAt; // millis() at begin() — bring-up deferred from here + bool bringUpHardware(); +#endif + + void clearBuffers() { + send_queue_len = 0; + _last_rx = -1; + _rr = 0; + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { rx_header[i].type = 0; rx_header[i].length = 0; } + } public: RAK13800EthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { - _isConnected = false; + _isEnabled = false; + _connected = false; + clearBuffers(); + #ifdef WITH_W5100S_POE + _hwReady = false; + _startedAt = 0; + #endif } - + bool begin(); void loop() override; // BaseSerialInterface methods - bool isConnected() const override; + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override { return _connected; } + bool isWriteBusy() const override { return false; } + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; - int available() override; - int read() override; - size_t write(const uint8_t *buf, size_t size) override; + // Unused — checkRecvFrame()/writeFrame() are fully overridden above for + // multi-client routing. Only implemented to satisfy SerialEthernetInterface's + // pure-virtual contract. + int available() override { return 0; } + int read() override { return -1; } + size_t write(const uint8_t *buf, size_t size) override { (void)buf; (void)size; return 0; } };