From 5fcfea6a9b450b1a04febb7b1cc158cb996ceb18 Mon Sep 17 00:00:00 2001 From: 1sthandy Date: Thu, 12 Nov 2026 21:18:46 +0100 Subject: [PATCH] Add PoE support for the RAK10720 (RAK4631 + RAK13800 + RAK19018) Lets a RAK4631 with a RAK13800 (W5100S) on a RAK19018 PoE base boot and stay up on PoE alone, with no battery. Fixes #2855. Builds on #1983 (WB_IO2 rail power) and the RAK13800EthernetInterface refactor (#3049). The RAK19018's Silvertel Ag9905MT converter folds back and resets unless it sees a continuous load of ~125 mA or more, which a bare MeshCore node does not draw. All changes are gated by the new WITH_W5100S_POE flag and only apply to the new *_ethernet_poe envs: - W5100SPoE: release the W5100S reset in an early constructor and run a bit-banged soft-reset/VERSIONR read from board.begin(), so the chip draws its full current before the converter's foldback timer expires. - RAK4631Board: skip the boot-voltage check (voltage_bootlock = 0); with no battery, the floating ADC pin would otherwise force a SYSTEMOFF loop. - RAK13800EthernetInterface / EthernetCLI: defer the disruptive library bring-up (PHY soft-reset + blocking DHCP) by ~6 s, until the converter has latched, and use a bounded DHCP timeout with a static-IP fallback. - simple_repeater / simple_room_server: shorter boot delay, and no CPU sleep on the repeater (it drops the load below the hold threshold). - New envs: RAK_4631_{repeater,room_server,companion_radio}_ethernet_poe. Tested on a RAK10720 behind a UniFi USW-Lite-16-PoE (companion variant): cold boot on PoE only, DHCP, and Home Assistant plus the phone app connected. The repeater and room-server envs build but are not yet tested on PoE hardware. Co-Authored-By: Claude Sonnet 5.5 --- examples/simple_repeater/main.cpp | 14 +++ examples/simple_room_server/main.cpp | 7 ++ .../RAK13800/RAK13800EthernetInterface.cpp | 46 +++++++++ .../RAK13800/RAK13800EthernetInterface.h | 11 +++ src/helpers/nrf52/EthernetCLI.h | 43 +++++++- variants/rak4631/RAK4631Board.cpp | 36 ++++++- variants/rak4631/W5100SPoE.cpp | 97 +++++++++++++++++++ variants/rak4631/W5100SPoE.h | 36 +++++++ variants/rak4631/platformio.ini | 24 +++++ variants/rak4631/target.cpp | 4 + 10 files changed, 312 insertions(+), 6 deletions(-) create mode 100644 variants/rak4631/W5100SPoE.cpp create mode 100644 variants/rak4631/W5100SPoE.h diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index d475b13d842..bbfb1c2a85b 100644 --- a/examples/simple_repeater/main.cpp +++ b/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(); @@ -205,6 +212,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 @@ -214,4 +227,5 @@ void loop() { } #endif } +#endif // WITH_W5100S_POE } diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 4bfad4e54d7..8adadf3a2bf 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -28,7 +28,14 @@ static char ethernet_command[MAX_POST_TEXT_LEN+1]; 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(); diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp index 027581e3039..0ec6aec08a9 100644 --- a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp @@ -13,7 +13,37 @@ 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 +#ifdef WITH_W5100S_POE + // Give the RAK19018 (Silvertel) PoE converter time to latch on the current the + // W5100S is already drawing (see variants/rak4631/W5100SPoE.cpp) before doing the + // disruptive Ethernet-library bring-up (PHY soft-reset + blocking DHCP), which + // otherwise collapses the marginal PoE supply at cold start. + #ifndef ETH_POE_DEFER_MS + #define ETH_POE_DEFER_MS 6000 + #endif + // Static fallback if no DHCP server answers (override per network). + #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 + bool RAK13800EthernetInterface::begin() { +#ifdef WITH_W5100S_POE + // Non-disruptive only: the real bring-up runs from loop() after ETH_POE_DEFER_MS. + _startedAt = millis(); + return true; +#else + return bringUp(); +#endif +} + +bool RAK13800EthernetInterface::bringUp() { // WB_IO2 (power enable) is already driven HIGH by early constructor // in RAK4631Board.cpp to support POE boot. @@ -45,6 +75,14 @@ bool RAK13800EthernetInterface::begin() { IPAddress subnet(ETHERNET_STATIC_SUBNET); IPAddress dns(ETHERNET_STATIC_DNS); Ethernet.begin(mac, ip, dns, gateway, subnet); + #elif defined(WITH_W5100S_POE) + // Bounded DHCP with a static-IP fallback, so the node stays reachable without a + // DHCP server and never blocks indefinitely at cold start. + if (Ethernet.begin(mac, 12000, 4000) == 0) { + ETHERNET_DEBUG_PRINTLN("DHCP failed, using static IP fallback"); + IPAddress ip(ETH_STATIC_IP), gw(ETH_GATEWAY), sn(ETH_SUBNET); + Ethernet.begin(mac, ip, gw, gw, sn); + } #else if (Ethernet.begin(mac) == 0) { ETHERNET_DEBUG_PRINTLN("Failed to initialize RAK13800 hardware."); @@ -88,6 +126,14 @@ bool RAK13800EthernetInterface::isConnected() const { } void RAK13800EthernetInterface::loop() { +#ifdef WITH_W5100S_POE + if (!_hwReady) { + if (millis() - _startedAt > ETH_POE_DEFER_MS) { + _hwReady = bringUp(); + } + return; + } +#endif Ethernet.maintain(); diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h index 34bb7587775..bd9a954b0ea 100644 --- a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h @@ -10,9 +10,20 @@ class RAK13800EthernetInterface : public SerialEthernetInterface { EthernetServer server; EthernetClient client; + bool bringUp(); // init the W5100S, obtain an IP and start listening + +#ifdef WITH_W5100S_POE + bool _hwReady; // true once the (deferred) bring-up has run + unsigned long _startedAt; // millis() at begin(); bring-up is deferred from here +#endif + public: RAK13800EthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { _isConnected = false; + #ifdef WITH_W5100S_POE + _hwReady = false; + _startedAt = 0; + #endif } bool begin(); diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h index 34802e567ae..5a9fe7e4095 100644 --- a/src/helpers/nrf52/EthernetCLI.h +++ b/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); } diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 1b5698d0ebd..dbb5f6c1ffa 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/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 -} \ No newline at end of file + +#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 +} diff --git a/variants/rak4631/W5100SPoE.cpp b/variants/rak4631/W5100SPoE.cpp new file mode 100644 index 00000000000..8ff9814af44 --- /dev/null +++ b/variants/rak4631/W5100SPoE.cpp @@ -0,0 +1,97 @@ +#ifdef WITH_W5100S_POE + +#include +#include +#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 diff --git a/variants/rak4631/W5100SPoE.h b/variants/rak4631/W5100SPoE.h new file mode 100644 index 00000000000..98c9140613a --- /dev/null +++ b/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(); diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index c5d94d51f52..17cd0145071 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -281,3 +281,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 diff --git a/variants/rak4631/target.cpp b/variants/rak4631/target.cpp index 813cb807ffb..52a16ca57db 100644 --- a/variants/rak4631/target.cpp +++ b/variants/rak4631/target.cpp @@ -2,6 +2,10 @@ #include "target.h" #include +#ifdef WITH_W5100S_POE + #include "W5100SPoE.h" +#endif + RAK4631Board board; #ifndef PIN_USER_BTN