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
127 changes: 125 additions & 2 deletions examples/companion_radio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,33 @@ 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
// 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)
#include <helpers/nrf52/SerialBLEInterface.h>
SerialBLEInterface serial_interface;
#else
Expand Down Expand Up @@ -111,6 +137,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 +203,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 +338,48 @@ 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
}
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;
}
Loading