diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 31923543fd..4e62a062ae 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -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 + #include + 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 SerialBLEInterface serial_interface; #else @@ -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); @@ -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); @@ -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 } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 297337ab5c..648bc3f862 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -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(); @@ -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 @@ -169,4 +181,5 @@ void loop() { } #endif } +#endif // WITH_W5100S_POE } diff --git a/src/helpers/SerialEthernetInterface.cpp b/src/helpers/SerialEthernetInterface.cpp new file mode 100644 index 0000000000..6509238dc7 --- /dev/null +++ b/src/helpers/SerialEthernetInterface.cpp @@ -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; +} diff --git a/src/helpers/SerialEthernetInterface.h b/src/helpers/SerialEthernetInterface.h new file mode 100644 index 0000000000..69f3fc44f5 --- /dev/null +++ b/src/helpers/SerialEthernetInterface.h @@ -0,0 +1,78 @@ +#pragma once + +#include "BaseSerialInterface.h" +#include + +// 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 + #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 diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 9fb47b432e..0305d14f41 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -3,13 +3,24 @@ #include "RAK4631Board.h" +#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) { @@ -50,4 +61,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 0000000000..f56ac0c958 --- /dev/null +++ b/variants/rak4631/W5100SPoE.cpp @@ -0,0 +1,113 @@ +#ifdef WITH_W5100S_POE + +#include +#include +#include "W5100SPoE.h" + +// ── Early power-rail + RST release (constructor priority 200) ──────────────── +// Runs before setup(), right after SystemInit. Two jobs, as early as possible +// so the W5100S draws its full operating current before the RAK19018 +// (Silvertel) converter folds back during PoE cold-start: +// +// 1. Drive PIN_3V3_EN (P1.02 / WB_IO2 / Arduino 34) HIGH — this is the +// RAK19007 3.3 V PERIPHERAL POWER ENABLE that feeds the RAK13800/W5100S. +// Meshtastic does this in initVariant(); stock MeshCore never did, so our +// W5100S was only weakly powered via a default path (responds on USB but +// can't pull its full ~130 mA on the marginal PoE rail). +// 2. Drive W5100S RST (P0.21 / WB_IO3 / Arduino 21) HIGH — out of reset. +// +// Raw registers because the Arduino GPIO layer isn't up this early. +static void __attribute__((constructor(200))) w5100s_early_power_init() { + // P1.02 = 3V3_EN → HIGH (power the peripheral rail FIRST) + NRF_P1->PIN_CNF[2] = + (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_P1->OUTSET = (1UL << 2); + + // 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 the 3V3 rail + RST are driven +// (Arduino API, in case the core re-init touched them), then soft-resets and +// reads VERSIONR. Returns VERSIONR (0x51 = healthy W5100S). +uint8_t w5100s_poe_init() { + // Make sure the peripheral power rail stays driven HIGH. + pinMode(W5100S_3V3_EN_PIN, OUTPUT); digitalWrite(W5100S_3V3_EN_PIN, HIGH); + delay(20); // let the rail/W5100S settle after 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 0000000000..c3582f0daf --- /dev/null +++ b/variants/rak4631/W5100SPoE.h @@ -0,0 +1,42 @@ +#pragma once + +// W5100S 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 that lets Meshtastic boot on PoE without a battery: bring the +// W5100S PHY into its active state. An active W5100S draws ~120 mA — enough +// to keep the Silvertel converter latched in continuous mode. +// +// The W5100S has no power-enable pin on this board (always powered), but its +// RST must be HIGH for the PHY to run. We release RST as early as possible +// (before setup(), via a constructor) so the PHY draws current and latches +// the converter before its foldback timer expires. +// +// 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 + +// RAK19007 3.3 V peripheral power enable (feeds the RAK13800/W5100S). +// Must be driven HIGH or the W5100S is only weakly powered — exactly what +// Meshtastic's initVariant() does and stock MeshCore omits. +#ifndef W5100S_3V3_EN_PIN + #define W5100S_3V3_EN_PIN 34 // P1.02 / WB_IO2 +#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 ea7e49c355..38e73b0a44 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -194,4 +194,66 @@ build_flags = build_src_filter = ${rak4631.build_src_filter} +<../examples/kiss_modem/> lib_deps = - ${rak4631.lib_deps} \ No newline at end of file + ${rak4631.lib_deps} + +; Shared base for the PoE repeater variants (not an env: -> extendable) +[rak4631_poe_base] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"RAK4631 PoE Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_W5100S_POE +build_src_filter = ${rak4631.build_src_filter} + + + +<../examples/simple_repeater> + +; RAK4631 repeater with RAK13800 (W5100S) + RAK19018 PoE +[env:RAK_4631_repeater_poe] +extends = rak4631_poe_base + +; Diagnostic build: MESH_DEBUG shows step-by-step boot output. +; Remove again once diagnosis is done. +[env:RAK_4631_repeater_poe_debug] +extends = rak4631_poe_base +build_flags = + ${rak4631_poe_base.build_flags} + -D MESH_DEBUG=1 + +; RAK4631-based Ethernet companion (e.g. RAK10720 WisMesh Ethernet gateway: +; RAK13800/W5100S + RAK19018 for PoE). +; Exposes the MeshCore companion protocol as a TCP server (port 5000) over +; the W5100S — Home Assistant connects to the device's IP. +; WITH_W5100S_POE provides chip power + reset (3V3_EN/RST) so PoE works; +; WITH_ETHERNET_COMPANION selects the Ethernet TCP interface. +[env:RAK_4631_companion_radio_eth] +extends = rak4631 +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${rak4631.build_flags} + -I examples/companion_radio/ui-new + -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D WITH_W5100S_POE + -D WITH_ETHERNET_COMPANION + -D TCP_PORT=5000 +; -D ETH_DEBUG_LOGGING=1 +; -D ETH_HEARTBEAT=1 +; -D MESH_DEBUG=1 +build_src_filter = ${rak4631.build_src_filter} + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${rak4631.lib_deps} + densaugeo/base64 @ ~1.4.0 + https://github.com/meshtastic/RAK13800-W5100S.git + diff --git a/variants/rak4631/target.cpp b/variants/rak4631/target.cpp index a41ba72075..039471ad78 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