Browse Source

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 `+<helpers/*.cpp>`. Its
`#include <RAK13800_W5100S.h>` 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
`+<helpers/nrf52/SerialEthernetInterface.cpp>`. Now every RAK4631 variant
builds, and only the Ethernet build pulls in the W5100S library.
pull/2679/head
1sthandy 4 weeks ago
parent
commit
65dab45286
  1. 127
      examples/companion_radio/main.cpp
  2. 151
      src/helpers/SerialEthernetInterface.cpp
  3. 78
      src/helpers/SerialEthernetInterface.h
  4. 385
      src/helpers/nrf52/SerialEthernetInterface.cpp
  5. 107
      src/helpers/nrf52/SerialEthernetInterface.h

127
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 <SPI.h>
#include <helpers/SerialEthernetInterface.h>
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 <helpers/nrf52/SerialBLEInterface.h>
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
}

151
src/helpers/SerialEthernetInterface.cpp

@ -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;
}

78
src/helpers/SerialEthernetInterface.h

@ -1,78 +0,0 @@
#pragma once
#include "BaseSerialInterface.h"
#include <RAK13800_W5100S.h>
// 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 <Arduino.h>
#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

385
src/helpers/nrf52/SerialEthernetInterface.cpp

@ -5,9 +5,9 @@
#include <SPI.h>
#include <EthernetUdp.h>
#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();
}

107
src/helpers/nrf52/SerialEthernetInterface.h

@ -2,81 +2,100 @@
#ifdef ETHERNET_ENABLED
#include "helpers/BaseSerialInterface.h"
#include "../BaseSerialInterface.h"
#include <SPI.h>
#include <RAK13800_W5100S.h>
#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 <Arduino.h>
#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

Loading…
Cancel
Save