From 70b51bd096da36d5186e08c46819d7e49e69fe9b Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Mon, 9 Mar 2026 14:52:01 -0700 Subject: [PATCH 1/6] Add native Ethernet support for RAK4631 repeater, room server, and companion Add W5100S Ethernet adapter support for RAK4631-based firmware, enabling TCP CLI access on port 23 as an alternative to BLE/Serial connections. - New SerialEthernetInterface for nRF52 with DHCP, reconnection handling, and shared WB_IO2 power pin management with GPS module - Ethernet build targets for repeater, room server, and companion firmware - Prevent GPS from toggling WB_IO2 when Ethernet module is active - CI build check for all three ETH firmware targets Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pr-build-check.yml | 3 + examples/companion_radio/main.cpp | 56 +++- examples/simple_repeater/main.cpp | 167 ++++++++++ examples/simple_room_server/main.cpp | 157 +++++++++ src/helpers/nrf52/SerialEthernetInterface.cpp | 303 ++++++++++++++++++ src/helpers/nrf52/SerialEthernetInterface.h | 83 +++++ .../sensors/EnvironmentSensorManager.cpp | 7 + variants/rak4631/RAK4631Board.cpp | 4 + variants/rak4631/platformio.ini | 73 ++++- 9 files changed, 842 insertions(+), 11 deletions(-) create mode 100644 src/helpers/nrf52/SerialEthernetInterface.cpp create mode 100644 src/helpers/nrf52/SerialEthernetInterface.h diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index 5ba677cd35..2d9dbf79f7 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -23,8 +23,11 @@ jobs: - Heltec_v3_room_server # nRF52 - RAK_4631_companion_radio_ble + - RAK_4631_companion_radio_eth - RAK_4631_repeater + - RAK_4631_repeater_eth - RAK_4631_room_server + - RAK_4631_room_server_eth # RP2040 - PicoW_repeater # STM32 diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index eff9efca47..a3c83f3294 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,19 +12,21 @@ static uint32_t _atoi(const char* sp) { return n; } +uint32_t tick_count = 0; + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #if defined(QSPIFLASH) #include DataStore store(InternalFS, QSPIFlash, rtc_clock); #else - #if defined(EXTRAFS) - #include - CustomLFS ExtraFS(0xD4000, 0x19000, 128); - DataStore store(InternalFS, ExtraFS, rtc_clock); - #else - DataStore store(InternalFS, rtc_clock); - #endif + #if defined(EXTRAFS) + #include + CustomLFS ExtraFS(0xD4000, 0x19000, 128); + DataStore store(InternalFS, ExtraFS, rtc_clock); + #else + DataStore store(InternalFS, rtc_clock); + #endif #endif #elif defined(RP2040_PLATFORM) #include @@ -74,13 +76,21 @@ static uint32_t _atoi(const char* sp) { #ifdef BLE_PIN_CODE #include SerialBLEInterface serial_interface; + #elif defined(ETH_ENABLED) + #include + SerialEthernetInterface serial_interface; #else #include ArduinoSerialInterface serial_interface; #endif #elif defined(STM32_PLATFORM) - #include - ArduinoSerialInterface serial_interface; + #ifdef ETH_ENABLED + #include + SerialEthernetInterface serial_interface; + #elif + #include + ArduinoSerialInterface serial_interface; + #endif #else #error "need to define a serial interface" #endif @@ -107,7 +117,6 @@ void halt() { void setup() { Serial.begin(115200); - board.begin(); #ifdef DISPLAY_CLASS @@ -152,6 +161,23 @@ void setup() { #ifdef BLE_PIN_CODE serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); +#elif ETH_ENABLED + + Serial.print("Waiting for serial to connect...\n"); + time_t timeout = millis(); + // Initialize Serial for debug output. + while (!Serial) + { + if ((millis() - timeout) < 5000) { delay(100); } else { break; } + } + Serial.print("Initalizing ethernet adapter....\n"); + bool result = serial_interface.begin(); + if (!result) { + while (true) + { + delay(1); // Do nothing, just love you. + } + } #else serial_interface.begin(Serial); #endif @@ -225,4 +251,14 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + + // Debugging only... making sure something is alive. + tick_count++; + if (tick_count % 5000 == 0) { + Serial.print("."); + } + +#ifdef ETH_ENABLED + serial_interface.maintain(); +#endif } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index d226d1fa76..88157171a5 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -8,6 +8,123 @@ static UITask ui_task(display); #endif +#ifdef ETH_ENABLED + #include + #include + + #define PIN_SPI1_MISO (29) + #define PIN_SPI1_MOSI (30) + #define PIN_SPI1_SCK (3) + SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + + #define PIN_ETH_POWER_EN WB_IO2 + #define PIN_ETHERNET_RESET 21 + #define PIN_ETHERNET_SS 26 + + #ifndef ETH_TCP_PORT + #define ETH_TCP_PORT 23 // telnet port for CLI access + #endif + + #define ETH_RETRY_INTERVAL_MS 30000 + + static EthernetServer eth_server(ETH_TCP_PORT); + static EthernetClient eth_client; + static volatile bool eth_running = false; + + static void generateDeviceMac(uint8_t mac[6]) { + uint32_t device_id = NRF_FICR->DEVICEID[0]; + mac[0] = 0x02; mac[1] = 0x92; mac[2] = 0x1F; + mac[3] = (device_id >> 16) & 0xFF; + mac[4] = (device_id >> 8) & 0xFF; + mac[5] = device_id & 0xFF; + } + + // FreeRTOS task: handles hw init, DHCP, and retries in the background + static void eth_task(void* param) { + (void)param; + + // Hardware init + Serial.println("ETH: Initializing hardware"); + pinMode(PIN_ETH_POWER_EN, OUTPUT); + digitalWrite(PIN_ETH_POWER_EN, HIGH); + vTaskDelay(pdMS_TO_TICKS(100)); + + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); + vTaskDelay(pdMS_TO_TICKS(100)); + digitalWrite(PIN_ETHERNET_RESET, HIGH); + + ETH_SPI_PORT.begin(); + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + uint8_t mac[6]; + generateDeviceMac(mac); + Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + // Retry loop: keep trying until we get an IP + while (!eth_running) { + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + Serial.println("ETH: Hardware not found, giving up"); + vTaskDelete(NULL); + return; + } + + if (Ethernet.linkStatus() == LinkOFF) { + vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + continue; + } + + Serial.println("ETH: Link detected, attempting DHCP..."); + if (Ethernet.begin(mac, 10000, 2000) == 0) { + Serial.println("ETH: DHCP failed, will retry"); + vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + continue; + } + + 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", ETH_TCP_PORT); + eth_server.begin(); + eth_running = true; + } + + // DHCP succeeded, task is done + vTaskDelete(NULL); + } + + static void eth_start_task() { + xTaskCreate(eth_task, "eth_init", 1024, NULL, 1, NULL); + } + + // Format ethernet status into reply buffer. Returns true if command was handled. + static bool eth_handle_command(const char* command, char* reply) { + if (strcmp(command, "eth") != 0) return false; + if (!eth_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETH_TCP_PORT); + } + return true; + } + + // Check for new TCP client connections + static void eth_check_client() { + if (eth_client && eth_client.connected()) return; + + auto newClient = eth_server.available(); + if (newClient) { + if (eth_client) eth_client.stop(); + eth_client = newClient; + IPAddress ip = eth_client.remoteIP(); + Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + eth_client.println("MeshCore Repeater CLI"); + eth_client.print("> "); + } + } +#endif + StdRNG fast_rng; SimpleMeshTables tables; @@ -18,6 +135,9 @@ void halt() { } static char command[160]; +#ifdef ETH_ENABLED +static char eth_command[160]; +#endif // For power saving unsigned long lastActive = 0; // mark last active time @@ -85,6 +205,9 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; +#ifdef ETH_ENABLED + eth_command[0] = 0; +#endif sensors.begin(); @@ -94,6 +217,10 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif +#ifdef ETH_ENABLED + eth_start_task(); +#endif + // send out initial zero hop Advertisement to the mesh #if ENABLE_ADVERT_ON_BOOT == 1 the_mesh.sendSelfAdvertisement(16000, false); @@ -101,6 +228,7 @@ void setup() { } void loop() { + // Handle Serial CLI int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); @@ -119,6 +247,10 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; + reply[0] = 0; +#ifdef ETH_ENABLED + if (!eth_handle_command(command, reply)) +#endif the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); @@ -127,6 +259,41 @@ void loop() { command[0] = 0; // reset command buffer } +#ifdef ETH_ENABLED + if (eth_running) { + eth_check_client(); + Ethernet.maintain(); + } + + if (eth_running && eth_client && eth_client.connected()) { + int elen = strlen(eth_command); + while (eth_client.available() && elen < (int)sizeof(eth_command)-1) { + char c = eth_client.read(); + if (c == '\n') continue; // ignore LF + eth_command[elen++] = c; + eth_command[elen] = 0; + if (c == '\r') break; + } + if (elen == sizeof(eth_command)-1) { + eth_command[sizeof(eth_command)-1] = '\r'; + } + + if (elen > 0 && eth_command[elen - 1] == '\r') { + eth_command[elen - 1] = 0; + eth_client.println(); + char reply[160]; + reply[0] = 0; + if (!eth_handle_command(eth_command, reply)) + the_mesh.handleCommand(0, eth_command, reply); + if (reply[0]) { + eth_client.print(" -> "); eth_client.println(reply); + } + eth_client.print("> "); + eth_command[0] = 0; + } + } +#endif + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007d5..a84a7ee930 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -3,6 +3,114 @@ #include "MyMesh.h" +#ifdef ETH_ENABLED + #include + #include + + #define PIN_SPI1_MISO (29) + #define PIN_SPI1_MOSI (30) + #define PIN_SPI1_SCK (3) + SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + + #define PIN_ETH_POWER_EN WB_IO2 + #define PIN_ETHERNET_RESET 21 + #define PIN_ETHERNET_SS 26 + + #ifndef ETH_TCP_PORT + #define ETH_TCP_PORT 23 + #endif + + #define ETH_RETRY_INTERVAL_MS 30000 + + static EthernetServer eth_server(ETH_TCP_PORT); + static EthernetClient eth_client; + static volatile bool eth_running = false; + + static void generateDeviceMac(uint8_t mac[6]) { + uint32_t device_id = NRF_FICR->DEVICEID[0]; + mac[0] = 0x02; mac[1] = 0x92; mac[2] = 0x1F; + mac[3] = (device_id >> 16) & 0xFF; + mac[4] = (device_id >> 8) & 0xFF; + mac[5] = device_id & 0xFF; + } + + static void eth_task(void* param) { + (void)param; + + Serial.println("ETH: Initializing hardware"); + pinMode(PIN_ETH_POWER_EN, OUTPUT); + digitalWrite(PIN_ETH_POWER_EN, HIGH); + vTaskDelay(pdMS_TO_TICKS(100)); + + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); + vTaskDelay(pdMS_TO_TICKS(100)); + digitalWrite(PIN_ETHERNET_RESET, HIGH); + + ETH_SPI_PORT.begin(); + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + uint8_t mac[6]; + generateDeviceMac(mac); + Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + while (!eth_running) { + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + Serial.println("ETH: Hardware not found, giving up"); + vTaskDelete(NULL); + return; + } + if (Ethernet.linkStatus() == LinkOFF) { + vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + continue; + } + + Serial.println("ETH: Link detected, attempting DHCP..."); + if (Ethernet.begin(mac, 10000, 2000) == 0) { + Serial.println("ETH: DHCP failed, will retry"); + vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + continue; + } + + 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", ETH_TCP_PORT); + eth_server.begin(); + eth_running = true; + } + vTaskDelete(NULL); + } + + static void eth_start_task() { + xTaskCreate(eth_task, "eth_init", 1024, NULL, 1, NULL); + } + + static bool eth_handle_command(const char* command, char* reply) { + if (strcmp(command, "eth") != 0) return false; + if (!eth_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETH_TCP_PORT); + } + return true; + } + + static void eth_check_client() { + if (eth_client && eth_client.connected()) return; + auto newClient = eth_server.available(); + if (newClient) { + if (eth_client) eth_client.stop(); + eth_client = newClient; + IPAddress ip = eth_client.remoteIP(); + Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + eth_client.println("MeshCore Room Server CLI"); + eth_client.print("> "); + } + } +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -17,6 +125,9 @@ void halt() { } static char command[MAX_POST_TEXT_LEN+1]; +#ifdef ETH_ENABLED +static char eth_command[MAX_POST_TEXT_LEN+1]; +#endif void setup() { Serial.begin(115200); @@ -67,6 +178,9 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; +#ifdef ETH_ENABLED + eth_command[0] = 0; +#endif sensors.begin(); @@ -76,6 +190,10 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif +#ifdef ETH_ENABLED + eth_start_task(); +#endif + // send out initial zero hop Advertisement to the mesh #if ENABLE_ADVERT_ON_BOOT == 1 the_mesh.sendSelfAdvertisement(16000, false); @@ -99,6 +217,10 @@ void loop() { if (len > 0 && command[len - 1] == '\r') { // received complete line command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; + reply[0] = 0; +#ifdef ETH_ENABLED + if (!eth_handle_command(command, reply)) +#endif the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); @@ -107,6 +229,41 @@ void loop() { command[0] = 0; // reset command buffer } +#ifdef ETH_ENABLED + if (eth_running) { + eth_check_client(); + Ethernet.maintain(); + } + + if (eth_running && eth_client && eth_client.connected()) { + int elen = strlen(eth_command); + while (eth_client.available() && elen < (int)sizeof(eth_command)-1) { + char c = eth_client.read(); + if (c == '\n') continue; + eth_command[elen++] = c; + eth_command[elen] = 0; + if (c == '\r') break; + } + if (elen == sizeof(eth_command)-1) { + eth_command[sizeof(eth_command)-1] = '\r'; + } + + if (elen > 0 && eth_command[elen - 1] == '\r') { + eth_command[elen - 1] = 0; + eth_client.println(); + char reply[160]; + reply[0] = 0; + if (!eth_handle_command(eth_command, reply)) + the_mesh.handleCommand(0, eth_command, reply); + if (reply[0]) { + eth_client.print(" -> "); eth_client.println(reply); + } + eth_client.print("> "); + eth_command[0] = 0; + } + } +#endif + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp new file mode 100644 index 0000000000..ba95153122 --- /dev/null +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -0,0 +1,303 @@ +#include "SerialEthernetInterface.h" +#include +#include + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + +#define PIN_ETH_POWER_EN WB_IO2 // output, high to enable +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS 26 +//#define STATIC_IP 1 + +#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() { + + ETH_DEBUG_PRINTLN("Ethernet initalizing"); + +#ifdef PIN_ETH_POWER_EN + ETH_DEBUG_PRINTLN("Ethernet power enable"); + pinMode(PIN_ETH_POWER_EN, OUTPUT); + digitalWrite(PIN_ETH_POWER_EN, HIGH); // Power up. + delay(100); + ETH_DEBUG_PRINTLN("Ethernet power enabled"); +#endif + +#ifdef PIN_ETHERNET_RESET + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. + delay(100); + digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. + ETH_DEBUG_PRINTLN("Ethernet reset pulse"); +#endif + + uint8_t mac[6]; + generateDeviceMac(mac); + ETH_DEBUG_PRINTLN( + "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", + mac[0], + mac[1], + mac[2], + mac[3], + mac[4], + mac[5]); + ETH_DEBUG_PRINTLN("Init"); + ETH_SPI_PORT.begin(); + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + // Hardcode IP address for now + #ifdef STATIC_IP + IPAddress ip(192, 168, 8, 118); + IPAddress gateway(192, 168, 8, 1); + IPAddress subnet(255, 255, 255, 0); + IPAddress dns(192, 168, 8, 1); + Ethernet.begin(mac, ip, dns, gateway, subnet); + #else + ETH_DEBUG_PRINTLN("Begin"); + if (Ethernet.begin(mac) == 0) { + ETH_DEBUG_PRINTLN("Begin failed."); + + // DHCP failed -- let's figure out why + if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check for Ethernet hardware present. + { + ETH_DEBUG_PRINTLN("Ethernet hardware not found."); + return false; + } + if (Ethernet.linkStatus() == LinkOFF) // No physical connection + { + ETH_DEBUG_PRINTLN("Ethernet cable not connected."); + return false; + } + ETH_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); + return false; + } + #endif + ETH_DEBUG_PRINTLN("Ethernet begin complete"); + IPAddress ip = Ethernet.localIP(); + ETH_DEBUG_PRINT_IP("IP", ip); + + IPAddress subnet = Ethernet.subnetMask(); + ETH_DEBUG_PRINT_IP("Subnet", subnet); + + IPAddress gateway = Ethernet.gatewayIP(); + ETH_DEBUG_PRINT_IP("Gateway", gateway); + + server.begin(); // start listening for clients + ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", TCP_PORT); + + return true; +} + +void SerialEthernetInterface::enable() { + if (_isEnabled) return; + + _isEnabled = true; + clearBuffers(); +} + +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\n", len); + return 0; + } + + if (deviceConnected && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + ETH_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + 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++; + + return len; + } + return 0; +} + +bool SerialEthernetInterface::isWriteBusy() const { + return false; +} + +size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { + // check if new client connected + if (client && client.connected()) { + // Avoid polling for new clients while an active connection exists. + } else { + auto newClient = server.available(); + if (newClient) { + IPAddress new_ip = newClient.remoteIP(); + uint16_t new_port = newClient.remotePort(); + ETH_DEBUG_PRINTLN( + "New client available %u.%u.%u.%u:%u", + new_ip[0], + new_ip[1], + new_ip[2], + new_ip[3], + new_port); + if (client && client.connected()) { + IPAddress cur_ip = client.remoteIP(); + uint16_t cur_port = client.remotePort(); + ETH_DEBUG_PRINTLN( + "Current client %u.%u.%u.%u:%u", + cur_ip[0], + cur_ip[1], + cur_ip[2], + cur_ip[3], + cur_port); + if (cur_ip == new_ip && cur_port == new_port) { + ETH_DEBUG_PRINTLN("Ignoring duplicate client"); + return 0; + } + } + + deviceConnected = false; + if (client) { + ETH_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + _state = RECV_STATE_IDLE; + _frame_len = 0; + _rx_len = 0; + client = newClient; + ETH_DEBUG_PRINTLN("Switched to new client"); + } + } + + if (client.connected()) { + if (!deviceConnected) { + ETH_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; + } + } else { + if (deviceConnected) { + deviceConnected = false; + ETH_DEBUG_PRINTLN("Disconnected"); + } + } + + if (deviceConnected) { + if (send_queue_len > 0) { // first, check send queue + + _last_write = millis(); + int len = send_queue[0].len; + +#if ETH_RAW_LINE + ETH_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); + ETH_DEBUG_PRINTLN("Sending frame len=%d", len); + #if ETH_DEBUG_LOGGING && ARDUINO + ETH_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 ETH_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 ETH_DEBUG_LOGGING && ARDUINO + ETH_DEBUG_PRINTLN("RX frame len=%d", _frame_len); + #endif + memcpy(dest, _rx_buf, _frame_len); + _state = RECV_STATE_IDLE; + return _frame_len; + } + } +#endif + } + } + } + + return 0; +} + +bool SerialEthernetInterface::isConnected() const { + return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; +} + +void SerialEthernetInterface::generateDeviceMac(uint8_t mac[6]) { + uint32_t device_id = NRF_FICR->DEVICEID[0]; + + mac[0] = 0x02; + mac[1] = 0x92; + mac[2] = 0x1F; + mac[3] = (device_id >> 16) & 0xFF; + mac[4] = (device_id >> 8) & 0xFF; + mac[5] = device_id & 0xFF; +} + +void SerialEthernetInterface::maintain() { + Ethernet.maintain(); +} diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h new file mode 100644 index 0000000000..39eefbb4fc --- /dev/null +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -0,0 +1,83 @@ + +#include "helpers/BaseSerialInterface.h" +#include +#include + +// expects ETH_ENABLED = 1 +#define TCP_PORT 5000 +// define ETH_RAW_LINE=1 to use raw line-based CLI instead of framed packets + +class SerialEthernetInterface : public BaseSerialInterface { + bool deviceConnected; + bool _isEnabled; + unsigned long _last_write; + unsigned long adv_restart_time; + uint8_t _state; + uint16_t _frame_len; + uint16_t _rx_len; + uint8_t _rx_buf[MAX_FRAME_SIZE]; + + EthernetServer server; + EthernetClient client; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 4 + int recv_queue_len; + Frame recv_queue[FRAME_QUEUE_SIZE]; + int send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { + recv_queue_len = 0; + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + + protected: + + public: + SerialEthernetInterface() : server(EthernetServer(TCP_PORT)) { + deviceConnected = false; + _isEnabled = false; + _last_write = 0; + send_queue_len = recv_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + bool begin(); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override; + bool isWriteBusy() const override; + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; + + void maintain(); + +private: + void generateDeviceMac(uint8_t mac[6]); +}; + + +#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__) + #define ETH_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) +#else + #define ETH_DEBUG_PRINT(...) {} + #define ETH_DEBUG_PRINTLN(...) {} + #define ETH_DEBUG_PRINT_IP(...) {} +#endif diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 07807011db..ee09d31d53 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -628,6 +628,13 @@ void EnvironmentSensorManager::rakGPSInit(){ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ + #if defined(ETH_ENABLED) && defined(RAK_BOARD) + if (ioPin == WB_IO2) { + // WB_IO2 powers the Ethernet module on RAK baseboards. + return false; + } + #endif + //set initial waking state pinMode(ioPin,OUTPUT); digitalWrite(ioPin,LOW); diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 9fb47b432e..767c833f4d 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -36,6 +36,10 @@ void RAK4631Board::begin() { pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP); #endif +#ifdef RAK_ETH_ENABLE + beginETH(); +#endif + #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); #endif diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 737ef5652f..39765b01ed 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -46,6 +46,26 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> +[env:RAK_4631_repeater_eth] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"RAK4631 Repeater ETH"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D ETH_ENABLED=1 + -D MESH_DEBUG=1 +build_src_filter = ${rak4631.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${rak4631.lib_deps} + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S + https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip + [env:RAK_4631_repeater_bridge_rs232_serial1] extends = rak4631 build_flags = @@ -108,6 +128,26 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_room_server> +[env:RAK_4631_room_server_eth] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Test Room ETH"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D ETH_ENABLED=1 + -D MESH_DEBUG=1 +build_src_filter = ${rak4631.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${rak4631.lib_deps} + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S + https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip + [env:RAK_4631_companion_radio_usb] extends = rak4631 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld @@ -129,6 +169,37 @@ lib_deps = ${rak4631.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:RAK_4631_companion_radio_eth] +extends = rak4631 +board_build.ldscript = boards/nrf52840_s140_v6.ld +board_upload.maximum_size = 712704 +build_unflags = + -D EXTRAFS=1 +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 ETH_ENABLED=1 +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 + -D MESH_DEBUG=1 + -D ETH_DEBUG_LOGGING=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 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S + https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip + + [env:RAK_4631_companion_radio_ble] extends = rak4631 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld @@ -192,4 +263,4 @@ 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} From aea64e1f1907650e993fbb82f06a90feb076d546 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Mon, 9 Mar 2026 15:15:21 -0700 Subject: [PATCH 2/6] Address PR review feedback for Ethernet support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add #pragma once to SerialEthernetInterface.h - Rename TCP_PORT to ETH_TCP_PORT with #ifndef guard - Fix typos: "initalizing" → "initializing" - Fix #elif without condition → #else for STM32 block - Replace infinite loop on ETH init failure with halt() - Remove heartbeat Serial.print(".") output - Remove dead beginETH() call (ETH_ENABLED, not RAK_ETH_ENABLE) - Comment out MESH_DEBUG and ETH_DEBUG_LOGGING build flags Co-Authored-By: Claude Opus 4.6 --- examples/companion_radio/main.cpp | 19 +++++-------------- src/helpers/nrf52/SerialEthernetInterface.cpp | 4 ++-- src/helpers/nrf52/SerialEthernetInterface.h | 8 +++++--- variants/rak4631/RAK4631Board.cpp | 3 --- variants/rak4631/platformio.ini | 8 ++++---- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index a3c83f3294..6dae1356df 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,7 +12,6 @@ static uint32_t _atoi(const char* sp) { return n; } -uint32_t tick_count = 0; #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include @@ -87,7 +86,7 @@ uint32_t tick_count = 0; #ifdef ETH_ENABLED #include SerialEthernetInterface serial_interface; - #elif + #else #include ArduinoSerialInterface serial_interface; #endif @@ -161,7 +160,7 @@ void setup() { #ifdef BLE_PIN_CODE serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); -#elif ETH_ENABLED +#elif defined(ETH_ENABLED) Serial.print("Waiting for serial to connect...\n"); time_t timeout = millis(); @@ -170,13 +169,11 @@ void setup() { { if ((millis() - timeout) < 5000) { delay(100); } else { break; } } - Serial.print("Initalizing ethernet adapter....\n"); + Serial.print("Initializing ethernet adapter....\n"); bool result = serial_interface.begin(); if (!result) { - while (true) - { - delay(1); // Do nothing, just love you. - } + Serial.println("ETH: Init failed, halting"); + halt(); } #else serial_interface.begin(Serial); @@ -252,12 +249,6 @@ void loop() { #endif rtc_clock.tick(); - // Debugging only... making sure something is alive. - tick_count++; - if (tick_count % 5000 == 0) { - Serial.print("."); - } - #ifdef ETH_ENABLED serial_interface.maintain(); #endif diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index ba95153122..3dc2067194 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -20,7 +20,7 @@ SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); bool SerialEthernetInterface::begin() { - ETH_DEBUG_PRINTLN("Ethernet initalizing"); + ETH_DEBUG_PRINTLN("Ethernet initializing"); #ifdef PIN_ETH_POWER_EN ETH_DEBUG_PRINTLN("Ethernet power enable"); @@ -90,7 +90,7 @@ bool SerialEthernetInterface::begin() { ETH_DEBUG_PRINT_IP("Gateway", gateway); server.begin(); // start listening for clients - ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", TCP_PORT); + ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETH_TCP_PORT); return true; } diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 39eefbb4fc..7adf3569eb 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -1,10 +1,12 @@ +#pragma once #include "helpers/BaseSerialInterface.h" #include #include -// expects ETH_ENABLED = 1 -#define TCP_PORT 5000 +#ifndef ETH_TCP_PORT + #define ETH_TCP_PORT 5000 +#endif // define ETH_RAW_LINE=1 to use raw line-based CLI instead of framed packets class SerialEthernetInterface : public BaseSerialInterface { @@ -42,7 +44,7 @@ class SerialEthernetInterface : public BaseSerialInterface { protected: public: - SerialEthernetInterface() : server(EthernetServer(TCP_PORT)) { + SerialEthernetInterface() : server(EthernetServer(ETH_TCP_PORT)) { deviceConnected = false; _isEnabled = false; _last_write = 0; diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 767c833f4d..08286604cd 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -36,9 +36,6 @@ void RAK4631Board::begin() { pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP); #endif -#ifdef RAK_ETH_ENABLE - beginETH(); -#endif #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 39765b01ed..96c8c73d20 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -57,7 +57,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 -D ETH_ENABLED=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> @@ -139,7 +139,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D ROOM_PASSWORD='"hello"' -D ETH_ENABLED=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_room_server> @@ -187,8 +187,8 @@ build_flags = -D ETH_ENABLED=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 - -D MESH_DEBUG=1 - -D ETH_DEBUG_LOGGING=1 +; -D MESH_DEBUG=1 +; -D ETH_DEBUG_LOGGING=1 build_src_filter = ${rak4631.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> From ffe0853d7fee02ab1ca4768d421bd081188363b1 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Tue, 10 Mar 2026 20:58:34 -0700 Subject: [PATCH 3/6] Add Ethernet documentation for RAK4631 ETH support Co-Authored-By: Claude Opus 4.6 --- docs/cli_commands.md | 23 +++++++++++++++++++++++ docs/faq.md | 25 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1d3430db21..ea2c941c22 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -19,6 +19,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - [GPS](#gps-when-gps-support-is-compiled-in) - [Sensors](#sensors-when-sensor-support-is-compiled-in) - [Bridge](#bridge-when-bridge-support-is-compiled-in) + - [Ethernet](#ethernet-when-ethernet-support-is-compiled-in) --- @@ -881,3 +882,25 @@ region save **Default:** Varies by board --- + +### Ethernet (when Ethernet support is compiled in) + +Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_eth` firmware variants (e.g. `RAK_4631_repeater_eth`) to enable this feature. + +--- + +#### View Ethernet connection status +**Usage:** +- `eth` + +**Output:** +- `ETH: :` when connected (e.g. `ETH: 192.168.1.50:5000`) +- `ETH: not connected` when Ethernet is not active + +**Notes:** +- The Ethernet interface obtains an IP address via DHCP automatically on boot. +- A TCP server listens on port 5000 (default) for CLI connections. +- For repeaters and room servers, connect with any TCP client (e.g. `nc`, PuTTY) to access the same CLI available over serial. +- For companion radio firmware, the Ethernet interface replaces BLE/USB as the transport to companion apps. + +--- diff --git a/docs/faq.md b/docs/faq.md index 220b8971cb..84ab022615 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -97,6 +97,7 @@ A list of frequently-asked questions and answers for MeshCore - [7.5. Q: What is the format of a contact or channel QR code?](#75-q-what-is-the-format-of-a-contact-or-channel-qr-code) - [7.6. Q: How do I connect to the companion via WIFI, e.g. using a heltec v3?](#76-q-how-do-i-connect-to-the-companion-via-wifi-eg-using-a-heltec-v3) - [7.7. Q: I have a Station G2, or a Heltec V4, or an Ikoka Stick, or a radio with a EByte E22-900M30S or a E22-900M33S module, what should their transmit power be set to?](#77-q-i-have-a-station-g2-or-a-heltec-v4-or-an-ikoka-stick-or-a-radio-with-a-ebyte-e22-900m30s-or-a-e22-900m33s-module-what-should-their-transmit-power-be-set-to) + - [7.8. Q: How do I use Ethernet with a RAK4631?](#78-q-how-do-i-use-ethernet-with-a-rak4631) ## 1. Introduction @@ -878,3 +879,27 @@ For companion radios, you can set these radios' transmit power in the smartphone | **Heltec V4** | Standard Output | 10 dBm | 22 dBm | | | | High Output | 22 dBm | 28 dBm | | --- + +### 7.8. Q: How do I use Ethernet with a RAK4631? + **A:** +MeshCore supports Ethernet on RAK4631 boards using the [RAK13800](https://docs.rakwireless.com/product-categories/wisblock/rak13800/datasheet/) WisBlock Ethernet module (based on the W5100S chip). + +**Hardware required:** +- RAK4631 WisBlock Core +- RAK19007 or RAK19018 WisBlock Base Board (with an available IO slot) +- RAK13800 WisBlock Ethernet module +- Ethernet cable connected to a network with a DHCP server + +**Firmware:** +Flash one of the Ethernet-enabled firmware variants: +- `RAK_4631_repeater_eth` - Repeater with Ethernet CLI access +- `RAK_4631_room_server_eth` - Room server with Ethernet CLI access +- `RAK_4631_companion_radio_eth` - Companion radio over Ethernet (replaces BLE) + +**Connecting:** +- The device obtains an IP address via DHCP automatically on boot. +- For repeaters and room servers, connect to the device on TCP port 5000 using any TCP client (e.g. `nc 5000` or PuTTY in raw mode). This gives you the same CLI available over serial/USB. +- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. +- Use the `eth` CLI command to check connection status and see the assigned IP address. + +--- From 3e9ceba24a65a6f13409b48614909237e737f5aa Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Tue, 10 Mar 2026 22:35:31 -0700 Subject: [PATCH 4/6] Updates based on PR review feedback from liamcottle --- .github/workflows/pr-build-check.yml | 6 +- examples/companion_radio/main.cpp | 20 +-- examples/simple_repeater/main.cpp | 139 +++++++++--------- examples/simple_room_server/main.cpp | 139 +++++++++--------- src/helpers/nrf52/EthernetMac.h | 13 ++ src/helpers/nrf52/SerialEthernetInterface.cpp | 119 +++++++-------- src/helpers/nrf52/SerialEthernetInterface.h | 27 ++-- .../sensors/EnvironmentSensorManager.cpp | 2 +- variants/rak4631/platformio.ini | 19 ++- 9 files changed, 236 insertions(+), 248 deletions(-) create mode 100644 src/helpers/nrf52/EthernetMac.h diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index 2d9dbf79f7..9292338fbb 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -23,11 +23,11 @@ jobs: - Heltec_v3_room_server # nRF52 - RAK_4631_companion_radio_ble - - RAK_4631_companion_radio_eth + - RAK_4631_companion_radio_ethernet - RAK_4631_repeater - - RAK_4631_repeater_eth + - RAK_4631_repeater_ethernet - RAK_4631_room_server - - RAK_4631_room_server_eth + - RAK_4631_room_server_ethernet # RP2040 - PicoW_repeater # STM32 diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6dae1356df..9fa1e3810f 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -75,7 +75,7 @@ static uint32_t _atoi(const char* sp) { #ifdef BLE_PIN_CODE #include SerialBLEInterface serial_interface; - #elif defined(ETH_ENABLED) + #elif defined(ETHERNET_ENABLED) #include SerialEthernetInterface serial_interface; #else @@ -83,7 +83,7 @@ static uint32_t _atoi(const char* sp) { ArduinoSerialInterface serial_interface; #endif #elif defined(STM32_PLATFORM) - #ifdef ETH_ENABLED + #ifdef ETHERNET_ENABLED #include SerialEthernetInterface serial_interface; #else @@ -160,18 +160,14 @@ void setup() { #ifdef BLE_PIN_CODE serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); -#elif defined(ETH_ENABLED) - +#elif defined(ETHERNET_ENABLED) Serial.print("Waiting for serial to connect...\n"); time_t timeout = millis(); - // Initialize Serial for debug output. - while (!Serial) - { + while (!Serial) { if ((millis() - timeout) < 5000) { delay(100); } else { break; } } - Serial.print("Initializing ethernet adapter....\n"); - bool result = serial_interface.begin(); - if (!result) { + Serial.println("Initializing Ethernet adapter..."); + if (!serial_interface.begin()) { Serial.println("ETH: Init failed, halting"); halt(); } @@ -249,7 +245,7 @@ void loop() { #endif rtc_clock.tick(); -#ifdef ETH_ENABLED - serial_interface.maintain(); +#ifdef ETHERNET_ENABLED + serial_interface.loop(); #endif } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 88157171a5..0f37aa6b7f 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -8,45 +8,38 @@ static UITask ui_task(display); #endif -#ifdef ETH_ENABLED +#ifdef ETHERNET_ENABLED #include #include + #include #define PIN_SPI1_MISO (29) #define PIN_SPI1_MOSI (30) #define PIN_SPI1_SCK (3) - SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); - #define PIN_ETH_POWER_EN WB_IO2 + #define PIN_ETHERNET_POWER_EN WB_IO2 #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS 26 - #ifndef ETH_TCP_PORT - #define ETH_TCP_PORT 23 // telnet port for CLI access + #ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 23 // telnet port for CLI access #endif - #define ETH_RETRY_INTERVAL_MS 30000 + #define ETHERNET_RETRY_INTERVAL_MS 30000 - static EthernetServer eth_server(ETH_TCP_PORT); - static EthernetClient eth_client; - static volatile bool eth_running = false; - - static void generateDeviceMac(uint8_t mac[6]) { - uint32_t device_id = NRF_FICR->DEVICEID[0]; - mac[0] = 0x02; mac[1] = 0x92; mac[2] = 0x1F; - mac[3] = (device_id >> 16) & 0xFF; - mac[4] = (device_id >> 8) & 0xFF; - mac[5] = device_id & 0xFF; - } + static EthernetServer ethernet_server(ETHERNET_TCP_PORT); + static EthernetClient ethernet_client; + static volatile bool ethernet_running = false; // FreeRTOS task: handles hw init, DHCP, and retries in the background - static void eth_task(void* param) { + static void ethernet_task(void* param) { (void)param; // Hardware init Serial.println("ETH: Initializing hardware"); - pinMode(PIN_ETH_POWER_EN, OUTPUT); - digitalWrite(PIN_ETH_POWER_EN, HIGH); + pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); + digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); vTaskDelay(pdMS_TO_TICKS(100)); pinMode(PIN_ETHERNET_RESET, OUTPUT); @@ -54,8 +47,8 @@ vTaskDelay(pdMS_TO_TICKS(100)); digitalWrite(PIN_ETHERNET_RESET, HIGH); - ETH_SPI_PORT.begin(); - Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); uint8_t mac[6]; generateDeviceMac(mac); @@ -63,7 +56,7 @@ mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); // Retry loop: keep trying until we get an IP - while (!eth_running) { + while (!ethernet_running) { if (Ethernet.hardwareStatus() == EthernetNoHardware) { Serial.println("ETH: Hardware not found, giving up"); vTaskDelete(NULL); @@ -71,56 +64,56 @@ } if (Ethernet.linkStatus() == LinkOFF) { - vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); continue; } Serial.println("ETH: Link detected, attempting DHCP..."); if (Ethernet.begin(mac, 10000, 2000) == 0) { Serial.println("ETH: DHCP failed, will retry"); - vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); continue; } 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", ETH_TCP_PORT); - eth_server.begin(); - eth_running = true; + Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT); + ethernet_server.begin(); + ethernet_running = true; } // DHCP succeeded, task is done vTaskDelete(NULL); } - static void eth_start_task() { - xTaskCreate(eth_task, "eth_init", 1024, NULL, 1, NULL); + static void ethernet_start_task() { + xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); } // Format ethernet status into reply buffer. Returns true if command was handled. - static bool eth_handle_command(const char* command, char* reply) { + static bool ethernet_handle_command(const char* command, char* reply) { if (strcmp(command, "eth") != 0) return false; - if (!eth_running) { + if (!ethernet_running) { strcpy(reply, "ETH: not connected"); } else { IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETH_TCP_PORT); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); } return true; } // Check for new TCP client connections - static void eth_check_client() { - if (eth_client && eth_client.connected()) return; + static void ethernet_check_client() { + if (ethernet_client && ethernet_client.connected()) return; - auto newClient = eth_server.available(); + auto newClient = ethernet_server.available(); if (newClient) { - if (eth_client) eth_client.stop(); - eth_client = newClient; - IPAddress ip = eth_client.remoteIP(); + if (ethernet_client) ethernet_client.stop(); + ethernet_client = newClient; + IPAddress ip = ethernet_client.remoteIP(); Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - eth_client.println("MeshCore Repeater CLI"); - eth_client.print("> "); + ethernet_client.println("MeshCore Repeater CLI"); + ethernet_client.print("> "); } } #endif @@ -135,8 +128,8 @@ void halt() { } static char command[160]; -#ifdef ETH_ENABLED -static char eth_command[160]; +#ifdef ETHERNET_ENABLED +static char ethernet_command[160]; #endif // For power saving @@ -205,8 +198,8 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; -#ifdef ETH_ENABLED - eth_command[0] = 0; +#ifdef ETHERNET_ENABLED + ethernet_command[0] = 0; #endif sensors.begin(); @@ -217,8 +210,8 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif -#ifdef ETH_ENABLED - eth_start_task(); +#ifdef ETHERNET_ENABLED + ethernet_start_task(); #endif // send out initial zero hop Advertisement to the mesh @@ -248,10 +241,13 @@ void loop() { command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; reply[0] = 0; -#ifdef ETH_ENABLED - if (!eth_handle_command(command, reply)) -#endif +#ifdef ETHERNET_ENABLED + if (!ethernet_handle_command(command, reply)) { + the_mesh.handleCommand(0, command, reply); + } +#else the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! +#endif if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } @@ -259,37 +255,38 @@ void loop() { command[0] = 0; // reset command buffer } -#ifdef ETH_ENABLED - if (eth_running) { - eth_check_client(); +#ifdef ETHERNET_ENABLED + if (ethernet_running) { + ethernet_check_client(); Ethernet.maintain(); } - if (eth_running && eth_client && eth_client.connected()) { - int elen = strlen(eth_command); - while (eth_client.available() && elen < (int)sizeof(eth_command)-1) { - char c = eth_client.read(); - if (c == '\n') continue; // ignore LF - eth_command[elen++] = c; - eth_command[elen] = 0; - if (c == '\r') break; + if (ethernet_running && ethernet_client && ethernet_client.connected()) { + int elen = strlen(ethernet_command); + while (ethernet_client.available() && elen < (int)sizeof(ethernet_command)-1) { + char c = ethernet_client.read(); + if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) + if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } + ethernet_command[elen++] = c; + ethernet_command[elen] = 0; } - if (elen == sizeof(eth_command)-1) { - eth_command[sizeof(eth_command)-1] = '\r'; + if (elen == sizeof(ethernet_command)-1) { + ethernet_command[sizeof(ethernet_command)-1] = '\r'; } - if (elen > 0 && eth_command[elen - 1] == '\r') { - eth_command[elen - 1] = 0; - eth_client.println(); + if (elen > 0 && ethernet_command[elen - 1] == '\r') { + ethernet_command[elen - 1] = 0; + ethernet_client.println(); char reply[160]; reply[0] = 0; - if (!eth_handle_command(eth_command, reply)) - the_mesh.handleCommand(0, eth_command, reply); + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); + } if (reply[0]) { - eth_client.print(" -> "); eth_client.println(reply); + ethernet_client.print(" -> "); ethernet_client.println(reply); } - eth_client.print("> "); - eth_command[0] = 0; + ethernet_client.print("> "); + ethernet_command[0] = 0; } } #endif diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index a84a7ee930..97a793f49c 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -3,43 +3,36 @@ #include "MyMesh.h" -#ifdef ETH_ENABLED +#ifdef ETHERNET_ENABLED #include #include + #include #define PIN_SPI1_MISO (29) #define PIN_SPI1_MOSI (30) #define PIN_SPI1_SCK (3) - SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); - #define PIN_ETH_POWER_EN WB_IO2 + #define PIN_ETHERNET_POWER_EN WB_IO2 #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS 26 - #ifndef ETH_TCP_PORT - #define ETH_TCP_PORT 23 + #ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 23 #endif - #define ETH_RETRY_INTERVAL_MS 30000 + #define ETHERNET_RETRY_INTERVAL_MS 30000 - static EthernetServer eth_server(ETH_TCP_PORT); - static EthernetClient eth_client; - static volatile bool eth_running = false; + static EthernetServer ethernet_server(ETHERNET_TCP_PORT); + static EthernetClient ethernet_client; + static volatile bool ethernet_running = false; - static void generateDeviceMac(uint8_t mac[6]) { - uint32_t device_id = NRF_FICR->DEVICEID[0]; - mac[0] = 0x02; mac[1] = 0x92; mac[2] = 0x1F; - mac[3] = (device_id >> 16) & 0xFF; - mac[4] = (device_id >> 8) & 0xFF; - mac[5] = device_id & 0xFF; - } - - static void eth_task(void* param) { + static void ethernet_task(void* param) { (void)param; Serial.println("ETH: Initializing hardware"); - pinMode(PIN_ETH_POWER_EN, OUTPUT); - digitalWrite(PIN_ETH_POWER_EN, HIGH); + pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); + digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); vTaskDelay(pdMS_TO_TICKS(100)); pinMode(PIN_ETHERNET_RESET, OUTPUT); @@ -47,66 +40,66 @@ vTaskDelay(pdMS_TO_TICKS(100)); digitalWrite(PIN_ETHERNET_RESET, HIGH); - ETH_SPI_PORT.begin(); - Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); uint8_t mac[6]; generateDeviceMac(mac); Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - while (!eth_running) { + while (!ethernet_running) { if (Ethernet.hardwareStatus() == EthernetNoHardware) { Serial.println("ETH: Hardware not found, giving up"); vTaskDelete(NULL); return; } if (Ethernet.linkStatus() == LinkOFF) { - vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); continue; } Serial.println("ETH: Link detected, attempting DHCP..."); if (Ethernet.begin(mac, 10000, 2000) == 0) { Serial.println("ETH: DHCP failed, will retry"); - vTaskDelay(pdMS_TO_TICKS(ETH_RETRY_INTERVAL_MS)); + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); continue; } 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", ETH_TCP_PORT); - eth_server.begin(); - eth_running = true; + Serial.printf("ETH: Listening on TCP port %d\n", ETHERNET_TCP_PORT); + ethernet_server.begin(); + ethernet_running = true; } vTaskDelete(NULL); } - static void eth_start_task() { - xTaskCreate(eth_task, "eth_init", 1024, NULL, 1, NULL); + static void ethernet_start_task() { + xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); } - static bool eth_handle_command(const char* command, char* reply) { + static bool ethernet_handle_command(const char* command, char* reply) { if (strcmp(command, "eth") != 0) return false; - if (!eth_running) { + if (!ethernet_running) { strcpy(reply, "ETH: not connected"); } else { IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETH_TCP_PORT); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); } return true; } - static void eth_check_client() { - if (eth_client && eth_client.connected()) return; - auto newClient = eth_server.available(); + static void ethernet_check_client() { + if (ethernet_client && ethernet_client.connected()) return; + auto newClient = ethernet_server.available(); if (newClient) { - if (eth_client) eth_client.stop(); - eth_client = newClient; - IPAddress ip = eth_client.remoteIP(); + if (ethernet_client) ethernet_client.stop(); + ethernet_client = newClient; + IPAddress ip = ethernet_client.remoteIP(); Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - eth_client.println("MeshCore Room Server CLI"); - eth_client.print("> "); + ethernet_client.println("MeshCore Room Server CLI"); + ethernet_client.print("> "); } } #endif @@ -125,8 +118,8 @@ void halt() { } static char command[MAX_POST_TEXT_LEN+1]; -#ifdef ETH_ENABLED -static char eth_command[MAX_POST_TEXT_LEN+1]; +#ifdef ETHERNET_ENABLED +static char ethernet_command[MAX_POST_TEXT_LEN+1]; #endif void setup() { @@ -178,8 +171,8 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; -#ifdef ETH_ENABLED - eth_command[0] = 0; +#ifdef ETHERNET_ENABLED + ethernet_command[0] = 0; #endif sensors.begin(); @@ -190,8 +183,8 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif -#ifdef ETH_ENABLED - eth_start_task(); +#ifdef ETHERNET_ENABLED + ethernet_start_task(); #endif // send out initial zero hop Advertisement to the mesh @@ -218,10 +211,13 @@ void loop() { command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; reply[0] = 0; -#ifdef ETH_ENABLED - if (!eth_handle_command(command, reply)) -#endif +#ifdef ETHERNET_ENABLED + if (!ethernet_handle_command(command, reply)) { + the_mesh.handleCommand(0, command, reply); + } +#else the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! +#endif if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } @@ -229,37 +225,38 @@ void loop() { command[0] = 0; // reset command buffer } -#ifdef ETH_ENABLED - if (eth_running) { - eth_check_client(); +#ifdef ETHERNET_ENABLED + if (ethernet_running) { + ethernet_check_client(); Ethernet.maintain(); } - if (eth_running && eth_client && eth_client.connected()) { - int elen = strlen(eth_command); - while (eth_client.available() && elen < (int)sizeof(eth_command)-1) { - char c = eth_client.read(); - if (c == '\n') continue; - eth_command[elen++] = c; - eth_command[elen] = 0; - if (c == '\r') break; + if (ethernet_running && ethernet_client && ethernet_client.connected()) { + int elen = strlen(ethernet_command); + while (ethernet_client.available() && elen < (int)sizeof(ethernet_command)-1) { + char c = ethernet_client.read(); + if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) + if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } + ethernet_command[elen++] = c; + ethernet_command[elen] = 0; } - if (elen == sizeof(eth_command)-1) { - eth_command[sizeof(eth_command)-1] = '\r'; + if (elen == sizeof(ethernet_command)-1) { + ethernet_command[sizeof(ethernet_command)-1] = '\r'; } - if (elen > 0 && eth_command[elen - 1] == '\r') { - eth_command[elen - 1] = 0; - eth_client.println(); + if (elen > 0 && ethernet_command[elen - 1] == '\r') { + ethernet_command[elen - 1] = 0; + ethernet_client.println(); char reply[160]; reply[0] = 0; - if (!eth_handle_command(eth_command, reply)) - the_mesh.handleCommand(0, eth_command, reply); + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); + } if (reply[0]) { - eth_client.print(" -> "); eth_client.println(reply); + ethernet_client.print(" -> "); ethernet_client.println(reply); } - eth_client.print("> "); - eth_command[0] = 0; + ethernet_client.print("> "); + ethernet_command[0] = 0; } } #endif diff --git a/src/helpers/nrf52/EthernetMac.h b/src/helpers/nrf52/EthernetMac.h new file mode 100644 index 0000000000..034acf704d --- /dev/null +++ b/src/helpers/nrf52/EthernetMac.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +static inline void generateDeviceMac(uint8_t mac[6]) { + uint32_t device_id = NRF_FICR->DEVICEID[0]; + mac[0] = 0x02; + mac[1] = 0x92; + mac[2] = 0x1F; + mac[3] = (device_id >> 16) & 0xFF; + mac[4] = (device_id >> 8) & 0xFF; + mac[5] = device_id & 0xFF; +} diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 3dc2067194..68f10b2101 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -1,4 +1,5 @@ #include "SerialEthernetInterface.h" +#include "EthernetMac.h" #include #include @@ -6,12 +7,11 @@ #define PIN_SPI1_MOSI (30) // (0 + 30) #define PIN_SPI1_SCK (3) // (0 + 3) -SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); +SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); -#define PIN_ETH_POWER_EN WB_IO2 // output, high to enable +#define PIN_ETHERNET_POWER_EN WB_IO2 // output, high to enable #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS 26 -//#define STATIC_IP 1 #define RECV_STATE_IDLE 0 #define RECV_STATE_HDR_FOUND 1 @@ -19,15 +19,15 @@ SPIClass ETH_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); #define RECV_STATE_LEN2_FOUND 3 bool SerialEthernetInterface::begin() { - - ETH_DEBUG_PRINTLN("Ethernet initializing"); -#ifdef PIN_ETH_POWER_EN - ETH_DEBUG_PRINTLN("Ethernet power enable"); - pinMode(PIN_ETH_POWER_EN, OUTPUT); - digitalWrite(PIN_ETH_POWER_EN, HIGH); // Power up. + ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); + +#ifdef PIN_ETHERNET_POWER_EN + ETHERNET_DEBUG_PRINTLN("Ethernet power enable"); + pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); + digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); // Power up. delay(100); - ETH_DEBUG_PRINTLN("Ethernet power enabled"); + ETHERNET_DEBUG_PRINTLN("Ethernet power enabled"); #endif #ifdef PIN_ETHERNET_RESET @@ -35,12 +35,12 @@ bool SerialEthernetInterface::begin() { digitalWrite(PIN_ETHERNET_RESET, LOW); // Reset Time. delay(100); digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time. - ETH_DEBUG_PRINTLN("Ethernet reset pulse"); + ETHERNET_DEBUG_PRINTLN("Ethernet reset pulse"); #endif uint8_t mac[6]; generateDeviceMac(mac); - ETH_DEBUG_PRINTLN( + ETHERNET_DEBUG_PRINTLN( "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], @@ -48,49 +48,49 @@ bool SerialEthernetInterface::begin() { mac[3], mac[4], mac[5]); - ETH_DEBUG_PRINTLN("Init"); - ETH_SPI_PORT.begin(); - Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); - - // Hardcode IP address for now - #ifdef STATIC_IP - IPAddress ip(192, 168, 8, 118); - IPAddress gateway(192, 168, 8, 1); - IPAddress subnet(255, 255, 255, 0); - IPAddress dns(192, 168, 8, 1); + 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) + 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 - ETH_DEBUG_PRINTLN("Begin"); + ETHERNET_DEBUG_PRINTLN("Begin"); if (Ethernet.begin(mac) == 0) { - ETH_DEBUG_PRINTLN("Begin failed."); + ETHERNET_DEBUG_PRINTLN("Begin failed."); // DHCP failed -- let's figure out why if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check for Ethernet hardware present. { - ETH_DEBUG_PRINTLN("Ethernet hardware not found."); + ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found."); return false; } if (Ethernet.linkStatus() == LinkOFF) // No physical connection { - ETH_DEBUG_PRINTLN("Ethernet cable not connected."); + ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected."); return false; } - ETH_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); + ETHERNET_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); return false; } #endif - ETH_DEBUG_PRINTLN("Ethernet begin complete"); + ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); IPAddress ip = Ethernet.localIP(); - ETH_DEBUG_PRINT_IP("IP", ip); - + ETHERNET_DEBUG_PRINT_IP("IP", ip); + IPAddress subnet = Ethernet.subnetMask(); - ETH_DEBUG_PRINT_IP("Subnet", subnet); - + ETHERNET_DEBUG_PRINT_IP("Subnet", subnet); + IPAddress gateway = Ethernet.gatewayIP(); - ETH_DEBUG_PRINT_IP("Gateway", gateway); + ETHERNET_DEBUG_PRINT_IP("Gateway", gateway); server.begin(); // start listening for clients - ETH_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETH_TCP_PORT); + ETHERNET_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETHERNET_TCP_PORT); return true; } @@ -108,13 +108,13 @@ void SerialEthernetInterface::disable() { 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\n", len); + ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); return 0; } if (deviceConnected && len > 0) { if (send_queue_len >= FRAME_QUEUE_SIZE) { - ETH_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); return 0; } @@ -140,7 +140,7 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { if (newClient) { IPAddress new_ip = newClient.remoteIP(); uint16_t new_port = newClient.remotePort(); - ETH_DEBUG_PRINTLN( + ETHERNET_DEBUG_PRINTLN( "New client available %u.%u.%u.%u:%u", new_ip[0], new_ip[1], @@ -150,7 +150,7 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { if (client && client.connected()) { IPAddress cur_ip = client.remoteIP(); uint16_t cur_port = client.remotePort(); - ETH_DEBUG_PRINTLN( + ETHERNET_DEBUG_PRINTLN( "Current client %u.%u.%u.%u:%u", cur_ip[0], cur_ip[1], @@ -158,27 +158,27 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { cur_ip[3], cur_port); if (cur_ip == new_ip && cur_port == new_port) { - ETH_DEBUG_PRINTLN("Ignoring duplicate client"); + ETHERNET_DEBUG_PRINTLN("Ignoring duplicate client"); return 0; } } deviceConnected = false; if (client) { - ETH_DEBUG_PRINTLN("Closing previous client"); + ETHERNET_DEBUG_PRINTLN("Closing previous client"); client.stop(); } _state = RECV_STATE_IDLE; _frame_len = 0; _rx_len = 0; client = newClient; - ETH_DEBUG_PRINTLN("Switched to new client"); + ETHERNET_DEBUG_PRINTLN("Switched to new client"); } } if (client.connected()) { if (!deviceConnected) { - ETH_DEBUG_PRINTLN( + ETHERNET_DEBUG_PRINTLN( "Got connection %u.%u.%u.%u:%u", client.remoteIP()[0], client.remoteIP()[1], @@ -190,18 +190,18 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { } else { if (deviceConnected) { deviceConnected = false; - ETH_DEBUG_PRINTLN("Disconnected"); + ETHERNET_DEBUG_PRINTLN("Disconnected"); } } if (deviceConnected) { if (send_queue_len > 0) { // first, check send queue - + _last_write = millis(); int len = send_queue[0].len; -#if ETH_RAW_LINE - ETH_DEBUG_PRINTLN("TX line len=%d", len); +#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 @@ -210,9 +210,9 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { pkt[1] = (len & 0xFF); // LSB pkt[2] = (len >> 8); // MSB memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); - ETH_DEBUG_PRINTLN("Sending frame len=%d", len); - #if ETH_DEBUG_LOGGING && ARDUINO - ETH_DEBUG_PRINTLN("TX frame len=%d", 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 @@ -225,7 +225,7 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { int c = client.read(); if (c < 0) break; -#if ETH_RAW_LINE +#if ETHERNET_RAW_LINE if (c == '\r' || c == '\n') { if (_rx_len == 0) { continue; @@ -267,8 +267,8 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { if (_frame_len > MAX_FRAME_SIZE) { _frame_len = MAX_FRAME_SIZE; } - #if ETH_DEBUG_LOGGING && ARDUINO - ETH_DEBUG_PRINTLN("RX frame len=%d", _frame_len); + #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; @@ -284,20 +284,9 @@ size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { } bool SerialEthernetInterface::isConnected() const { - return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; -} - -void SerialEthernetInterface::generateDeviceMac(uint8_t mac[6]) { - uint32_t device_id = NRF_FICR->DEVICEID[0]; - - mac[0] = 0x02; - mac[1] = 0x92; - mac[2] = 0x1F; - mac[3] = (device_id >> 16) & 0xFF; - mac[4] = (device_id >> 8) & 0xFF; - mac[5] = device_id & 0xFF; + return deviceConnected; } -void SerialEthernetInterface::maintain() { +void SerialEthernetInterface::loop() { Ethernet.maintain(); } diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 7adf3569eb..5f06f688ec 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -4,10 +4,10 @@ #include #include -#ifndef ETH_TCP_PORT - #define ETH_TCP_PORT 5000 +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 5000 #endif -// define ETH_RAW_LINE=1 to use raw line-based CLI instead of framed packets +// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets class SerialEthernetInterface : public BaseSerialInterface { bool deviceConnected; @@ -44,7 +44,7 @@ class SerialEthernetInterface : public BaseSerialInterface { protected: public: - SerialEthernetInterface() : server(EthernetServer(ETH_TCP_PORT)) { + SerialEthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { deviceConnected = false; _isEnabled = false; _last_write = 0; @@ -66,20 +66,17 @@ class SerialEthernetInterface : public BaseSerialInterface { size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; - void maintain(); - -private: - void generateDeviceMac(uint8_t mac[6]); + void loop(); }; -#if ETH_DEBUG_LOGGING && ARDUINO +#if ETHERNET_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__) - #define ETH_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) + #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]) #else - #define ETH_DEBUG_PRINT(...) {} - #define ETH_DEBUG_PRINTLN(...) {} - #define ETH_DEBUG_PRINT_IP(...) {} + #define ETHERNET_DEBUG_PRINT(...) {} + #define ETHERNET_DEBUG_PRINTLN(...) {} + #define ETHERNET_DEBUG_PRINT_IP(...) {} #endif diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index ee09d31d53..55134e8c10 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -628,7 +628,7 @@ void EnvironmentSensorManager::rakGPSInit(){ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ - #if defined(ETH_ENABLED) && defined(RAK_BOARD) + #if defined(ETHERNET_ENABLED) && defined(RAK_BOARD) if (ioPin == WB_IO2) { // WB_IO2 powers the Ethernet module on RAK baseboards. return false; diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 96c8c73d20..cb280e31fa 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -46,17 +46,17 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> -[env:RAK_4631_repeater_eth] +[env:RAK_4631_repeater_ethernet] extends = rak4631 build_flags = ${rak4631.build_flags} -D DISPLAY_CLASS=SSD1306Display - -D ADVERT_NAME='"RAK4631 Repeater ETH"' + -D ADVERT_NAME='"RAK4631 Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D ETH_ENABLED=1 + -D ETHERNET_ENABLED=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + @@ -128,17 +128,17 @@ build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_room_server> -[env:RAK_4631_room_server_eth] +[env:RAK_4631_room_server_ethernet] extends = rak4631 build_flags = ${rak4631.build_flags} -D DISPLAY_CLASS=SSD1306Display - -D ADVERT_NAME='"Test Room ETH"' + -D ADVERT_NAME='"RAK4631 Room Server"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D ROOM_PASSWORD='"hello"' - -D ETH_ENABLED=1 + -D ETHERNET_ENABLED=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + @@ -170,7 +170,7 @@ lib_deps = densaugeo/base64 @ ~1.4.0 -[env:RAK_4631_companion_radio_eth] +[env:RAK_4631_companion_radio_ethernet] extends = rak4631 board_build.ldscript = boards/nrf52840_s140_v6.ld board_upload.maximum_size = 712704 @@ -184,11 +184,10 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 - -D ETH_ENABLED=1 + -D ETHERNET_ENABLED=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 -; -D MESH_DEBUG=1 -; -D ETH_DEBUG_LOGGING=1 +; -D ETHERNET_DEBUG_LOGGING=1 build_src_filter = ${rak4631.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> From 61ba79966b01b72476913f7783b2473b3ecb0652 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Wed, 11 Mar 2026 12:43:08 -0700 Subject: [PATCH 5/6] Address PR review feedback from liamcottle (second round) - Rename eth command to eth.status for consistency with other commands - Rename generateDeviceMac to generateEthernetMac for clarity - Refactor ethernet_handle_command to return false by default - Allow new TCP clients to replace existing connections (repeater, room server, SerialEthernetInterface) - Boot companion radio without Ethernet on init failure (LoRa-only recovery mode) - Remove > prompt from ethernet CLI for consistency with serial interface - Fix variable redeclaration compile error in SerialEthernetInterface when ETHERNET_STATIC_IP is defined - Fix TCP socket leak when duplicate client detection fires - Remove dead recv_queue and adv_restart_time members from SerialEthernetInterface - Fix port numbers in docs (port 23 for repeater/room server CLI, port 5000 for companion radio) - Clarify eth.status command is only available in repeater and room server firmware Co-Authored-By: Claude Sonnet 4.6 --- docs/cli_commands.md | 12 +-- docs/faq.md | 12 +-- examples/companion_radio/main.cpp | 10 ++- examples/simple_repeater/main.cpp | 24 +++--- examples/simple_room_server/main.cpp | 22 ++--- src/helpers/nrf52/EthernetMac.h | 2 +- src/helpers/nrf52/SerialEthernetInterface.cpp | 86 +++++++++---------- src/helpers/nrf52/SerialEthernetInterface.h | 6 +- 8 files changed, 81 insertions(+), 93 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index ea2c941c22..18ddf59ef9 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -885,22 +885,22 @@ region save ### Ethernet (when Ethernet support is compiled in) -Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_eth` firmware variants (e.g. `RAK_4631_repeater_eth`) to enable this feature. +Ethernet support is available on RAK4631 boards with a RAK13800 (W5100S) Ethernet module. Use the `_ethernet` firmware variants (e.g. `RAK_4631_repeater_ethernet`) to enable this feature. --- #### View Ethernet connection status **Usage:** -- `eth` +- `eth.status` **Output:** -- `ETH: :` when connected (e.g. `ETH: 192.168.1.50:5000`) +- `ETH: :` when connected (e.g. `ETH: 192.168.1.50:23`) - `ETH: not connected` when Ethernet is not active **Notes:** +- Available on repeater and room server firmware only. Companion radio ethernet firmware does not expose a CLI. - The Ethernet interface obtains an IP address via DHCP automatically on boot. -- A TCP server listens on port 5000 (default) for CLI connections. -- For repeaters and room servers, connect with any TCP client (e.g. `nc`, PuTTY) to access the same CLI available over serial. -- For companion radio firmware, the Ethernet interface replaces BLE/USB as the transport to companion apps. +- A TCP server listens on port 23 (default) for CLI connections. +- Connect with any TCP client (e.g. `nc`, PuTTY) to access the same CLI available over serial. --- diff --git a/docs/faq.md b/docs/faq.md index 84ab022615..8991508ff4 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -892,14 +892,14 @@ MeshCore supports Ethernet on RAK4631 boards using the [RAK13800](https://docs.r **Firmware:** Flash one of the Ethernet-enabled firmware variants: -- `RAK_4631_repeater_eth` - Repeater with Ethernet CLI access -- `RAK_4631_room_server_eth` - Room server with Ethernet CLI access -- `RAK_4631_companion_radio_eth` - Companion radio over Ethernet (replaces BLE) +- `RAK_4631_repeater_ethernet` - Repeater with Ethernet CLI access +- `RAK_4631_room_server_ethernet` - Room server with Ethernet CLI access +- `RAK_4631_companion_radio_ethernet` - Companion radio over Ethernet (replaces BLE) **Connecting:** - The device obtains an IP address via DHCP automatically on boot. -- For repeaters and room servers, connect to the device on TCP port 5000 using any TCP client (e.g. `nc 5000` or PuTTY in raw mode). This gives you the same CLI available over serial/USB. -- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. -- Use the `eth` CLI command to check connection status and see the assigned IP address. +- For repeaters and room servers, connect to the device on TCP port 23 using any TCP client (e.g. `nc 23` or PuTTY in raw mode). This gives you the same CLI available over serial/USB. +- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect on TCP port 5000 (same as the WiFi companion radio). +- Use the `eth.status` CLI command to check connection status and see the assigned IP address. --- diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 9fa1e3810f..d48e223d2d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -160,6 +160,7 @@ void setup() { #ifdef 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) Serial.print("Waiting for serial to connect...\n"); time_t timeout = millis(); @@ -167,14 +168,15 @@ void setup() { if ((millis() - timeout) < 5000) { delay(100); } else { break; } } Serial.println("Initializing Ethernet adapter..."); - if (!serial_interface.begin()) { - Serial.println("ETH: Init failed, halting"); - halt(); + if (serial_interface.begin()) { + the_mesh.startInterface(serial_interface); + } else { + Serial.println("ETH: Init failed, continuing without Ethernet (mesh only)"); } #else serial_interface.begin(Serial); -#endif the_mesh.startInterface(serial_interface); +#endif #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 0f37aa6b7f..00fad17547 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -51,7 +51,7 @@ Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); uint8_t mac[6]; - generateDeviceMac(mac); + generateEthernetMac(mac); Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); @@ -92,20 +92,20 @@ // Format ethernet status into reply buffer. Returns true if command was handled. static bool ethernet_handle_command(const char* command, char* reply) { - if (strcmp(command, "eth") != 0) return false; - if (!ethernet_running) { - strcpy(reply, "ETH: not connected"); - } else { - IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + if (strcmp(command, "eth.status") == 0) { + if (!ethernet_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + } + return true; } - return true; + return false; } - // Check for new TCP client connections + // Check for new TCP client connections, replacing any existing connection static void ethernet_check_client() { - if (ethernet_client && ethernet_client.connected()) return; - auto newClient = ethernet_server.available(); if (newClient) { if (ethernet_client) ethernet_client.stop(); @@ -113,7 +113,6 @@ IPAddress ip = ethernet_client.remoteIP(); Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); ethernet_client.println("MeshCore Repeater CLI"); - ethernet_client.print("> "); } } #endif @@ -285,7 +284,6 @@ void loop() { if (reply[0]) { ethernet_client.print(" -> "); ethernet_client.println(reply); } - ethernet_client.print("> "); ethernet_command[0] = 0; } } diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 97a793f49c..435d562824 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -44,7 +44,7 @@ Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); uint8_t mac[6]; - generateDeviceMac(mac); + generateEthernetMac(mac); Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); @@ -80,18 +80,20 @@ } static bool ethernet_handle_command(const char* command, char* reply) { - if (strcmp(command, "eth") != 0) return false; - if (!ethernet_running) { - strcpy(reply, "ETH: not connected"); - } else { - IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + if (strcmp(command, "eth.status") == 0) { + if (!ethernet_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + } + return true; } - return true; + return false; } + // Check for new TCP client connections, replacing any existing connection static void ethernet_check_client() { - if (ethernet_client && ethernet_client.connected()) return; auto newClient = ethernet_server.available(); if (newClient) { if (ethernet_client) ethernet_client.stop(); @@ -99,7 +101,6 @@ IPAddress ip = ethernet_client.remoteIP(); Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); ethernet_client.println("MeshCore Room Server CLI"); - ethernet_client.print("> "); } } #endif @@ -255,7 +256,6 @@ void loop() { if (reply[0]) { ethernet_client.print(" -> "); ethernet_client.println(reply); } - ethernet_client.print("> "); ethernet_command[0] = 0; } } diff --git a/src/helpers/nrf52/EthernetMac.h b/src/helpers/nrf52/EthernetMac.h index 034acf704d..0ee2ac06f5 100644 --- a/src/helpers/nrf52/EthernetMac.h +++ b/src/helpers/nrf52/EthernetMac.h @@ -2,7 +2,7 @@ #include -static inline void generateDeviceMac(uint8_t mac[6]) { +static inline void generateEthernetMac(uint8_t mac[6]) { uint32_t device_id = NRF_FICR->DEVICEID[0]; mac[0] = 0x02; mac[1] = 0x92; diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 68f10b2101..92001b1b26 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -39,7 +39,7 @@ bool SerialEthernetInterface::begin() { #endif uint8_t mac[6]; - generateDeviceMac(mac); + generateEthernetMac(mac); ETHERNET_DEBUG_PRINTLN( "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], @@ -80,14 +80,9 @@ bool SerialEthernetInterface::begin() { } #endif ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); - IPAddress ip = Ethernet.localIP(); - ETHERNET_DEBUG_PRINT_IP("IP", ip); - - IPAddress subnet = Ethernet.subnetMask(); - ETHERNET_DEBUG_PRINT_IP("Subnet", subnet); - - IPAddress gateway = Ethernet.gatewayIP(); - ETHERNET_DEBUG_PRINT_IP("Gateway", gateway); + ETHERNET_DEBUG_PRINT_IP("IP", Ethernet.localIP()); + ETHERNET_DEBUG_PRINT_IP("Subnet", Ethernet.subnetMask()); + ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP()); server.begin(); // start listening for clients ETHERNET_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETHERNET_TCP_PORT); @@ -132,48 +127,45 @@ bool SerialEthernetInterface::isWriteBusy() const { } size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { - // check if new client connected - if (client && client.connected()) { - // Avoid polling for new clients while an active connection exists. - } else { - auto newClient = server.available(); - if (newClient) { - IPAddress new_ip = newClient.remoteIP(); - uint16_t new_port = newClient.remotePort(); + // check if new client connected; new connections replace existing ones + auto newClient = server.available(); + if (newClient) { + IPAddress new_ip = newClient.remoteIP(); + uint16_t new_port = newClient.remotePort(); + ETHERNET_DEBUG_PRINTLN( + "New client available %u.%u.%u.%u:%u", + new_ip[0], + new_ip[1], + new_ip[2], + new_ip[3], + new_port); + if (client && client.connected()) { + IPAddress cur_ip = client.remoteIP(); + uint16_t cur_port = client.remotePort(); ETHERNET_DEBUG_PRINTLN( - "New client available %u.%u.%u.%u:%u", - new_ip[0], - new_ip[1], - new_ip[2], - new_ip[3], - new_port); - if (client && client.connected()) { - IPAddress cur_ip = client.remoteIP(); - uint16_t cur_port = client.remotePort(); - ETHERNET_DEBUG_PRINTLN( - "Current client %u.%u.%u.%u:%u", - cur_ip[0], - cur_ip[1], - cur_ip[2], - cur_ip[3], - cur_port); - if (cur_ip == new_ip && cur_port == new_port) { - ETHERNET_DEBUG_PRINTLN("Ignoring duplicate client"); - return 0; - } + "Current client %u.%u.%u.%u:%u", + cur_ip[0], + cur_ip[1], + cur_ip[2], + cur_ip[3], + cur_port); + if (cur_ip == new_ip && cur_port == new_port) { + ETHERNET_DEBUG_PRINTLN("Ignoring duplicate client"); + newClient.stop(); + return 0; } + } - deviceConnected = false; - if (client) { - ETHERNET_DEBUG_PRINTLN("Closing previous client"); - client.stop(); - } - _state = RECV_STATE_IDLE; - _frame_len = 0; - _rx_len = 0; - client = newClient; - ETHERNET_DEBUG_PRINTLN("Switched to new client"); + deviceConnected = false; + if (client) { + ETHERNET_DEBUG_PRINTLN("Closing previous client"); + client.stop(); } + _state = RECV_STATE_IDLE; + _frame_len = 0; + _rx_len = 0; + client = newClient; + ETHERNET_DEBUG_PRINTLN("Switched to new client"); } if (client.connected()) { diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 5f06f688ec..95ce8a5217 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -13,7 +13,6 @@ class SerialEthernetInterface : public BaseSerialInterface { bool deviceConnected; bool _isEnabled; unsigned long _last_write; - unsigned long adv_restart_time; uint8_t _state; uint16_t _frame_len; uint16_t _rx_len; @@ -28,13 +27,10 @@ class SerialEthernetInterface : public BaseSerialInterface { }; #define FRAME_QUEUE_SIZE 4 - int recv_queue_len; - Frame recv_queue[FRAME_QUEUE_SIZE]; int send_queue_len; Frame send_queue[FRAME_QUEUE_SIZE]; void clearBuffers() { - recv_queue_len = 0; send_queue_len = 0; _state = 0; _frame_len = 0; @@ -48,7 +44,7 @@ class SerialEthernetInterface : public BaseSerialInterface { deviceConnected = false; _isEnabled = false; _last_write = 0; - send_queue_len = recv_queue_len = 0; + send_queue_len = 0; _state = 0; _frame_len = 0; _rx_len = 0; From 88892de864b811fe286dedc510fb4e64f5955f7c Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Fri, 13 Mar 2026 10:48:42 -0700 Subject: [PATCH 6/6] Fix Ethernet init checking hardwareStatus before begin() and deduplicate CLI code The Ethernet retry loop in repeater and room server checked hardwareStatus() and linkStatus() before calling Ethernet.begin(), which always returned EthernetNoHardware since hardware detection only happens during begin(). Extract shared Ethernet CLI code into EthernetCLI.h to prevent future divergence. Also fix time_t type mismatch in companion radio Ethernet init. Co-Authored-By: Claude Opus 4.6 --- examples/companion_radio/main.cpp | 2 +- examples/simple_repeater/main.cpp | 146 ++----------------------- examples/simple_room_server/main.cpp | 139 ++---------------------- src/helpers/nrf52/EthernetCLI.h | 156 +++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 266 deletions(-) create mode 100644 src/helpers/nrf52/EthernetCLI.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d48e223d2d..a88da48fc2 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -163,7 +163,7 @@ void setup() { the_mesh.startInterface(serial_interface); #elif defined(ETHERNET_ENABLED) Serial.print("Waiting for serial to connect...\n"); - time_t timeout = millis(); + unsigned long timeout = millis(); while (!Serial) { if ((millis() - timeout) < 5000) { delay(100); } else { break; } } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 00fad17547..b9fa1aa89f 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -9,112 +9,8 @@ #endif #ifdef ETHERNET_ENABLED - #include - #include - #include - - #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); - - #define PIN_ETHERNET_POWER_EN WB_IO2 - #define PIN_ETHERNET_RESET 21 - #define PIN_ETHERNET_SS 26 - - #ifndef ETHERNET_TCP_PORT - #define ETHERNET_TCP_PORT 23 // telnet port for CLI access - #endif - - #define ETHERNET_RETRY_INTERVAL_MS 30000 - - static EthernetServer ethernet_server(ETHERNET_TCP_PORT); - static EthernetClient ethernet_client; - static volatile bool ethernet_running = false; - - // FreeRTOS task: handles hw init, DHCP, and retries in the background - static void ethernet_task(void* param) { - (void)param; - - // Hardware init - Serial.println("ETH: Initializing hardware"); - pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); - digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); - vTaskDelay(pdMS_TO_TICKS(100)); - - pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, LOW); - vTaskDelay(pdMS_TO_TICKS(100)); - digitalWrite(PIN_ETHERNET_RESET, HIGH); - - ETHERNET_SPI_PORT.begin(); - Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); - - uint8_t mac[6]; - generateEthernetMac(mac); - Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - - // Retry loop: keep trying until we get an IP - while (!ethernet_running) { - if (Ethernet.hardwareStatus() == EthernetNoHardware) { - Serial.println("ETH: Hardware not found, giving up"); - vTaskDelete(NULL); - return; - } - - if (Ethernet.linkStatus() == LinkOFF) { - vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); - continue; - } - - Serial.println("ETH: Link detected, attempting DHCP..."); - if (Ethernet.begin(mac, 10000, 2000) == 0) { - Serial.println("ETH: DHCP failed, will retry"); - vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); - continue; - } - - 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; - } - - // DHCP succeeded, task is done - vTaskDelete(NULL); - } - - static void ethernet_start_task() { - xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); - } - - // Format ethernet status into reply buffer. Returns true if command was handled. - static bool ethernet_handle_command(const char* command, char* reply) { - if (strcmp(command, "eth.status") == 0) { - if (!ethernet_running) { - strcpy(reply, "ETH: not connected"); - } else { - IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); - } - return true; - } - return false; - } - - // Check for new TCP client connections, replacing any existing connection - static void ethernet_check_client() { - auto newClient = ethernet_server.available(); - if (newClient) { - if (ethernet_client) ethernet_client.stop(); - ethernet_client = newClient; - IPAddress ip = ethernet_client.remoteIP(); - Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - ethernet_client.println("MeshCore Repeater CLI"); - } - } + #define ETHERNET_CLI_BANNER "MeshCore Repeater CLI" + #include #endif StdRNG fast_rng; @@ -255,37 +151,15 @@ void loop() { } #ifdef ETHERNET_ENABLED - if (ethernet_running) { - ethernet_check_client(); - Ethernet.maintain(); - } - - if (ethernet_running && ethernet_client && ethernet_client.connected()) { - int elen = strlen(ethernet_command); - while (ethernet_client.available() && elen < (int)sizeof(ethernet_command)-1) { - char c = ethernet_client.read(); - if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) - if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } - ethernet_command[elen++] = c; - ethernet_command[elen] = 0; - } - if (elen == sizeof(ethernet_command)-1) { - ethernet_command[sizeof(ethernet_command)-1] = '\r'; - } - - if (elen > 0 && ethernet_command[elen - 1] == '\r') { - ethernet_command[elen - 1] = 0; - ethernet_client.println(); - char reply[160]; - reply[0] = 0; - if (!ethernet_handle_command(ethernet_command, reply)) { - the_mesh.handleCommand(0, ethernet_command, reply); - } - if (reply[0]) { - ethernet_client.print(" -> "); ethernet_client.println(reply); - } - ethernet_command[0] = 0; + ethernet_loop_maintain(); + if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) { + char reply[160]; + reply[0] = 0; + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); } + ethernet_send_reply(reply); + ethernet_command[0] = 0; } #endif diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 435d562824..8439f500c3 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -4,105 +4,8 @@ #include "MyMesh.h" #ifdef ETHERNET_ENABLED - #include - #include - #include - - #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); - - #define PIN_ETHERNET_POWER_EN WB_IO2 - #define PIN_ETHERNET_RESET 21 - #define PIN_ETHERNET_SS 26 - - #ifndef ETHERNET_TCP_PORT - #define ETHERNET_TCP_PORT 23 - #endif - - #define ETHERNET_RETRY_INTERVAL_MS 30000 - - static EthernetServer ethernet_server(ETHERNET_TCP_PORT); - static EthernetClient ethernet_client; - static volatile bool ethernet_running = false; - - static void ethernet_task(void* param) { - (void)param; - - Serial.println("ETH: Initializing hardware"); - pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); - digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); - vTaskDelay(pdMS_TO_TICKS(100)); - - pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, LOW); - vTaskDelay(pdMS_TO_TICKS(100)); - digitalWrite(PIN_ETHERNET_RESET, HIGH); - - ETHERNET_SPI_PORT.begin(); - Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); - - uint8_t mac[6]; - generateEthernetMac(mac); - Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - - while (!ethernet_running) { - if (Ethernet.hardwareStatus() == EthernetNoHardware) { - Serial.println("ETH: Hardware not found, giving up"); - vTaskDelete(NULL); - return; - } - if (Ethernet.linkStatus() == LinkOFF) { - vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); - continue; - } - - Serial.println("ETH: Link detected, attempting DHCP..."); - if (Ethernet.begin(mac, 10000, 2000) == 0) { - Serial.println("ETH: DHCP failed, will retry"); - vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); - continue; - } - - 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; - } - vTaskDelete(NULL); - } - - static void ethernet_start_task() { - xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); - } - - static bool ethernet_handle_command(const char* command, char* reply) { - if (strcmp(command, "eth.status") == 0) { - if (!ethernet_running) { - strcpy(reply, "ETH: not connected"); - } else { - IPAddress ip = Ethernet.localIP(); - sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); - } - return true; - } - return false; - } - - // Check for new TCP client connections, replacing any existing connection - static void ethernet_check_client() { - auto newClient = ethernet_server.available(); - if (newClient) { - if (ethernet_client) ethernet_client.stop(); - ethernet_client = newClient; - IPAddress ip = ethernet_client.remoteIP(); - Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); - ethernet_client.println("MeshCore Room Server CLI"); - } - } + #define ETHERNET_CLI_BANNER "MeshCore Room Server CLI" + #include #endif #ifdef DISPLAY_CLASS @@ -227,37 +130,15 @@ void loop() { } #ifdef ETHERNET_ENABLED - if (ethernet_running) { - ethernet_check_client(); - Ethernet.maintain(); - } - - if (ethernet_running && ethernet_client && ethernet_client.connected()) { - int elen = strlen(ethernet_command); - while (ethernet_client.available() && elen < (int)sizeof(ethernet_command)-1) { - char c = ethernet_client.read(); - if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) - if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } - ethernet_command[elen++] = c; - ethernet_command[elen] = 0; - } - if (elen == sizeof(ethernet_command)-1) { - ethernet_command[sizeof(ethernet_command)-1] = '\r'; - } - - if (elen > 0 && ethernet_command[elen - 1] == '\r') { - ethernet_command[elen - 1] = 0; - ethernet_client.println(); - char reply[160]; - reply[0] = 0; - if (!ethernet_handle_command(ethernet_command, reply)) { - the_mesh.handleCommand(0, ethernet_command, reply); - } - if (reply[0]) { - ethernet_client.print(" -> "); ethernet_client.println(reply); - } - ethernet_command[0] = 0; + ethernet_loop_maintain(); + if (ethernet_read_line(ethernet_command, sizeof(ethernet_command))) { + char reply[160]; + reply[0] = 0; + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); } + ethernet_send_reply(reply); + ethernet_command[0] = 0; } #endif diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h new file mode 100644 index 0000000000..6a59bda68e --- /dev/null +++ b/src/helpers/nrf52/EthernetCLI.h @@ -0,0 +1,156 @@ +#pragma once + +#ifdef ETHERNET_ENABLED + +#include +#include +#include +#include + +#define PIN_SPI1_MISO (29) +#define PIN_SPI1_MOSI (30) +#define PIN_SPI1_SCK (3) + +static SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + +#define PIN_ETHERNET_POWER_EN WB_IO2 +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS 26 + +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 23 // telnet port for CLI access +#endif + +#ifndef ETHERNET_CLI_BANNER + #define ETHERNET_CLI_BANNER "MeshCore CLI" +#endif + +#define ETHERNET_RETRY_INTERVAL_MS 30000 + +static EthernetServer ethernet_server(ETHERNET_TCP_PORT); +static EthernetClient ethernet_client; +static volatile bool ethernet_running = false; + +// FreeRTOS task: handles hw init, DHCP, and retries in the background +static void ethernet_task(void* param) { + (void)param; + + Serial.println("ETH: Initializing hardware"); + pinMode(PIN_ETHERNET_POWER_EN, OUTPUT); + digitalWrite(PIN_ETHERNET_POWER_EN, HIGH); + vTaskDelay(pdMS_TO_TICKS(100)); + + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); + vTaskDelay(pdMS_TO_TICKS(100)); + digitalWrite(PIN_ETHERNET_RESET, HIGH); + + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); + + uint8_t mac[6]; + generateEthernetMac(mac); + Serial.printf("ETH: MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + // Retry loop: keep trying until we get an IP + while (!ethernet_running) { + Serial.println("ETH: Attempting DHCP..."); + if (Ethernet.begin(mac, 10000, 2000) == 0) { + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + Serial.println("ETH: Hardware not found, giving up"); + vTaskDelete(NULL); + return; + } + if (Ethernet.linkStatus() == LinkOFF) { + Serial.println("ETH: Cable not connected, will retry"); + } else { + Serial.println("ETH: DHCP failed, will retry"); + } + vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS)); + continue; + } + + 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; + } + + // DHCP succeeded, task is done + vTaskDelete(NULL); +} + +static void ethernet_start_task() { + xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL); +} + +// Format ethernet status into reply buffer. Returns true if command was handled. +static bool ethernet_handle_command(const char* command, char* reply) { + if (strcmp(command, "eth.status") == 0) { + if (!ethernet_running) { + strcpy(reply, "ETH: not connected"); + } else { + IPAddress ip = Ethernet.localIP(); + sprintf(reply, "ETH: %u.%u.%u.%u:%d", ip[0], ip[1], ip[2], ip[3], ETHERNET_TCP_PORT); + } + return true; + } + return false; +} + +// Check for new TCP client connections, replacing any existing connection +static void ethernet_check_client() { + auto newClient = ethernet_server.available(); + if (newClient) { + if (ethernet_client) ethernet_client.stop(); + ethernet_client = newClient; + IPAddress ip = ethernet_client.remoteIP(); + Serial.printf("ETH: Client connected from %u.%u.%u.%u\n", ip[0], ip[1], ip[2], ip[3]); + ethernet_client.println(ETHERNET_CLI_BANNER); + } +} + +// Call from loop() to maintain DHCP and check for new clients +static void ethernet_loop_maintain() { + if (ethernet_running) { + ethernet_check_client(); + Ethernet.maintain(); + } +} + +// Read a line from the Ethernet client into the command buffer. +// Returns true when a complete line is ready to process (command is null-terminated). +// The caller should process the command and then reset ethernet_command[0] = 0. +static bool ethernet_read_line(char* ethernet_command, size_t buf_size) { + if (!ethernet_running || !ethernet_client || !ethernet_client.connected()) return false; + + int elen = strlen(ethernet_command); + while (ethernet_client.available() && elen < (int)buf_size - 1) { + char c = ethernet_client.read(); + if (c == '\n' && elen == 0) continue; // ignore leading LF (from CR+LF) + if (c == '\r' || c == '\n') { ethernet_command[elen++] = '\r'; break; } + ethernet_command[elen++] = c; + ethernet_command[elen] = 0; + } + if (elen == (int)buf_size - 1) { + ethernet_command[buf_size - 1] = '\r'; + } + + if (elen > 0 && ethernet_command[elen - 1] == '\r') { + ethernet_command[elen - 1] = 0; + ethernet_client.println(); + return true; + } + return false; +} + +// Send a reply to the Ethernet client +static void ethernet_send_reply(const char* reply) { + if (reply[0]) { + ethernet_client.print(" -> "); ethernet_client.println(reply); + } +} + +#endif // ETHERNET_ENABLED