Browse Source

Merge 108bced3aa into adc631f315

pull/2679/merge
1sthandy 20 hours ago
committed by GitHub
parent
commit
caaab1d375
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      examples/companion_radio/main.cpp
  2. 14
      examples/simple_repeater/main.cpp
  3. 43
      src/helpers/nrf52/EthernetCLI.h
  4. 385
      src/helpers/nrf52/SerialEthernetInterface.cpp
  5. 107
      src/helpers/nrf52/SerialEthernetInterface.h
  6. 36
      variants/rak4631/RAK4631Board.cpp
  7. 97
      variants/rak4631/W5100SPoE.cpp
  8. 36
      variants/rak4631/W5100SPoE.h
  9. 26
      variants/rak4631/platformio.ini
  10. 4
      variants/rak4631/target.cpp

4
examples/companion_radio/main.cpp

@ -71,7 +71,7 @@ static uint32_t _atoi(const char* sp) {
ArduinoSerialInterface serial_interface;
#endif
#elif defined(NRF52_PLATFORM)
#ifdef BLE_PIN_CODE
#if defined(BLE_PIN_CODE)
#include <helpers/nrf52/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#elif defined(ETHERNET_ENABLED)
@ -162,7 +162,7 @@ void setup() {
#endif
);
#ifdef 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)

14
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
}

43
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);
}

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(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

36
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
}
#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
}

97
variants/rak4631/W5100SPoE.cpp

@ -0,0 +1,97 @@
#ifdef WITH_W5100S_POE
#include <Arduino.h>
#include <nrf.h>
#include "W5100SPoE.h"
// ── 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_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) |
(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 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() {
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);
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

36
variants/rak4631/W5100SPoE.h

@ -0,0 +1,36 @@
#pragma once
// 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
// "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: 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)
// 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
// 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();

26
variants/rak4631/platformio.ini

@ -192,7 +192,7 @@ build_flags =
-D ETHERNET_ENABLED=1
; 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>
@ -267,3 +267,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

4
variants/rak4631/target.cpp

@ -2,6 +2,10 @@
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#ifdef WITH_W5100S_POE
#include "W5100SPoE.h"
#endif
RAK4631Board board;
#ifndef PIN_USER_BTN

Loading…
Cancel
Save