From 70b51bd096da36d5186e08c46819d7e49e69fe9b Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Mon, 9 Mar 2026 14:52:01 -0700 Subject: [PATCH 01/37] 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 5ba677cd..2d9dbf79 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 eff9efca..a3c83f32 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 d226d1fa..88157171 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 825fb007..a84a7ee9 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 00000000..ba951531 --- /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 00000000..39eefbb4 --- /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 07807011..ee09d31d 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 9fb47b43..767c833f 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 737ef565..39765b01 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 02/37] 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 a3c83f32..6dae1356 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 ba951531..3dc20671 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 39eefbb4..7adf3569 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 767c833f..08286604 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 39765b01..96c8c73d 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 03/37] 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 1d3430db..ea2c941c 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 220b8971..84ab0226 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 04/37] 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 | 141 +++++++++--------- examples/simple_room_server/main.cpp | 141 +++++++++--------- src/helpers/nrf52/EthernetMac.h | 13 ++ src/helpers/nrf52/SerialEthernetInterface.cpp | 117 +++++++-------- src/helpers/nrf52/SerialEthernetInterface.h | 27 ++-- .../sensors/EnvironmentSensorManager.cpp | 2 +- variants/rak4631/platformio.ini | 19 ++- 9 files changed, 237 insertions(+), 249 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 2d9dbf79..9292338f 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 6dae1356..9fa1e381 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 88157171..0f37aa6b 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 (reply[0]) { - eth_client.print(" -> "); eth_client.println(reply); + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); } - eth_client.print("> "); - eth_command[0] = 0; + if (reply[0]) { + ethernet_client.print(" -> "); ethernet_client.println(reply); + } + 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 a84a7ee9..97a793f4 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 (reply[0]) { - eth_client.print(" -> "); eth_client.println(reply); + if (!ethernet_handle_command(ethernet_command, reply)) { + the_mesh.handleCommand(0, ethernet_command, reply); } - eth_client.print("> "); - eth_command[0] = 0; + if (reply[0]) { + ethernet_client.print(" -> "); ethernet_client.println(reply); + } + 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 00000000..034acf70 --- /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 3dc20671..68f10b21 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); + ETHERNET_DEBUG_PRINTLN("Init"); + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_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); + // 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; + return deviceConnected; } -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() { +void SerialEthernetInterface::loop() { Ethernet.maintain(); } diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 7adf3569..5f06f688 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 ee09d31d..55134e8c 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 96c8c73d..cb280e31 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 05/37] 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 | 88 +++++++++---------- src/helpers/nrf52/SerialEthernetInterface.h | 6 +- 8 files changed, 82 insertions(+), 94 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index ea2c941c..18ddf59e 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 84ab0226..8991508f 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 9fa1e381..d48e223d 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 0f37aa6b..00fad175 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 97a793f4..435d5628 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 034acf70..0ee2ac06 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 68f10b21..92001b1b 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 5f06f688..95ce8a52 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 06/37] 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 d48e223d..a88da48f 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 00fad175..b9fa1aa8 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 435d5628..8439f500 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 00000000..6a59bda6 --- /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 From 3c1a0941aa56cc733cacb385d7784cafb78f736e Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Fri, 20 Mar 2026 17:56:04 -0700 Subject: [PATCH 07/37] Fix POE boot failure on RAK4631 Ethernet builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drive WB_IO2 (slot power switch) HIGH via early constructor before the Arduino framework initializes. Without this, the board brownouts on POE-only power because the slot MOSFET isn't enabled early enough for the RAK13800 POE module to deliver power to the baseboard. Also remove the W5100S hardware reset pulse from Ethernet init — the chip comes out of power-on reset cleanly, and toggling reset kills the PHY link which breaks POE power delivery. --- src/helpers/nrf52/EthernetCLI.h | 10 ++++------ src/helpers/nrf52/SerialEthernetInterface.cpp | 17 +++++------------ variants/rak4631/RAK4631Board.cpp | 13 +++++++++++++ 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h index 6a59bda6..9ccc34f4 100644 --- a/src/helpers/nrf52/EthernetCLI.h +++ b/src/helpers/nrf52/EthernetCLI.h @@ -36,13 +36,11 @@ 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)); - + // WB_IO2 (power enable) is already driven HIGH by early constructor + // in RAK4631Board.cpp to support POE boot. + // Skip hardware reset — the W5100S comes out of power-on reset cleanly, + // and toggling reset kills the PHY link which breaks POE power. pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, LOW); - vTaskDelay(pdMS_TO_TICKS(100)); digitalWrite(PIN_ETHERNET_RESET, HIGH); ETHERNET_SPI_PORT.begin(); diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 92001b1b..4288c0f7 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -22,20 +22,13 @@ 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 - + // WB_IO2 (power enable) is already driven HIGH by early constructor + // in RAK4631Board.cpp to support POE boot. + // Skip hardware reset — the W5100S comes out of power-on reset cleanly, + // and toggling reset kills the PHY link which breaks POE power. #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"); + digitalWrite(PIN_ETHERNET_RESET, HIGH); #endif uint8_t mac[6]; diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 08286604..1b5698d0 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -1,8 +1,21 @@ #include #include +#include "nrf_gpio.h" #include "RAK4631Board.h" +#ifdef ETHERNET_ENABLED +// Drive WB_IO2 HIGH as early as possible using direct register access. +// WB_IO2 (P1.02, Arduino pin 34) controls the WisBlock slot power switch. +// With POE through RAK13800, this must be enabled before the framework +// initializes or the board will brownout from insufficient power delivery. +// Priority 102 runs just after SystemInit. +static void __attribute__((constructor(102))) rak4631_early_poe_power() { + nrf_gpio_cfg_output(NRF_GPIO_PIN_MAP(1, 2)); // WB_IO2 = P1.02 + nrf_gpio_pin_set(NRF_GPIO_PIN_MAP(1, 2)); +} +#endif + #ifdef NRF52_POWER_MANAGEMENT // Static configuration for power management // Values set in variant.h defines From 0ffe1c2cb9fa9fcb14b8c577dee39a04176eff5c Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Sat, 21 Mar 2026 19:28:44 -0700 Subject: [PATCH 08/37] bug fix: ethernet cli dropping connection after each command --- src/helpers/nrf52/EthernetCLI.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h index 9ccc34f4..508d3aa6 100644 --- a/src/helpers/nrf52/EthernetCLI.h +++ b/src/helpers/nrf52/EthernetCLI.h @@ -102,6 +102,8 @@ static bool ethernet_handle_command(const char* command, char* reply) { static void ethernet_check_client() { auto newClient = ethernet_server.available(); if (newClient) { + // Only replace if this is actually a different client + if (newClient == ethernet_client && ethernet_client.connected()) return; if (ethernet_client) ethernet_client.stop(); ethernet_client = newClient; IPAddress ip = ethernet_client.remoteIP(); From a3354db5214d9ea60c16671c5682b69d4d098fd5 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Sun, 26 Apr 2026 20:22:35 +0000 Subject: [PATCH 09/37] Address PR review feedback from oltaco - Remove extra blank line in companion_radio main.cpp before NRF52/STM32 block - Revert unintended STM32_PLATFORM ETHERNET_ENABLED branch (no STM32 ethernet target exists, and it incorrectly included nrf52 headers) - Drop renovate annotations from rak4631 platformio.ini (no renovate config in this repo) --- examples/companion_radio/main.cpp | 10 ++-------- variants/rak4631/platformio.ini | 3 --- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 822100c9..61df6c0b 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; } - #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #if defined(QSPIFLASH) @@ -83,13 +82,8 @@ static uint32_t _atoi(const char* sp) { ArduinoSerialInterface serial_interface; #endif #elif defined(STM32_PLATFORM) - #ifdef ETHERNET_ENABLED - #include - SerialEthernetInterface serial_interface; - #else - #include - ArduinoSerialInterface serial_interface; - #endif + #include + ArduinoSerialInterface serial_interface; #else #error "need to define a serial interface" #endif diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 7a0b031f..d24678f4 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -65,7 +65,6 @@ 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] @@ -147,7 +146,6 @@ 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] @@ -197,7 +195,6 @@ build_src_filter = ${rak4631.build_src_filter} 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 From c183050935f3ca5e589c7759e14262be9cf24f25 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 15 May 2026 16:53:18 +0800 Subject: [PATCH 10/37] Add heltec -v4-r8 board --- boards/heltec_v4_r8.json | 43 +++ src/helpers/ui/ST7789LCDDisplay.cpp | 8 +- src/helpers/ui/ST7789LCDDisplay.h | 4 +- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 82 ++++++ variants/heltec_v4_r8/HeltecV4R8Board.h | 39 +++ variants/heltec_v4_r8/LoRaFEMControl.cpp | 52 ++++ variants/heltec_v4_r8/LoRaFEMControl.h | 24 ++ variants/heltec_v4_r8/pins_arduino.h | 56 ++++ variants/heltec_v4_r8/platformio.ini | 342 ++++++++++++++++++++++ variants/heltec_v4_r8/target.cpp | 45 +++ variants/heltec_v4_r8/target.h | 31 ++ 11 files changed, 722 insertions(+), 4 deletions(-) create mode 100644 boards/heltec_v4_r8.json create mode 100644 variants/heltec_v4_r8/HeltecV4R8Board.cpp create mode 100644 variants/heltec_v4_r8/HeltecV4R8Board.h create mode 100644 variants/heltec_v4_r8/LoRaFEMControl.cpp create mode 100644 variants/heltec_v4_r8/LoRaFEMControl.h create mode 100644 variants/heltec_v4_r8/pins_arduino.h create mode 100644 variants/heltec_v4_r8/platformio.ini create mode 100644 variants/heltec_v4_r8/target.cpp create mode 100644 variants/heltec_v4_r8/target.h diff --git a/boards/heltec_v4_r8.json b/boards/heltec_v4_r8.json new file mode 100644 index 00000000..6dd97c84 --- /dev/null +++ b/boards/heltec_v4_r8.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_v4_r8" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "heltec_wifi_lora_32 v4 r8 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "heltec" +} diff --git a/src/helpers/ui/ST7789LCDDisplay.cpp b/src/helpers/ui/ST7789LCDDisplay.cpp index dc75e963..7a02668b 100644 --- a/src/helpers/ui/ST7789LCDDisplay.cpp +++ b/src/helpers/ui/ST7789LCDDisplay.cpp @@ -1,5 +1,9 @@ #include "ST7789LCDDisplay.h" +#ifndef PIN_TFT_MISO + #define PIN_TFT_MISO -1 +#endif + #ifndef DISPLAY_ROTATION #define DISPLAY_ROTATION 3 #endif @@ -29,8 +33,8 @@ bool ST7789LCDDisplay::begin() { } // Im not sure if this is just a t-deck problem or not, if your display is slow try this. - #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) - displaySPI.begin(PIN_TFT_SCL, -1, PIN_TFT_SDA, PIN_TFT_CS); + #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT) + displaySPI.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS); #endif display.init(DISPLAY_WIDTH, DISPLAY_HEIGHT); diff --git a/src/helpers/ui/ST7789LCDDisplay.h b/src/helpers/ui/ST7789LCDDisplay.h index 5b960ca1..03a6d3f1 100644 --- a/src/helpers/ui/ST7789LCDDisplay.h +++ b/src/helpers/ui/ST7789LCDDisplay.h @@ -8,7 +8,7 @@ #include class ST7789LCDDisplay : public DisplayDriver { - #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) + #if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT) SPIClass displaySPI; #endif Adafruit_ST7789 display; @@ -25,7 +25,7 @@ public: { _isOn = false; } -#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) +#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT) ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), displaySPI(HSPI), display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST), diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp new file mode 100644 index 00000000..1d481fcc --- /dev/null +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -0,0 +1,82 @@ +#include "HeltecV4R8Board.h" + +void HeltecV4R8Board::begin() { + ESP32Board::begin(); + + periph_power.begin(); + periph_power.claim(); // R8 VEXT also feeds the LoRa antenna boost rail. + + loRaFEMControl.init(); + +#ifdef PIN_TOUCH_RST + pinMode(PIN_TOUCH_RST, OUTPUT); + digitalWrite(PIN_TOUCH_RST, HIGH); +#endif + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1 << P_LORA_DIO_1)) { + startup_reason = BD_STARTUP_RX_PACKET; + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } +} + +void HeltecV4R8Board::onBeforeTransmit(void) { + digitalWrite(P_LORA_TX_LED, HIGH); + loRaFEMControl.setTxModeEnable(); +} + +void HeltecV4R8Board::onAfterTransmit(void) { + digitalWrite(P_LORA_TX_LED, LOW); + loRaFEMControl.setRxModeEnable(); +} + +void HeltecV4R8Board::enterDeepSleep(uint32_t secs, int pin_wake_btn) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + loRaFEMControl.setRxModeEnableWhenMCUSleep(); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); + } else { + esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + esp_deep_sleep_start(); +} + +void HeltecV4R8Board::powerOff() { + enterDeepSleep(0); +} + +uint16_t HeltecV4R8Board::getBattMilliVolts() { + analogReadResolution(12); + + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / 8; + + return (adc_mult * (3.3f / 4096.0f) * raw) * 1000; +} + +const char* HeltecV4R8Board::getManufacturerName() const { +#ifdef HELTEC_V4_R8_TFT + return "Heltec V4 R8 TFT"; +#else + return "Heltec V4 R8 OLED"; +#endif +} diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.h b/variants/heltec_v4_r8/HeltecV4R8Board.h new file mode 100644 index 00000000..20811abb --- /dev/null +++ b/variants/heltec_v4_r8/HeltecV4R8Board.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include "LoRaFEMControl.h" + +#ifndef ADC_MULTIPLIER + #define ADC_MULTIPLIER (4.9f * 1.035f) +#endif + +class HeltecV4R8Board : public ESP32Board { +protected: + float adc_mult = ADC_MULTIPLIER; + +public: + RefCountedDigitalPin periph_power; + LoRaFEMControl loRaFEMControl; + + HeltecV4R8Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + + void begin(); + void onBeforeTransmit(void) override; + void onAfterTransmit(void) override; + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); + void powerOff() override; + uint16_t getBattMilliVolts() override; + bool setAdcMultiplier(float multiplier) override { + if (multiplier == 0.0f) { + adc_mult = ADC_MULTIPLIER; + } else { + adc_mult = multiplier; + } + return true; + } + float getAdcMultiplier() const override { return adc_mult; } + const char* getManufacturerName() const override; +}; diff --git a/variants/heltec_v4_r8/LoRaFEMControl.cpp b/variants/heltec_v4_r8/LoRaFEMControl.cpp new file mode 100644 index 00000000..bb530de3 --- /dev/null +++ b/variants/heltec_v4_r8/LoRaFEMControl.cpp @@ -0,0 +1,52 @@ +#include "LoRaFEMControl.h" + +#include +#include +#include + +void LoRaFEMControl::init(void) { + pinMode(P_LORA_PA_POWER, OUTPUT); + digitalWrite(P_LORA_PA_POWER, HIGH); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_POWER); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason != ESP_RST_DEEPSLEEP) { + delay(1); + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_KCT8103L_PA_CSD); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_KCT8103L_PA_CTX); + + pinMode(P_LORA_KCT8103L_PA_CSD, OUTPUT); + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + pinMode(P_LORA_KCT8103L_PA_CTX, OUTPUT); + digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH); +} + +void LoRaFEMControl::setSleepModeEnable(void) { + digitalWrite(P_LORA_KCT8103L_PA_CSD, LOW); +} + +void LoRaFEMControl::setTxModeEnable(void) { + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(P_LORA_KCT8103L_PA_CTX, HIGH); +} + +void LoRaFEMControl::setRxModeEnable(void) { + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH); +} + +void LoRaFEMControl::setRxModeEnableWhenMCUSleep(void) { + digitalWrite(P_LORA_PA_POWER, HIGH); + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); + + digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); + rtc_gpio_hold_en((gpio_num_t)P_LORA_KCT8103L_PA_CSD); + digitalWrite(P_LORA_KCT8103L_PA_CTX, lna_enabled ? LOW : HIGH); + rtc_gpio_hold_en((gpio_num_t)P_LORA_KCT8103L_PA_CTX); +} + +void LoRaFEMControl::setLNAEnable(bool enabled) { + lna_enabled = enabled; +} diff --git a/variants/heltec_v4_r8/LoRaFEMControl.h b/variants/heltec_v4_r8/LoRaFEMControl.h new file mode 100644 index 00000000..961cfd07 --- /dev/null +++ b/variants/heltec_v4_r8/LoRaFEMControl.h @@ -0,0 +1,24 @@ +#pragma once + +typedef enum { + KCT8103L_PA, + OTHER_FEM_TYPES +} LoRaFEMType; + +class LoRaFEMControl { +public: + LoRaFEMControl() { } + virtual ~LoRaFEMControl() { } + void init(void); + void setSleepModeEnable(void); + void setTxModeEnable(void); + void setRxModeEnable(void); + void setRxModeEnableWhenMCUSleep(void); + void setLNAEnable(bool enabled); + bool isLnaCanControl(void) { return true; } + void setLnaCanControl(bool can_control) { } + LoRaFEMType getFEMType(void) const { return KCT8103L_PA; } + +private: + bool lna_enabled = false; +}; diff --git a/variants/heltec_v4_r8/pins_arduino.h b/variants/heltec_v4_r8/pins_arduino.h new file mode 100644 index 00000000..9e412aac --- /dev/null +++ b/variants/heltec_v4_r8/pins_arduino.h @@ -0,0 +1,56 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 17; +static const uint8_t SCL = 18; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini new file mode 100644 index 00000000..4057d6f1 --- /dev/null +++ b/variants/heltec_v4_r8/platformio.ini @@ -0,0 +1,342 @@ +[Heltec_v4_r8] +extends = esp32_base +board = heltec_v4_r8 +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/heltec_v4_r8 + -D HELTEC_V4_R8 + -D USE_SX1262 + -D ESP32_CPU_FREQ=80 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=14 + -D P_LORA_NSS=8 + -D P_LORA_RESET=12 + -D P_LORA_BUSY=13 + -D P_LORA_SCLK=9 + -D P_LORA_MISO=11 + -D P_LORA_MOSI=10 + -D P_LORA_PA_POWER=7 + -D P_LORA_KCT8103L_PA_CSD=2 + -D P_LORA_KCT8103L_PA_CTX=5 + -D P_LORA_TX_LED=46 + -D PIN_USER_BTN=0 + -D PIN_VEXT_EN=40 + -D PIN_VEXT_EN_ACTIVE=LOW + -D ADC_MULTIPLIER=5.0715f + -D PIN_VBAT_READ=1 + -D LORA_TX_POWER=10 + -D MAX_LORA_TX_POWER=22 + -D SX126X_REGISTER_PATCH=1 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_GPS_RX=38 + -D PIN_GPS_TX=39 + -D PIN_GPS_EN=42 + -D PIN_GPS_EN_ACTIVE=LOW + -D ENV_INCLUDE_GPS=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/heltec_v4_r8> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[heltec_v4_r8_oled] +extends = Heltec_v4_r8 +build_flags = + ${Heltec_v4_r8.build_flags} + -D HELTEC_V4_R8_OLED + -D PIN_BOARD_SDA=17 + -D PIN_BOARD_SCL=18 + -D PIN_OLED_RESET=21 +build_src_filter = ${Heltec_v4_r8.build_src_filter} +lib_deps = ${Heltec_v4_r8.lib_deps} + +[heltec_v4_r8_tft] +extends = Heltec_v4_r8 +build_flags = + ${Heltec_v4_r8.build_flags} + -D HELTEC_V4_R8_TFT + -D PIN_BOARD_SDA=17 + -D PIN_BOARD_SCL=18 + -D DISPLAY_SCALE_X=2.5 + -D DISPLAY_SCALE_Y=3.75 + -D PIN_TFT_RST=-1 + -D PIN_TFT_VDD_CTL=-1 + -D PIN_TFT_LEDA_CTL=44 + -D PIN_TFT_LEDA_CTL_ACTIVE=HIGH + -D PIN_TFT_CS=47 + -D PIN_TFT_DC=48 + -D PIN_TFT_SCL=16 + -D PIN_TFT_SDA=15 + -D PIN_TFT_MISO=45 + -D PIN_BUZZER=4 + -D PIN_TOUCH_RST=21 +build_src_filter = ${Heltec_v4_r8.build_src_filter} + + +lib_deps = + ${Heltec_v4_r8.lib_deps} + adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_v4_r8_repeater] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Heltec R8 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_v4_r8_room_server] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Heltec R8 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_terminal_chat] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_companion_radio_usb] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_companion_radio_ble] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_companion_radio_wifi] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SSD1306Display + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_sensor] +extends = heltec_v4_r8_oled +build_flags = + ${heltec_v4_r8_oled.build_flags} + -D ADVERT_NAME='"Heltec R8 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D DISPLAY_CLASS=SSD1306Display +build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + + +<../examples/simple_sensor> +lib_deps = + ${heltec_v4_r8_oled.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_tft_repeater] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D DISPLAY_CLASS=ST7789LCDDisplay + -D ADVERT_NAME='"Heltec R8 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_v4_r8_tft_room_server] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D DISPLAY_CLASS=ST7789LCDDisplay + -D ADVERT_NAME='"Heltec R8 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_tft_terminal_chat] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_companion_radio_usb] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=ST7789LCDDisplay +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_companion_radio_ble] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=ST7789LCDDisplay + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_companion_radio_wifi] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=ST7789LCDDisplay + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_v4_r8_tft_sensor] +extends = heltec_v4_r8_tft +build_flags = + ${heltec_v4_r8_tft.build_flags} + -D ADVERT_NAME='"Heltec R8 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D DISPLAY_CLASS=ST7789LCDDisplay +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + + +<../examples/simple_sensor> +lib_deps = + ${heltec_v4_r8_tft.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_v4_r8_kiss_modem] +extends = Heltec_v4_r8 +build_src_filter = ${Heltec_v4_r8.build_src_filter} + +<../examples/kiss_modem/> + +[env:heltec_v4_r8_tft_kiss_modem] +extends = heltec_v4_r8_tft +build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/heltec_v4_r8/target.cpp b/variants/heltec_v4_r8/target.cpp new file mode 100644 index 00000000..0b38531e --- /dev/null +++ b/variants/heltec_v4_r8/target.cpp @@ -0,0 +1,45 @@ +#include +#include "target.h" + +HeltecV4R8Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, GPS_RESET, GPS_EN, &board.periph_power); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display(&board.periph_power); + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#if defined(P_LORA_SCLK) + return radio.std_init(&spi); +#else + return radio.std_init(); +#endif +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/heltec_v4_r8/target.h b/variants/heltec_v4_r8/target.h new file mode 100644 index 00000000..2d1d7a5b --- /dev/null +++ b/variants/heltec_v4_r8/target.h @@ -0,0 +1,31 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #ifdef HELTEC_V4_R8_OLED + #include + #elif defined(HELTEC_V4_R8_TFT) + #include + #endif + #include +#endif + +extern HeltecV4R8Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); From 1ac5f359ca572141ca0fa297a9540656a959d842 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Sat, 16 May 2026 10:59:02 +0800 Subject: [PATCH 11/37] Added touch reset --- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index 1d481fcc..f9a771df 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -11,6 +11,10 @@ void HeltecV4R8Board::begin() { #ifdef PIN_TOUCH_RST pinMode(PIN_TOUCH_RST, OUTPUT); digitalWrite(PIN_TOUCH_RST, HIGH); + delay(10); + digitalWrite(PIN_TOUCH_RST, LOW); + delay(100); + digitalWrite(PIN_TOUCH_RST, HIGH); #endif esp_reset_reason_t reason = esp_reset_reason(); From 40bd7d511b18cf1a94f0a5601666eb822f7f9541 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Mon, 18 May 2026 21:04:09 +0000 Subject: [PATCH 12/37] Fix companion disconnect on inbound packet; use server.accept() EthernetServer::available() returns any socket with data, including the already-connected client's socket. The companion path then called stop() on what it thought was a duplicate client, closing the shared socket and disconnecting the companion after the first packet. Switch both SerialEthernetInterface and EthernetCLI to server.accept(), which only returns newly-accepted sockets. Removes the IP/port duplicate detection in the companion path (no longer reachable) and the same-socket early-return in the CLI path. Reported by mcode6726 on PR #1983. --- src/helpers/nrf52/EthernetCLI.h | 9 ++++--- src/helpers/nrf52/SerialEthernetInterface.cpp | 25 +++++-------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/helpers/nrf52/EthernetCLI.h b/src/helpers/nrf52/EthernetCLI.h index 508d3aa6..34802e56 100644 --- a/src/helpers/nrf52/EthernetCLI.h +++ b/src/helpers/nrf52/EthernetCLI.h @@ -98,12 +98,13 @@ static bool ethernet_handle_command(const char* command, char* reply) { return false; } -// Check for new TCP client connections, replacing any existing connection +// Check for new TCP client connections, replacing any existing connection. +// Use accept() (not available()) so we only see newly-accepted sockets; +// available() also returns existing connected sockets that have data, which +// would force us to disambiguate every inbound packet from a real new client. static void ethernet_check_client() { - auto newClient = ethernet_server.available(); + auto newClient = ethernet_server.accept(); if (newClient) { - // Only replace if this is actually a different client - if (newClient == ethernet_client && ethernet_client.connected()) return; if (ethernet_client) ethernet_client.stop(); ethernet_client = newClient; IPAddress ip = ethernet_client.remoteIP(); diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 4288c0f7..70891023 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -120,34 +120,21 @@ bool SerialEthernetInterface::isWriteBusy() const { } size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { - // check if new client connected; new connections replace existing ones - auto newClient = server.available(); + // Use accept() (not available()) so we only see newly-accepted sockets. + // available() also returns existing connected sockets that have data, + // which would cause us to treat each inbound packet as a "new client" + // and stop() the underlying socket — disconnecting the companion. + auto newClient = server.accept(); 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 client accepted %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) { From e5a3839d64192c984e2fa5c76450c77bd06dc36e Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 27 May 2026 15:12:59 +0800 Subject: [PATCH 13/37] Optimize ADC readout --- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index f9a771df..1fb123b2 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -70,11 +70,11 @@ uint16_t HeltecV4R8Board::getBattMilliVolts() { uint32_t raw = 0; for (int i = 0; i < 8; i++) { - raw += analogRead(PIN_VBAT_READ); + raw += analogReadMilliVolts(PIN_VBAT_READ); } raw = raw / 8; - return (adc_mult * (3.3f / 4096.0f) * raw) * 1000; + return (adc_mult * raw); } const char* HeltecV4R8Board::getManufacturerName() const { From 584f9e909be71610ffe3b789f33ca02d10ea11f9 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:52:18 +1000 Subject: [PATCH 14/37] Lilygo T-Echo Lite - Change SX1262 to use LDO instead of DC-DC (DCC pin on SX1262 not wired in, according to schematic) --- variants/lilygo_techo_lite/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index 4b5edf69..a9b3d124 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -15,6 +15,7 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_USE_REGULATOR_LDO=1 -D P_LORA_TX_LED=LED_GREEN -D DISABLE_DIAGNOSTIC_OUTPUT -D ENV_INCLUDE_GPS=1 From cea246d0eba80af7d7a4447d7ad6f972222f83b7 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 6 Jul 2026 15:09:18 +1000 Subject: [PATCH 15/37] * super fast ST7735 display driver --- src/helpers/ui/ST7735Display.cpp | 553 +++++++++++++++++++++++++--- src/helpers/ui/ST7735Display.h | 9 +- variants/heltec_t096/platformio.ini | 2 +- 3 files changed, 502 insertions(+), 62 deletions(-) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index a6087dd8..2b9f3ebb 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -1,12 +1,404 @@ #include "ST7735Display.h" -#ifndef DISPLAY_ROTATION - #define DISPLAY_ROTATION 2 -#endif +//#include + +// Optimised ST7735 display driver, derived from Adafruit_ST7735 library. + +#define ST_CMD_DELAY 0x80 // special signifier for command lists + +#define ST77XX_NOP 0x00 +#define ST77XX_SWRESET 0x01 +#define ST77XX_RDDID 0x04 +#define ST77XX_RDDST 0x09 + +#define ST77XX_SLPIN 0x10 +#define ST77XX_SLPOUT 0x11 +#define ST77XX_PTLON 0x12 +#define ST77XX_NORON 0x13 + +#define ST77XX_INVOFF 0x20 +#define ST77XX_INVON 0x21 +#define ST77XX_DISPOFF 0x28 +#define ST77XX_DISPON 0x29 +#define ST77XX_CASET 0x2A +#define ST77XX_RASET 0x2B +#define ST77XX_RAMWR 0x2C +#define ST77XX_RAMRD 0x2E + +#define ST77XX_PTLAR 0x30 +#define ST77XX_TEOFF 0x34 +#define ST77XX_TEON 0x35 +#define ST77XX_MADCTL 0x36 +#define ST77XX_COLMOD 0x3A + +#define ST77XX_MADCTL_MY 0x80 +#define ST77XX_MADCTL_MX 0x40 +#define ST77XX_MADCTL_MV 0x20 +#define ST77XX_MADCTL_ML 0x10 +#define ST77XX_MADCTL_RGB 0x00 + +#define ST77XX_RDID1 0xDA +#define ST77XX_RDID2 0xDB +#define ST77XX_RDID3 0xDC +#define ST77XX_RDID4 0xDD + +// Some ready-made 16-bit ('565') color settings: +#define ST77XX_BLACK 0x0000 +#define ST77XX_WHITE 0xFFFF +#define ST77XX_RED 0xF800 +#define ST77XX_GREEN 0x07E0 +#define ST77XX_BLUE 0x001F +#define ST77XX_CYAN 0x07FF +#define ST77XX_MAGENTA 0xF81F +#define ST77XX_YELLOW 0xFFE0 +#define ST77XX_ORANGE 0xFC00 + + +// some flags for initR() :( +#define INITR_GREENTAB 0x00 +#define INITR_REDTAB 0x01 +#define INITR_BLACKTAB 0x02 +#define INITR_18GREENTAB INITR_GREENTAB +#define INITR_18REDTAB INITR_REDTAB +#define INITR_18BLACKTAB INITR_BLACKTAB +#define INITR_144GREENTAB 0x01 +#define INITR_MINI160x80 0x04 +#define INITR_HALLOWING 0x05 +#define INITR_MINI160x80_PLUGIN 0x06 + +// Some register settings +#define ST7735_MADCTL_BGR 0x08 +#define ST7735_MADCTL_MH 0x04 + +#define ST7735_FRMCTR1 0xB1 +#define ST7735_FRMCTR2 0xB2 +#define ST7735_FRMCTR3 0xB3 +#define ST7735_INVCTR 0xB4 +#define ST7735_DISSET5 0xB6 + +#define ST7735_PWCTR1 0xC0 +#define ST7735_PWCTR2 0xC1 +#define ST7735_PWCTR3 0xC2 +#define ST7735_PWCTR4 0xC3 +#define ST7735_PWCTR5 0xC4 +#define ST7735_VMCTR1 0xC5 + +#define ST7735_PWCTR6 0xFC + +#define ST7735_GMCTRP1 0xE0 +#define ST7735_GMCTRN1 0xE1 + +// Some ready-made 16-bit ('565') color settings: +#define ST7735_BLACK ST77XX_BLACK +#define ST7735_WHITE ST77XX_WHITE +#define ST7735_RED ST77XX_RED +#define ST7735_GREEN ST77XX_GREEN +#define ST7735_BLUE ST77XX_BLUE +#define ST7735_CYAN ST77XX_CYAN +#define ST7735_MAGENTA ST77XX_MAGENTA +#define ST7735_YELLOW ST77XX_YELLOW +#define ST7735_ORANGE ST77XX_ORANGE + +static TFT_eSPI lcd = TFT_eSPI(160, 80); +static uint32_t curr_color; + +#define _spi (&SPI1) + +SPISettings _spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0); + +// clang-format off +static const uint8_t PROGMEM + Bcmd[] = { // Init commands for 7735B screens + 18, // 18 commands in list: + ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay + 50, // 50 ms delay + ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, no args, w/delay + 255, // 255 = max (500 ms) delay + ST77XX_COLMOD, 1+ST_CMD_DELAY, // 3: Set color mode, 1 arg + delay: + 0x05, // 16-bit color + 10, // 10 ms delay + ST7735_FRMCTR1, 3+ST_CMD_DELAY, // 4: Frame rate control, 3 args + delay: + 0x00, // fastest refresh + 0x06, // 6 lines front porch + 0x03, // 3 lines back porch + 10, // 10 ms delay + ST77XX_MADCTL, 1, // 5: Mem access ctl (directions), 1 arg: + 0x08, // Row/col addr, bottom-top refresh + ST7735_DISSET5, 2, // 6: Display settings #5, 2 args: + 0x15, // 1 clk cycle nonoverlap, 2 cycle gate + // rise, 3 cycle osc equalize + 0x02, // Fix on VTL + ST7735_INVCTR, 1, // 7: Display inversion control, 1 arg: + 0x0, // Line inversion + ST7735_PWCTR1, 2+ST_CMD_DELAY, // 8: Power control, 2 args + delay: + 0x02, // GVDD = 4.7V + 0x70, // 1.0uA + 10, // 10 ms delay + ST7735_PWCTR2, 1, // 9: Power control, 1 arg, no delay: + 0x05, // VGH = 14.7V, VGL = -7.35V + ST7735_PWCTR3, 2, // 10: Power control, 2 args, no delay: + 0x01, // Opamp current small + 0x02, // Boost frequency + ST7735_VMCTR1, 2+ST_CMD_DELAY, // 11: Power control, 2 args + delay: + 0x3C, // VCOMH = 4V + 0x38, // VCOML = -1.1V + 10, // 10 ms delay + ST7735_PWCTR6, 2, // 12: Power control, 2 args, no delay: + 0x11, 0x15, + ST7735_GMCTRP1,16, // 13: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x09, 0x16, 0x09, 0x20, // (Not entirely necessary, but provides + 0x21, 0x1B, 0x13, 0x19, // accurate colors) + 0x17, 0x15, 0x1E, 0x2B, + 0x04, 0x05, 0x02, 0x0E, + ST7735_GMCTRN1,16+ST_CMD_DELAY, // 14: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x0B, 0x14, 0x08, 0x1E, // (Not entirely necessary, but provides + 0x22, 0x1D, 0x18, 0x1E, // accurate colors) + 0x1B, 0x1A, 0x24, 0x2B, + 0x06, 0x06, 0x02, 0x0F, + 10, // 10 ms delay + ST77XX_CASET, 4, // 15: Column addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 2 + 0x00, 0x81, // XEND = 129 + ST77XX_RASET, 4, // 16: Row addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 1 + 0x00, 0x81, // XEND = 160 + ST77XX_NORON, ST_CMD_DELAY, // 17: Normal display on, no args, w/delay + 10, // 10 ms delay + ST77XX_DISPON, ST_CMD_DELAY, // 18: Main screen turn on, no args, delay + 255 }, // 255 = max (500 ms) delay + + Rcmd1[] = { // 7735R init, part 1 (red or green tab) + 15, // 15 commands in list: + ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay + 150, // 150 ms delay + ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, 0 args, w/delay + 255, // 500 ms delay + ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args: + 0x01, 0x2C, 0x2D, // Dot inversion mode + 0x01, 0x2C, 0x2D, // Line inversion mode + ST7735_INVCTR, 1, // 6: Display inversion ctrl, 1 arg: + 0x07, // No inversion + ST7735_PWCTR1, 3, // 7: Power control, 3 args, no delay: + 0xA2, + 0x02, // -4.6V + 0x84, // AUTO mode + ST7735_PWCTR2, 1, // 8: Power control, 1 arg, no delay: + 0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD + ST7735_PWCTR3, 2, // 9: Power control, 2 args, no delay: + 0x0A, // Opamp current small + 0x00, // Boost frequency + ST7735_PWCTR4, 2, // 10: Power control, 2 args, no delay: + 0x8A, // BCLK/2, + 0x2A, // opamp current small & medium low + ST7735_PWCTR5, 2, // 11: Power control, 2 args, no delay: + 0x8A, 0xEE, + ST7735_VMCTR1, 1, // 12: Power control, 1 arg, no delay: + 0x0E, + ST77XX_INVOFF, 0, // 13: Don't invert display, no args + ST77XX_MADCTL, 1, // 14: Mem access ctl (directions), 1 arg: + 0xC8, // row/col addr, bottom-top refresh + ST77XX_COLMOD, 1, // 15: set color mode, 1 arg, no delay: + 0x05 }, // 16-bit color + + Rcmd2green[] = { // 7735R init, part 2 (green tab only) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 0 + 0x00, 0x7F+0x02, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x01, // XSTART = 0 + 0x00, 0x9F+0x01 }, // XEND = 159 + + Rcmd2red[] = { // 7735R init, part 2 (red tab only) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + Rcmd2green144[] = { // 7735R init, part 2 (green 1.44 tab) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F }, // XEND = 127 + + Rcmd2green160x80[] = { // 7735R init, part 2 (mini 160x80) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x4F, // XEND = 79 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + Rcmd2green160x80plugin[] = { // 7735R init, part 2 (mini 160x80 with plugin FPC) + 3, // 3 commands in list: + ST77XX_INVON, 0, // 1: Display is inverted + ST77XX_CASET, 4, // 2: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x4F, // XEND = 79 + ST77XX_RASET, 4, // 3: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + Rcmd3[] = { // 7735R init, part 3 (red or green tab) + 4, // 4 commands in list: + ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides + 0x37, 0x32, 0x29, 0x2d, // accurate colors) + 0x29, 0x25, 0x2B, 0x39, + 0x00, 0x01, 0x03, 0x10, + ST7735_GMCTRN1, 16 , // 2: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides + 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) + 0x2E, 0x2E, 0x37, 0x3F, + 0x00, 0x00, 0x02, 0x10, + ST77XX_NORON, ST_CMD_DELAY, // 3: Normal display on, no args, w/delay + 10, // 10 ms delay + ST77XX_DISPON, ST_CMD_DELAY, // 4: Main screen turn on, no args w/delay + 100 }; // 100 ms delay + +static int16_t _xstart = 0; ///< Internal framebuffer X offset +static int16_t _ystart = 0; ///< Internal framebuffer Y offset +static uint8_t _colstart = 0; ///< Some displays need this changed to offset +static uint8_t _rowstart = 0; ///< Some displays need this changed to offset +static uint8_t rotation = 0; +static int16_t _width = 0; ///< Display width as modified by current rotation +static int16_t _height = 0; ///< Display height as modified by current rotation + +static void set_CS(uint8_t level) { + //if (_cs != (uint8_t) -1) { + digitalWrite(PIN_TFT_CS, level); + //} +} +static void sendCommand(uint8_t com) { + set_CS(HIGH); + digitalWrite(PIN_TFT_DC, LOW); + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + _spi->transfer(com); + _spi->endTransaction(); + set_CS(HIGH); + digitalWrite(PIN_TFT_DC, HIGH); +} + +static void WriteData(uint8_t data) { + digitalWrite(PIN_TFT_CS, LOW); + _spi->beginTransaction(_spiSettings); + _spi->transfer(data); + _spi->endTransaction(); + digitalWrite(PIN_TFT_CS, HIGH); +} +static void SPI_WRITE32(uint32_t l) { + _spi->transfer(l >> 24); + _spi->transfer(l >> 16); + _spi->transfer(l >> 8); + _spi->transfer(l); +} +static void writeCommand(uint8_t cmd) { + digitalWrite(PIN_TFT_DC, LOW); + _spi->transfer(cmd); + digitalWrite(PIN_TFT_DC, HIGH); +} + +static void displayInit(const uint8_t *addr) { + uint8_t numCommands, cmd, numArgs; + uint16_t ms; + + numCommands = pgm_read_byte(addr++); // Number of commands to follow + while (numCommands--) { // For each command... + cmd = pgm_read_byte(addr++); // Read command + numArgs = pgm_read_byte(addr++); // Number of args to follow + ms = numArgs & ST_CMD_DELAY; // If hibit set, delay follows args + numArgs &= ~ST_CMD_DELAY; // Mask out delay bit + sendCommand(cmd); + for (int k = 0; k < numArgs; k++) { + WriteData(addr[k]); + } + addr += numArgs; + + if (ms) { + ms = pgm_read_byte(addr++); // Read post-command delay time (ms) + if (ms == 255) + ms = 500; // If 255, delay for 500 ms + delay(ms); + } + } +} + +static void setRotation(uint8_t m) { + uint8_t madctl = 0; + + rotation = m & 3; // can't be higher than 3 + + switch (rotation) { + case 0: + madctl = ST77XX_MADCTL_MX | ST77XX_MADCTL_MY | ST7735_MADCTL_BGR; + + _height = 160; + _width = 80; + _xstart = _colstart; + _ystart = _rowstart; + break; + case 1: + madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV | ST7735_MADCTL_BGR; + + _width = 160; + _height = 80; + _ystart = _colstart; + _xstart = _rowstart; + break; + case 2: + madctl = ST7735_MADCTL_BGR; + + _height = 160; + _width = 80; + _xstart = _colstart; + _ystart = _rowstart; + break; + case 3: + madctl = ST77XX_MADCTL_MX | ST77XX_MADCTL_MV | ST7735_MADCTL_BGR; + + _width = 160; + _height = 80; + _ystart = _colstart; + _xstart = _rowstart; + break; + } + + sendCommand(ST77XX_MADCTL); + WriteData(madctl); +} + +static void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + x += _xstart; + y += _ystart; + uint32_t xa = ((uint32_t)x << 16) | (x + w - 1); + uint32_t ya = ((uint32_t)y << 16) | (y + h - 1); + + writeCommand(ST77XX_CASET); // Column addr set + SPI_WRITE32(xa); + + writeCommand(ST77XX_RASET); // Row addr set + SPI_WRITE32(ya); + + writeCommand(ST77XX_RAMWR); // write to RAM +} #define SCALE_X 1.25f // 160 / 128 #define SCALE_Y 1.25f // 80 / 64 +static TFT_eSprite *sprite = NULL; + bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { return true; /* @@ -16,35 +408,69 @@ bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { */ } +#ifndef PIN_TFT_LEDA_CTL_ACTIVE + #define PIN_TFT_LEDA_CTL_ACTIVE HIGH +#endif + bool ST7735Display::begin() { if (!_isOn) { if (_peripher_power) _peripher_power->claim(); - pinMode(PIN_TFT_LEDA_CTL, OUTPUT); -#if defined(PIN_TFT_LEDA_CTL_ACTIVE) - digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); + delay(3000); // TEMP!! + pinMode(PIN_TFT_RST, OUTPUT); + pinMode(PIN_TFT_CS, OUTPUT); + pinMode(PIN_TFT_DC, OUTPUT); + + // Pulse Reset low for 10ms + digitalWrite(PIN_TFT_RST, HIGH); + delay(1); + digitalWrite(PIN_TFT_RST, LOW); + delay(10); + digitalWrite(PIN_TFT_RST, HIGH); +#ifdef ESP_PLATFORM + _spi->begin(_clk,_miso,_mosi,-1); #else - digitalWrite(PIN_TFT_LEDA_CTL, HIGH); + _spi->begin(); #endif + _spi->setClockDivider(SPI_CLOCK_DIV2); + + pinMode(PIN_TFT_LEDA_CTL, OUTPUT); + digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); digitalWrite(PIN_TFT_RST, HIGH); -#if defined(HELTEC_T1) - display.initR(INITR_MINI160x80); - display.setRotation(DISPLAY_ROTATION); -#elif defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096) - display.initR(INITR_MINI160x80); - display.setRotation(DISPLAY_ROTATION); - uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR - display.sendCommand(ST77XX_MADCTL, &madctl, 1); -#else - display.initR(INITR_MINI160x80_PLUGIN); - display.setRotation(DISPLAY_ROTATION); + displayInit(Rcmd1); + + _height = 80; + _width = 160; + _colstart = 24; + _rowstart = 0; + +#if defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096) + displayInit(Rcmd2green160x80); + //uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR + //display.sendCommand(ST77XX_MADCTL, &madctl, 1); #endif - display.setSPISpeed(40000000); - display.fillScreen(ST77XX_BLACK); - display.setTextColor(ST77XX_WHITE); - display.setTextSize(2); - display.cp437(true); // Use full 256 char 'Code Page 437' font + + displayInit(Rcmd3); + + setRotation(DISPLAY_ROTATION); + + sendCommand(ST77XX_DISPON); + + if (!sprite) { + // alloc offscreen canvas + sprite = new TFT_eSprite(&lcd); + if (sprite) { + if (sprite->createSprite(160, 80)) { + sprite->fillScreen(ST77XX_BLACK); + sprite->setTextColor(curr_color = ST77XX_WHITE); + } else { + Serial.printf("ST7735Display: failed to alloc canvas pixels"); + } + } else { + Serial.printf("ST7735Display: failed to alloc canvas"); + } + } _isOn = true; } @@ -52,17 +478,22 @@ bool ST7735Display::begin() { } void ST7735Display::turnOn() { - ST7735Display::begin(); + if (!_isOn) { + sendCommand(ST77XX_DISPON); + + // Now turn on the backlight + digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); + _isOn = true; + } } void ST7735Display::turnOff() { if (_isOn) { - digitalWrite(PIN_TFT_RST, LOW); -#if defined(PIN_TFT_LEDA_CTL_ACTIVE) + sendCommand(ST77XX_DISPOFF); + + //digitalWrite(PIN_TFT_RST, LOW); + // Now turn off the backlight digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE); -#else - digitalWrite(PIN_TFT_LEDA_CTL, LOW); -#endif _isOn = false; if (_peripher_power) _peripher_power->release(); @@ -70,78 +501,90 @@ void ST7735Display::turnOff() { } void ST7735Display::clear() { - //Serial.println("DBG: display.Clear"); - display.fillScreen(ST77XX_BLACK); + sprite->fillScreen(ST77XX_BLACK); } void ST7735Display::startFrame(Color bkg) { - display.fillScreen(0x00); - display.setTextColor(ST77XX_WHITE); - display.setTextSize(1); // This one affects size of Please wait... message - display.cp437(true); // Use full 256 char 'Code Page 437' font + sprite->fillScreen(ST77XX_BLACK); + sprite->setTextColor(curr_color = ST77XX_WHITE); + //sprite->setFreeFont(&FreeSans7pt7b); + sprite->setTextSize(1); // This one affects size of Please wait... message + //sprite->cp437(true); // Use full 256 char 'Code Page 437' font } void ST7735Display::setTextSize(int sz) { - display.setTextSize(sz); + sprite->setTextSize(sz); } void ST7735Display::setColor(Color c) { switch (c) { case DisplayDriver::DARK : - _color = ST77XX_BLACK; + curr_color = ST77XX_BLACK; break; case DisplayDriver::LIGHT : - _color = ST77XX_WHITE; + curr_color = ST77XX_WHITE; break; case DisplayDriver::RED : - _color = ST77XX_RED; + curr_color = ST77XX_RED; break; case DisplayDriver::GREEN : - _color = ST77XX_GREEN; + curr_color = ST77XX_GREEN; break; case DisplayDriver::BLUE : - _color = ST77XX_BLUE; + curr_color = ST77XX_BLUE; break; case DisplayDriver::YELLOW : - _color = ST77XX_YELLOW; + curr_color = ST77XX_YELLOW; break; case DisplayDriver::ORANGE : - _color = ST77XX_ORANGE; + curr_color = ST77XX_ORANGE; break; default: - _color = ST77XX_WHITE; + curr_color = ST77XX_WHITE; break; } - display.setTextColor(_color); + sprite->setTextColor(curr_color); } void ST7735Display::setCursor(int x, int y) { - display.setCursor(x*SCALE_X, y*SCALE_Y); + sprite->setCursor(x*SCALE_X, y*SCALE_Y); } void ST7735Display::print(const char* str) { - display.print(str); + sprite->print(str); } void ST7735Display::fillRect(int x, int y, int w, int h) { - display.fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _color); + sprite->fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, curr_color); } void ST7735Display::drawRect(int x, int y, int w, int h) { - display.drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _color); + sprite->drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, curr_color); } void ST7735Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { - display.drawBitmap(x*SCALE_X, y*SCALE_Y, bits, w, h, _color); + sprite->drawBitmap(x*SCALE_X, y*SCALE_Y, bits, w, h, curr_color); } uint16_t ST7735Display::getTextWidth(const char* str) { - int16_t x1, y1; - uint16_t w, h; - display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); - return w / SCALE_X; + return sprite->textWidth(str) / SCALE_X; } void ST7735Display::endFrame() { - // display.display(); + // blit the canvas buffer to LCD + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + uint16_t x, y; + uint16_t* pixels = (uint16_t *) ((TFT_eSprite *) sprite)->getPointer(); + for (y = 0; y < 80; y++, pixels += 160) { + setAddrWindow(0, y, 160, 1); +#ifdef ESP_PLATFORM + _spi->transferBytes((uint8_t *)pixels, NULL, 2 * 160); +#else + _spi->transfer(pixels, NULL, 2 * 160); +#endif + } + + _spi->endTransaction(); + set_CS(HIGH); } diff --git a/src/helpers/ui/ST7735Display.h b/src/helpers/ui/ST7735Display.h index 94797503..08778efd 100644 --- a/src/helpers/ui/ST7735Display.h +++ b/src/helpers/ui/ST7735Display.h @@ -3,28 +3,25 @@ #include "DisplayDriver.h" #include #include -#include -#include +#include "TFT_eSPI.h" #include class ST7735Display : public DisplayDriver { - Adafruit_ST7735 display; bool _isOn; - uint16_t _color; RefCountedDigitalPin* _peripher_power; bool i2c_probe(TwoWire& wire, uint8_t addr); public: #ifdef USE_PIN_TFT ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), - display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST), + // display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST), _peripher_power(peripher_power) { _isOn = false; } #else ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), - display(&SPI1, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST), + // display(&SPI1, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST), _peripher_power(peripher_power) { _isOn = false; diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index e820bf58..0a062f00 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -51,7 +51,7 @@ build_src_filter = ${nrf52_base.build_src_filter} lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + bodmer/TFT_eSPI @ ^2.4.31 debug_tool = jlink upload_protocol = nrfutil From 4fafcd0e23e95d44d2b4ba3f3d7282b1439fd51e Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:21:09 +1000 Subject: [PATCH 16/37] Lilygo T-Echo Lite - Enable nRF52840 DC-DC - board idle usage reduced from ~12mA to ~7mA --- variants/lilygo_techo_lite/TechoBoard.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/lilygo_techo_lite/TechoBoard.cpp b/variants/lilygo_techo_lite/TechoBoard.cpp index a11d31b2..e0b69f98 100644 --- a/variants/lilygo_techo_lite/TechoBoard.cpp +++ b/variants/lilygo_techo_lite/TechoBoard.cpp @@ -6,7 +6,7 @@ #ifdef LILYGO_TECHO void TechoBoard::begin() { - NRF52Board::begin(); + NRF52BoardDCDC::begin(); // Configure battery measurement control BEFORE Wire.begin() // to ensure P0.02 is not claimed by another peripheral From 2efb3af69b9207b23d57b630bd0b1380c4f4405c Mon Sep 17 00:00:00 2001 From: Florent Date: Thu, 2 Jul 2026 21:55:19 -0400 Subject: [PATCH 17/37] repeater: poweroff on long press --- .vscode/extensions.json | 9 ---- examples/simple_repeater/UITask.cpp | 79 +++++++++++++++++++---------- examples/simple_repeater/UITask.h | 5 +- examples/simple_repeater/main.cpp | 5 +- 4 files changed, 60 insertions(+), 38 deletions(-) delete mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 8057bc70..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "recommendations": [ - "pioarduino.pioarduino-ide", - "platformio.platformio-ide" - ], - "unwantedRecommendations": [ - "ms-vscode.cpptools-extension-pack" - ] -} diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 05d863fc..17c708e3 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -1,4 +1,5 @@ #include "UITask.h" +#include "target.h" #include #include @@ -9,6 +10,8 @@ #define AUTO_OFF_MILLIS 20000 // 20 seconds #define BOOT_SCREEN_MILLIS 4000 // 4 seconds +#define POWEROFF_DELAY 3000 + // 'meshcore', 128x13px static const uint8_t meshcore_logo [] PROGMEM = { 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, @@ -29,9 +32,14 @@ static const uint8_t meshcore_logo [] PROGMEM = { void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; + _started_at = millis(); _node_prefs = node_prefs; _display->turnOn(); +#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) + user_btn.begin(); +#endif + // strip off dash and commit hash by changing dash to null terminator // e.g: v1.2.3-abcdef -> v1.2.3 char *version = strdup(firmware_version); @@ -47,7 +55,7 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi void UITask::renderCurrScreen() { char tmp[80]; - if (millis() < BOOT_SCREEN_MILLIS) { // boot screen + if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo _display->setColor(DisplayDriver::BLUE); int logoWidth = 128; @@ -57,24 +65,34 @@ void UITask::renderCurrScreen() { const char* website = "https://meshcore.io"; _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); - uint16_t websiteWidth = _display->getTextWidth(website); - _display->setCursor((_display->width() - websiteWidth) / 2, 22); - _display->print(website); + _display->drawTextCentered(_display->width() / 2, 22, website); // version info _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); - uint16_t versionWidth = _display->getTextWidth(_version_info); - _display->setCursor((_display->width() - versionWidth) / 2, 35); - _display->print(_version_info); + _display->drawTextCentered(_display->width() / 2, 35, _version_info); // node type const char* node_type = "< Repeater >"; - uint16_t typeWidth = _display->getTextWidth(node_type); - _display->setCursor((_display->width() - typeWidth) / 2, 48); - _display->print(node_type); - } else { // home screen - // node name + _display->drawTextCentered(_display->width() / 2, 48, node_type); + } else if (_powering_off_at > 0) { + // meshcore logo + _display->setColor(DisplayDriver::BLUE); + int logoWidth = 128; + _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); + + // meshcore website + const char* website = "https://meshcore.io"; + _display->setColor(DisplayDriver::LIGHT); + _display->setTextSize(1); + _display->drawTextCentered(_display->width()/ 2, 22, website); + + // Powering off + const char* poweroff_string = "Turning OFF"; + uint16_t poffWidth = _display->getTextWidth(poweroff_string); + _display->setCursor((_display->width() - poffWidth) / 2, 48); + _display->drawTextCentered(_display->width()/2, 48, poweroff_string); + } else { _display->setCursor(0, 0); _display->setTextSize(1); _display->setColor(DisplayDriver::GREEN); @@ -94,21 +112,19 @@ void UITask::renderCurrScreen() { } void UITask::loop() { -#ifdef PIN_USER_BTN - if (millis() >= _next_read) { - int btnState = digitalRead(PIN_USER_BTN); - if (btnState != _prevBtnState) { - if (btnState == USER_BTN_PRESSED) { // pressed? - if (_display->isOn()) { - // TODO: any action ? - } else { - _display->turnOn(); - } - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer - } - _prevBtnState = btnState; +#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS) + int ev = user_btn.check(); + if (ev == BUTTON_EVENT_CLICK) { + if (_display->isOn()) { + // TODO: any action ? + } else { + _display->turnOn(); } - _next_read = millis() + 200; // 5 reads per second + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + _display->turnOn(); + Serial.println("Powering Off"); + _powering_off_at = millis() + POWEROFF_DELAY; } #endif @@ -124,4 +140,15 @@ void UITask::loop() { _display->turnOff(); } } + + if (_powering_off_at > 0) { // power off timer armed +#ifdef LED_PIN + digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff +#endif + if (millis() > _powering_off_at) { + _display->turnOff(); + radio_driver.powerOff(); + _board->powerOff(); // should not return + } + } } diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index a27259f1..d8e3ce1d 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -4,15 +4,18 @@ #include class UITask { + mesh::MainBoard* _board; DisplayDriver* _display; unsigned long _next_read, _next_refresh, _auto_off; int _prevBtnState; NodePrefs* _node_prefs; char _version_info[32]; + unsigned long _powering_off_at = 0; + unsigned long _started_at = 0; void renderCurrScreen(); public: - UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + UITask(mesh::MainBoard& board, DisplayDriver& display) : _board(&board), _display(&display) { _next_read = _next_refresh = 0; } void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); void loop(); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 2ce056f5..c13cd99d 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -5,7 +5,7 @@ #ifdef DISPLAY_CLASS #include "UITask.h" - static UITask ui_task(display); + static UITask ui_task(board, display); #endif StdRNG fast_rng; @@ -130,7 +130,8 @@ void loop() { command[0] = 0; // reset command buffer } -#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) +#if defined(PIN_USER_BTN) && defined(_SEEED_SENSECAP_SOLAR_H_) && !defined(DISPLAY_CLASS) + // // Hold the user button to power off the SenseCAP Solar repeater. int btnState = digitalRead(PIN_USER_BTN); if (btnState == LOW) { From 2c9ab2fe5e817a48bd4db6efa8b6ad43c631ddfe Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 7 Jul 2026 13:54:14 +1000 Subject: [PATCH 18/37] * timing fixes (courtesy of Taco) --- src/helpers/ui/ST7735Display.cpp | 107 ++++++++++++++++--------------- src/helpers/ui/ST7735Display.h | 3 + 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index 2b9f3ebb..aad12d1b 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -168,11 +168,11 @@ static const uint8_t PROGMEM 255 }, // 255 = max (500 ms) delay Rcmd1[] = { // 7735R init, part 1 (red or green tab) - 15, // 15 commands in list: - ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay - 150, // 150 ms delay + 14, // 14 commands in list: + /*ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay + 150, */ // 150 ms delay ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, 0 args, w/delay - 255, // 500 ms delay + 120, // 120 ms delay ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: @@ -251,7 +251,7 @@ static const uint8_t PROGMEM 0x00, 0x9F }, // XEND = 159 Rcmd3[] = { // 7735R init, part 3 (red or green tab) - 4, // 4 commands in list: + 2, // 2 commands in list: ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides 0x37, 0x32, 0x29, 0x2d, // accurate colors) @@ -261,11 +261,7 @@ static const uint8_t PROGMEM 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) 0x2E, 0x2E, 0x37, 0x3F, - 0x00, 0x00, 0x02, 0x10, - ST77XX_NORON, ST_CMD_DELAY, // 3: Normal display on, no args, w/delay - 10, // 10 ms delay - ST77XX_DISPON, ST_CMD_DELAY, // 4: Main screen turn on, no args w/delay - 100 }; // 100 ms delay + 0x00, 0x00, 0x02, 0x10 }; // 100 ms delay static int16_t _xstart = 0; ///< Internal framebuffer X offset static int16_t _ystart = 0; ///< Internal framebuffer Y offset @@ -413,20 +409,30 @@ bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { #endif bool ST7735Display::begin() { + if (!sprite) { + // alloc offscreen canvas + sprite = new TFT_eSprite(&lcd); + if (sprite) { + if (sprite->createSprite(160, 80)) { + sprite->fillScreen(ST77XX_BLACK); + sprite->setTextColor(curr_color = ST77XX_WHITE); + } else { + Serial.printf("ST7735Display: failed to alloc canvas pixels"); + } + } else { + Serial.printf("ST7735Display: failed to alloc canvas"); + } + } + if (!_isOn) { if (_peripher_power) _peripher_power->claim(); - delay(3000); // TEMP!! + delay(100); // TEMP!! pinMode(PIN_TFT_RST, OUTPUT); pinMode(PIN_TFT_CS, OUTPUT); pinMode(PIN_TFT_DC, OUTPUT); + pinMode(PIN_TFT_LEDA_CTL, OUTPUT); - // Pulse Reset low for 10ms - digitalWrite(PIN_TFT_RST, HIGH); - delay(1); - digitalWrite(PIN_TFT_RST, LOW); - delay(10); - digitalWrite(PIN_TFT_RST, HIGH); #ifdef ESP_PLATFORM _spi->begin(_clk,_miso,_mosi,-1); #else @@ -434,55 +440,56 @@ bool ST7735Display::begin() { #endif _spi->setClockDivider(SPI_CLOCK_DIV2); - pinMode(PIN_TFT_LEDA_CTL, OUTPUT); - digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); - digitalWrite(PIN_TFT_RST, HIGH); - - displayInit(Rcmd1); - _height = 80; _width = 160; _colstart = 24; _rowstart = 0; -#if defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096) - displayInit(Rcmd2green160x80); - //uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR - //display.sendCommand(ST77XX_MADCTL, &madctl, 1); -#endif - - displayInit(Rcmd3); - - setRotation(DISPLAY_ROTATION); - + _resetAndInit(); + sendCommand(ST77XX_DISPON); - - if (!sprite) { - // alloc offscreen canvas - sprite = new TFT_eSprite(&lcd); - if (sprite) { - if (sprite->createSprite(160, 80)) { - sprite->fillScreen(ST77XX_BLACK); - sprite->setTextColor(curr_color = ST77XX_WHITE); - } else { - Serial.printf("ST7735Display: failed to alloc canvas pixels"); - } - } else { - Serial.printf("ST7735Display: failed to alloc canvas"); - } - } _isOn = true; } return true; } +void ST7735Display::_resetAndInit() { + // Pulse Reset low for 10ms + digitalWrite(PIN_TFT_RST, HIGH); + delay(2); + digitalWrite(PIN_TFT_RST, LOW); + delay(10); + digitalWrite(PIN_TFT_RST, HIGH); + delay(2); + + // run init commands + displayInit(Rcmd1); +#if defined(HELTEC_TRACKER_V2) || defined(HELTEC_T096) + displayInit(Rcmd2green160x80); + //uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR + //display.sendCommand(ST77XX_MADCTL, &madctl, 1); +#endif + displayInit(Rcmd3); + setRotation(DISPLAY_ROTATION); + + // clear the buffer before display on + sprite->fillScreen(ST77XX_BLACK); + endFrame(); + + // turn on backlight + digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); + +} + void ST7735Display::turnOn() { if (!_isOn) { + if (_peripher_power) _peripher_power->claim(); + _resetAndInit(); sendCommand(ST77XX_DISPON); // Now turn on the backlight - digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); + // digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE); _isOn = true; } } @@ -507,7 +514,7 @@ void ST7735Display::clear() { void ST7735Display::startFrame(Color bkg) { sprite->fillScreen(ST77XX_BLACK); sprite->setTextColor(curr_color = ST77XX_WHITE); - //sprite->setFreeFont(&FreeSans7pt7b); + sprite->setFreeFont(); sprite->setTextSize(1); // This one affects size of Please wait... message //sprite->cp437(true); // Use full 256 char 'Code Page 437' font } diff --git a/src/helpers/ui/ST7735Display.h b/src/helpers/ui/ST7735Display.h index 08778efd..68c83db2 100644 --- a/src/helpers/ui/ST7735Display.h +++ b/src/helpers/ui/ST7735Display.h @@ -43,4 +43,7 @@ public: void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; uint16_t getTextWidth(const char* str) override; void endFrame() override; + +protected: + void _resetAndInit(); }; From 7f8522a3625a33a49599ad022695fe752763a06f Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:12:59 +1000 Subject: [PATCH 19/37] Heltec T114 - nRF52840 DC-DC Regulator - Enable DC-DC regulator on nRF52840 for Heltec T114 board (idle current reduced from ~12mA to ~9mA) --- variants/heltec_t114/T114Board.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_t114/T114Board.cpp b/variants/heltec_t114/T114Board.cpp index c03d39af..51db72fc 100644 --- a/variants/heltec_t114/T114Board.cpp +++ b/variants/heltec_t114/T114Board.cpp @@ -33,7 +33,7 @@ void T114Board::initiateShutdown(uint8_t reason) { #endif // NRF52_POWER_MANAGEMENT void T114Board::begin() { - NRF52Board::begin(); + NRF52BoardDCDC::begin(); pinMode(PIN_VBAT_READ, INPUT); From 369016acc952eda75ebbd53fe74380cb617dea67 Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 7 Jul 2026 10:20:03 -0400 Subject: [PATCH 20/37] introduce shutdownPeripherals in NRF52Board to prepare for shutdown --- src/helpers/NRF52Board.cpp | 6 +++++- src/helpers/NRF52Board.h | 1 + variants/lilygo_techo/TechoBoard.h | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index beee3212..088ddc23 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -298,7 +298,7 @@ float NRF52Board::getMCUTemperature() { return temp * 0.25f; // Convert to *C } -void NRF52Board::powerOff() { +void NRF52Board::shutdownPeripherals() { // Power off the display if any #ifdef DISPLAY_CLASS display.turnOff(); @@ -318,6 +318,10 @@ void NRF52Board::powerOff() { // Flush serial buffers Serial.flush(); delay(100); +} + +void NRF52Board::powerOff() { + shutdownPeripherals(); // Enter SYSTEMOFF uint8_t sd_enabled = 0; diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index cbf4cd49..dba15f97 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -50,6 +50,7 @@ public: virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; virtual void reboot() override { NVIC_SystemReset(); } + virtual void shutdownPeripherals(); virtual void powerOff() override; virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool startOTAUpdate(const char *id, char reply[]) override; diff --git a/variants/lilygo_techo/TechoBoard.h b/variants/lilygo_techo/TechoBoard.h index e957d2e5..867fc24c 100644 --- a/variants/lilygo_techo/TechoBoard.h +++ b/variants/lilygo_techo/TechoBoard.h @@ -23,8 +23,8 @@ public: return "LilyGo T-Echo"; } - void powerOff() override { - NRF52Board::powerOff(); + void shutdownPeripherals() override { + NRF52Board::shutdownPeripherals(); #ifdef LED_RED digitalWrite(LED_RED, HIGH); #endif From 17d68e328f0eb5d016fe3c6cf4ada6ba64f7aee2 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 8 Jul 2026 16:20:12 +0800 Subject: [PATCH 21/37] Add heltec rc32 board --- boards/heltec-rc32.json | 43 ++ src/helpers/ui/NV3001BDisplay.cpp | 553 +++++++++++++++++++++++ src/helpers/ui/NV3001BDisplay.h | 68 +++ variants/heltec_rc32/HeltecRC32Board.cpp | 66 +++ variants/heltec_rc32/HeltecRC32Board.h | 32 ++ variants/heltec_rc32/pins_arduino.h | 60 +++ variants/heltec_rc32/platformio.ini | 346 ++++++++++++++ variants/heltec_rc32/target.cpp | 45 ++ variants/heltec_rc32/target.h | 31 ++ variants/heltec_rc32/variant.h | 51 +++ 10 files changed, 1295 insertions(+) create mode 100644 boards/heltec-rc32.json create mode 100644 src/helpers/ui/NV3001BDisplay.cpp create mode 100644 src/helpers/ui/NV3001BDisplay.h create mode 100644 variants/heltec_rc32/HeltecRC32Board.cpp create mode 100644 variants/heltec_rc32/HeltecRC32Board.h create mode 100644 variants/heltec_rc32/pins_arduino.h create mode 100644 variants/heltec_rc32/platformio.ini create mode 100644 variants/heltec_rc32/target.cpp create mode 100644 variants/heltec_rc32/target.h create mode 100644 variants/heltec_rc32/variant.h diff --git a/boards/heltec-rc32.json b/boards/heltec-rc32.json new file mode 100644 index 00000000..b9bafa26 --- /dev/null +++ b/boards/heltec-rc32.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_rc32" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec RC32 (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "Heltec" +} diff --git a/src/helpers/ui/NV3001BDisplay.cpp b/src/helpers/ui/NV3001BDisplay.cpp new file mode 100644 index 00000000..03825cc0 --- /dev/null +++ b/src/helpers/ui/NV3001BDisplay.cpp @@ -0,0 +1,553 @@ +#include "NV3001BDisplay.h" +#include +#include + +#ifndef SPI_FREQUENCY + #define SPI_FREQUENCY 8000000 +#endif + +#ifndef PIN_TFT_SCL + #error "PIN_TFT_SCL must be defined" +#endif + +#ifndef PIN_TFT_SDA + #error "PIN_TFT_SDA must be defined" +#endif + +#ifndef PIN_TFT_CS + #error "PIN_TFT_CS must be defined" +#endif + +#ifndef PIN_TFT_DC + #error "PIN_TFT_DC must be defined" +#endif + +#ifndef PIN_TFT_MISO + #define PIN_TFT_MISO -1 +#endif + +#ifndef PIN_TFT_RST + #define PIN_TFT_RST -1 +#endif + +#ifndef PIN_TFT_EN + #define PIN_TFT_EN -1 +#endif + +#ifndef PIN_TFT_BL + #define PIN_TFT_BL -1 +#endif + +#ifndef PIN_TFT_EN_ACTIVE + #define PIN_TFT_EN_ACTIVE LOW +#endif + +#ifndef PIN_TFT_BL_ACTIVE + #define PIN_TFT_BL_ACTIVE HIGH +#endif + +#ifndef DISPLAY_ROTATION + #define DISPLAY_ROTATION 0 +#endif + +#ifndef NV3001B_SCREEN_WIDTH + #define NV3001B_SCREEN_WIDTH 220 +#endif + +#ifndef NV3001B_SCREEN_HEIGHT + #define NV3001B_SCREEN_HEIGHT 128 +#endif + +#ifndef DISPLAY_SCALE_X + #define DISPLAY_SCALE_X ((float)NV3001B_SCREEN_WIDTH / NV3001B_LOGICAL_WIDTH) +#endif + +#ifndef DISPLAY_SCALE_Y + #define DISPLAY_SCALE_Y ((float)NV3001B_SCREEN_HEIGHT / NV3001B_LOGICAL_HEIGHT) +#endif + +#define NV3001B_SWRESET 0x01 +#define NV3001B_SLPOUT 0x11 +#define NV3001B_DISPON 0x29 +#define NV3001B_CASET 0x2A +#define NV3001B_RASET 0x2B +#define NV3001B_RAMWR 0x2C +#define NV3001B_MADCTL 0x36 +#define NV3001B_COLMOD 0x3A + +#define NV3001B_MADCTL_MY 0x80 +#define NV3001B_MADCTL_MX 0x40 +#define NV3001B_MADCTL_MV 0x20 +#define NV3001B_MADCTL_RGB 0x00 + +#ifndef NV3001B_TEXT_SIZE1_SCALE_X + #define NV3001B_TEXT_SIZE1_SCALE_X 1 +#endif + +#ifndef NV3001B_TEXT_SIZE1_SCALE_Y + #define NV3001B_TEXT_SIZE1_SCALE_Y 2 +#endif + +#ifndef NV3001B_TEXT_SIZE2_SCALE_X + #define NV3001B_TEXT_SIZE2_SCALE_X 2 +#endif + +#ifndef NV3001B_TEXT_SIZE2_SCALE_Y + #define NV3001B_TEXT_SIZE2_SCALE_Y 3 +#endif + +static uint16_t mapColor(DisplayDriver::Color c) { + switch (c) { + case DisplayDriver::DARK: return 0x0000; + case DisplayDriver::LIGHT: return 0xffff; + case DisplayDriver::RED: return 0xf800; + case DisplayDriver::GREEN: return 0x07e0; + case DisplayDriver::BLUE: return 0x001f; + case DisplayDriver::YELLOW: return 0xffe0; + case DisplayDriver::ORANGE: return 0xfd20; + default: return 0xffff; + } +} + +static int scaleX(int x) { + return (int)(x * DISPLAY_SCALE_X); +} + +static int scaleY(int y) { + return (int)(y * DISPLAY_SCALE_Y); +} + +static int scaleWidth(int x, int w) { + if (w <= 0) return 0; + int scaled = scaleX(x + w) - scaleX(x); + return scaled > 0 ? scaled : 1; +} + +static int scaleHeight(int y, int h) { + if (h <= 0) return 0; + int scaled = scaleY(y + h) - scaleY(y); + return scaled > 0 ? scaled : 1; +} + +static uint8_t nv3001bMADCTL(uint8_t rotation) { + uint8_t madctl; + switch (rotation & 3) { + case 0: + madctl = NV3001B_MADCTL_MY | NV3001B_MADCTL_MV | NV3001B_MADCTL_RGB; + break; + case 1: + madctl = NV3001B_MADCTL_MY | NV3001B_MADCTL_MX | NV3001B_MADCTL_RGB; + break; + case 2: + madctl = NV3001B_MADCTL_RGB; + break; + default: + madctl = NV3001B_MADCTL_MX | NV3001B_MADCTL_MV | NV3001B_MADCTL_RGB; + break; + } + return madctl; +} + +static const uint8_t font5x7[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x14, + 0x7f, 0x14, 0x7f, 0x14, 0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x23, 0x13, 0x08, 0x64, 0x62, 0x36, 0x49, + 0x55, 0x22, 0x50, 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, 0x1c, 0x22, 0x41, 0x00, 0x00, 0x41, 0x22, + 0x1c, 0x00, 0x14, 0x08, 0x3e, 0x08, 0x14, 0x08, 0x08, 0x3e, 0x08, 0x08, 0x00, 0x50, 0x30, 0x00, + 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x60, 0x60, 0x00, 0x00, 0x20, 0x10, 0x08, 0x04, 0x02, + 0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00, 0x42, 0x7f, 0x40, 0x00, 0x42, 0x61, 0x51, 0x49, 0x46, 0x21, + 0x41, 0x45, 0x4b, 0x31, 0x18, 0x14, 0x12, 0x7f, 0x10, 0x27, 0x45, 0x45, 0x45, 0x39, 0x3c, 0x4a, + 0x49, 0x49, 0x30, 0x01, 0x71, 0x09, 0x05, 0x03, 0x36, 0x49, 0x49, 0x49, 0x36, 0x06, 0x49, 0x49, + 0x29, 0x1e, 0x00, 0x36, 0x36, 0x00, 0x00, 0x00, 0x56, 0x36, 0x00, 0x00, 0x08, 0x14, 0x22, 0x41, + 0x00, 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, 0x41, 0x22, 0x14, 0x08, 0x02, 0x01, 0x51, 0x09, 0x06, + 0x32, 0x49, 0x79, 0x41, 0x3e, 0x7e, 0x11, 0x11, 0x11, 0x7e, 0x7f, 0x49, 0x49, 0x49, 0x36, 0x3e, + 0x41, 0x41, 0x41, 0x22, 0x7f, 0x41, 0x41, 0x22, 0x1c, 0x7f, 0x49, 0x49, 0x49, 0x41, 0x7f, 0x09, + 0x09, 0x09, 0x01, 0x3e, 0x41, 0x49, 0x49, 0x7a, 0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00, 0x41, 0x7f, + 0x41, 0x00, 0x20, 0x40, 0x41, 0x3f, 0x01, 0x7f, 0x08, 0x14, 0x22, 0x41, 0x7f, 0x40, 0x40, 0x40, + 0x40, 0x7f, 0x02, 0x0c, 0x02, 0x7f, 0x7f, 0x04, 0x08, 0x10, 0x7f, 0x3e, 0x41, 0x41, 0x41, 0x3e, + 0x7f, 0x09, 0x09, 0x09, 0x06, 0x3e, 0x41, 0x51, 0x21, 0x5e, 0x7f, 0x09, 0x19, 0x29, 0x46, 0x46, + 0x49, 0x49, 0x49, 0x31, 0x01, 0x01, 0x7f, 0x01, 0x01, 0x3f, 0x40, 0x40, 0x40, 0x3f, 0x1f, 0x20, + 0x40, 0x20, 0x1f, 0x3f, 0x40, 0x38, 0x40, 0x3f, 0x63, 0x14, 0x08, 0x14, 0x63, 0x07, 0x08, 0x70, + 0x08, 0x07, 0x61, 0x51, 0x49, 0x45, 0x43, 0x00, 0x7f, 0x41, 0x41, 0x00, 0x02, 0x04, 0x08, 0x10, + 0x20, 0x00, 0x41, 0x41, 0x7f, 0x00, 0x04, 0x02, 0x01, 0x02, 0x04, 0x40, 0x40, 0x40, 0x40, 0x40, + 0x00, 0x01, 0x02, 0x04, 0x00, 0x20, 0x54, 0x54, 0x54, 0x78, 0x7f, 0x48, 0x44, 0x44, 0x38, 0x38, + 0x44, 0x44, 0x44, 0x20, 0x38, 0x44, 0x44, 0x48, 0x7f, 0x38, 0x54, 0x54, 0x54, 0x18, 0x08, 0x7e, + 0x09, 0x01, 0x02, 0x0c, 0x52, 0x52, 0x52, 0x3e, 0x7f, 0x08, 0x04, 0x04, 0x78, 0x00, 0x44, 0x7d, + 0x40, 0x00, 0x20, 0x40, 0x44, 0x3d, 0x00, 0x7f, 0x10, 0x28, 0x44, 0x00, 0x00, 0x41, 0x7f, 0x40, + 0x00, 0x7c, 0x04, 0x18, 0x04, 0x78, 0x7c, 0x08, 0x04, 0x04, 0x78, 0x38, 0x44, 0x44, 0x44, 0x38, + 0x7c, 0x14, 0x14, 0x14, 0x08, 0x08, 0x14, 0x14, 0x18, 0x7c, 0x7c, 0x08, 0x04, 0x04, 0x08, 0x48, + 0x54, 0x54, 0x54, 0x20, 0x04, 0x3f, 0x44, 0x40, 0x20, 0x3c, 0x40, 0x40, 0x20, 0x7c, 0x1c, 0x20, + 0x40, 0x20, 0x1c, 0x3c, 0x40, 0x30, 0x40, 0x3c, 0x44, 0x28, 0x10, 0x28, 0x44, 0x0c, 0x50, 0x50, + 0x50, 0x3c, 0x44, 0x64, 0x54, 0x4c, 0x44, 0x00, 0x08, 0x36, 0x41, 0x00, 0x00, 0x00, 0x7f, 0x00, + 0x00, 0x00, 0x41, 0x36, 0x08, 0x00, 0x08, 0x08, 0x2a, 0x1c, 0x08, 0x00, 0x06, 0x09, 0x09, 0x06 +}; + +static int textPixelScaleX(uint8_t size) { + return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_X : NV3001B_TEXT_SIZE2_SCALE_X; +} + +static int textPixelScaleY(uint8_t size) { + return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_Y : NV3001B_TEXT_SIZE2_SCALE_Y; +} + +static void setupOptionalOutput(int pin, int level) { + if (pin < 0) return; + + pinMode(pin, OUTPUT); + digitalWrite(pin, level); +} + +static void writeOptionalPin(int pin, int level) { + if (pin < 0) return; + + digitalWrite(pin, level); +} + +void NV3001BDisplay::writeCommand(uint8_t cmd) { + spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_TFT_DC, LOW); + digitalWrite(PIN_TFT_CS, LOW); + spi.transfer(cmd); + digitalWrite(PIN_TFT_CS, HIGH); + spi.endTransaction(); +} + +void NV3001BDisplay::writeBytes(const uint8_t* data, size_t len) { + if (!data || len == 0) return; + + spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_TFT_DC, HIGH); + digitalWrite(PIN_TFT_CS, LOW); + for (size_t i = 0; i < len; i++) { + spi.transfer(data[i]); + } + digitalWrite(PIN_TFT_CS, HIGH); + spi.endTransaction(); +} + +void NV3001BDisplay::writeCommandData(uint8_t cmd, const uint8_t* data, size_t len) { + writeCommand(cmd); + writeBytes(data, len); +} + +void NV3001BDisplay::setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + uint16_t x2 = x + w - 1; + uint16_t y2 = y + h - 1; + uint8_t data[4]; + + data[0] = x >> 8; + data[1] = x & 0xff; + data[2] = x2 >> 8; + data[3] = x2 & 0xff; + writeCommandData(NV3001B_CASET, data, sizeof(data)); + + data[0] = y >> 8; + data[1] = y & 0xff; + data[2] = y2 >> 8; + data[3] = y2 & 0xff; + writeCommandData(NV3001B_RASET, data, sizeof(data)); + + writeCommand(NV3001B_RAMWR); +} + +void NV3001BDisplay::writeColor(uint16_t rgb, uint32_t count) { + uint8_t hi = rgb >> 8; + uint8_t lo = rgb & 0xff; + + spi.beginTransaction(SPISettings(SPI_FREQUENCY, MSBFIRST, SPI_MODE0)); + digitalWrite(PIN_TFT_DC, HIGH); + digitalWrite(PIN_TFT_CS, LOW); + while (count--) { + spi.transfer(hi); + spi.transfer(lo); + } + digitalWrite(PIN_TFT_CS, HIGH); + spi.endTransaction(); +} + +void NV3001BDisplay::initPanel() { +#define CMD0(C) do { writeCommand(C); } while (0) +#define CMD1(C, A) do { const uint8_t d[] = { A }; writeCommandData(C, d, sizeof(d)); } while (0) +#define CMD2(C, A, B) do { const uint8_t d[] = { A, B }; writeCommandData(C, d, sizeof(d)); } while (0) + + CMD0(NV3001B_SWRESET); + delay(120); + CMD1(0xFF, 0xA5); + CMD1(0x41, 0x00); + CMD1(0x50, 0x02); + CMD1(0x52, 0x6E); + CMD1(0x57, 0x02); + CMD1(0x46, 0x11); + CMD2(0x47, 0x00, 0x01); + CMD2(0x8F, 0x22, 0x03); + CMD1(0x9A, 0x78); + CMD1(0x9B, 0x78); + CMD1(0x9C, 0xA0); + CMD1(0x9D, 0x17); + CMD1(0x9E, 0xC1); + CMD1(0x83, 0x5A); + CMD1(0x84, 0xB6); + CMD1(0xFF, 0xA5); + CMD1(0x85, 0x5F); + CMD1(0x6E, 0x0F); + CMD1(0x7E, 0x0F); + CMD1(0x60, 0x00); + CMD1(0x70, 0x00); + CMD1(0x6D, 0x33); + CMD1(0x7D, 0x37); + CMD1(0x61, 0x09); + CMD1(0x71, 0x0A); + CMD1(0x6C, 0x2A); + CMD1(0x7C, 0x36); + CMD1(0x62, 0x11); + CMD1(0x72, 0x10); + CMD1(0x68, 0x4E); + CMD1(0x78, 0x4E); + CMD1(0x66, 0x36); + CMD1(0x76, 0x3C); + CMD1(0x1A, 0x1C); + CMD1(0x7B, 0x14); + CMD1(0x63, 0x0D); + CMD1(0x73, 0x0A); + CMD1(0x6A, 0x16); + CMD1(0x7A, 0x12); + CMD1(0x64, 0x0B); + CMD1(0x74, 0x0A); + CMD1(0x69, 0x08); + CMD1(0x79, 0x0A); + CMD1(0x65, 0x06); + CMD1(0x75, 0x07); + CMD1(0x67, 0x23); + CMD1(0x77, 0x44); + CMD1(0xE0, 0x00); + CMD1(0xE9, 0x30); + CMD1(0xEB, 0xB7); + CMD1(0xEC, 0x00); + CMD1(0xED, 0x11); + CMD1(0xF0, 0xB7); + CMD1(0x53, 0x04); + CMD1(0x54, 0x04); + CMD1(0x55, 0x40); + CMD1(0x56, 0x40); + CMD2(0xA0, 0x60, 0x01); + CMD1(0xA1, 0x84); + CMD1(0xA2, 0x85); + CMD2(0xAB, 0x00, 0x02); + CMD2(0xAC, 0x00, 0x06); + CMD2(0xAD, 0x00, 0x03); + CMD2(0xAE, 0x00, 0x07); + CMD1(0xC7, 0x01); + CMD1(0xB9, 0x82); + CMD1(0xBA, 0x83); + CMD1(0xBB, 0x00); + CMD1(0xBC, 0x81); + CMD1(0xBD, 0x02); + CMD1(0xBE, 0x01); + CMD1(0xBF, 0x04); + CMD1(0xC0, 0x03); + CMD1(0xC8, 0x55); + CMD1(0xC9, 0xC9); + CMD1(0xCA, 0xC8); + CMD1(0xCB, 0xCB); + CMD1(0xCC, 0xCA); + CMD1(0xCD, 0x55); + CMD1(0xCE, 0xCE); + CMD1(0xCF, 0xCD); + CMD1(0xD0, 0xD0); + CMD1(0xD1, 0xCF); + CMD1(0xF2, 0x46); + CMD1(0xA8, 0x04); + CMD1(0xA9, 0xB0); + CMD1(0xAA, 0xA3); + CMD1(0xB6, 0x00); + CMD1(0xB7, 0xB0); + CMD1(0xB8, 0xA3); + CMD1(0xC4, 0x03); + CMD1(0xC5, 0xB0); + CMD1(0xC6, 0xA3); + CMD1(0x80, 0x10); + CMD1(0xFF, 0x00); + CMD1(0x35, 0x00); + CMD0(NV3001B_SLPOUT); + delay(120); + CMD1(NV3001B_COLMOD, 0x05); + CMD1(NV3001B_MADCTL, nv3001bMADCTL(DISPLAY_ROTATION)); + CMD0(NV3001B_DISPON); + delay(10); + +#undef CMD0 +#undef CMD1 +#undef CMD2 +} + +void NV3001BDisplay::fillPhysicalRect(int x, int y, int w, int h) { + if (!is_on || w <= 0 || h <= 0) return; + + if (x < 0) { + w += x; + x = 0; + } + if (y < 0) { + h += y; + y = 0; + } + if (x + w > NV3001B_SCREEN_WIDTH) w = NV3001B_SCREEN_WIDTH - x; + if (y + h > NV3001B_SCREEN_HEIGHT) h = NV3001B_SCREEN_HEIGHT - y; + if (w <= 0 || h <= 0) return; + + setAddrWindow(x, y, w, h); + writeColor(color, (uint32_t)w * h); +} + +void NV3001BDisplay::drawChar(int x, int y, char ch) { + if (ch < 32 || ch > 127) ch = '?'; + + uint16_t index = (uint16_t)(ch - 32) * 5; + int scale_x = textPixelScaleX(text_size); + int scale_y = textPixelScaleY(text_size); + for (int col = 0; col < 5; col++) { + uint8_t line = pgm_read_byte(font5x7 + index + col); + for (int row = 0; row < 7; row++) { + if (line & (1 << row)) { + fillPhysicalRect(x + col * scale_x, y + row * scale_y, scale_x, scale_y); + } + } + } +} + +bool NV3001BDisplay::begin() { + if (is_on) return true; + + if (periph_power) periph_power->claim(); + + setupOptionalOutput(PIN_TFT_EN, PIN_TFT_EN_ACTIVE); + setupOptionalOutput(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE); + pinMode(PIN_TFT_CS, OUTPUT); + pinMode(PIN_TFT_DC, OUTPUT); + digitalWrite(PIN_TFT_CS, HIGH); + digitalWrite(PIN_TFT_DC, HIGH); + delay(20); + + spi.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS); + if (PIN_TFT_RST >= 0) { + pinMode(PIN_TFT_RST, OUTPUT); + digitalWrite(PIN_TFT_RST, HIGH); + delay(10); + digitalWrite(PIN_TFT_RST, LOW); + delay(20); + digitalWrite(PIN_TFT_RST, HIGH); + delay(120); + } + + initPanel(); + is_on = true; + color = 0x0000; + fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); + color = 0xffff; + text_size = 1; + cursor_x = 0; + cursor_y = 0; + writeOptionalPin(PIN_TFT_BL, PIN_TFT_BL_ACTIVE); + return true; +} + +void NV3001BDisplay::turnOn() { + begin(); +} + +void NV3001BDisplay::turnOff() { + if (!is_on) return; + + writeOptionalPin(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE); + writeOptionalPin(PIN_TFT_EN, !PIN_TFT_EN_ACTIVE); + is_on = false; + if (periph_power) periph_power->release(); +} + +void NV3001BDisplay::clear() { + uint16_t saved = color; + color = 0x0000; + fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); + color = saved; +} + +void NV3001BDisplay::startFrame(Color bkg) { + color = mapColor(bkg); + fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); + color = 0xffff; + text_size = 1; + cursor_x = 0; + cursor_y = 0; +} + +void NV3001BDisplay::setTextSize(int sz) { + text_size = sz < 1 ? 1 : sz; +} + +void NV3001BDisplay::setColor(Color c) { + color = mapColor(c); +} + +void NV3001BDisplay::setCursor(int x, int y) { + cursor_x = scaleX(x); + cursor_y = scaleY(y); +} + +void NV3001BDisplay::print(const char* str) { + if (!str || !is_on) return; + + int scale_x = textPixelScaleX(text_size); + int scale_y = textPixelScaleY(text_size); + while (*str) { + if (*str == '\n') { + cursor_x = 0; + cursor_y += 8 * scale_y; + } else if (*str == '\r') { + cursor_x = 0; + } else { + drawChar(cursor_x, cursor_y, *str); + cursor_x += 6 * scale_x; + } + str++; + } +} + +void NV3001BDisplay::fillRect(int x, int y, int w, int h) { + fillPhysicalRect(scaleX(x), scaleY(y), scaleWidth(x, w), scaleHeight(y, h)); +} + +void NV3001BDisplay::drawRect(int x, int y, int w, int h) { + int x1 = scaleX(x); + int y1 = scaleY(y); + int sw = scaleWidth(x, w); + int sh = scaleHeight(y, h); + + fillPhysicalRect(x1, y1, sw, 1); + fillPhysicalRect(x1, y1 + sh - 1, sw, 1); + fillPhysicalRect(x1, y1, 1, sh); + fillPhysicalRect(x1 + sw - 1, y1, 1, sh); +} + +void NV3001BDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { + if (!bits || !is_on) return; + + int byte_width = (w + 7) / 8; + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + uint8_t byte = pgm_read_byte(bits + j * byte_width + i / 8); + if (byte & (0x80 >> (i & 7))) { + fillPhysicalRect(scaleX(x + i), scaleY(y + j), scaleWidth(x + i, 1), scaleHeight(y + j, 1)); + } + } + } +} + +uint16_t NV3001BDisplay::getTextWidth(const char* str) { + if (!str) return 0; + + uint16_t len = 0; + while (str[len] && str[len] != '\n' && str[len] != '\r') len++; + return (uint16_t)((len * 6 * textPixelScaleX(text_size)) / DISPLAY_SCALE_X); +} + +void NV3001BDisplay::endFrame() { +} diff --git a/src/helpers/ui/NV3001BDisplay.h b/src/helpers/ui/NV3001BDisplay.h new file mode 100644 index 00000000..98cdaae8 --- /dev/null +++ b/src/helpers/ui/NV3001BDisplay.h @@ -0,0 +1,68 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include + +#ifndef NV3001B_LOGICAL_WIDTH + #define NV3001B_LOGICAL_WIDTH 128 +#endif + +#ifndef NV3001B_LOGICAL_HEIGHT + #define NV3001B_LOGICAL_HEIGHT 64 +#endif + +#ifndef NV3001B_PANEL_WIDTH + #define NV3001B_PANEL_WIDTH 128 +#endif + +#ifndef NV3001B_PANEL_HEIGHT + #define NV3001B_PANEL_HEIGHT 220 +#endif + +#ifndef NV3001B_SPI_HOST + #define NV3001B_SPI_HOST HSPI +#endif + +class NV3001BDisplay : public DisplayDriver { + SPIClass spi; + RefCountedDigitalPin* periph_power; + bool is_on = false; + uint16_t color = 0xffff; + uint8_t text_size = 1; + int cursor_x = 0; + int cursor_y = 0; + + void writeCommand(uint8_t cmd); + void writeBytes(const uint8_t* data, size_t len); + void writeCommandData(uint8_t cmd, const uint8_t* data, size_t len); + void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void writeColor(uint16_t rgb, uint32_t count); + void fillPhysicalRect(int x, int y, int w, int h); + void initPanel(); + void drawChar(int x, int y, char ch); + +public: + NV3001BDisplay(RefCountedDigitalPin* power = nullptr) : + DisplayDriver(NV3001B_LOGICAL_WIDTH, NV3001B_LOGICAL_HEIGHT), spi(NV3001B_SPI_HOST), periph_power(power) { } + + bool begin(); + static const char* driverName() { return "NV3001B"; } + static uint16_t physicalWidth() { return NV3001B_PANEL_WIDTH; } + static uint16_t physicalHeight() { return NV3001B_PANEL_HEIGHT; } + + bool isOn() override { return is_on; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(Color bkg = DARK) override; + void setTextSize(int sz) override; + void setColor(Color c) override; + void setCursor(int x, int y) override; + void print(const char* str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; + void endFrame() override; +}; diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp new file mode 100644 index 00000000..37188e9e --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -0,0 +1,66 @@ +#include "HeltecRC32Board.h" + +void HeltecRC32Board::begin() { + ESP32Board::begin(); + + pinMode(PIN_ADC_CTRL, OUTPUT); + digitalWrite(PIN_ADC_CTRL, !ADC_CTRL_ENABLED); + +#ifdef SENSOR_RST_PIN + pinMode(SENSOR_RST_PIN, OUTPUT); + digitalWrite(SENSOR_RST_PIN, HIGH); +#endif + +#ifdef LED_POWER + pinMode(LED_POWER, OUTPUT); + digitalWrite(LED_POWER, LOW); +#endif + + periph_power.begin(); + vext_power.begin(); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1L << P_LORA_DIO_1)) { + startup_reason = BD_STARTUP_RX_PACKET; + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } +} + +void HeltecRC32Board::powerOff() { + enterDeepSleep(0); +} + +void HeltecRC32Board::onBeforeTransmit() { + digitalWrite(P_LORA_TX_LED, HIGH); +} + +void HeltecRC32Board::onAfterTransmit() { + digitalWrite(P_LORA_TX_LED, LOW); +} + +uint16_t HeltecRC32Board::getBattMilliVolts() { + analogReadResolution(12); + digitalWrite(PIN_ADC_CTRL, ADC_CTRL_ENABLED); + delay(10); + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogReadMilliVolts(PIN_VBAT_READ); + } + raw = raw / 8; + + return (adc_mult * raw); +} + +bool HeltecRC32Board::setAdcMultiplier(float multiplier) { + adc_mult = multiplier == 0.0f ? ADC_MULTIPLIER : multiplier; + return true; +} + +const char* HeltecRC32Board::getManufacturerName() const { + return "Heltec RC32"; +} diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h new file mode 100644 index 00000000..256cb964 --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#ifndef ADC_MULTIPLIER + #define ADC_MULTIPLIER 4.9f +#endif + +class HeltecRC32Board : public ESP32Board { +protected: + float adc_mult = ADC_MULTIPLIER; + +public: + RefCountedDigitalPin periph_power; + RefCountedDigitalPin vext_power; + + HeltecRC32Board() : + periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON), + vext_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + + void begin(); + void onBeforeTransmit() override; + void onAfterTransmit() override; + void powerOff() override; + uint16_t getBattMilliVolts() override; + bool setAdcMultiplier(float multiplier) override; + float getAdcMultiplier() const override { return adc_mult; } + const char* getManufacturerName() const override; +}; diff --git a/variants/heltec_rc32/pins_arduino.h b/variants/heltec_rc32/pins_arduino.h new file mode 100644 index 00000000..8ea3a0c1 --- /dev/null +++ b/variants/heltec_rc32/pins_arduino.h @@ -0,0 +1,60 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 18; + +static const uint8_t SS = 10; +static const uint8_t MOSI = 12; +static const uint8_t MISO = 13; +static const uint8_t SCK = 11; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t RST_LoRa = 9; +static const uint8_t BUSY_LoRa = 1; +static const uint8_t DIO1_LoRa = 14; + +#endif diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini new file mode 100644 index 00000000..9284650b --- /dev/null +++ b/variants/heltec_rc32/platformio.ini @@ -0,0 +1,346 @@ +[Heltec_RC32] +extends = esp32_base +board = heltec-rc32 +board_build.partitions = default_16MB.csv +build_flags = + ${esp32_base.build_flags} + ${sensor_base.build_flags} + -I variants/heltec_rc32 + -I src/helpers/ui + -D HELTEC_RC32 + -D USE_SX1262 + -D ESP32_CPU_FREQ=160 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=14 + -D P_LORA_NSS=10 + -D P_LORA_RESET=9 + -D P_LORA_BUSY=1 + -D P_LORA_SCLK=11 + -D P_LORA_MISO=13 + -D P_LORA_MOSI=12 + -D LORA_TX_POWER=22 + -D PIN_USER_BTN=0 + -D PIN_BOARD_SDA=21 + -D PIN_BOARD_SCL=18 + -D PIN_VEXT_EN=3 + -D PIN_VEXT_EN_ACTIVE=HIGH + -D SENSOR_POWER_CTRL_PIN=46 + -D SENSOR_POWER_ON=HIGH + -D SENSOR_RST_PIN=2 + -D P_LORA_TX_LED=47 + -D PIN_BUZZER=48 + -D PIN_TFT_SCL=17 + -D PIN_TFT_SDA=38 + -D PIN_TFT_CS=39 + -D PIN_TFT_DC=16 + -D PIN_TFT_RST=4 + -D PIN_TFT_EN=6 + -D PIN_TFT_EN_ACTIVE=LOW + -D PIN_TFT_BL=5 + -D PIN_TFT_BL_ACTIVE=HIGH + -D SPI_FREQUENCY=8000000 + -D PIN_GPS_TX=44 + -D PIN_GPS_RX=43 + -D PIN_GPS_EN=45 + -D PIN_GPS_EN_ACTIVE=HIGH + -D PIN_GPS_RESET=40 + -D PIN_GPS_RESET_ACTIVE=LOW + -D PIN_GPS_PPS=41 + -D GPS_BAUD_RATE=9600 + -D ENV_INCLUDE_GPS=1 + -D PIN_ADC_CTRL=15 + -D PIN_VBAT_READ=7 + -D ADC_CTRL_ENABLED=HIGH + -D ADC_MULTIPLIER=4.9 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/heltec_rc32> + + +lib_deps = + ${esp32_base.lib_deps} + ${sensor_base.lib_deps} + +[Heltec_RC32_with_display] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D HELTEC_RC32_WITH_DISPLAY + -D DISPLAY_CLASS=NV3001BDisplay +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + +lib_deps = + ${Heltec_RC32.lib_deps} + +[env:heltec_rc32_without_display_repeater] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_rc32_without_display_repeater_bridge_espnow] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_without_display_room_server] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_without_display_companion_radio_usb] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_without_display_companion_radio_ble] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_without_display_companion_radio_wifi] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=NullDisplayDriver + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_without_display_sensor] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D ADVERT_NAME='"Heltec RC32 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ENV_PIN_SDA=21 + -D ENV_PIN_SCL=18 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_sensor> +lib_deps = + ${Heltec_RC32.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_repeater] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:heltec_rc32_repeater_bridge_espnow] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_room_server] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_terminal_chat] +extends = Heltec_RC32 +build_flags = + ${Heltec_RC32.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${Heltec_RC32.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:heltec_rc32_companion_radio_usb] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_companion_radio_ble] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_companion_radio_wifi] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 + +[env:heltec_rc32_sensor] +extends = Heltec_RC32_with_display +build_flags = + ${Heltec_RC32_with_display.build_flags} + -D ADVERT_NAME='"Heltec RC32 Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ENV_PIN_SDA=21 + -D ENV_PIN_SCL=18 +build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + +<../examples/simple_sensor> +lib_deps = + ${Heltec_RC32_with_display.lib_deps} + ${esp32_ota.lib_deps} + +[env:heltec_rc32_kiss_modem] +extends = Heltec_RC32 +build_src_filter = ${Heltec_RC32.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/heltec_rc32/target.cpp b/variants/heltec_rc32/target.cpp new file mode 100644 index 00000000..6c88d2bb --- /dev/null +++ b/variants/heltec_rc32/target.cpp @@ -0,0 +1,45 @@ +#include +#include "target.h" + +HeltecRC32Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi(FSPI); + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN, &board.periph_power); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#if defined(P_LORA_SCLK) + return radio.std_init(&spi); +#else + return radio.std_init(); +#endif +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/heltec_rc32/target.h b/variants/heltec_rc32/target.h new file mode 100644 index 00000000..ae692598 --- /dev/null +++ b/variants/heltec_rc32/target.h @@ -0,0 +1,31 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include + +#ifdef DISPLAY_CLASS +#include +#ifdef HELTEC_RC32_WITH_DISPLAY +#include +#else +#include +#endif +#endif + +extern HeltecRC32Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/heltec_rc32/variant.h b/variants/heltec_rc32/variant.h new file mode 100644 index 00000000..d510ab9b --- /dev/null +++ b/variants/heltec_rc32/variant.h @@ -0,0 +1,51 @@ +#ifndef _VARIANT_HELTEC_RC32_ +#define _VARIANT_HELTEC_RC32_ + +#define BUTTON_PIN 0 + +#define HAS_GPS 1 +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#define PIN_GPS_EN 45 +#define GPS_EN_ACTIVE HIGH +#define PIN_GPS_RESET 40 +#define GPS_RESET_MODE LOW +#define PIN_GPS_PPS 41 + +#define I2C_SCL 18 +#define I2C_SDA 21 +#define SENSOR_INT_PIN 42 +#define SENSOR_RST_PIN 2 +#define SENSOR_POWER_CTRL_PIN 46 +#define SENSOR_POWER_ON HIGH +#define PERIPHERAL_WARMUP_MS 100 + +#define VEXT_ENABLE 3 +#define VEXT_ON_VALUE HIGH + +#define USE_SX1262 +#define LORA_SCK 11 +#define LORA_MISO 13 +#define LORA_MOSI 12 +#define LORA_CS 10 +#define LORA_DIO0 RADIOLIB_NC +#define LORA_DIO1 14 +#define LORA_RESET 9 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 1 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define BATTERY_PIN 7 +#define ADC_CHANNEL ADC_CHANNEL_6 +#define ADC_CTRL 15 +#define ADC_CTRL_ENABLED HIGH +#define ADC_MULTIPLIER 4.9 +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 + +#endif From 8ef369058df018e5c41d7fea6a438f6ec180254d Mon Sep 17 00:00:00 2001 From: jirogit Date: Thu, 9 Jul 2026 19:59:05 -0700 Subject: [PATCH 22/37] fix(RegionMap): distinguish clean EOF from partial read in load() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously load() returned true unconditionally on file-open success, masking truncated or corrupt /regions2 files. Additionally, the first field of each entry record (r->id) used the same success-chaining pattern as subsequent fields, so a clean EOF at a record boundary set success=false and would have been indistinguishable from real corruption once the return value was fixed. The r->id read is now split out: n==0 is a clean EOF (break, success retains its prior value from the header read), n!=sizeof(r->id) is a partial read or corruption (break, success=false). load() now returns success instead of an unconditional true, so its return value reflects the actual parse outcome. Companion fix to #2372, which fixed the same return-true hardcoding in save(). (#1891 originally reported this on load() but was closed when #2372 landed — that PR only touched save(); this addresses the load() side.) --- src/helpers/RegionMap.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 36c1d19d..4667e003 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -93,13 +93,15 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) { while (num_regions < MAX_REGION_ENTRIES) { auto r = ®ions[num_regions]; - success = file.read((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id); + int n = file.read((uint8_t *) &r->id, sizeof(r->id)); + if (n == 0) break; // clean EOF + success = (n == sizeof(r->id)); success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent); success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name); success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags); success = success && file.read(pad, sizeof(pad)) == sizeof(pad); - if (!success) break; // EOF + if (!success) break; // partial read or corruption if (r->id >= next_id) { // make sure next_id is valid next_id = r->id + 1; @@ -108,7 +110,7 @@ bool RegionMap::load(FILESYSTEM* _fs, const char* path) { } } file.close(); - return true; + return success; } } return false; // failed From 7e95c52972f237dd4b238efef4a5af13a2b72ea4 Mon Sep 17 00:00:00 2001 From: Will Dillon Date: Mon, 13 Jul 2026 05:44:55 -0700 Subject: [PATCH 23/37] Add StreamSensor allocation for GroupData Adds a small allocation for groupdata packets for the Meshcore firmware for the StreamSensor product. It was previously a LoRaWAN platform, and we've moved to Meshcore. Let me know when and how to demonstrate. --- docs/number_allocations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/number_allocations.md b/docs/number_allocations.md index 2ccf950f..b8cc22a2 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -18,6 +18,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | | 0120 | MCO Advanced | most.original.address@gmail.com — https://hdden.ru/MCOa/ | +| 0130 - 013F | StreamSensor | william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) From 1bdacb324710f9f8c14e9dc50e8c79aab36e663b Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 13 Jul 2026 23:27:50 +1000 Subject: [PATCH 24/37] * trying to get Heltec Tracker and Tracker V2 working --- src/helpers/ui/ST7735Display.cpp | 9 +++++++-- variants/heltec_tracker/platformio.ini | 2 +- variants/heltec_tracker_v2/platformio.ini | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index aad12d1b..cb5737e1 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -102,7 +102,12 @@ static TFT_eSPI lcd = TFT_eSPI(160, 80); static uint32_t curr_color; -#define _spi (&SPI1) +#if defined(HELTEC_LORA_V3) || defined(HELTEC_TRACKER_V2) + static SPIClass tft_spi(SPI3_HOST); + #define _spi (&tft_spi) +#else + #define _spi (&SPI1) +#endif SPISettings _spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0); @@ -434,7 +439,7 @@ bool ST7735Display::begin() { pinMode(PIN_TFT_LEDA_CTL, OUTPUT); #ifdef ESP_PLATFORM - _spi->begin(_clk,_miso,_mosi,-1); + _spi->begin(PIN_TFT_SCL, -1 /* _miso */, PIN_TFT_SDA /* _mosi */, -1); #else _spi->begin(); #endif diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 69293d70..07d2e987 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -47,7 +47,7 @@ build_src_filter = ${esp32_base.build_src_filter} lib_deps = ${esp32_base.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + bodmer/TFT_eSPI @ ^2.4.31 [env:Heltec_Wireless_Tracker_companion_radio_usb] extends = Heltec_tracker_base diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d57c2113..f1fe9dd0 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -55,7 +55,7 @@ build_src_filter = ${esp32_base.build_src_filter} lib_deps = ${esp32_base.lib_deps} ${sensor_base.lib_deps} - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + bodmer/TFT_eSPI @ ^2.4.31 [env:heltec_tracker_v2_repeater] extends = Heltec_tracker_v2 From d30d8ed7483d45f5e0faa53e9e56a846e67e5145 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 14 Jul 2026 15:46:36 +1000 Subject: [PATCH 25/37] * HSPI fix --- src/helpers/ui/ST7735Display.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index cb5737e1..cdd15aeb 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -103,7 +103,7 @@ static TFT_eSPI lcd = TFT_eSPI(160, 80); static uint32_t curr_color; #if defined(HELTEC_LORA_V3) || defined(HELTEC_TRACKER_V2) - static SPIClass tft_spi(SPI3_HOST); + static SPIClass tft_spi(HSPI); #define _spi (&tft_spi) #else #define _spi (&SPI1) From 4d0ba1516c539c86d2ef8a481b2acc943ea1e167 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 14 Jul 2026 17:23:13 +0800 Subject: [PATCH 26/37] Remove the watchdog feeding operation from each iteration of the loop to reduce power consumption. --- examples/simple_repeater/main.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 7532eb47..72bf8d57 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -160,9 +160,6 @@ void loop() { external_watchdog.loop(); #endif if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { -#ifdef HAS_EXTERNAL_WATCHDOG - external_watchdog.feed(); -#endif #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible #else From 15e259c57228bf6a2be92f820b2e312f954eecb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20=C5=A0karvada?= Date: Mon, 6 Jul 2026 01:33:01 +0200 Subject: [PATCH 27/37] Added MCU temp sensor to the companion Fixes #2896 --- examples/companion_radio/MyMesh.cpp | 10 ++++++++++ src/helpers/NRF52Board.cpp | 27 ++++++++++++++++++++------- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5fb9bf9d..a78fd29a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -657,6 +657,11 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim // query other sensors -- target specific sensors.querySensors(permissions, telemetry); + float temperature = board.getMCUTemperature(); + if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } + memcpy(reply, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') @@ -1640,6 +1645,11 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request telemetry.reset(); telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + float temperature = board.getMCUTemperature(); + if(!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN + telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature + } + // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index beee3212..46dbd329 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -280,16 +280,29 @@ void NRF52Board::sleep(uint32_t secs) { // Temperature from NRF52 MCU float NRF52Board::getMCUTemperature() { - NRF_TEMP->TASKS_START = 1; // Start temperature measurement - - long startTime = millis(); - while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us - if(millis() - startTime > 5) { // To wait 5ms just in case - NRF_TEMP->TASKS_STOP = 1; + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { + uint32_t err_code; + int32_t temp; + err_code = sd_temp_get(&temp); + if (err_code == NRF_SUCCESS) { + return (float)temp * 0.25f; + } else { return NAN; } + } else { + NRF_TEMP->TASKS_START = 1; // Start temperature measurement + + long startTime = millis(); + while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us + if(millis() - startTime > 5) { // To wait 5ms just in case + NRF_TEMP->TASKS_STOP = 1; + return NAN; + } + } } - + NRF_TEMP->EVENTS_DATARDY = 0; // Clear event flag int32_t temp = NRF_TEMP->TEMP; // In 0.25 *C units From 5a14f36e25777fac4aa4f4e63ff53786575819b5 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 15 Jul 2026 14:00:10 +1000 Subject: [PATCH 28/37] * ST7735 color/offset fix for Heltec Tracker V1 --- src/helpers/ui/ST7735Display.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index cdd15aeb..905ff503 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -255,6 +255,10 @@ static const uint8_t PROGMEM 0x00, 0x00, // XSTART = 0 0x00, 0x9F }, // XEND = 159 + Rcmd2invert[] = { // Tracker V1, part 2 + 1, // 1 command in list: + ST77XX_INVON, 0 }, // 1: Display is inverted + Rcmd3[] = { // 7735R init, part 3 (red or green tab) 2, // 2 commands in list: ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: @@ -447,8 +451,13 @@ bool ST7735Display::begin() { _height = 80; _width = 160; +#if defined(HELTEC_LORA_V3) // Tracker v1 + _colstart = 26; + _rowstart = 1; +#else _colstart = 24; _rowstart = 0; +#endif _resetAndInit(); @@ -474,6 +483,8 @@ void ST7735Display::_resetAndInit() { displayInit(Rcmd2green160x80); //uint8_t madctl = ST77XX_MADCTL_MY | ST77XX_MADCTL_MV |ST7735_MADCTL_BGR;//Adjust color to BGR //display.sendCommand(ST77XX_MADCTL, &madctl, 1); +#elif defined(HELTEC_LORA_V3) // Tracker v1 + displayInit(Rcmd2invert); // invert RGB #endif displayInit(Rcmd3); setRotation(DISPLAY_ROTATION); From a99f4a01600d7c03341d34ad1789173dd7230e93 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 15 Jul 2026 16:05:37 +1200 Subject: [PATCH 29/37] increase thinknode m7 max neighbours to 50 --- variants/thinknode_m7/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index ea421b07..af5d8ea0 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -44,7 +44,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' - -D MAX_NEIGHBOURS=8 + -D MAX_NEIGHBOURS=50 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = From 05d4bd2f7f363f2d69a776ed758838328eea2fa3 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 15 Jul 2026 17:45:24 +0800 Subject: [PATCH 30/37] Add a rotary encoder --- examples/companion_radio/ui-new/UITask.cpp | 10 ++ src/helpers/ui/RotaryInput.h | 18 +++ variants/heltec_rc32/HeltecRC32Board.cpp | 12 +- variants/heltec_rc32/HeltecRC32Board.h | 11 +- .../heltec_rc32/HeltecRC32RotaryInput.cpp | 122 ++++++++++++++++++ variants/heltec_rc32/HeltecRC32RotaryInput.h | 26 ++++ variants/heltec_rc32/platformio.ini | 5 +- variants/heltec_rc32/target.cpp | 5 +- variants/heltec_rc32/target.h | 6 + variants/heltec_rc32/variant.h | 3 - 10 files changed, 207 insertions(+), 11 deletions(-) create mode 100644 src/helpers/ui/RotaryInput.h create mode 100644 variants/heltec_rc32/HeltecRC32RotaryInput.cpp create mode 100644 variants/heltec_rc32/HeltecRC32RotaryInput.h diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 28591cc1..64e61bac 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -750,6 +750,16 @@ void UITask::loop() { c = handleTripleClick(KEY_SELECT); } #endif +#if defined(UI_HAS_ROTARY_INPUT) + if (c == 0) { + RotaryInputEvent ev = rotary_input.poll(); + if (ev == RotaryInputEvent::Next) { + c = checkDisplayOn(KEY_NEXT); + } else if (ev == RotaryInputEvent::Prev) { + c = checkDisplayOn(KEY_PREV); + } + } +#endif #if defined(PIN_USER_BTN_ANA) if (abs(millis() - _analogue_pin_read_millis) > 10) { int ev = analog_btn.check(); diff --git a/src/helpers/ui/RotaryInput.h b/src/helpers/ui/RotaryInput.h new file mode 100644 index 00000000..2dda695b --- /dev/null +++ b/src/helpers/ui/RotaryInput.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +enum class RotaryInputEvent : uint8_t { + None, + Next, + Prev, +}; + +class RotaryInput { +public: + virtual ~RotaryInput() = default; + + virtual bool begin() = 0; + virtual RotaryInputEvent poll() = 0; + virtual bool isReady() const = 0; +}; diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 37188e9e..11be483e 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -1,4 +1,14 @@ #include "HeltecRC32Board.h" +#if defined(UI_HAS_ROTARY_INPUT) +#include "HeltecRC32RotaryInput.h" +#endif + +#if defined(UI_HAS_ROTARY_INPUT) +RotaryInput& HeltecRC32Board::rotaryInput() { + static HeltecRC32RotaryInput input(&periph_power); + return input; +} +#endif void HeltecRC32Board::begin() { ESP32Board::begin(); @@ -17,7 +27,7 @@ void HeltecRC32Board::begin() { #endif periph_power.begin(); - vext_power.begin(); + periph_power.claim(); esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h index 256cb964..5f3a0adf 100644 --- a/variants/heltec_rc32/HeltecRC32Board.h +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -4,6 +4,9 @@ #include #include #include +#if defined(UI_HAS_ROTARY_INPUT) +#include +#endif #ifndef ADC_MULTIPLIER #define ADC_MULTIPLIER 4.9f @@ -15,13 +18,13 @@ protected: public: RefCountedDigitalPin periph_power; - RefCountedDigitalPin vext_power; - HeltecRC32Board() : - periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON), - vext_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } + HeltecRC32Board() : periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON){} void begin(); +#if defined(UI_HAS_ROTARY_INPUT) + RotaryInput& rotaryInput(); +#endif void onBeforeTransmit() override; void onAfterTransmit() override; void powerOff() override; diff --git a/variants/heltec_rc32/HeltecRC32RotaryInput.cpp b/variants/heltec_rc32/HeltecRC32RotaryInput.cpp new file mode 100644 index 00000000..e8b282ff --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32RotaryInput.cpp @@ -0,0 +1,122 @@ +#include "HeltecRC32RotaryInput.h" + +#include + +namespace { +constexpr uint8_t TCA6408_ADDR = 0x20; +constexpr uint8_t TCA6408_INPUT_REG = 0x00; +constexpr uint8_t TCA6408_POLARITY_REG = 0x02; +constexpr uint8_t TCA6408_CONFIG_REG = 0x03; +constexpr uint8_t TCA6408_ROTARY_A_MASK = 0x01; +constexpr uint8_t TCA6408_ROTARY_B_MASK = 0x02; +constexpr uint8_t TCA6408_ROTARY_MASK = TCA6408_ROTARY_A_MASK | TCA6408_ROTARY_B_MASK; +constexpr uint32_t TCA6408_DEBOUNCE_MS = 5; +} + +bool HeltecRC32RotaryInput::begin() { + initialized = true; + ready = false; + input_state = TCA6408_ROTARY_MASK; + active_low_phase = false; + + if (periph_power && !power_claimed) { + periph_power->claim(); + power_claimed = true; + delay(12); + } + + if (!writeRegister(TCA6408_POLARITY_REG, 0x00) || !writeRegister(TCA6408_CONFIG_REG, 0xFF)) { + return false; + } + + uint8_t state = 0; + if (!readInput(state)) { + return false; + } + + input_state = state & TCA6408_ROTARY_MASK; + ready = true; + return true; +} + +RotaryInputEvent HeltecRC32RotaryInput::poll() { + if (!initialized) { + begin(); + return RotaryInputEvent::None; + } + + if (!ready) { + return RotaryInputEvent::None; + } + + uint8_t new_state = 0; + if (!readInput(new_state)) { + return RotaryInputEvent::None; + } + + new_state &= TCA6408_ROTARY_MASK; + RotaryInputEvent event = handleTransition(new_state); + input_state = new_state; + return event; +} + +bool HeltecRC32RotaryInput::writeRegister(uint8_t reg, uint8_t value) { + Wire.beginTransmission(TCA6408_ADDR); + Wire.write(reg); + Wire.write(value); + return Wire.endTransmission() == 0; +} + +bool HeltecRC32RotaryInput::readInput(uint8_t& value) { + Wire.beginTransmission(TCA6408_ADDR); + Wire.write(TCA6408_INPUT_REG); + if (Wire.endTransmission(false) != 0) { + return false; + } + if (Wire.requestFrom(TCA6408_ADDR, static_cast(1)) != 1) { + return false; + } + + value = Wire.read(); + return true; +} + +RotaryInputEvent HeltecRC32RotaryInput::handleTransition(uint8_t newState) { + uint8_t changed = (input_state ^ newState) & TCA6408_ROTARY_MASK; + RotaryInputEvent event = RotaryInputEvent::None; + bool a_low = (newState & TCA6408_ROTARY_A_MASK) == 0; + bool b_low = (newState & TCA6408_ROTARY_B_MASK) == 0; + + if (!a_low && !b_low) { + active_low_phase = false; + } + + if (!active_low_phase && (changed & TCA6408_ROTARY_A_MASK) && a_low && !b_low) { + event = RotaryInputEvent::Prev; + active_low_phase = true; + } else if (!active_low_phase && (changed & TCA6408_ROTARY_B_MASK) && b_low && !a_low) { + event = RotaryInputEvent::Next; + active_low_phase = true; + } + + if (event == RotaryInputEvent::None && !active_low_phase && (changed & TCA6408_ROTARY_A_MASK)) { + bool a_rising = (newState & TCA6408_ROTARY_A_MASK) != 0; + if (a_rising && b_low) { + event = RotaryInputEvent::Prev; + } + } + + if (event == RotaryInputEvent::None && !active_low_phase && (changed & TCA6408_ROTARY_B_MASK)) { + bool b_rising = (newState & TCA6408_ROTARY_B_MASK) != 0; + if (b_rising && a_low) { + event = RotaryInputEvent::Next; + } + } + + if (event == RotaryInputEvent::None || (millis() - last_event_ms) < TCA6408_DEBOUNCE_MS) { + return RotaryInputEvent::None; + } + + last_event_ms = millis(); + return event; +} diff --git a/variants/heltec_rc32/HeltecRC32RotaryInput.h b/variants/heltec_rc32/HeltecRC32RotaryInput.h new file mode 100644 index 00000000..36557dca --- /dev/null +++ b/variants/heltec_rc32/HeltecRC32RotaryInput.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +class HeltecRC32RotaryInput : public RotaryInput { +public: + explicit HeltecRC32RotaryInput(RefCountedDigitalPin* periphPower = nullptr) : periph_power(periphPower) { } + + bool begin() override; + RotaryInputEvent poll() override; + bool isReady() const override { return ready; } + +private: + bool writeRegister(uint8_t reg, uint8_t value); + bool readInput(uint8_t& value); + RotaryInputEvent handleTransition(uint8_t newState); + + uint8_t input_state = 0x03; + uint32_t last_event_ms = 0; + bool ready = false; + bool initialized = false; + bool power_claimed = false; + bool active_low_phase = false; + RefCountedDigitalPin* periph_power = nullptr; +}; diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 9284650b..545472d8 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -23,8 +23,6 @@ build_flags = -D PIN_USER_BTN=0 -D PIN_BOARD_SDA=21 -D PIN_BOARD_SCL=18 - -D PIN_VEXT_EN=3 - -D PIN_VEXT_EN_ACTIVE=HIGH -D SENSOR_POWER_CTRL_PIN=46 -D SENSOR_POWER_ON=HIGH -D SENSOR_RST_PIN=2 @@ -270,6 +268,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 build_src_filter = ${Heltec_RC32_with_display.build_src_filter} @@ -287,6 +286,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 @@ -308,6 +308,7 @@ extends = Heltec_RC32_with_display build_flags = ${Heltec_RC32_with_display.build_flags} -I examples/companion_radio/ui-new + -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 diff --git a/variants/heltec_rc32/target.cpp b/variants/heltec_rc32/target.cpp index 6c88d2bb..386fc559 100644 --- a/variants/heltec_rc32/target.cpp +++ b/variants/heltec_rc32/target.cpp @@ -17,7 +17,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include - MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN, &board.periph_power); + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, PIN_GPS_RESET, PIN_GPS_EN); EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); #else EnvironmentSensorManager sensors; @@ -26,6 +26,9 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#if defined(UI_HAS_ROTARY_INPUT) + RotaryInput& rotary_input = board.rotaryInput(); +#endif #endif bool radio_init() { diff --git a/variants/heltec_rc32/target.h b/variants/heltec_rc32/target.h index ae692598..04cb6d94 100644 --- a/variants/heltec_rc32/target.h +++ b/variants/heltec_rc32/target.h @@ -10,6 +10,9 @@ #ifdef DISPLAY_CLASS #include +#if defined(UI_HAS_ROTARY_INPUT) +#include +#endif #ifdef HELTEC_RC32_WITH_DISPLAY #include #else @@ -25,6 +28,9 @@ extern EnvironmentSensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; extern MomentaryButton user_btn; +#if defined(UI_HAS_ROTARY_INPUT) + extern RotaryInput& rotary_input; +#endif #endif bool radio_init(); diff --git a/variants/heltec_rc32/variant.h b/variants/heltec_rc32/variant.h index d510ab9b..0546fbc4 100644 --- a/variants/heltec_rc32/variant.h +++ b/variants/heltec_rc32/variant.h @@ -22,9 +22,6 @@ #define SENSOR_POWER_ON HIGH #define PERIPHERAL_WARMUP_MS 100 -#define VEXT_ENABLE 3 -#define VEXT_ON_VALUE HIGH - #define USE_SX1262 #define LORA_SCK 11 #define LORA_MISO 13 From 3469e924586151f2209dc5a2d5a353e8959e0081 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 16 Jul 2026 11:37:12 +0800 Subject: [PATCH 31/37] Enable patch reception --- variants/heltec_rc32/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 545472d8..d535fb77 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -55,6 +55,7 @@ build_flags = -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX build_src_filter = ${esp32_base.build_src_filter} +<../variants/heltec_rc32> + From a472cd7956cb5cb31f650cdb2f4f04d4451ea259 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 16 Jul 2026 15:14:17 +0800 Subject: [PATCH 32/37] Delete duplicate pin definitions --- variants/heltec_rc32/variant.h | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/variants/heltec_rc32/variant.h b/variants/heltec_rc32/variant.h index 0546fbc4..5f7b8d81 100644 --- a/variants/heltec_rc32/variant.h +++ b/variants/heltec_rc32/variant.h @@ -3,26 +3,11 @@ #define BUTTON_PIN 0 -#define HAS_GPS 1 -#undef GPS_RX_PIN -#undef GPS_TX_PIN -#define GPS_RX_PIN 44 -#define GPS_TX_PIN 43 -#define PIN_GPS_EN 45 -#define GPS_EN_ACTIVE HIGH -#define PIN_GPS_RESET 40 -#define GPS_RESET_MODE LOW -#define PIN_GPS_PPS 41 - #define I2C_SCL 18 #define I2C_SDA 21 #define SENSOR_INT_PIN 42 -#define SENSOR_RST_PIN 2 -#define SENSOR_POWER_CTRL_PIN 46 -#define SENSOR_POWER_ON HIGH #define PERIPHERAL_WARMUP_MS 100 -#define USE_SX1262 #define LORA_SCK 11 #define LORA_MISO 13 #define LORA_MOSI 12 @@ -35,14 +20,10 @@ #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY 1 #define SX126X_RESET LORA_RESET -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define BATTERY_PIN 7 #define ADC_CHANNEL ADC_CHANNEL_6 #define ADC_CTRL 15 -#define ADC_CTRL_ENABLED HIGH -#define ADC_MULTIPLIER 4.9 #define ADC_ATTENUATION ADC_ATTEN_DB_2_5 #endif From 61a8821d3c6f950738c3bd35bd13b0dfbefc6af0 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 16 Jul 2026 17:45:00 +0800 Subject: [PATCH 33/37] Optimize battery voltage reading --- variants/heltec_rc32/HeltecRC32Board.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 11be483e..0517f5ff 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -55,6 +55,7 @@ void HeltecRC32Board::onAfterTransmit() { uint16_t HeltecRC32Board::getBattMilliVolts() { analogReadResolution(12); + analogSetAttenuation(ADC_2_5db); digitalWrite(PIN_ADC_CTRL, ADC_CTRL_ENABLED); delay(10); uint32_t raw = 0; @@ -62,6 +63,7 @@ uint16_t HeltecRC32Board::getBattMilliVolts() { raw += analogReadMilliVolts(PIN_VBAT_READ); } raw = raw / 8; + digitalWrite(PIN_ADC_CTRL, !ADC_CTRL_ENABLED); return (adc_mult * raw); } From b26f53a263c41b1587065a5cfcb9049de3999184 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 17 Jul 2026 15:35:07 +0800 Subject: [PATCH 34/37] Avoid waking display from rotary input --- examples/companion_radio/ui-new/UITask.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 64e61bac..5a23324c 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -751,12 +751,12 @@ void UITask::loop() { } #endif #if defined(UI_HAS_ROTARY_INPUT) - if (c == 0) { - RotaryInputEvent ev = rotary_input.poll(); - if (ev == RotaryInputEvent::Next) { - c = checkDisplayOn(KEY_NEXT); - } else if (ev == RotaryInputEvent::Prev) { - c = checkDisplayOn(KEY_PREV); + RotaryInputEvent rotaryEv = rotary_input.poll(); + if (c == 0 && _display != NULL && _display->isOn()) { + if (rotaryEv == RotaryInputEvent::Next) { + c = KEY_NEXT; + } else if (rotaryEv == RotaryInputEvent::Prev) { + c = KEY_PREV; } } #endif From fd7b35f457a580c1e43724135b68a0c1b6d801ba Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 17 Jul 2026 15:50:25 +0800 Subject: [PATCH 35/37] Move the rotation input to target.cpp --- variants/heltec_rc32/HeltecRC32Board.cpp | 10 ---------- variants/heltec_rc32/HeltecRC32Board.h | 6 ------ variants/heltec_rc32/target.cpp | 6 +++++- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 0517f5ff..59920d23 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -1,14 +1,4 @@ #include "HeltecRC32Board.h" -#if defined(UI_HAS_ROTARY_INPUT) -#include "HeltecRC32RotaryInput.h" -#endif - -#if defined(UI_HAS_ROTARY_INPUT) -RotaryInput& HeltecRC32Board::rotaryInput() { - static HeltecRC32RotaryInput input(&periph_power); - return input; -} -#endif void HeltecRC32Board::begin() { ESP32Board::begin(); diff --git a/variants/heltec_rc32/HeltecRC32Board.h b/variants/heltec_rc32/HeltecRC32Board.h index 5f3a0adf..5b2093c8 100644 --- a/variants/heltec_rc32/HeltecRC32Board.h +++ b/variants/heltec_rc32/HeltecRC32Board.h @@ -4,9 +4,6 @@ #include #include #include -#if defined(UI_HAS_ROTARY_INPUT) -#include -#endif #ifndef ADC_MULTIPLIER #define ADC_MULTIPLIER 4.9f @@ -22,9 +19,6 @@ public: HeltecRC32Board() : periph_power(SENSOR_POWER_CTRL_PIN, SENSOR_POWER_ON){} void begin(); -#if defined(UI_HAS_ROTARY_INPUT) - RotaryInput& rotaryInput(); -#endif void onBeforeTransmit() override; void onAfterTransmit() override; void powerOff() override; diff --git a/variants/heltec_rc32/target.cpp b/variants/heltec_rc32/target.cpp index 386fc559..cc7e4deb 100644 --- a/variants/heltec_rc32/target.cpp +++ b/variants/heltec_rc32/target.cpp @@ -1,5 +1,8 @@ #include #include "target.h" +#if defined(UI_HAS_ROTARY_INPUT) +#include "HeltecRC32RotaryInput.h" +#endif HeltecRC32Board board; @@ -27,7 +30,8 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #if defined(UI_HAS_ROTARY_INPUT) - RotaryInput& rotary_input = board.rotaryInput(); + static HeltecRC32RotaryInput rotaryInputImpl(&board.periph_power); + RotaryInput& rotary_input = rotaryInputImpl; #endif #endif From 84d1e24338138327394a447b7faad27934e4c314 Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 17 Jul 2026 07:36:39 -0400 Subject: [PATCH 36/37] nrf52_variants: override shutdownPeripherals instead of powerOff for TechoLite, TInpulse and TechoBoard (protect the gate) --- variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h | 4 ++-- variants/lilygo_techo_card/TechoCardBoard.cpp | 4 ++-- variants/lilygo_techo_card/TechoCardBoard.h | 2 +- variants/lilygo_techo_lite/TechoBoard.h | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h index 5fafcad8..30bbc3a5 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -47,9 +47,9 @@ public: return "LilyGo T-Impulse-Plus"; } - void powerOff() override { + void shutdownPeripherals() override { // power off system - NRF52Board::powerOff(); + NRF52Board::shutdownPeripherals(); // turn off 3.3v digitalWrite(RT9080_EN, LOW); diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index 8143587d..8a3a54b1 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -87,11 +87,11 @@ void TechoCardBoard::turnOffLeds() { } } -void TechoCardBoard::powerOff() { +void TechoCardBoard::shutdownPeripherals() { nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); turnOffLeds(); digitalWrite(PIN_PWR_EN, LOW); - NRF52Board::powerOff(); + NRF52Board::shutdownPeripherals(); } #endif diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h index 8a2913a6..a2ee3ea1 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -28,7 +28,7 @@ public: return "LilyGo T-Echo Card"; } - void powerOff() override; + void shutdownPeripherals() override; void toggleTorch(); void turnOffLeds(); diff --git a/variants/lilygo_techo_lite/TechoBoard.h b/variants/lilygo_techo_lite/TechoBoard.h index f4c16016..1e5651e7 100644 --- a/variants/lilygo_techo_lite/TechoBoard.h +++ b/variants/lilygo_techo_lite/TechoBoard.h @@ -21,8 +21,8 @@ public: return "LilyGo T-Echo Lite"; } - void powerOff() override { - NRF52Board::powerOff(); + void shutdownPeripherals() override { + NRF52Board::shutdownPeripherals(); digitalWrite(PIN_VBAT_MEAS_EN, LOW); #ifdef LED_RED From c644720ea7f969d45731e7cd018f1b57d9186a08 Mon Sep 17 00:00:00 2001 From: Florent Date: Fri, 17 Jul 2026 08:29:31 -0400 Subject: [PATCH 37/37] uitask: screen and radio poweroff moved to board --- examples/companion_radio/ui-new/UITask.cpp | 3 --- examples/companion_radio/ui-orig/UITask.cpp | 2 -- examples/companion_radio/ui-tiny/UITask.cpp | 2 -- examples/simple_repeater/UITask.cpp | 2 -- 4 files changed, 9 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 28591cc1..403ea463 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -697,9 +697,6 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - // still necessary until all boards are refactored to use poweroff - _display->turnOff(); - radio_driver.powerOff(); // Power off board including radio, display, GPS and components _board->powerOff(); } diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 34a7342b..b48f6412 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -307,8 +307,6 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - _display->turnOff(); - radio_driver.powerOff(); // Power off board including radio, display, GPS and components _board->powerOff(); } diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index a6cbe9de..452c02d4 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -566,8 +566,6 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - _display->turnOff(); - radio_driver.powerOff(); // Power off board including radio, display, GPS and components _board->powerOff(); } diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 17c708e3..6751aad6 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -146,8 +146,6 @@ void UITask::loop() { digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff #endif if (millis() > _powering_off_at) { - _display->turnOff(); - radio_driver.powerOff(); _board->powerOff(); // should not return } }