diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 8a9ecec61..03a1949d7 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 6509238dc..000000000 --- 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 69f3fc44f..000000000 --- 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 diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 36a998a4e..e766b9157 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -5,9 +5,9 @@ #include #include -#define PIN_SPI1_MISO (29) // (0 + 29) -#define PIN_SPI1_MOSI (30) // (0 + 30) -#define PIN_SPI1_SCK (3) // (0 + 3) +#define PIN_SPI1_MISO (29) +#define PIN_SPI1_MOSI (30) +#define PIN_SPI1_SCK (3) SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); @@ -15,79 +15,111 @@ 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 -#define RECV_STATE_IDLE 0 -#define RECV_STATE_HDR_FOUND 1 -#define RECV_STATE_LEN1_FOUND 2 -#define RECV_STATE_LEN2_FOUND 3 - -bool SerialEthernetInterface::begin() { - - ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); +#ifdef WITH_W5100S_POE + // Deferred bring-up: 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 - // WB_IO2 (power enable) is already driven HIGH by early constructor +static void eth_init_spi_and_pins() { + // WB_IO2 (power enable) is already driven HIGH by the 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); -#endif + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, HIGH); - 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_DEBUG_PRINTLN("Init"); 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(const 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. + ETH_DEBUG_PRINTLN("Trying DHCP (deferred)..."); + if (Ethernet.begin(mac, 12000, 4000) == 0) { + ETH_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 - ETHERNET_DEBUG_PRINTLN("Begin"); +#else if (Ethernet.begin(mac) == 0) { - ETHERNET_DEBUG_PRINTLN("Begin failed."); - - // DHCP failed -- let's figure out why - if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check for Ethernet hardware present. - { - ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found."); + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + ETH_DEBUG_PRINTLN("Ethernet hardware not found."); return false; } - if (Ethernet.linkStatus() == LinkOFF) // No physical connection - { - ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected."); + if (Ethernet.linkStatus() == LinkOFF) { + ETH_DEBUG_PRINTLN("Ethernet cable not connected."); return false; } - ETHERNET_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); + ETH_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); return false; } - #endif - ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); - ETHERNET_DEBUG_PRINT_IP("IP", Ethernet.localIP()); - ETHERNET_DEBUG_PRINT_IP("Subnet", Ethernet.subnetMask()); - ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP()); +#endif + + IPAddress ip = Ethernet.localIP(); + ETH_DEBUG_PRINTLN("Ethernet up: %u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]); + return true; +} - server.begin(); // start listening for clients - ETHERNET_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETHERNET_TCP_PORT); +bool SerialEthernetInterface::begin() { + uint8_t mac[6]; + generateEthernetMac(mac); + ETH_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(); + ETH_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(); + ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port %d", ETHERNET_TCP_PORT); + return true; +#endif +} +#ifdef WITH_W5100S_POE +bool SerialEthernetInterface::bringUpHardware() { + eth_init_spi_and_pins(); + uint8_t mac[6]; + generateEthernetMac(mac); + if (!eth_bring_up(mac)) return false; + server.begin(); + ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port %d", ETHERNET_TCP_PORT); return true; } +#endif void SerialEthernetInterface::enable() { if (_isEnabled) return; - _isEnabled = true; clearBuffers(); } @@ -98,170 +130,151 @@ void SerialEthernetInterface::disable() { size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { if (len > MAX_FRAME_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + ETH_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 (deviceConnected && len > 0) { - if (send_queue_len >= FRAME_QUEUE_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); - 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; + } - send_queue[send_queue_len].len = len; // add to send queue - memcpy(send_queue[send_queue_len].buf, src, len); - send_queue_len++; + // 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; - return len; - } - return 0; -} + ETH_DEBUG_PRINTLN("TX code=0x%02x len=%d -> %s", src[0], (int)len, + target < 0 ? "all" : (target == 0 ? "slot0" : target == 1 ? "slot1" : "slot2")); -bool SerialEthernetInterface::isWriteBusy() const { - return false; + 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[]) { - // Use accept() (not available()) so we only see newly-accepted sockets. - // available() also returns existing connected sockets that have data, - // which would cause us to treat each inbound packet as a "new client" - // and stop() the underlying socket — disconnecting the companion. - auto newClient = server.accept(); - if (newClient) { - IPAddress new_ip = newClient.remoteIP(); - uint16_t new_port = newClient.remotePort(); - ETHERNET_DEBUG_PRINTLN( - "New client accepted %u.%u.%u.%u:%u", - new_ip[0], - new_ip[1], - new_ip[2], - new_ip[3], - new_port); - - deviceConnected = false; - if (client) { - ETHERNET_DEBUG_PRINTLN("Closing previous client"); - client.stop(); +#ifdef WITH_W5100S_POE + if (!_hwReady) return 0; +#endif + + // ---- 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); } - _state = RECV_STATE_IDLE; - _frame_len = 0; - _rx_len = 0; - client = newClient; - ETHERNET_DEBUG_PRINTLN("Switched to new client"); } - if (client.connected()) { - if (!deviceConnected) { - ETHERNET_DEBUG_PRINTLN( - "Got connection %u.%u.%u.%u:%u", - client.remoteIP()[0], - client.remoteIP()[1], - client.remoteIP()[2], - client.remoteIP()[3], - client.remotePort()); - deviceConnected = true; + // ---- 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); } - } else { - if (deviceConnected) { - deviceConnected = false; - ETHERNET_DEBUG_PRINTLN("Disconnected"); + } + _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]; } - if (deviceConnected) { - if (send_queue_len > 0) { // first, check send queue + // ---- 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); + } + } - _last_write = millis(); - int len = send_queue[0].len; + 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 ETHERNET_RAW_LINE - ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); - client.write(send_queue[0].buf, len); - client.write("\r\n", 2); -#else - uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames - pkt[0] = '>'; - pkt[1] = (len & 0xFF); // LSB - pkt[2] = (len >> 8); // MSB - memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); - ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); - #endif - client.write(pkt, 3 + len); -#endif - send_queue_len--; - for (int i = 0; i < send_queue_len; i++) { // delete top item from queue - send_queue[i] = send_queue[i + 1]; - } - } else { - while (client.available()) { - int c = client.read(); - if (c < 0) break; - -#if ETHERNET_RAW_LINE - if (c == '\r' || c == '\n') { - if (_rx_len == 0) { - continue; - } - uint16_t out_len = _rx_len; - if (out_len > MAX_FRAME_SIZE) { - out_len = MAX_FRAME_SIZE; - } - memcpy(dest, _rx_buf, out_len); - _rx_len = 0; - return out_len; - } - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - _rx_len++; - } -#else - switch (_state) { - case RECV_STATE_IDLE: - if (c == '<') { - _state = RECV_STATE_HDR_FOUND; - } - break; - case RECV_STATE_HDR_FOUND: - _frame_len = (uint8_t)c; - _state = RECV_STATE_LEN1_FOUND; - break; - case RECV_STATE_LEN1_FOUND: - _frame_len |= ((uint16_t)c) << 8; - _rx_len = 0; - _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; - break; - default: - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - } - _rx_len++; - if (_rx_len >= _frame_len) { - if (_frame_len > MAX_FRAME_SIZE) { - _frame_len = MAX_FRAME_SIZE; - } - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); - #endif - memcpy(dest, _rx_buf, _frame_len); - _state = RECV_STATE_IDLE; - return _frame_len; - } + 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; } -#endif + 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; } -bool SerialEthernetInterface::isConnected() const { - return deviceConnected; -} - void SerialEthernetInterface::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/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index b8b4e94b2..0991dea6a 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -2,81 +2,100 @@ #ifdef ETHERNET_ENABLED -#include "helpers/BaseSerialInterface.h" +#include "../BaseSerialInterface.h" #include #include #ifndef ETHERNET_TCP_PORT #define ETHERNET_TCP_PORT 5000 #endif -// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets + +// 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 + +#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 deviceConnected; bool _isEnabled; - unsigned long _last_write; - uint8_t _state; - uint16_t _frame_len; - uint16_t _rx_len; - uint8_t _rx_buf[MAX_FRAME_SIZE]; + 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 FRAME_QUEUE_SIZE 4 - int send_queue_len; - Frame send_queue[FRAME_QUEUE_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; - _state = 0; - _frame_len = 0; - _rx_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; } } - protected: +public: + SerialEthernetInterface() : server(ETHERNET_TCP_PORT) { + _isEnabled = false; + _connected = false; + clearBuffers(); + #ifdef WITH_W5100S_POE + _hwReady = false; + _startedAt = 0; + #endif + } - public: - SerialEthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { - deviceConnected = false; - _isEnabled = false; - _last_write = 0; - send_queue_len = 0; - _state = 0; - _frame_len = 0; - _rx_len = 0; - } - bool begin(); + bool begin(); - // BaseSerialInterface methods - void enable() override; - void disable() override; - bool isEnabled() const override { return _isEnabled; } + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } - bool isConnected() const override; - bool isWriteBusy() const override; + 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; + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; - void loop(); + void loop(); }; - -#if ETHERNET_DEBUG_LOGGING && ARDUINO +#if ETH_DEBUG_LOGGING && ARDUINO #include - #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) + #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 ETHERNET_DEBUG_PRINT(...) {} - #define ETHERNET_DEBUG_PRINTLN(...) {} - #define ETHERNET_DEBUG_PRINT_IP(...) {} + #define ETH_DEBUG_PRINT(...) {} + #define ETH_DEBUG_PRINTLN(...) {} #endif #endif // ETHERNET_ENABLED