diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index 37f3701b1a..45959817a1 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -31,8 +31,11 @@ jobs: - Heltec_v3_room_server # nRF52 - RAK_4631_companion_radio_ble + - RAK_4631_companion_radio_ethernet - RAK_4631_repeater + - RAK_4631_repeater_ethernet - RAK_4631_room_server + - RAK_4631_room_server_ethernet # RP2040 - PicoW_repeater # STM32 diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 3747d4b2ad..b4d699ae93 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) --- @@ -975,3 +976,25 @@ region save **Note:** Returns an error on boards without power management support. --- + +### Ethernet (when Ethernet support is compiled in) + +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.status` + +**Output:** +- `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 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 530f97013b..543a2a5468 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 @@ -880,3 +881,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_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 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 eff9efca47..a88da48fc2 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,19 +12,20 @@ static uint32_t _atoi(const char* sp) { return n; } + #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 +75,21 @@ static uint32_t _atoi(const char* sp) { #ifdef BLE_PIN_CODE #include SerialBLEInterface serial_interface; + #elif defined(ETHERNET_ENABLED) + #include + SerialEthernetInterface serial_interface; #else #include ArduinoSerialInterface serial_interface; #endif #elif defined(STM32_PLATFORM) - #include - ArduinoSerialInterface serial_interface; + #ifdef ETHERNET_ENABLED + #include + SerialEthernetInterface serial_interface; + #else + #include + ArduinoSerialInterface serial_interface; + #endif #else #error "need to define a serial interface" #endif @@ -107,7 +116,6 @@ void halt() { void setup() { Serial.begin(115200); - board.begin(); #ifdef DISPLAY_CLASS @@ -152,10 +160,23 @@ 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"); + unsigned long timeout = millis(); + while (!Serial) { + if ((millis() - timeout) < 5000) { delay(100); } else { break; } + } + Serial.println("Initializing Ethernet adapter..."); + 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(); @@ -225,4 +246,8 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + +#ifdef ETHERNET_ENABLED + serial_interface.loop(); +#endif } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..ab76f4c1a4 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -8,6 +8,11 @@ static UITask ui_task(display); #endif +#ifdef ETHERNET_ENABLED + #define ETHERNET_CLI_BANNER "MeshCore Repeater CLI" + #include +#endif + StdRNG fast_rng; SimpleMeshTables tables; @@ -18,6 +23,9 @@ void halt() { } static char command[160]; +#ifdef ETHERNET_ENABLED +static char ethernet_command[160]; +#endif // For power saving unsigned long lastActive = 0; // mark last active time @@ -90,6 +98,9 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; +#ifdef ETHERNET_ENABLED + ethernet_command[0] = 0; +#endif sensors.begin(); @@ -99,6 +110,10 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif +#ifdef ETHERNET_ENABLED + ethernet_start_task(); +#endif + // send out initial zero hop Advertisement to the mesh #if ENABLE_ADVERT_ON_BOOT == 1 the_mesh.sendSelfAdvertisement(16000, false); @@ -106,6 +121,7 @@ void setup() { } void loop() { + // Handle Serial CLI int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); @@ -124,7 +140,14 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; + reply[0] = 0; +#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); } @@ -132,6 +155,19 @@ void loop() { command[0] = 0; // reset command buffer } +#ifdef ETHERNET_ENABLED + 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 + #if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) // Hold the user button to power off the SenseCAP Solar repeater. int btnState = digitalRead(PIN_USER_BTN); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007d5..8439f500c3 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -3,6 +3,11 @@ #include "MyMesh.h" +#ifdef ETHERNET_ENABLED + #define ETHERNET_CLI_BANNER "MeshCore Room Server CLI" + #include +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -17,6 +22,9 @@ void halt() { } static char command[MAX_POST_TEXT_LEN+1]; +#ifdef ETHERNET_ENABLED +static char ethernet_command[MAX_POST_TEXT_LEN+1]; +#endif void setup() { Serial.begin(115200); @@ -67,6 +75,9 @@ void setup() { mesh::Utils::printHex(Serial, the_mesh.self_id.pub_key, PUB_KEY_SIZE); Serial.println(); command[0] = 0; +#ifdef ETHERNET_ENABLED + ethernet_command[0] = 0; +#endif sensors.begin(); @@ -76,6 +87,10 @@ void setup() { ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif +#ifdef ETHERNET_ENABLED + ethernet_start_task(); +#endif + // send out initial zero hop Advertisement to the mesh #if ENABLE_ADVERT_ON_BOOT == 1 the_mesh.sendSelfAdvertisement(16000, false); @@ -99,7 +114,14 @@ 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 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); } @@ -107,6 +129,19 @@ void loop() { command[0] = 0; // reset command buffer } +#ifdef ETHERNET_ENABLED + 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 + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS 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 diff --git a/src/helpers/nrf52/EthernetMac.h b/src/helpers/nrf52/EthernetMac.h new file mode 100644 index 0000000000..0ee2ac06f5 --- /dev/null +++ b/src/helpers/nrf52/EthernetMac.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +static inline void generateEthernetMac(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 new file mode 100644 index 0000000000..92001b1b26 --- /dev/null +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -0,0 +1,284 @@ +#include "SerialEthernetInterface.h" +#include "EthernetMac.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 ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + +#define PIN_ETHERNET_POWER_EN WB_IO2 // output, high to enable +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS 26 + +#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() { + + 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); + ETHERNET_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. + ETHERNET_DEBUG_PRINTLN("Ethernet reset pulse"); +#endif + + uint8_t mac[6]; + generateEthernetMac(mac); + ETHERNET_DEBUG_PRINTLN( + "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", + mac[0], + mac[1], + mac[2], + mac[3], + mac[4], + mac[5]); + 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 + ETHERNET_DEBUG_PRINTLN("Begin"); + if (Ethernet.begin(mac) == 0) { + ETHERNET_DEBUG_PRINTLN("Begin failed."); + + // DHCP failed -- let's figure out why + if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check for Ethernet hardware present. + { + ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found."); + return false; + } + if (Ethernet.linkStatus() == LinkOFF) // No physical connection + { + ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected."); + return false; + } + ETHERNET_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); + return false; + } + #endif + ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); + 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); + + 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) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + return 0; + } + + if (deviceConnected && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + ETHERNET_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; 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( + "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"); + } + + if (client.connected()) { + if (!deviceConnected) { + ETHERNET_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; + 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 ETHERNET_RAW_LINE + ETHERNET_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); + 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 + 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 ETHERNET_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 ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_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; +} + +void SerialEthernetInterface::loop() { + Ethernet.maintain(); +} diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h new file mode 100644 index 0000000000..95ce8a5217 --- /dev/null +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -0,0 +1,78 @@ +#pragma once + +#include "helpers/BaseSerialInterface.h" +#include +#include + +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 5000 +#endif +// define ETHERNET_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; + 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 send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + + protected: + + public: + SerialEthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { + deviceConnected = false; + _isEnabled = false; + _last_write = 0; + send_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 loop(); +}; + + +#if ETHERNET_DEBUG_LOGGING && ARDUINO + #include + #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 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 07807011db..55134e8c10 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(ETHERNET_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..08286604cd 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -36,6 +36,7 @@ void RAK4631Board::begin() { pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP); #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..cb280e31fa 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_ethernet] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"RAK4631 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D ETHERNET_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_ethernet] +extends = rak4631 +build_flags = + ${rak4631.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -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 ETHERNET_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,36 @@ lib_deps = ${rak4631.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:RAK_4631_companion_radio_ethernet] +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 ETHERNET_ENABLED=1 +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +; -D ETHERNET_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 +262,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}