Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 100 additions & 2 deletions examples/companion_radio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,32 @@ static uint32_t _atoi(const char* sp) {
ArduinoSerialInterface serial_interface;
#endif
#elif defined(NRF52_PLATFORM)
#ifdef BLE_PIN_CODE
#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
// Static IP (no DHCP): predictable address for Home Assistant AND avoids
// the multi-second blocking DHCP that reboot-loops the device on PoE.
// Override per network if needed (octets are comma-separated).
#ifndef ETH_STATIC_IP
#define ETH_STATIC_IP 192,168,1,50
#endif
#ifndef ETH_GATEWAY
#define ETH_GATEWAY 192,168,1,1
#endif
#ifndef ETH_SUBNET
#define ETH_SUBNET 255,255,255,0
#endif
#elif defined(BLE_PIN_CODE)
#include <helpers/nrf52/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#else
Expand Down Expand Up @@ -111,6 +136,27 @@ void halt() {
unsigned long last_wifi_reconnect_attempt = 0;
#endif

#if defined(WITH_ETHERNET_COMPANION)
// Direct W5100S register write via eth_spi (proven path). Common-register
// block addresses are fixed: GAR=0x0001, SUBR=0x0005, SHAR=0x0009, SIPR=0x000F.
static void eth_wr(uint16_t a, uint8_t v) {
eth_spi.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
digitalWrite(26, LOW);
eth_spi.transfer(0xF0); eth_spi.transfer(a >> 8); eth_spi.transfer(a & 0xFF); eth_spi.transfer(v);
digitalWrite(26, HIGH);
eth_spi.endTransaction();
}
static void eth_write_netcfg(const uint8_t* mac) {
const uint8_t ip[4] = { ETH_STATIC_IP };
const uint8_t gw[4] = { ETH_GATEWAY };
const uint8_t sn[4] = { ETH_SUBNET };
for (int i = 0; i < 4; i++) eth_wr(0x0001 + i, gw[i]); // GAR
for (int i = 0; i < 4; i++) eth_wr(0x0005 + i, sn[i]); // SUBR
for (int i = 0; i < 6; i++) eth_wr(0x0009 + i, mac[i]); // SHAR
for (int i = 0; i < 4; i++) eth_wr(0x000F + i, ip[i]); // SIPR
}
#endif

void setup() {
Serial.begin(115200);

Expand Down Expand Up @@ -156,7 +202,39 @@ void setup() {
#endif
);

#ifdef BLE_PIN_CODE
#if defined(WITH_ETHERNET_COMPANION)
{
// Bring up the W5100S (RAK13800) TCP/IP stack so the companion protocol is
// reachable over Ethernet (Home Assistant connects to this IP : TCP_PORT).
// Chip power + reset is handled in board.begin() (WITH_W5100S_POE: 3V3_EN +
// RST). The W5100S has its OWN SPI peripheral (eth_spi on SPIM2, pins
// SCK=3/MISO=29/MOSI=30, CS=26) — separate from the radio, which uses the
// global SPI on SPIM3 remapped to the LoRa pins. Derive a stable
// locally-administered MAC from the nRF52 device ID.
// Compute a stable locally-administered MAC from the nRF52 device ID.
// IMPORTANT: the W5100S/Ethernet library bring-up (W5100.init does a PHY
// soft-reset) is DEFERRED to loop() — see below. Doing it here in setup
// dipped the W5100S current during the marginal PoE cold-start window and
// collapsed the RAK19018 (Silvertel) converter → reboot loop. board.begin
// already has the W5100S drawing current (3V3_EN + RST + bit-bang reset),
// which latches the PoE converter just like the plain repeater build.
g_eth_mac[0] = 0x02; // locally administered, unicast
uint32_t id0 = NRF_FICR->DEVICEID[0];
uint32_t id1 = NRF_FICR->DEVICEID[1];
g_eth_mac[1] = (id0 >> 24) & 0xFF;
g_eth_mac[2] = (id0 >> 16) & 0xFF;
g_eth_mac[3] = (id0 >> 8) & 0xFF;
g_eth_mac[4] = (id0) & 0xFF;
g_eth_mac[5] = (id1) & 0xFF;

// Non-disruptive SPI setup here (no chip reset); the disruptive part — the
// lib's Ethernet.begin() / W5100.init() PHY soft-reset — is deferred to
// loop() (~6 s) so it can't collapse the marginal PoE supply at cold start.
eth_spi.begin();
Ethernet.init(eth_spi, 26);
Serial.println("Ethernet companion: bring-up deferred to loop()");
}
#elif defined(BLE_PIN_CODE)
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#else
serial_interface.begin(Serial);
Expand Down Expand Up @@ -259,4 +337,24 @@ void loop() {
last_wifi_reconnect_attempt = millis();
}
#endif

#if defined(WITH_ETHERNET_COMPANION)
// Deferred Ethernet bring-up: only AFTER the device has booted and the PoE
// converter is solidly latched (~6 s). The W5100.init() PHY soft-reset would
// collapse the marginal PoE supply if done during setup() (reboot loop).
static bool _eth_up = false;
if (!_eth_up && millis() > 6000) {
IPAddress sip(ETH_STATIC_IP), sgw(ETH_GATEWAY), ssn(ETH_SUBNET);
Ethernet.begin(g_eth_mac, sip, sgw, sgw, ssn); // inits chip mode/sockets (PHY soft-reset)
serial_interface.begin(TCP_PORT); // start TCP server
delay(50);
eth_write_netcfg(g_eth_mac); // force IP/GW/SN/MAC (reliable here)
_eth_up = true;
IPAddress ip = Ethernet.localIP();
Serial.print("Ethernet up (deferred): ");
Serial.print(ip[0]); Serial.print('.'); Serial.print(ip[1]); Serial.print('.');
Serial.print(ip[2]); Serial.print('.'); Serial.print(ip[3]);
Serial.print(":"); Serial.println(TCP_PORT);
}
#endif
}
13 changes: 13 additions & 0 deletions examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,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();

Expand Down Expand Up @@ -156,6 +163,11 @@ void loop() {
#endif
rtc_clock.tick();

#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(1800); // nrf ignores seconds param, sleeps whenever possible
Expand All @@ -169,4 +181,5 @@ void loop() {
}
#endif
}
#endif // WITH_W5100S_POE
}
151 changes: 151 additions & 0 deletions src/helpers/SerialEthernetInterface.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#include "SerialEthernetInterface.h"

void SerialEthernetInterface::begin(int port) {
// Ethernet hardware (Ethernet.init/begin) is brought up in setup();
// here we only start the TCP server.
server = new EthernetServer(port);
server->begin();
}

void SerialEthernetInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
send_queue_len = 0;
}

void SerialEthernetInterface::disable() {
_isEnabled = false;
}

size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
ETH_DEBUG_PRINTLN("writeFrame(): frame too big, len=%d", (int)len);
return 0;
}
if (!_connected || len == 0) return 0;

if (send_queue_len >= ETH_FRAME_QUEUE_SIZE) {
ETH_DEBUG_PRINTLN("writeFrame(): send_queue full (dropping code=0x%02x)", src[0]);
return 0;
}

// PUSH codes (>= 0x80) go to all clients; command responses go to the
// client that issued the most recent command.
int8_t target = (src[0] >= 0x80) ? -1 : (int8_t)_last_rx;

ETH_DEBUG_PRINTLN("TX code=0x%02x len=%d -> %s", src[0], (int)len,
target < 0 ? "all" : (target == 0 ? "slot0" : target == 1 ? "slot1" : "slot2"));

send_queue[send_queue_len].target = target;
send_queue[send_queue_len].len = (uint8_t)len;
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
return len;
}

size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) {
if (server == NULL) return 0;

// ---- accept a new connection into a free slot --------------------------
// accept() returns each new connection once and maintains the listen socket,
// so it must be called every loop.
EthernetClient nc = server->accept();
if (nc) {
int slot = -1;
for (int i = 0; i < MAX_ETH_CLIENTS; i++) {
if (!clients[i].connected()) { slot = i; break; }
}
if (slot >= 0) {
clients[slot].stop(); // free any lingering socket in this slot
clients[slot] = nc;
rx_header[slot].type = 0;
rx_header[slot].length = 0;
ETH_DEBUG_PRINTLN("Got connection (slot %d)", slot);
} else {
nc.stop(); // all slots busy — reject
ETH_DEBUG_PRINTLN("Rejected connection (all %d slots busy)", MAX_ETH_CLIENTS);
}
}

// ---- refresh connected state, free dropped sockets ---------------------
bool any = false;
for (int i = 0; i < MAX_ETH_CLIENTS; i++) {
if (clients[i].connected()) {
any = true;
} else if (rx_header[i].type || rx_header[i].length) {
// a client that was active just dropped — reset its parse state
rx_header[i].type = 0;
rx_header[i].length = 0;
clients[i].stop();
ETH_DEBUG_PRINTLN("Disconnected (slot %d)", i);
}
}
_connected = any;

// ---- drain the outbound queue ------------------------------------------
while (send_queue_len > 0) {
Frame &f = send_queue[0];
uint8_t pkt[3 + MAX_FRAME_SIZE];
pkt[0] = '>';
pkt[1] = (f.len & 0xFF);
pkt[2] = (f.len >> 8);
memcpy(&pkt[3], f.buf, f.len);

if (f.target < 0) { // broadcast (push)
for (int i = 0; i < MAX_ETH_CLIENTS; i++) {
if (clients[i].connected()) clients[i].write(pkt, 3 + f.len);
}
} else if (f.target < MAX_ETH_CLIENTS && clients[f.target].connected()) {
clients[f.target].write(pkt, 3 + f.len); // response to the requester
}

send_queue_len--;
for (int i = 0; i < send_queue_len; i++) send_queue[i] = send_queue[i + 1];
}

// ---- read ONE inbound frame (round-robin across clients) ---------------
for (int k = 0; k < MAX_ETH_CLIENTS; k++) {
int i = (_rr + k) % MAX_ETH_CLIENTS;
EthernetClient &c = clients[i];
if (!c.connected()) continue;

// frame header = [type][len_lo][len_hi]
if (rx_header[i].type == 0 || rx_header[i].length == 0) {
if (c.available() >= 3) {
c.readBytes(&rx_header[i].type, 1);
c.readBytes((uint8_t *)&rx_header[i].length, 2);
}
}

if (rx_header[i].type != 0 && rx_header[i].length != 0) {
int avail = c.available();
int frame_type = rx_header[i].type;
int frame_length = rx_header[i].length;

if (frame_length > avail) continue; // wait for the rest

if (frame_length > MAX_FRAME_SIZE || frame_type != '<') {
// oversized or unexpected type — discard
while (frame_length > 0) {
uint8_t skip[1];
int n = c.read(skip, 1);
if (n <= 0) break;
frame_length -= n;
}
rx_header[i].type = 0;
rx_header[i].length = 0;
continue;
}

c.readBytes(dest, frame_length);
rx_header[i].type = 0;
rx_header[i].length = 0;
_last_rx = i; // route responses back here
_rr = (i + 1) % MAX_ETH_CLIENTS; // fairness
ETH_DEBUG_PRINTLN("RX[%d] cmd=0x%02x len=%d", i, dest[0], frame_length);
return frame_length;
}
}

return 0;
}
78 changes: 78 additions & 0 deletions src/helpers/SerialEthernetInterface.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#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
Loading