From a904cdbf9e30c6c844a42c92eb8c0fbbd1abbbc7 Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Mon, 30 Mar 2026 02:01:05 +0200 Subject: [PATCH 01/11] Add LilyGo T-ETH-Elite support (SX1262 and SX1276) --- src/helpers/esp32/TEthEliteBoard.cpp | 162 ++++++++++++++++ src/helpers/esp32/TEthEliteBoard.h | 7 + src/helpers/esp32/TEthEliteBoard_SX1262.h | 102 ++++++++++ src/helpers/esp32/TEthEliteBoard_SX1276.h | 102 ++++++++++ .../lilygo_t_eth_elite_sx1262/platformio.ini | 177 ++++++++++++++++++ variants/lilygo_t_eth_elite_sx1262/target.cpp | 53 ++++++ variants/lilygo_t_eth_elite_sx1262/target.h | 30 +++ .../lilygo_t_eth_elite_sx1276/platformio.ini | 176 +++++++++++++++++ variants/lilygo_t_eth_elite_sx1276/target.cpp | 56 ++++++ variants/lilygo_t_eth_elite_sx1276/target.h | 30 +++ 10 files changed, 895 insertions(+) create mode 100644 src/helpers/esp32/TEthEliteBoard.cpp create mode 100644 src/helpers/esp32/TEthEliteBoard.h create mode 100644 src/helpers/esp32/TEthEliteBoard_SX1262.h create mode 100644 src/helpers/esp32/TEthEliteBoard_SX1276.h create mode 100644 variants/lilygo_t_eth_elite_sx1262/platformio.ini create mode 100644 variants/lilygo_t_eth_elite_sx1262/target.cpp create mode 100644 variants/lilygo_t_eth_elite_sx1262/target.h create mode 100644 variants/lilygo_t_eth_elite_sx1276/platformio.ini create mode 100644 variants/lilygo_t_eth_elite_sx1276/target.cpp create mode 100644 variants/lilygo_t_eth_elite_sx1276/target.h diff --git a/src/helpers/esp32/TEthEliteBoard.cpp b/src/helpers/esp32/TEthEliteBoard.cpp new file mode 100644 index 000000000..42c669a52 --- /dev/null +++ b/src/helpers/esp32/TEthEliteBoard.cpp @@ -0,0 +1,162 @@ +#if defined(T_ETH_ELITE_SX1262) || defined(T_ETH_ELITE_SX1276) + +#include +#include +#include "driver/spi_common.h" +#include "driver/gpio.h" +#include +#include "TEthEliteBoard.h" +#include "target.h" +#include "helpers/ui/MomentaryButton.h" + +extern MomentaryButton user_btn; + +uint32_t deviceOnline = 0x00; +static SPIClass spi_eth(FSPI); +static ETHClass2 ETH; + +void TEthEliteBoard::begin() { + ESP32Board::begin(); + user_btn.begin(); + Wire1.begin(PIN_BOARD_SDA1, PIN_BOARD_SCL1); + #if ENV_INCLUDE_GPS + Serial1.begin(9600, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); + #endif + +#ifdef USE_ETHERNET + WiFi.mode(WIFI_OFF); +#endif + startNetwork(); + + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { +#if defined(T_ETH_ELITE_SX1262) + 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); +#elif defined(T_ETH_ELITE_SX1276) + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1 << P_LORA_DIO_0)) { + 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_0); +#endif + } +} + +#ifdef MESH_DEBUG +void TEthEliteBoard::scanDevices(TwoWire *w) +{ + uint8_t err, addr; + int nDevices = 0; + + Serial.println("Scanning I2C for Devices"); + for (addr = 1; addr < 127; addr++) { + w->beginTransmission(addr); delay(2); + err = w->endTransmission(); + if (err == 0) { + nDevices++; + switch (addr) { + case 0x77: + case 0x76: + Serial.println("\tFound BME280 Sensor"); + deviceOnline |= BME280_ONLINE; + break; + case 0x34: + Serial.println("\tFound AXP192/AXP2101 PMU"); + deviceOnline |= POWERMANAGE_ONLINE; + break; + case 0x3C: + Serial.println("\tFound SSD1306/SH1106 display"); + deviceOnline |= DISPLAY_ONLINE; + break; + case 0x51: + Serial.println("\tFound PCF8563 RTC"); + deviceOnline |= PCF8563_ONLINE; + break; + case 0x1C: + Serial.println("\tFound QMC6310 MAG Sensor"); + deviceOnline |= QMC6310_ONLINE; + break; + default: + Serial.printf("\tI2C device found at address 0x%02X\n", addr); + break; + } + } else if (err == 4) { + Serial.printf("Unknown error at address 0x%02X\n", addr); + } + } + if (nDevices == 0) + Serial.println("No I2C devices found"); + + Serial.println("Scan complete."); + Serial.printf("GPS RX pin: %d, GPS TX pin: %d\n", PIN_GPS_RX, PIN_GPS_TX); +} + +void TEthEliteBoard::printPMU() +{ + Serial.print("isCharging:"); Serial.println(PMU->isCharging() ? "YES" : "NO"); + Serial.print("isDischarge:"); Serial.println(PMU->isDischarge() ? "YES" : "NO"); + Serial.print("isVbusIn:"); Serial.println(PMU->isVbusIn() ? "YES" : "NO"); + Serial.print("getBattVoltage:"); Serial.print(PMU->getBattVoltage()); Serial.println("mV"); + Serial.print("getVbusVoltage:"); Serial.print(PMU->getVbusVoltage()); Serial.println("mV"); + Serial.print("getSystemVoltage:"); Serial.print(PMU->getSystemVoltage()); Serial.println("mV"); + if (PMU->isBatteryConnect()) { + Serial.print("getBatteryPercent:"); Serial.print(PMU->getBatteryPercent()); Serial.println("%"); + } +} +#endif + +void TEthEliteBoard::startNetwork() { +#ifdef USE_ETHERNET + startEthernet(); +#elif defined(WIFI_SSID) + startWifi(); +#endif +} + +void TEthEliteBoard::startEthernet() { + pinMode(ETH_CS, OUTPUT); + digitalWrite(ETH_CS, HIGH); + + ETH.begin(ETH_PHY_W5500, ETH_ADDR, ETH_CS, ETH_INT, -1, SPI2_HOST, ETH_SCLK, ETH_MISO, ETH_MOSI); + delay(100); + +#ifdef ETH_STATIC_IP + IPAddress ip(ETH_STATIC_IP); + IPAddress gw(ETH_GATEWAY); + IPAddress mask(ETH_SUBNET); + IPAddress dns(ETH_DNS); + ETH.config(ip, gw, mask, dns); +#endif + + unsigned long t0 = millis(); + while (!ETH.linkUp() && millis() - t0 < 5000) { + delay(100); + } + + t0 = millis(); + while (ETH.localIP() == IPAddress(0, 0, 0, 0) && millis() - t0 < 5000) { + delay(100); + } + + if (ETH.localIP() == IPAddress(0, 0, 0, 0)) { + Serial.println("DHCP timeout, using fallback IP"); + ETH.config(IPAddress(192, 168, 4, 2), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); + } + + uint8_t mac[6]; + ETH.macAddress(mac); + Serial.printf("ETH MAC %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + Serial.print("ETH IP "); Serial.println(ETH.localIP()); + Serial.println(ETH.linkUp() ? "ETH LINK UP" : "ETH LINK DOWN"); +} + +#endif diff --git a/src/helpers/esp32/TEthEliteBoard.h b/src/helpers/esp32/TEthEliteBoard.h new file mode 100644 index 000000000..d009be3f4 --- /dev/null +++ b/src/helpers/esp32/TEthEliteBoard.h @@ -0,0 +1,7 @@ +#pragma once + +#if defined(T_ETH_ELITE_SX1262) + #include "TEthEliteBoard_SX1262.h" +#elif defined(T_ETH_ELITE_SX1276) + #include "TEthEliteBoard_SX1276.h" +#endif diff --git a/src/helpers/esp32/TEthEliteBoard_SX1262.h b/src/helpers/esp32/TEthEliteBoard_SX1262.h new file mode 100644 index 000000000..2105aa2dc --- /dev/null +++ b/src/helpers/esp32/TEthEliteBoard_SX1262.h @@ -0,0 +1,102 @@ +#pragma once + +#if defined(T_ETH_ELITE_SX1262) + +// Define pin mappings BEFORE including ESP32Board.h so sleep() can use P_LORA_DIO_1 + +// LoRa SX1262 pins (T-ETH Elite LoRa Shield) +#define P_LORA_NSS 40 // CS +#define P_LORA_RESET 46 // RESET +#define P_LORA_BUSY 16 // BUSY +#define P_LORA_DIO_0 -1 // NC +#define P_LORA_DIO_1 8 // IRQ +#define P_LORA_TX_LED 38 +#define P_LORA_SCLK 10 +#define P_LORA_MISO 9 +#define P_LORA_MOSI 11 + +// ETH W5500 pins (T-ETH Elite main board) +#define ETH_MISO 47 +#define ETH_MOSI 21 +#define ETH_SCLK 48 +#define ETH_CS 45 +#define ETH_INT 14 +#define ETH_RST -1 +#define ETH_ADDR 1 + +// I2C bus (Wire1) +#define PIN_BOARD_SDA1 17 +#define PIN_BOARD_SCL1 18 + +// GPS pins (LoRa Shield) +#define PIN_GPS_RX 39 +#define PIN_GPS_TX 42 +#define PIN_GPS_EN -1 + +// Analog button +#define PIN_USER_BTN_ANA 7 + +// Include headers AFTER pin definitions so ESP32Board::sleep() can use P_LORA_DIO_1 +#include +#include +#include "XPowersLib.h" +#include "helpers/ESP32Board.h" +#include + +class TEthEliteBoard : public ESP32Board { + XPowersLibInterface *PMU = NULL; + + enum { + POWERMANAGE_ONLINE = _BV(0), + DISPLAY_ONLINE = _BV(1), + RADIO_ONLINE = _BV(2), + GPS_ONLINE = _BV(3), + PSRAM_ONLINE = _BV(4), + SDCARD_ONLINE = _BV(5), + AXDL345_ONLINE = _BV(6), + BME280_ONLINE = _BV(7), + BMP280_ONLINE = _BV(8), + BME680_ONLINE = _BV(9), + QMC6310_ONLINE = _BV(10), + QMI8658_ONLINE = _BV(11), + PCF8563_ONLINE = _BV(12), + OSC32768_ONLINE = _BV(13), + }; + +public: + +#ifdef MESH_DEBUG + void printPMU(); + void scanDevices(TwoWire *w); +#endif + void begin(); + void startNetwork(); + void startEthernet(); + void startWifi(); + + void 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); + + 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(); + } + + uint16_t getBattMilliVolts() { return 0; } + + const char* getManufacturerName() const { return "LilyGo T-ETH-Elite"; } +}; + +#endif diff --git a/src/helpers/esp32/TEthEliteBoard_SX1276.h b/src/helpers/esp32/TEthEliteBoard_SX1276.h new file mode 100644 index 000000000..a18d76258 --- /dev/null +++ b/src/helpers/esp32/TEthEliteBoard_SX1276.h @@ -0,0 +1,102 @@ +#pragma once + +#if defined(T_ETH_ELITE_SX1276) + +// Define pin mappings BEFORE including ESP32Board.h so sleep() can use P_LORA_DIO_0 + +// LoRa SX1276 pins (T-ETH Elite LoRa Shield) +#define P_LORA_DIO_0 8 // IRQ (DIO0 on SX1276) +#define P_LORA_DIO_1 16 // DIO1 +#define P_LORA_NSS 40 // CS +#define P_LORA_RESET 46 // RESET +#define P_LORA_BUSY -1 // SX1276 has no BUSY pin +#define P_LORA_TX_LED 38 +#define P_LORA_SCLK 10 +#define P_LORA_MISO 9 +#define P_LORA_MOSI 11 + +// ETH W5500 pins (T-ETH Elite main board) +#define ETH_MISO 47 +#define ETH_MOSI 21 +#define ETH_SCLK 48 +#define ETH_CS 45 +#define ETH_INT 14 +#define ETH_RST -1 +#define ETH_ADDR 1 + +// I2C bus (Wire1) +#define PIN_BOARD_SDA1 17 +#define PIN_BOARD_SCL1 18 + +// GPS pins (LoRa Shield) +#define PIN_GPS_RX 39 +#define PIN_GPS_TX 42 +#define PIN_GPS_EN -1 + +// Analog button +#define PIN_USER_BTN_ANA 7 + +// Include headers AFTER pin definitions so ESP32Board::sleep() can use P_LORA_DIO_0 +#include +#include +#include "XPowersLib.h" +#include "helpers/ESP32Board.h" +#include + +class TEthEliteBoard : public ESP32Board { + XPowersLibInterface *PMU = NULL; + + enum { + POWERMANAGE_ONLINE = _BV(0), + DISPLAY_ONLINE = _BV(1), + RADIO_ONLINE = _BV(2), + GPS_ONLINE = _BV(3), + PSRAM_ONLINE = _BV(4), + SDCARD_ONLINE = _BV(5), + AXDL345_ONLINE = _BV(6), + BME280_ONLINE = _BV(7), + BMP280_ONLINE = _BV(8), + BME680_ONLINE = _BV(9), + QMC6310_ONLINE = _BV(10), + QMI8658_ONLINE = _BV(11), + PCF8563_ONLINE = _BV(12), + OSC32768_ONLINE = _BV(13), + }; + +public: + +#ifdef MESH_DEBUG + void printPMU(); + void scanDevices(TwoWire *w); +#endif + void begin(); + void startNetwork(); + void startEthernet(); + void startWifi(); + + void 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_0, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_0); + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_0), ESP_EXT1_WAKEUP_ANY_HIGH); + } else { + esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_0) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + esp_deep_sleep_start(); + } + + uint16_t getBattMilliVolts() { return 0; } + + const char* getManufacturerName() const { return "LilyGo T-ETH-Elite"; } +}; + +#endif diff --git a/variants/lilygo_t_eth_elite_sx1262/platformio.ini b/variants/lilygo_t_eth_elite_sx1262/platformio.ini new file mode 100644 index 000000000..f3f0c2cbd --- /dev/null +++ b/variants/lilygo_t_eth_elite_sx1262/platformio.ini @@ -0,0 +1,177 @@ +[LilyGo_T_ETH_Elite_SX1262] +extends = esp32_base +board = t_eth_elite +upload_protocol = esptool +board_build.partitions = default_16MB.csv +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +build_flags = + ${esp32_base.build_flags} + -I variants/lilygo_t_eth_elite_sx1262 + -D T_ETH_ELITE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D LORA_TX_POWER=22 + -D ENV_INCLUDE_GPS=1 + -D PIN_GPS_RX=39 + -D PIN_GPS_TX=42 + -D PIN_GPS_EN=-1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/lilygo_t_eth_elite_sx1262> + + + + +lib_deps = + ${esp32_base.lib_deps} + lewisxhe/XPowersLib @ ^0.2.7 + stevemarple/MicroNMEA @ ^2.0.6 + adafruit/Adafruit BME280 Library @ ^2.3.0 + +; === LilyGo T-ETH-Elite with SX1262 environments === + +[env:LilyGo_T_ETH_Elite_SX1262_repeater] +extends = LilyGo_T_ETH_Elite_SX1262 +build_flags = + ${LilyGo_T_ETH_Elite_SX1262.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1262 Repeater"' + -D ADVERT_LAT=0 + -D ADVERT_LON=0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1262.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1262_repeater_eth] +extends = LilyGo_T_ETH_Elite_SX1262 +build_flags = + ${LilyGo_T_ETH_Elite_SX1262.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1262 Repeater"' + -D ADVERT_LAT=0 + -D ADVERT_LON=0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D USE_ETHERNET + -D ETH_STATIC_IP=192,168,254,20 + -D ETH_GATEWAY=192,168,254,254 + -D ETH_SUBNET=255,255,255,0 + -D ETH_DNS=8,8,8,8 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1262.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1262_repeater_bridge_espnow] +extends = LilyGo_T_ETH_Elite_SX1262 +build_flags = + ${LilyGo_T_ETH_Elite_SX1262.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1262 ESPNow Bridge"' + -D ADVERT_LAT=0 + -D ADVERT_LON=0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1262.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1262_room_server] +extends = LilyGo_T_ETH_Elite_SX1262 +build_flags = + ${LilyGo_T_ETH_Elite_SX1262.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1262 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1262.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1262_room_server_eth] +extends = LilyGo_T_ETH_Elite_SX1262 +build_flags = + ${LilyGo_T_ETH_Elite_SX1262.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1262 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D USE_ETHERNET + -D ETH_STATIC_IP=192,168,254,20 + -D ETH_GATEWAY=192,168,254,254 + -D ETH_SUBNET=255,255,255,0 + -D ETH_DNS=8,8,8,8 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1262.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1262_companion_radio_eth] +extends = LilyGo_T_ETH_Elite_SX1262 +build_flags = + ${LilyGo_T_ETH_Elite_SX1262.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D USE_ETHERNET + -D ETH_STATIC_IP=192,168,254,20 + -D ETH_GATEWAY=192,168,254,254 + -D ETH_SUBNET=255,255,255,0 + -D ETH_DNS=8,8,8,8 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1262.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_T_ETH_Elite_SX1262_companion_radio_wifi] +extends = LilyGo_T_ETH_Elite_SX1262 +build_flags = + ${LilyGo_T_ETH_Elite_SX1262.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D WIFI_SSID='"WIFI_SSID"' + -D WIFI_PWD='"Password"' +build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1262.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_T_ETH_Elite_SX1262_companion_radio_ble] +extends = LilyGo_T_ETH_Elite_SX1262 +build_flags = + ${LilyGo_T_ETH_Elite_SX1262.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1262.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/lilygo_t_eth_elite_sx1262/target.cpp b/variants/lilygo_t_eth_elite_sx1262/target.cpp new file mode 100644 index 000000000..b6ce2c49d --- /dev/null +++ b/variants/lilygo_t_eth_elite_sx1262/target.cpp @@ -0,0 +1,53 @@ +#include +#include "helpers/ui/MomentaryButton.h" +#include "target.h" + +TEthEliteBoard board; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif + +MomentaryButton user_btn(PIN_USER_BTN_ANA, 1000, true); + +static SPIClass spi; +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire1); + return radio.std_init(&spi); +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/lilygo_t_eth_elite_sx1262/target.h b/variants/lilygo_t_eth_elite_sx1262/target.h new file mode 100644 index 000000000..e3635b571 --- /dev/null +++ b/variants/lilygo_t_eth_elite_sx1262/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define HAS_ETHERNET 1 + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include + +extern MomentaryButton user_btn; + +#ifdef DISPLAY_CLASS + #include + extern DISPLAY_CLASS display; +#endif + +extern TEthEliteBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/lilygo_t_eth_elite_sx1276/platformio.ini b/variants/lilygo_t_eth_elite_sx1276/platformio.ini new file mode 100644 index 000000000..512c650bb --- /dev/null +++ b/variants/lilygo_t_eth_elite_sx1276/platformio.ini @@ -0,0 +1,176 @@ +[LilyGo_T_ETH_Elite_SX1276] +extends = esp32_base +board = t_eth_elite +upload_protocol = esptool +board_build.partitions = default_16MB.csv +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +build_flags = + ${esp32_base.build_flags} + -I variants/lilygo_t_eth_elite_sx1276 + -D T_ETH_ELITE_SX1276 + -D RADIO_CLASS=CustomSX1276 + -D WRAPPER_CLASS=CustomSX1276Wrapper + -D SX127X_CURRENT_LIMIT=120 + -D LORA_TX_POWER=20 + -D ENV_INCLUDE_GPS=1 + -D PIN_GPS_RX=39 + -D PIN_GPS_TX=42 + -D PIN_GPS_EN=-1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/lilygo_t_eth_elite_sx1276> + + + + +lib_deps = + ${esp32_base.lib_deps} + lewisxhe/XPowersLib @ ^0.2.7 + stevemarple/MicroNMEA @ ^2.0.6 + adafruit/Adafruit BME280 Library @ ^2.3.0 + +; === LilyGo T-ETH-Elite with SX1276 environments === + +[env:LilyGo_T_ETH_Elite_SX1276_repeater] +extends = LilyGo_T_ETH_Elite_SX1276 +build_flags = + ${LilyGo_T_ETH_Elite_SX1276.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1276 Repeater"' + -D ADVERT_LAT=0 + -D ADVERT_LON=0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1276.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1276_repeater_eth] +extends = LilyGo_T_ETH_Elite_SX1276 +build_flags = + ${LilyGo_T_ETH_Elite_SX1276.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1276 Repeater"' + -D ADVERT_LAT=0 + -D ADVERT_LON=0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D USE_ETHERNET + -D ETH_STATIC_IP=192,168,254,20 + -D ETH_GATEWAY=192,168,254,254 + -D ETH_SUBNET=255,255,255,0 + -D ETH_DNS=8,8,8,8 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1276.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1276_repeater_bridge_espnow] +extends = LilyGo_T_ETH_Elite_SX1276 +build_flags = + ${LilyGo_T_ETH_Elite_SX1276.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1276 ESPNow Bridge"' + -D ADVERT_LAT=0 + -D ADVERT_LON=0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1276.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1276_room_server] +extends = LilyGo_T_ETH_Elite_SX1276 +build_flags = + ${LilyGo_T_ETH_Elite_SX1276.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1276 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1276.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1276_room_server_eth] +extends = LilyGo_T_ETH_Elite_SX1276 +build_flags = + ${LilyGo_T_ETH_Elite_SX1276.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1276 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D USE_ETHERNET + -D ETH_STATIC_IP=192,168,254,20 + -D ETH_GATEWAY=192,168,254,254 + -D ETH_SUBNET=255,255,255,0 + -D ETH_DNS=8,8,8,8 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1276.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T_ETH_Elite_SX1276_companion_radio_eth] +extends = LilyGo_T_ETH_Elite_SX1276 +build_flags = + ${LilyGo_T_ETH_Elite_SX1276.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D USE_ETHERNET + -D ETH_STATIC_IP=192,168,254,20 + -D ETH_GATEWAY=192,168,254,254 + -D ETH_SUBNET=255,255,255,0 + -D ETH_DNS=8,8,8,8 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1276.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_T_ETH_Elite_SX1276_companion_radio_wifi] +extends = LilyGo_T_ETH_Elite_SX1276 +build_flags = + ${LilyGo_T_ETH_Elite_SX1276.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D WIFI_SSID='"WIFI_SSID"' + -D WIFI_PWD='"Password"' +build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1276.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_T_ETH_Elite_SX1276_companion_radio_ble] +extends = LilyGo_T_ETH_Elite_SX1276 +build_flags = + ${LilyGo_T_ETH_Elite_SX1276.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1276.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/lilygo_t_eth_elite_sx1276/target.cpp b/variants/lilygo_t_eth_elite_sx1276/target.cpp new file mode 100644 index 000000000..a311c1d7e --- /dev/null +++ b/variants/lilygo_t_eth_elite_sx1276/target.cpp @@ -0,0 +1,56 @@ +#include +#include "helpers/ui/MomentaryButton.h" +#include "target.h" + +TEthEliteBoard board; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif + +MomentaryButton user_btn(PIN_USER_BTN_ANA, 1000, true); + +static SPIClass spi; +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi); + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire1); + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + bool ok = radio.std_init(&spi); + delay(100); // allow SX1276 to stabilize after init + return ok; +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/lilygo_t_eth_elite_sx1276/target.h b/variants/lilygo_t_eth_elite_sx1276/target.h new file mode 100644 index 000000000..4861b535a --- /dev/null +++ b/variants/lilygo_t_eth_elite_sx1276/target.h @@ -0,0 +1,30 @@ +#pragma once + +#define HAS_ETHERNET 1 + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include + +extern MomentaryButton user_btn; + +#ifdef DISPLAY_CLASS + #include + extern DISPLAY_CLASS display; +#endif + +extern TEthEliteBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); From 2c611e741fae7cfe0e5801832299e1769e70f885 Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Mon, 30 Mar 2026 02:07:17 +0200 Subject: [PATCH 02/11] Add ETHClass2 lib, board definition and ETH companion radio support --- boards/t_eth_elite.json | 51 + examples/companion_radio/main.cpp | 9 +- lib/ETHClass2/library.properties | 9 + lib/ETHClass2/src/ETHClass2.cpp | 1075 +++++++++++++++++++++ lib/ETHClass2/src/ETHClass2.h | 219 +++++ src/helpers/esp32/SerialWifiInterface.cpp | 8 +- 6 files changed, 1364 insertions(+), 7 deletions(-) create mode 100644 boards/t_eth_elite.json create mode 100644 lib/ETHClass2/library.properties create mode 100644 lib/ETHClass2/src/ETHClass2.cpp create mode 100644 lib/ETHClass2/src/ETHClass2.h diff --git a/boards/t_eth_elite.json b/boards/t_eth_elite.json new file mode 100644 index 000000000..e2b039d1d --- /dev/null +++ b/boards/t_eth_elite.json @@ -0,0 +1,51 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_USB_MODE=1", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "LilyGo T-ETH-ELite (16MB Flash 8MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 8388608, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://lilygo.cc/products/t-eth-elite-1", + "vendor": "LilyGo" + } \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 876dc9c33..6bf090224 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -35,7 +35,7 @@ static uint32_t _atoi(const char* sp) { #endif #ifdef ESP32 - #ifdef WIFI_SSID + #if defined(WIFI_SSID) || defined(USE_ETHERNET) #include SerialWifiInterface serial_interface; #ifndef TCP_PORT @@ -122,7 +122,7 @@ void setup() { disp->endFrame(); } #endif - + if (!radio_init()) { halt(); } fast_rng.begin(radio_get_rng_seed()); @@ -194,9 +194,12 @@ void setup() { ); #ifdef WIFI_SSID - board.setInhibitSleep(true); // prevent sleep when WiFi is active + board.setInhibitSleep(true); WiFi.begin(WIFI_SSID, WIFI_PWD); serial_interface.begin(TCP_PORT); +#elif defined(USE_ETHERNET) + serial_interface.begin(TCP_PORT); + Serial.printf("TCP server started on port %d\n", TCP_PORT); #elif defined(BLE_PIN_CODE) serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); #elif defined(SERIAL_RX) diff --git a/lib/ETHClass2/library.properties b/lib/ETHClass2/library.properties new file mode 100644 index 000000000..73e9002a7 --- /dev/null +++ b/lib/ETHClass2/library.properties @@ -0,0 +1,9 @@ +name=ETHClass2 +version=1.0.0 +author=Lewis He +maintainer=Lewis He +sentence=Enables network connection (local and Internet) using the ESP32/ESP32S3 Ethernet. +paragraph=With this library you can instantiate Servers, Clients and send/receive UDP packets through Ethernet. The IP address can be assigned statically or through a DHCP. The library can also manage DNS. +category=Communication +url= +architectures=esp32 diff --git a/lib/ETHClass2/src/ETHClass2.cpp b/lib/ETHClass2/src/ETHClass2.cpp new file mode 100644 index 000000000..57c2cb7e0 --- /dev/null +++ b/lib/ETHClass2/src/ETHClass2.cpp @@ -0,0 +1,1075 @@ +/* + ETH.h - espre ETH PHY support. + Based on WiFi.h from Arduino WiFi shield library. + Copyright (c) 2011-2014 Arduino. All right reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +// Disable the automatic pin remapping of the API calls in this file +#define ARDUINO_CORE_BUILD + +#include "ETHClass2.h" +#include "esp_system.h" +#include "esp_event.h" +#include "esp_eth.h" +#include "esp_eth_mac.h" +#include "esp_eth_com.h" +#include "driver/gpio.h" +#include "driver/spi_master.h" +#if CONFIG_ETH_USE_ESP32_EMAC +#include "soc/emac_ext_struct.h" +#include "soc/rtc.h" +#endif /* CONFIG_ETH_USE_ESP32_EMAC */ +// #include "esp32-hal-periman.h" +#include "lwip/err.h" +#include "lwip/dns.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include "esp_netif_types.h" +#include "esp_netif_defaults.h" +#include "esp_eth_phy.h" + +#ifndef ETH_ADDR_LEN +#define ETH_ADDR_LEN 6 +#endif + + +#undef CONFIG_ETH_SPI_ETHERNET_DM9051 + +extern void tcpipInit(); +extern void add_esp_interface_netif(esp_interface_t interface, esp_netif_t *esp_netif); /* from WiFiGeneric */ + + +ETHClass2::ETHClass2(uint8_t eth_index) + : _eth_started(false) + , _eth_handle(NULL) + , _esp_netif(NULL) + , _eth_index(eth_index) + , _phy_type(ETH_PHY_MAX) +#if ETH_SPI_SUPPORTS_CUSTOM + , _spi(NULL) +#endif + , _spi_freq_mhz(20) + , _pin_cs(-1) + , _pin_irq(-1) + , _pin_rst(-1) + , _pin_sck(-1) + , _pin_miso(-1) + , _pin_mosi(-1) +#if CONFIG_ETH_USE_ESP32_EMAC + , _pin_mcd(-1) + , _pin_mdio(-1) + , _pin_power(-1) + , _pin_rmii_clock(-1) +#endif /* CONFIG_ETH_USE_ESP32_EMAC */ +{} + +ETHClass2::~ETHClass2() +{} + +bool ETHClass2::ethDetachBus(void *bus_pointer) +{ + ETHClass2 *bus = (ETHClass2 *) bus_pointer; + if (bus->_eth_started) { + bus->end(); + } + return true; +} + +#if CONFIG_ETH_USE_ESP32_EMAC +bool ETHClass2::begin(eth_phy_type_t type, uint8_t phy_addr, int mdc, int mdio, int power, eth_clock_mode_t clock_mode) +{ + esp_err_t ret = ESP_OK; + if (_esp_netif != NULL) { + return true; + } + + // perimanSetBusDeinit(ESP32_BUS_TYPE_ETHERNET_RMII, ETHClass2::ethDetachBus); + // perimanSetBusDeinit(ESP32_BUS_TYPE_ETHERNET_CLK, ETHClass2::ethDetachBus); + // perimanSetBusDeinit(ESP32_BUS_TYPE_ETHERNET_MCD, ETHClass2::ethDetachBus); + // perimanSetBusDeinit(ESP32_BUS_TYPE_ETHERNET_MDIO, ETHClass2::ethDetachBus); + // if(power != -1){ + // perimanSetBusDeinit(ESP32_BUS_TYPE_ETHERNET_PWR, ETHClass2::ethDetachBus); + // } + + tcpipInit(); + + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + mac_config.clock_config.rmii.clock_mode = (clock_mode) ? EMAC_CLK_OUT : EMAC_CLK_EXT_IN; + mac_config.clock_config.rmii.clock_gpio = (1 == clock_mode) ? EMAC_APPL_CLK_OUT_GPIO : (2 == clock_mode) ? EMAC_CLK_OUT_GPIO : (3 == clock_mode) ? EMAC_CLK_OUT_180_GPIO : EMAC_CLK_IN_GPIO; + mac_config.smi_mdc_gpio_num = digitalPinToGPIONumber(mdc); + mac_config.smi_mdio_gpio_num = digitalPinToGPIONumber(mdio); + + _pin_mcd = digitalPinToGPIONumber(mdc); + _pin_mdio = digitalPinToGPIONumber(mdio); + _pin_rmii_clock = mac_config.clock_config.rmii.clock_gpio; + _pin_power = digitalPinToGPIONumber(power); + + // if(!perimanClearPinBus(_pin_rmii_clock)){ return false; } + // if(!perimanClearPinBus(_pin_mcd)){ return false; } + // if(!perimanClearPinBus(_pin_mdio)){ return false; } + // if(!perimanClearPinBus(ETH_RMII_TX_EN)){ return false; } + // if(!perimanClearPinBus(ETH_RMII_TX0)){ return false; } + // if(!perimanClearPinBus(ETH_RMII_TX1)){ return false; } + // if(!perimanClearPinBus(ETH_RMII_RX0)){ return false; } + // if(!perimanClearPinBus(ETH_RMII_RX1_EN)){ return false; } + // if(!perimanClearPinBus(ETH_RMII_CRS_DV)){ return false; } + // if(_pin_power != -1){ + // if(!perimanClearPinBus(_pin_power)){ return false; } + // } + + eth_mac_config_t eth_mac_config = ETH_MAC_DEFAULT_CONFIG(); + eth_mac_config.sw_reset_timeout_ms = 1000; + + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config); + if (mac == NULL) { + log_e("esp_eth_mac_new_esp32 failed"); + return false; + } + + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.phy_addr = phy_addr; + phy_config.reset_gpio_num = _pin_power; + + esp_eth_phy_t *phy = NULL; + switch (type) { + case ETH_PHY_LAN8720: + phy = esp_eth_phy_new_lan87xx(&phy_config); + break; + case ETH_PHY_TLK110: + // phy = esp_eth_phy_new_ip101(&phy_config); + break; + case ETH_PHY_RTL8201: + phy = esp_eth_phy_new_rtl8201(&phy_config); + break; + case ETH_PHY_DP83848: + // phy = esp_eth_phy_new_dp83848(&phy_config); + break; + case ETH_PHY_KSZ8041: + // phy = esp_eth_phy_new_ksz80xx(&phy_config); + break; + case ETH_PHY_KSZ8081: + // phy = esp_eth_phy_new_ksz80xx(&phy_config); + break; + default: + log_e("Unsupported PHY %d", type); + break; + } + if (phy == NULL) { + log_e("esp_eth_phy_new failed"); + return false; + } + + _eth_handle = NULL; + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + ret = esp_eth_driver_install(ð_config, &_eth_handle); + if (ret != ESP_OK) { + log_e("SPI Ethernet driver install failed: %d", ret); + return false; + } + if (_eth_handle == NULL) { + log_e("esp_eth_driver_install failed! eth_handle is NULL"); + return false; + } + + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); + + // Use ESP_NETIF_INHERENT_DEFAULT_ETH when multiple Ethernet interfaces are used and so you need to modify + // esp-netif configuration parameters for each interface (name, priority, etc.). + char if_key_str[10]; + char if_desc_str[10]; + if (_eth_index == 0) { + strcpy(if_key_str, "ETH_DEF"); + strcpy(if_desc_str, "eth"); + } else { + char num_str[3]; + itoa(_eth_index, num_str, 10); + strcat(strcpy(if_key_str, "ETH_"), num_str); + strcat(strcpy(if_desc_str, "eth"), num_str); + } + + esp_netif_inherent_config_t esp_netif_config = ESP_NETIF_INHERENT_DEFAULT_ETH(); + esp_netif_config.if_key = if_key_str; + esp_netif_config.if_desc = if_desc_str; + esp_netif_config.route_prio -= _eth_index * 5; + + cfg.base = &esp_netif_config; + + _esp_netif = esp_netif_new(&cfg); + + /* attach Ethernet driver to TCP/IP stack */ + ret = esp_netif_attach(_esp_netif, esp_eth_new_netif_glue(_eth_handle)); + if (ret != ESP_OK) { + log_e("esp_netif_attach failed: %d", ret); + return false; + } + + /* attach to WiFiGeneric to receive events */ + add_esp_interface_netif(ESP_IF_ETH, _esp_netif); + + ret = esp_eth_start(_eth_handle); + if (ret != ESP_OK) { + log_e("esp_eth_start failed: %d", ret); + return false; + } + _eth_started = true; + + // if(!perimanSetPinBus(_pin_rmii_clock, ESP32_BUS_TYPE_ETHERNET_CLK, (void *)(this), -1, -1)){ goto err; } + // if(!perimanSetPinBus(_pin_mcd, ESP32_BUS_TYPE_ETHERNET_MCD, (void *)(this), -1, -1)){ goto err; } + // if(!perimanSetPinBus(_pin_mdio, ESP32_BUS_TYPE_ETHERNET_MDIO, (void *)(this), -1, -1)){ goto err; } + + // if(!perimanSetPinBus(ETH_RMII_TX_EN, ESP32_BUS_TYPE_ETHERNET_RMII, (void *)(this), -1, -1)){ goto err; } + // if(!perimanSetPinBus(ETH_RMII_TX0, ESP32_BUS_TYPE_ETHERNET_RMII, (void *)(this), -1, -1)){ goto err; } + // if(!perimanSetPinBus(ETH_RMII_TX1, ESP32_BUS_TYPE_ETHERNET_RMII, (void *)(this), -1, -1)){ goto err; } + // if(!perimanSetPinBus(ETH_RMII_RX0, ESP32_BUS_TYPE_ETHERNET_RMII, (void *)(this), -1, -1)){ goto err; } + // if(!perimanSetPinBus(ETH_RMII_RX1_EN, ESP32_BUS_TYPE_ETHERNET_RMII, (void *)(this), -1, -1)){ goto err; } + // if(!perimanSetPinBus(ETH_RMII_CRS_DV, ESP32_BUS_TYPE_ETHERNET_RMII, (void *)(this), -1, -1)){ goto err; } + + // if(_pin_power != -1){ + // if(!perimanSetPinBus(_pin_power, ESP32_BUS_TYPE_ETHERNET_PWR, (void *)(this), -1, -1)){ goto err; } + // } + // holds a few milliseconds to let DHCP start and enter into a good state + // FIX ME -- adresses issue https://github.com/espressif/arduino-esp32/issues/5733 + delay(50); + + return true; + +err: + log_e("Failed to set all pins bus to ETHERNET"); + ETHClass2::ethDetachBus((void *)(this)); + return false; +} +#endif /* CONFIG_ETH_USE_ESP32_EMAC */ + +#if ETH_SPI_SUPPORTS_CUSTOM +static void *_eth_spi_init(const void *ctx) +{ + return (void *)ctx; +} + +static esp_err_t _eth_spi_deinit(void *ctx) +{ + return ESP_OK; +} + +esp_err_t ETHClass2::_eth_spi_read(void *ctx, uint32_t cmd, uint32_t addr, void *data, uint32_t data_len) +{ + return ((ETHClass2 *)ctx)->eth_spi_read(cmd, addr, data, data_len); +} + +esp_err_t ETHClass2::_eth_spi_write(void *ctx, uint32_t cmd, uint32_t addr, const void *data, uint32_t data_len) +{ + return ((ETHClass2 *)ctx)->eth_spi_write(cmd, addr, data, data_len); +} + +esp_err_t ETHClass2::eth_spi_read(uint32_t cmd, uint32_t addr, void *data, uint32_t data_len) +{ + if (_spi == NULL) { + return ESP_FAIL; + } + // log_i(" 0x%04lx 0x%04lx %lu", cmd, addr, data_len); + _spi->beginTransaction(SPISettings(_spi_freq_mhz * 1000 * 1000, MSBFIRST, SPI_MODE0)); + digitalWrite(_pin_cs, LOW); + +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + if (_phy_type == ETH_PHY_DM9051) { + _spi->write(((cmd & 0x01) << 7) | (addr & 0x7F)); + } else +#endif +#if CONFIG_ETH_SPI_ETHERNET_W5500 + if (_phy_type == ETH_PHY_W5500) { + _spi->write16(cmd); + _spi->write(addr); + } else +#endif +#if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL + if (_phy_type == ETH_PHY_KSZ8851) { + if (cmd > 1) { + _spi->write(cmd << 6 | addr); + } else { + _spi->write16(cmd << 14 | addr); + } + } else +#endif + { + log_e("Unsupported PHY module: %d", _phy_type); + digitalWrite(_pin_cs, HIGH); + _spi->endTransaction(); + return ESP_FAIL; + } + _spi->transferBytes(NULL, (uint8_t *)data, data_len); + + digitalWrite(_pin_cs, HIGH); + _spi->endTransaction(); + return ESP_OK; +} + +esp_err_t ETHClass2::eth_spi_write(uint32_t cmd, uint32_t addr, const void *data, uint32_t data_len) +{ + if (_spi == NULL) { + return ESP_FAIL; + } + // log_i("0x%04lx 0x%04lx %lu", cmd, addr, data_len); + _spi->beginTransaction(SPISettings(_spi_freq_mhz * 1000 * 1000, MSBFIRST, SPI_MODE0)); + digitalWrite(_pin_cs, LOW); + +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + if (_phy_type == ETH_PHY_DM9051) { + _spi->write(((cmd & 0x01) << 7) | (addr & 0x7F)); + } else +#endif +#if CONFIG_ETH_SPI_ETHERNET_W5500 + if (_phy_type == ETH_PHY_W5500) { + _spi->write16(cmd); + _spi->write(addr); + } else +#endif +#if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL + if (_phy_type == ETH_PHY_KSZ8851) { + if (cmd > 1) { + _spi->write(cmd << 6 | addr); + } else { + _spi->write16(cmd << 14 | addr); + } + } else +#endif + { + log_e("Unsupported PHY module: %d", _phy_type); + digitalWrite(_pin_cs, HIGH); + _spi->endTransaction(); + return ESP_FAIL; + } + _spi->writeBytes((const uint8_t *)data, data_len); + + digitalWrite(_pin_cs, HIGH); + _spi->endTransaction(); + return ESP_OK; +} +#endif + +bool ETHClass2::beginSPI(eth_phy_type_t type, uint8_t phy_addr, int cs, int irq, int rst, +#if ETH_SPI_SUPPORTS_CUSTOM + SPIClass *spi, +#endif + int sck, int miso, int mosi, spi_host_device_t spi_host, uint8_t spi_freq_mhz) +{ + esp_err_t ret = ESP_OK; + + if (_eth_started || _esp_netif != NULL || _eth_handle != NULL) { + log_w("ETH Already Started"); + return true; + } + if (cs < 0 || irq < 0) { + log_e("CS and IRQ pins must be defined!"); + return false; + } + + // perimanSetBusDeinit(ESP32_BUS_TYPE_ETHERNET_SPI, ETHClass2::ethDetachBus); + + // if(_pin_cs != -1){ + // if(!perimanClearPinBus(_pin_cs)){ return false; } + // } + // if(_pin_rst != -1){ + // if(!perimanClearPinBus(_pin_rst)){ return false; } + // } + // if(_pin_irq != -1){ + // if(!perimanClearPinBus(_pin_irq)){ return false; } + // } + // if(_pin_sck != -1){ + // if(!perimanClearPinBus(_pin_sck)){ return false; } + // } + // if(_pin_miso != -1){ + // if(!perimanClearPinBus(_pin_miso)){ return false; } + // } + // if(_pin_mosi != -1){ + // if(!perimanClearPinBus(_pin_mosi)){ return false; } + // } + +#if ETH_SPI_SUPPORTS_CUSTOM + _spi = spi; +#endif + if (spi_freq_mhz) { + _spi_freq_mhz = spi_freq_mhz; + } + _phy_type = type; + _pin_cs = digitalPinToGPIONumber(cs); + _pin_irq = digitalPinToGPIONumber(irq); + _pin_rst = digitalPinToGPIONumber(rst); + _pin_sck = digitalPinToGPIONumber(sck); + _pin_miso = digitalPinToGPIONumber(miso); + _pin_mosi = digitalPinToGPIONumber(mosi); + +#if ETH_SPI_SUPPORTS_CUSTOM + if (_spi != NULL) { + pinMode(_pin_cs, OUTPUT); + digitalWrite(_pin_cs, HIGH); + // perimanSetPinBusExtraType(_pin_cs, "ETH_CS"); + } +#endif + + + tcpipInit(); + + // Install GPIO ISR handler to be able to service SPI Eth modules interrupts + ret = gpio_install_isr_service(0); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + log_e("GPIO ISR handler install failed: %d", ret); + return false; + } + + // Init common MAC and PHY configs to default + eth_mac_config_t eth_mac_config = ETH_MAC_DEFAULT_CONFIG(); + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + + // Update PHY config based on board specific configuration + phy_config.phy_addr = phy_addr; + phy_config.reset_gpio_num = _pin_rst; + + // Init SPI bus + spi_device_handle_t spi_handle = {0}; + spi_bus_config_t buscfg = {0}; + buscfg.miso_io_num = _pin_miso; + buscfg.mosi_io_num = _pin_mosi; + buscfg.sclk_io_num = _pin_sck; + buscfg.quadwp_io_num = -1; + buscfg.quadhd_io_num = -1; + ret = spi_bus_initialize(spi_host, &buscfg, SPI_DMA_CH_AUTO); + if (ret != ESP_OK) { + log_e("spi_bus_initialize failed"); + return false; + } + + // Configure SPI interface for specific SPI module + spi_device_interface_config_t spi_devcfg = { + .command_bits = 16, // Actually it's the address phase in W5500 SPI frame + .address_bits = 8, // Actually it's the control phase in W5500 SPI frame + .mode = 0, + .clock_speed_hz = _spi_freq_mhz * 1000 * 1000, + .queue_size = 20 + }; + // Set SPI module Chip Select GPIO + spi_devcfg.spics_io_num = _pin_cs; + + ret = spi_bus_add_device(spi_host, &spi_devcfg, &spi_handle); + if (ret != ESP_OK) { + log_e("spi_bus_add_device failed"); + return false; + } + + esp_eth_mac_t *mac = NULL; + esp_eth_phy_t *phy = NULL; +#if CONFIG_ETH_SPI_ETHERNET_W5500 + if (type == ETH_PHY_W5500) { + + eth_w5500_config_t mac_config = ETH_W5500_DEFAULT_CONFIG(spi_handle); + + mac_config.int_gpio_num = _pin_irq; +#if ETH_SPI_SUPPORTS_CUSTOM + if (_spi != NULL) { + mac_config.custom_spi_driver.config = this; + mac_config.custom_spi_driver.init = _eth_spi_init; + mac_config.custom_spi_driver.deinit = _eth_spi_deinit; + mac_config.custom_spi_driver.read = _eth_spi_read; + mac_config.custom_spi_driver.write = _eth_spi_write; + } +#endif + mac = esp_eth_mac_new_w5500(&mac_config, ð_mac_config); + phy = esp_eth_phy_new_w5500(&phy_config); + } else +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + if (type == ETH_PHY_DM9051) { + eth_dm9051_config_t mac_config = ETH_DM9051_DEFAULT_CONFIG(spi_host, &spi_devcfg); + mac_config.int_gpio_num = _pin_irq; +#if ETH_SPI_SUPPORTS_CUSTOM + if (_spi != NULL) { + mac_config.custom_spi_driver.config = this; + mac_config.custom_spi_driver.init = _eth_spi_init; + mac_config.custom_spi_driver.deinit = _eth_spi_deinit; + mac_config.custom_spi_driver.read = _eth_spi_read; + mac_config.custom_spi_driver.write = _eth_spi_write; + } +#endif + mac = esp_eth_mac_new_dm9051(&mac_config, ð_mac_config); + phy = esp_eth_phy_new_dm9051(&phy_config); + } else +#endif +#if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL + if (type == ETH_PHY_KSZ8851) { + eth_ksz8851snl_config_t mac_config = ETH_KSZ8851SNL_DEFAULT_CONFIG(spi_host, &spi_devcfg); + mac_config.int_gpio_num = _pin_irq; +#if ETH_SPI_SUPPORTS_CUSTOM + if (_spi != NULL) { + mac_config.custom_spi_driver.config = this; + mac_config.custom_spi_driver.init = _eth_spi_init; + mac_config.custom_spi_driver.deinit = _eth_spi_deinit; + mac_config.custom_spi_driver.read = _eth_spi_read; + mac_config.custom_spi_driver.write = _eth_spi_write; + } +#endif + mac = esp_eth_mac_new_ksz8851snl(&mac_config, ð_mac_config); + phy = esp_eth_phy_new_ksz8851snl(&phy_config); + } else +#endif + { + log_e("Unsupported PHY module: %d", (int)type); + return false; + } + + // Init Ethernet driver to default and install it + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + ret = esp_eth_driver_install(ð_config, &_eth_handle); + if (ret != ESP_OK) { + log_e("SPI Ethernet driver install failed: %d", ret); + return false; + } + if (_eth_handle == NULL) { + log_e("esp_eth_driver_install failed! eth_handle is NULL"); + return false; + } + + // Derive a new MAC address for this interface + uint8_t base_mac_addr[ETH_ADDR_LEN]; + ret = esp_efuse_mac_get_default(base_mac_addr); + if (ret != ESP_OK) { + log_e("Get EFUSE MAC failed: %d", ret); + return false; + } + uint8_t mac_addr[ETH_ADDR_LEN]; + base_mac_addr[ETH_ADDR_LEN - 1] += _eth_index+1; // Increment by the ETH number, and we start at 0 so add 1 more + esp_derive_local_mac(mac_addr, base_mac_addr); + + ret = esp_eth_ioctl(_eth_handle, ETH_CMD_S_MAC_ADDR, mac_addr); + if (ret != ESP_OK) { + log_e("SPI Ethernet MAC address config failed: %d", ret); + return false; + } + + // Use ESP_NETIF_DEFAULT_ETH when just one Ethernet interface is used and you don't need to modify + // default esp-netif configuration parameters. + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); + + // Use ESP_NETIF_INHERENT_DEFAULT_ETH when multiple Ethernet interfaces are used and so you need to modify + // esp-netif configuration parameters for each interface (name, priority, etc.). + char if_key_str[10]; + char if_desc_str[10]; + if (_eth_index == 0) { + strcpy(if_key_str, "ETH_DEF"); + strcpy(if_desc_str, "eth"); + } else { + char num_str[3]; + itoa(_eth_index, num_str, 10); + strcat(strcpy(if_key_str, "ETH_"), num_str); + strcat(strcpy(if_desc_str, "eth"), num_str); + } + + esp_netif_inherent_config_t esp_netif_config = ESP_NETIF_INHERENT_DEFAULT_ETH(); + esp_netif_config.if_key = if_key_str; + esp_netif_config.if_desc = if_desc_str; + esp_netif_config.route_prio -= _eth_index * 5; + + cfg.base = &esp_netif_config; + + _esp_netif = esp_netif_new(&cfg); + if (_esp_netif == NULL) { + log_e("esp_netif_new failed"); + return false; + } + // Attach Ethernet driver to TCP/IP stack + esp_eth_netif_glue_handle_t new_netif_glue = esp_eth_new_netif_glue(_eth_handle); + if (new_netif_glue == NULL) { + log_e("esp_eth_new_netif_glue failed"); + return false; + } + + ret = esp_netif_attach(_esp_netif, new_netif_glue); + if (ret != ESP_OK) { + log_e("esp_netif_attach failed: %d", ret); + return false; + } + + // attach to WiFiGeneric to receive events + add_esp_interface_netif(ESP_IF_ETH, _esp_netif); + + // Start Ethernet driver state machine + ret = esp_eth_start(_eth_handle); + if (ret != ESP_OK) { + log_e("esp_eth_start failed: %d", ret); + return false; + } + + _eth_started = true; + + // If Arduino's SPI is used, cs pin is in GPIO mode +#if ETH_SPI_SUPPORTS_CUSTOM + if (_spi == NULL) { +#endif + // if(!perimanSetPinBus(_pin_cs, ESP32_BUS_TYPE_ETHERNET_SPI, (void *)(this), -1, -1)){ goto err; } +#if ETH_SPI_SUPPORTS_CUSTOM + } +#endif + // if(!perimanSetPinBus(_pin_irq, ESP32_BUS_TYPE_ETHERNET_SPI, (void *)(this), -1, -1)){ goto err; } + + // if(_pin_sck != -1){ + // if(!perimanSetPinBus(_pin_sck, ESP32_BUS_TYPE_ETHERNET_SPI, (void *)(this), -1, -1)){ goto err; } + // } + // if(_pin_miso != -1){ + // if(!perimanSetPinBus(_pin_miso, ESP32_BUS_TYPE_ETHERNET_SPI, (void *)(this), -1, -1)){ goto err; } + // } + // if(_pin_mosi != -1){ + // if(!perimanSetPinBus(_pin_mosi, ESP32_BUS_TYPE_ETHERNET_SPI, (void *)(this), -1, -1)){ goto err; } + // } + // if(_pin_rst != -1){ + // if(!perimanSetPinBus(_pin_rst, ESP32_BUS_TYPE_ETHERNET_SPI, (void *)(this), -1, -1)){ goto err; } + // } + + return true; + +err: + log_e("Failed to set all pins bus to ETHERNET"); + ETHClass2::ethDetachBus((void *)(this)); + return false; +} + +#if ETH_SPI_SUPPORTS_CUSTOM +bool ETHClass2::begin(eth_phy_type_t type, uint8_t phy_addr, int cs, int irq, int rst, SPIClass &spi, uint8_t spi_freq_mhz) +{ + + return beginSPI(type, phy_addr, cs, irq, rst, &spi, -1, -1, -1, SPI2_HOST, spi_freq_mhz); +} +#endif + +bool ETHClass2::begin(eth_phy_type_t type, uint8_t phy_addr, int cs, int irq, int rst, spi_host_device_t spi_host, int sck, int miso, int mosi, uint8_t spi_freq_mhz) +{ + + return beginSPI(type, phy_addr, cs, irq, rst, +#if ETH_SPI_SUPPORTS_CUSTOM + NULL, +#endif + sck, miso, mosi, spi_host, spi_freq_mhz); +} + +void ETHClass2::end(void) +{ + _eth_started = false; + + if (_esp_netif != NULL) { + esp_netif_destroy(_esp_netif); + _esp_netif = NULL; + } + + if (_eth_handle != NULL) { + if (esp_eth_stop(_eth_handle) != ESP_OK) { + log_e("Failed to stop Ethernet"); + return; + } + if (esp_eth_driver_uninstall(_eth_handle) != ESP_OK) { + log_e("Failed to stop Ethernet"); + return; + } + _eth_handle = NULL; + } + +#if ETH_SPI_SUPPORTS_CUSTOM + _spi = NULL; +#endif + +#if CONFIG_ETH_USE_ESP32_EMAC + if (_pin_rmii_clock != -1 && _pin_mcd != -1 && _pin_mdio != -1) { + // perimanClearPinBus(_pin_rmii_clock); + // perimanClearPinBus(_pin_mcd); + // perimanClearPinBus(_pin_mdio); + + // perimanClearPinBus(ETH_RMII_TX_EN); + // perimanClearPinBus(ETH_RMII_TX0); + // perimanClearPinBus(ETH_RMII_TX1); + // perimanClearPinBus(ETH_RMII_RX0); + // perimanClearPinBus(ETH_RMII_RX1_EN); + // perimanClearPinBus(ETH_RMII_CRS_DV); + + _pin_rmii_clock = -1; + _pin_mcd = -1; + _pin_mdio = -1; + } + + if (_pin_power != -1) { + // perimanClearPinBus(_pin_power); + _pin_power = -1; + } +#endif /* CONFIG_ETH_USE_ESP32_EMAC */ + if (_pin_cs != -1) { + // perimanClearPinBus(_pin_cs); + _pin_cs = -1; + } + if (_pin_irq != -1) { + // perimanClearPinBus(_pin_irq); + _pin_irq = -1; + } + if (_pin_sck != -1) { + // perimanClearPinBus(_pin_sck); + _pin_sck = -1; + } + if (_pin_miso != -1) { + // perimanClearPinBus(_pin_miso); + _pin_miso = -1; + } + if (_pin_mosi != -1) { + // perimanClearPinBus(_pin_mosi); + _pin_mosi = -1; + } + if (_pin_rst != -1) { + // perimanClearPinBus(_pin_rst); + _pin_rst = -1; + } +} + +bool ETHClass2::config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1, IPAddress dns2) +{ + if (_esp_netif == NULL) { + return false; + } + esp_err_t err = ESP_OK; + esp_netif_ip_info_t info; + esp_netif_dns_info_t d1; + esp_netif_dns_info_t d2; + d1.ip.type = IPADDR_TYPE_V4; + d2.ip.type = IPADDR_TYPE_V4; + + if (static_cast(local_ip) != 0) { + info.ip.addr = static_cast(local_ip); + info.gw.addr = static_cast(gateway); + info.netmask.addr = static_cast(subnet); + d1.ip.u_addr.ip4.addr = static_cast(dns1); + d2.ip.u_addr.ip4.addr = static_cast(dns2); + } else { + info.ip.addr = 0; + info.gw.addr = 0; + info.netmask.addr = 0; + d1.ip.u_addr.ip4.addr = 0; + d2.ip.u_addr.ip4.addr = 0; + } + + // Stop DHCPC + err = esp_netif_dhcpc_stop(_esp_netif); + if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { + log_e("DHCP could not be stopped! Error: %d", err); + return false; + } + + // Set IPv4, Netmask, Gateway + err = esp_netif_set_ip_info(_esp_netif, &info); + if (err != ERR_OK) { + log_e("ETH IP could not be configured! Error: %d", err); + return false; + } + + // Set DNS1-Server + esp_netif_set_dns_info(_esp_netif, ESP_NETIF_DNS_MAIN, &d1); + + // Set DNS2-Server + esp_netif_set_dns_info(_esp_netif, ESP_NETIF_DNS_BACKUP, &d2); + + // Start DHCPC if static IP was set + if (info.ip.addr == 0) { + err = esp_netif_dhcpc_start(_esp_netif); + if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { + log_w("DHCP could not be started! Error: %d", err); + return false; + } + } + + return true; +} + +IPAddress ETHClass2::localIP() +{ + if (_esp_netif == NULL) { + return IPAddress(); + } + esp_netif_ip_info_t ip; + if (esp_netif_get_ip_info(_esp_netif, &ip)) { + return IPAddress(); + } + return IPAddress(ip.ip.addr); +} + +IPAddress ETHClass2::subnetMask() +{ + if (_esp_netif == NULL) { + return IPAddress(); + } + esp_netif_ip_info_t ip; + if (esp_netif_get_ip_info(_esp_netif, &ip)) { + return IPAddress(); + } + return IPAddress(ip.netmask.addr); +} + +IPAddress ETHClass2::gatewayIP() +{ + if (_esp_netif == NULL) { + return IPAddress(); + } + esp_netif_ip_info_t ip; + if (esp_netif_get_ip_info(_esp_netif, &ip)) { + return IPAddress(); + } + return IPAddress(ip.gw.addr); +} + +IPAddress ETHClass2::dnsIP(uint8_t dns_no) +{ + if (_esp_netif == NULL) { + return IPAddress(); + } + esp_netif_dns_info_t d; + if (esp_netif_get_dns_info(_esp_netif, dns_no ? ESP_NETIF_DNS_BACKUP : ESP_NETIF_DNS_MAIN, &d) != ESP_OK) { + return IPAddress(); + } + return IPAddress(d.ip.u_addr.ip4.addr); +} + +IPAddress ETHClass2::broadcastIP() +{ + if (_esp_netif == NULL) { + return IPAddress(); + } + esp_netif_ip_info_t ip; + if (esp_netif_get_ip_info(_esp_netif, &ip)) { + return IPAddress(); + } + return WiFiGenericClass::calculateBroadcast(IPAddress(ip.gw.addr), IPAddress(ip.netmask.addr)); +} + +IPAddress ETHClass2::networkID() +{ + if (_esp_netif == NULL) { + return IPAddress(); + } + esp_netif_ip_info_t ip; + if (esp_netif_get_ip_info(_esp_netif, &ip)) { + return IPAddress(); + } + return WiFiGenericClass::calculateNetworkID(IPAddress(ip.gw.addr), IPAddress(ip.netmask.addr)); +} + +uint8_t ETHClass2::subnetCIDR() +{ + if (_esp_netif == NULL) { + return (uint8_t)0; + } + esp_netif_ip_info_t ip; + if (esp_netif_get_ip_info(_esp_netif, &ip)) { + return (uint8_t)0; + } + return WiFiGenericClass::calculateSubnetCIDR(IPAddress(ip.netmask.addr)); +} + +const char *ETHClass2::getHostname() +{ + if (_esp_netif == NULL) { + return ""; + } + const char *hostname; + if (esp_netif_get_hostname(_esp_netif, &hostname)) { + return NULL; + } + return hostname; +} + +bool ETHClass2::setHostname(const char *hostname) +{ + if (_esp_netif == NULL) { + return false; + } + return esp_netif_set_hostname(_esp_netif, hostname) == 0; +} + +bool ETHClass2::enableIpV6() +{ + if (_esp_netif == NULL) { + return false; + } + return esp_netif_create_ip6_linklocal(_esp_netif) == 0; +} + +IPv6Address ETHClass2::localIPv6() +{ + if (_esp_netif == NULL) { + return IPv6Address(); + } + static esp_ip6_addr_t addr; + if (esp_netif_get_ip6_linklocal(_esp_netif, &addr)) { + return IPv6Address(); + } + return IPv6Address(addr.addr); +} + +const char *ETHClass2::ifkey(void) +{ + if (_esp_netif == NULL) { + return ""; + } + return esp_netif_get_ifkey(_esp_netif); +} + +const char *ETHClass2::desc(void) +{ + if (_esp_netif == NULL) { + return ""; + } + return esp_netif_get_desc(_esp_netif); +} + +String ETHClass2::impl_name(void) +{ + if (_esp_netif == NULL) { + return String(""); + } + char netif_name[8]; + esp_err_t err = esp_netif_get_netif_impl_name(_esp_netif, netif_name); + if (err != ESP_OK) { + log_e("Failed to get netif impl_name: %d", err); + return String(""); + } + return String(netif_name); +} + +bool ETHClass2::connected() +{ + return WiFiGenericClass::getStatusBits() & ETH_CONNECTED_BIT; +} + +bool ETHClass2::hasIP() +{ + return WiFiGenericClass::getStatusBits() & ETH_HAS_IP_BIT; +} + +bool ETHClass2::linkUp() +{ + if (_esp_netif == NULL) { + return false; + } + return esp_netif_is_netif_up(_esp_netif); +} + +bool ETHClass2::fullDuplex() +{ + if (_eth_handle == NULL) { + return false; + } + eth_duplex_t link_duplex; + esp_eth_ioctl(_eth_handle, ETH_CMD_G_DUPLEX_MODE, &link_duplex); + return (link_duplex == ETH_DUPLEX_FULL); +} + +bool ETHClass2::autoNegotiation() +{ + if (_eth_handle == NULL) { + return false; + } + bool auto_nego = false;; + // esp_eth_ioctl(_eth_handle, ETH_CMD_G_AUTONEGO, &auto_nego); + return auto_nego; +} + +uint32_t ETHClass2::phyAddr() +{ + if (_eth_handle == NULL) { + return 0; + } + uint32_t phy_addr; + esp_eth_ioctl(_eth_handle, ETH_CMD_G_PHY_ADDR, &phy_addr); + return phy_addr; +} + +uint8_t ETHClass2::linkSpeed() +{ + if (_eth_handle == NULL) { + return 0; + } + eth_speed_t link_speed; + esp_eth_ioctl(_eth_handle, ETH_CMD_G_SPEED, &link_speed); + return (link_speed == ETH_SPEED_10M) ? 10 : 100; +} + +uint8_t *ETHClass2::macAddress(uint8_t *mac) +{ + if (_eth_handle == NULL) { + return NULL; + } + if (!mac) { + return NULL; + } + esp_eth_ioctl(_eth_handle, ETH_CMD_G_MAC_ADDR, mac); + return mac; +} + +String ETHClass2::macAddress(void) +{ + uint8_t mac[6] = {0, 0, 0, 0, 0, 0}; + char macStr[18] = { 0 }; + macAddress(mac); + sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return String(macStr); +} + +void ETHClass2::printInfo(Print &out) +{ + out.print(desc()); + out.print(":"); + if (linkUp()) { + out.print(" "); + + out.print(" "); + out.print("ether "); + out.print(macAddress()); + out.printf(" phy 0x%lX", phyAddr()); + out.println(); + + out.print(" "); + out.print("inet "); + out.print(localIP()); + out.print(" netmask "); + out.print(subnetMask()); + out.print(" broadcast "); + out.print(broadcastIP()); + out.println(); + + out.print(" "); + out.print("gateway "); + out.print(gatewayIP()); + out.print(" dns "); + out.print(dnsIP()); + out.println(); + + out.println(); +} + +ETHClass2 ETH2; diff --git a/lib/ETHClass2/src/ETHClass2.h b/lib/ETHClass2/src/ETHClass2.h new file mode 100644 index 000000000..8c4685fbd --- /dev/null +++ b/lib/ETHClass2/src/ETHClass2.h @@ -0,0 +1,219 @@ +/* + ETH.h - espre ETH PHY support. + Based on WiFi.h from Ardiono WiFi shield library. + Copyright (c) 2011-2014 Arduino. All right reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _ETH_H_ +#define _ETH_H_ + +// +// Example configurations for pins_arduino.h to allow starting with ETH.begin(); +// + +// // Example RMII LAN8720 (Olimex, etc.) +// #define ETH_PHY_TYPE ETH_PHY_LAN8720 +// #define ETH_PHY_ADDR 0 +// #define ETH_PHY_MDC 23 +// #define ETH_PHY_MDIO 18 +// #define ETH_PHY_POWER -1 +// #define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN + +// // Example RMII ESP32_Ethernet_V4 +// #define ETH_PHY_TYPE ETH_PHY_TLK110 +// #define ETH_PHY_ADDR 1 +// #define ETH_PHY_MDC 23 +// #define ETH_PHY_MDIO 18 +// #define ETH_PHY_POWER -1 +// #define ETH_CLK_MODE ETH_CLOCK_GPIO0_OUT + +// // Example SPI using ESP-IDF's driver +// #define ETH_PHY_TYPE ETH_PHY_W5500 +// #define ETH_PHY_ADDR 1 +// #define ETH_PHY_CS 15 +// #define ETH_PHY_IRQ 4 +// #define ETH_PHY_RST 5 +// #define ETH_PHY_SPI_HOST SPI2_HOST +// #define ETH_PHY_SPI_SCK 14 +// #define ETH_PHY_SPI_MISO 12 +// #define ETH_PHY_SPI_MOSI 13 + +// // Example SPI using Arduino's driver +// #define ETH_PHY_TYPE ETH_PHY_W5500 +// #define ETH_PHY_ADDR 1 +// #define ETH_PHY_CS 15 +// #define ETH_PHY_IRQ 4 +// #define ETH_PHY_RST 5 +// #define ETH_PHY_SPI SPI + +// This will be uncommented once custom SPI support is available in ESP-IDF +#define ETH_SPI_SUPPORTS_CUSTOM 0 +#include "WiFi.h" +#if ETH_SPI_SUPPORTS_CUSTOM +#include "SPI.h" +#endif +#include "esp_system.h" +#include "esp_eth.h" +#include "esp_netif.h" +#include "hal/spi_types.h" + +#if CONFIG_ETH_USE_ESP32_EMAC +#define ETH_PHY_IP101 ETH_PHY_TLK110 +typedef enum { ETH_CLOCK_GPIO0_IN, ETH_CLOCK_GPIO0_OUT, ETH_CLOCK_GPIO16_OUT, ETH_CLOCK_GPIO17_OUT } eth_clock_mode_t; +//Dedicated GPIOs for RMII +#define ETH_RMII_TX_EN 21 +#define ETH_RMII_TX0 19 +#define ETH_RMII_TX1 22 +#define ETH_RMII_RX0 25 +#define ETH_RMII_RX1_EN 26 +#define ETH_RMII_CRS_DV 27 +#endif /* CONFIG_ETH_USE_ESP32_EMAC */ + +#ifndef ETH_PHY_SPI_FREQ_MHZ +#define ETH_PHY_SPI_FREQ_MHZ 20 +#endif /* ETH_PHY_SPI_FREQ_MHZ */ + +typedef enum { +#if CONFIG_ETH_USE_ESP32_EMAC + ETH_PHY_LAN8720, ETH_PHY_TLK110, ETH_PHY_RTL8201, ETH_PHY_DP83848, ETH_PHY_KSZ8041, ETH_PHY_KSZ8081, +#endif /* CONFIG_ETH_USE_ESP32_EMAC */ +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + ETH_PHY_DM9051, +#endif +#if CONFIG_ETH_SPI_ETHERNET_W5500 + ETH_PHY_W5500, +#endif +#if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL + ETH_PHY_KSZ8851, +#endif + ETH_PHY_MAX +} eth_phy_type_t; + +class ETHClass2 { + public: + ETHClass2(uint8_t eth_index=0); + ~ETHClass2(); + +#if CONFIG_ETH_USE_ESP32_EMAC + bool begin(eth_phy_type_t type, uint8_t phy_addr, int mdc, int mdio, int power, eth_clock_mode_t clk_mode); +#endif /* CONFIG_ETH_USE_ESP32_EMAC */ +#if ETH_SPI_SUPPORTS_CUSTOM + bool begin(eth_phy_type_t type, uint8_t phy_addr, int cs, int irq, int rst, SPIClass &spi, uint8_t spi_freq_mhz=ETH_PHY_SPI_FREQ_MHZ); +#endif + bool begin(eth_phy_type_t type, uint8_t phy_addr, int cs, int irq, int rst, spi_host_device_t spi_host, int sck=-1, int miso=-1, int mosi=-1, uint8_t spi_freq_mhz=ETH_PHY_SPI_FREQ_MHZ); + + bool begin(){ +#if defined(ETH_PHY_TYPE) && defined(ETH_PHY_ADDR) + #if defined(CONFIG_ETH_USE_ESP32_EMAC) && defined(ETH_PHY_POWER) && defined(ETH_PHY_MDC) && defined(ETH_PHY_MDIO) && defined(ETH_CLK_MODE) + return begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_POWER, ETH_CLK_MODE); + #elif defined(ETH_PHY_CS) && defined(ETH_PHY_IRQ) && defined(ETH_PHY_RST) + #if ETH_SPI_SUPPORTS_CUSTOM && defined(ETH_PHY_SPI) + return begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, ETH_PHY_SPI, ETH_PHY_SPI_FREQ_MHZ); + #elif defined(ETH_PHY_SPI_HOST) && defined(ETH_PHY_SPI_SCK) && defined(ETH_PHY_SPI_MISO) && defined(ETH_PHY_SPI_MOSI) + return begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, ETH_PHY_SPI_HOST, ETH_PHY_SPI_SCK, ETH_PHY_SPI_MISO, ETH_PHY_SPI_MOSI, ETH_PHY_SPI_FREQ_MHZ); + #endif + #endif +#endif + return false; + } + + void end(); + + // Netif APIs + esp_netif_t * netif(void){ return _esp_netif; } + bool config(IPAddress local_ip = (uint32_t)0x00000000, IPAddress gateway = (uint32_t)0x00000000, IPAddress subnet = (uint32_t)0x00000000, IPAddress dns1 = (uint32_t)0x00000000, IPAddress dns2 = (uint32_t)0x00000000); + const char * getHostname(); + bool setHostname(const char * hostname); + IPAddress localIP(); + IPAddress subnetMask(); + IPAddress gatewayIP(); + IPAddress dnsIP(uint8_t dns_no = 0); + IPAddress broadcastIP(); + IPAddress networkID(); + uint8_t subnetCIDR(); + bool enableIpV6(); + IPv6Address localIPv6(); + const char * ifkey(void); + const char * desc(void); + String impl_name(void); + + // Event based getters + bool connected(); + bool hasIP(); + + // ETH Handle APIs + uint8_t * macAddress(uint8_t* mac); + String macAddress(); + bool fullDuplex(); + bool linkUp(); + uint8_t linkSpeed(); + bool autoNegotiation(); + uint32_t phyAddr(); + + // Info APIs + void printInfo(Print & out); + + friend class WiFiClient; + friend class WiFiServer; + +#if ETH_SPI_SUPPORTS_CUSTOM + static esp_err_t _eth_spi_read(void *ctx, uint32_t cmd, uint32_t addr, void *data, uint32_t data_len); + static esp_err_t _eth_spi_write(void *ctx, uint32_t cmd, uint32_t addr, const void *data, uint32_t data_len); +#endif + + protected: +#if ETH_SPI_SUPPORTS_CUSTOM + esp_err_t eth_spi_read(uint32_t cmd, uint32_t addr, void *data, uint32_t data_len); + esp_err_t eth_spi_write(uint32_t cmd, uint32_t addr, const void *data, uint32_t data_len); +#endif + + static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); + + private: + bool _eth_started; + esp_eth_handle_t _eth_handle; + esp_netif_t *_esp_netif; + uint8_t _eth_index; + eth_phy_type_t _phy_type; +#if ETH_SPI_SUPPORTS_CUSTOM + SPIClass * _spi; +#endif + uint8_t _spi_freq_mhz; + int8_t _pin_cs; + int8_t _pin_irq; + int8_t _pin_rst; + int8_t _pin_sck; + int8_t _pin_miso; + int8_t _pin_mosi; +#if CONFIG_ETH_USE_ESP32_EMAC + int8_t _pin_mcd; + int8_t _pin_mdio; + int8_t _pin_power; + int8_t _pin_rmii_clock; +#endif /* CONFIG_ETH_USE_ESP32_EMAC */ + + static bool ethDetachBus(void * bus_pointer); + bool beginSPI(eth_phy_type_t type, uint8_t phy_addr, int cs, int irq, int rst, +#if ETH_SPI_SUPPORTS_CUSTOM + SPIClass * spi, +#endif + int sck, int miso, int mosi, spi_host_device_t spi_host, uint8_t spi_freq_mhz); +}; + +extern ETHClass2 ETH2; + +#endif /* _ETH_H_ */ diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/esp32/SerialWifiInterface.cpp index 462e3ecc3..eb62457ac 100644 --- a/src/helpers/esp32/SerialWifiInterface.cpp +++ b/src/helpers/esp32/SerialWifiInterface.cpp @@ -2,10 +2,9 @@ #include void SerialWifiInterface::begin(int port) { - // wifi setup is handled outside of this class, only starts the server - server.begin(port); + server = WiFiServer(port); + server.begin(); } - // ---------- public methods void SerialWifiInterface::enable() { if (_isEnabled) return; @@ -169,4 +168,5 @@ size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) { bool SerialWifiInterface::isConnected() const { return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; -} \ No newline at end of file +} + From 332a8f49081a3ae227cb78ded86491ad81352763 Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Mon, 30 Mar 2026 15:31:14 +0200 Subject: [PATCH 03/11] Add TCP console for remote management via telnet/netcat --- examples/companion_radio/main.cpp | 20 ++- examples/simple_repeater/main.cpp | 13 ++ examples/simple_room_server/main.cpp | 19 ++- src/helpers/esp32/TCPConsole.h | 142 ++++++++++++++++++ .../lilygo_t_eth_elite_sx1262/platformio.ini | 8 +- .../lilygo_t_eth_elite_sx1276/platformio.ini | 8 +- 6 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 src/helpers/esp32/TCPConsole.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 6bf090224..d9a1c3df2 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2,6 +2,11 @@ #include #include "MyMesh.h" +#if defined(ESP32) && defined(TCP_CONSOLE_PORT) && defined(ADMIN_PASSWORD) + #include + TCPConsole tcp_console(ADMIN_PASSWORD, ADVERT_NAME); +#endif + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -109,6 +114,10 @@ void setup() { Serial.begin(115200); board.begin(); + + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) && defined(ADMIN_PASSWORD) + tcp_console.begin(); + #endif #ifdef DISPLAY_CLASS DisplayDriver* disp = NULL; @@ -227,9 +236,14 @@ void setup() { void loop() { the_mesh.loop(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) && defined(ADMIN_PASSWORD) + tcp_console.loop(the_mesh); + #endif + sensors.loop(); -#ifdef DISPLAY_CLASS - ui_task.loop(); -#endif + #ifdef DISPLAY_CLASS + ui_task.loop(); + #endif + rtc_clock.tick(); } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5..f00b12199 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -3,6 +3,11 @@ #include "MyMesh.h" +#if defined(ESP32) && defined(TCP_CONSOLE_PORT) + #include + TCPConsole tcp_console(ADMIN_PASSWORD, ADVERT_NAME); +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -34,6 +39,10 @@ void setup() { board.begin(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + tcp_console.begin(); + #endif + #if defined(MESH_DEBUG) && defined(NRF52_PLATFORM) // give some extra time for serial to settle so // boot debug messages can be seen on terminal @@ -148,6 +157,10 @@ void loop() { #endif the_mesh.loop(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + tcp_console.loop(the_mesh); + #endif + sensors.loop(); #ifdef DISPLAY_CLASS ui_task.loop(); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007d..1237c5758 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -3,6 +3,11 @@ #include "MyMesh.h" +#if defined(ESP32) && defined(TCP_CONSOLE_PORT) + #include + TCPConsole tcp_console(ADMIN_PASSWORD, ADVERT_NAME); +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -24,6 +29,10 @@ void setup() { board.begin(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + tcp_console.begin(); + #endif + #ifdef DISPLAY_CLASS if (display.begin()) { display.startFrame(); @@ -108,9 +117,13 @@ void loop() { } the_mesh.loop(); + #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + tcp_console.loop(the_mesh); + #endif + sensors.loop(); -#ifdef DISPLAY_CLASS - ui_task.loop(); -#endif + #ifdef DISPLAY_CLASS + ui_task.loop(); + #endif rtc_clock.tick(); } diff --git a/src/helpers/esp32/TCPConsole.h b/src/helpers/esp32/TCPConsole.h new file mode 100644 index 000000000..cbc98d266 --- /dev/null +++ b/src/helpers/esp32/TCPConsole.h @@ -0,0 +1,142 @@ +#pragma once + +#if defined(ESP32) && defined(TCP_CONSOLE_PORT) && defined(ADMIN_PASSWORD) + +#include +#include +#include +#include + +#ifndef TCP_CONSOLE_MAX_CLIENTS + #define TCP_CONSOLE_MAX_CLIENTS 2 +#endif + +#ifndef TCP_CONSOLE_TIMEOUT_MS + #define TCP_CONSOLE_TIMEOUT_MS 300000 // 5 minutes inactivity timeout +#endif + +class TCPConsole { + WiFiServer _server; + WiFiClient _clients[TCP_CONSOLE_MAX_CLIENTS]; + bool _authenticated[TCP_CONSOLE_MAX_CLIENTS]; + unsigned long _last_active[TCP_CONSOLE_MAX_CLIENTS]; + char _cmd_buf[TCP_CONSOLE_MAX_CLIENTS][160]; + int _cmd_len[TCP_CONSOLE_MAX_CLIENTS]; + const char* _password; + const char* _node_name; + + void sendToClient(int i, const char* msg) { + if (_clients[i] && _clients[i].connected()) { + _clients[i].print(msg); + } + } + + void disconnectClient(int i) { + _clients[i].stop(); + _authenticated[i] = false; + _cmd_buf[i][0] = 0; + _cmd_len[i] = 0; + } + +public: + TCPConsole(const char* password, const char* node_name) + : _server(TCP_CONSOLE_PORT), _password(password), _node_name(node_name) { + for (int i = 0; i < TCP_CONSOLE_MAX_CLIENTS; i++) { + _authenticated[i] = false; + _cmd_buf[i][0] = 0; + _cmd_len[i] = 0; + _last_active[i] = 0; + } + } + + void begin() { + _server.begin(); + Serial.printf("TCP Console listening on port %d\n", TCP_CONSOLE_PORT); + } + + // Call this from loop(), passing the mesh's handleCommand function + template + void loop(T& mesh) { + // Accept new clients + WiFiClient newClient = _server.available(); + if (newClient) { + for (int i = 0; i < TCP_CONSOLE_MAX_CLIENTS; i++) { + if (!_clients[i] || !_clients[i].connected()) { + _clients[i] = newClient; + _authenticated[i] = false; + _cmd_buf[i][0] = 0; + _cmd_len[i] = 0; + _last_active[i] = millis(); + sendToClient(i, "MeshCore Console\r\nPassword: "); + break; + } + } + } + + // Handle connected clients + for (int i = 0; i < TCP_CONSOLE_MAX_CLIENTS; i++) { + if (!_clients[i] || !_clients[i].connected()) continue; + + // Inactivity timeout + if (millis() - _last_active[i] > TCP_CONSOLE_TIMEOUT_MS) { + sendToClient(i, "\r\nTimeout. Disconnecting.\r\n"); + disconnectClient(i); + continue; + } + + // Read available data + while (_clients[i].available()) { + _last_active[i] = millis(); + char c = _clients[i].read(); + + if (c == '\n') continue; // ignore LF, handle CR only + + if (c != '\r' && _cmd_len[i] < 158) { + _cmd_buf[i][_cmd_len[i]++] = c; + _cmd_buf[i][_cmd_len[i]] = 0; + if (!_authenticated[i]) { + sendToClient(i, "*"); // mask password input + } else { + _clients[i].print(c); // echo command + } + continue; + } + + // Got CR — process command + sendToClient(i, "\r\n"); + _cmd_buf[i][_cmd_len[i]] = 0; + + if (!_authenticated[i]) { + // Authentication + if (strcmp(_cmd_buf[i], _password) == 0) { + _authenticated[i] = true; + char welcome[80]; + snprintf(welcome, sizeof(welcome), "Welcome to %s console.\r\n> ", _node_name); + sendToClient(i, welcome); + } else { + sendToClient(i, "Wrong password. Disconnecting.\r\n"); + disconnectClient(i); + } + } else { + // Execute command + if (strlen(_cmd_buf[i]) > 0) { + char reply[160]; + reply[0] = 0; + mesh.handleCommand(0, _cmd_buf[i], reply); + if (reply[0]) { + sendToClient(i, " -> "); + sendToClient(i, reply); + sendToClient(i, "\r\n"); + } + } + sendToClient(i, "> "); + } + + _cmd_buf[i][0] = 0; + _cmd_len[i] = 0; + } + } + } +}; + +#endif // ESP32 && TCP_CONSOLE_PORT diff --git a/variants/lilygo_t_eth_elite_sx1262/platformio.ini b/variants/lilygo_t_eth_elite_sx1262/platformio.ini index f3f0c2cbd..08410cdee 100644 --- a/variants/lilygo_t_eth_elite_sx1262/platformio.ini +++ b/variants/lilygo_t_eth_elite_sx1262/platformio.ini @@ -49,7 +49,7 @@ lib_deps = extends = LilyGo_T_ETH_Elite_SX1262 build_flags = ${LilyGo_T_ETH_Elite_SX1262.build_flags} - -D ADVERT_NAME='"T-ETH Elite SX1262 Repeater"' + -D ADVERT_NAME='"T-ETH Elite SX1262 Repeater eth"' -D ADVERT_LAT=0 -D ADVERT_LON=0 -D ADMIN_PASSWORD='"password"' @@ -59,6 +59,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} +<../examples/simple_repeater> lib_deps = @@ -101,7 +102,7 @@ lib_deps = extends = LilyGo_T_ETH_Elite_SX1262 build_flags = ${LilyGo_T_ETH_Elite_SX1262.build_flags} - -D ADVERT_NAME='"T-ETH Elite SX1262 Room"' + -D ADVERT_NAME='"T-ETH Elite SX1262 Room eth"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' @@ -111,6 +112,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} +<../examples/simple_room_server> lib_deps = @@ -130,6 +132,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + + @@ -149,6 +152,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D WIFI_SSID='"WIFI_SSID"' -D WIFI_PWD='"Password"' + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + + diff --git a/variants/lilygo_t_eth_elite_sx1276/platformio.ini b/variants/lilygo_t_eth_elite_sx1276/platformio.ini index 512c650bb..3b6184cd6 100644 --- a/variants/lilygo_t_eth_elite_sx1276/platformio.ini +++ b/variants/lilygo_t_eth_elite_sx1276/platformio.ini @@ -48,7 +48,7 @@ lib_deps = extends = LilyGo_T_ETH_Elite_SX1276 build_flags = ${LilyGo_T_ETH_Elite_SX1276.build_flags} - -D ADVERT_NAME='"T-ETH Elite SX1276 Repeater"' + -D ADVERT_NAME='"T-ETH Elite SX1276 Repeater eth"' -D ADVERT_LAT=0 -D ADVERT_LON=0 -D ADMIN_PASSWORD='"password"' @@ -58,6 +58,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} +<../examples/simple_repeater> lib_deps = @@ -100,7 +101,7 @@ lib_deps = extends = LilyGo_T_ETH_Elite_SX1276 build_flags = ${LilyGo_T_ETH_Elite_SX1276.build_flags} - -D ADVERT_NAME='"T-ETH Elite SX1276 Room"' + -D ADVERT_NAME='"T-ETH Elite SX1276 Room eth"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' @@ -110,6 +111,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} +<../examples/simple_room_server> lib_deps = @@ -129,6 +131,7 @@ build_flags = -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + + @@ -148,6 +151,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D WIFI_SSID='"WIFI_SSID"' -D WIFI_PWD='"Password"' + -D TCP_CONSOLE_PORT=4242 build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + + From 01df266b2cd0287297ab69e81d855d6f05a0536a Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Mon, 30 Mar 2026 23:04:49 +0200 Subject: [PATCH 04/11] Fix UITask.cpp compilation errors on ESP32-S3 with PIN_USER_BTN_ANA --- examples/companion_radio/ui-new/UITask.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 94a8ee3ef..c98f3eadc 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -740,8 +740,8 @@ void UITask::loop() { } #endif #if defined(PIN_USER_BTN_ANA) - if (abs(millis() - _analogue_pin_read_millis) > 10) { - ev = analog_btn.check(); + if ((long)(millis() - _analogue_pin_read_millis) > 10) { + int ev = analog_btn.check(); if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_NEXT); } else if (ev == BUTTON_EVENT_LONG_PRESS) { From 08198e968eb25817ca7407a1a138b9e14e1a4ca9 Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Tue, 31 Mar 2026 08:52:00 +0200 Subject: [PATCH 05/11] Fix analog_btn and UITask.cpp ESP32-S3 compilation errors --- src/helpers/esp32/TEthEliteBoard_SX1262.h | 2 +- src/helpers/esp32/TEthEliteBoard_SX1276.h | 2 +- variants/lilygo_t_eth_elite_sx1262/target.cpp | 1 + variants/lilygo_t_eth_elite_sx1262/target.h | 6 ++++-- variants/lilygo_t_eth_elite_sx1276/target.cpp | 1 + variants/lilygo_t_eth_elite_sx1276/target.h | 6 ++++-- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/helpers/esp32/TEthEliteBoard_SX1262.h b/src/helpers/esp32/TEthEliteBoard_SX1262.h index 2105aa2dc..545680256 100644 --- a/src/helpers/esp32/TEthEliteBoard_SX1262.h +++ b/src/helpers/esp32/TEthEliteBoard_SX1262.h @@ -34,7 +34,7 @@ #define PIN_GPS_EN -1 // Analog button -#define PIN_USER_BTN_ANA 7 +// #define PIN_USER_BTN_ANA 7 // Include headers AFTER pin definitions so ESP32Board::sleep() can use P_LORA_DIO_1 #include diff --git a/src/helpers/esp32/TEthEliteBoard_SX1276.h b/src/helpers/esp32/TEthEliteBoard_SX1276.h index a18d76258..a6d77701e 100644 --- a/src/helpers/esp32/TEthEliteBoard_SX1276.h +++ b/src/helpers/esp32/TEthEliteBoard_SX1276.h @@ -34,7 +34,7 @@ #define PIN_GPS_EN -1 // Analog button -#define PIN_USER_BTN_ANA 7 +// #define PIN_USER_BTN_ANA 7 // Include headers AFTER pin definitions so ESP32Board::sleep() can use P_LORA_DIO_0 #include diff --git a/variants/lilygo_t_eth_elite_sx1262/target.cpp b/variants/lilygo_t_eth_elite_sx1262/target.cpp index b6ce2c49d..c9d9e1897 100644 --- a/variants/lilygo_t_eth_elite_sx1262/target.cpp +++ b/variants/lilygo_t_eth_elite_sx1262/target.cpp @@ -9,6 +9,7 @@ TEthEliteBoard board; #endif MomentaryButton user_btn(PIN_USER_BTN_ANA, 1000, true); +MomentaryButton analog_btn(PIN_USER_BTN_ANA, 1000, true); // alias for UITask analog button support static SPIClass spi; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); diff --git a/variants/lilygo_t_eth_elite_sx1262/target.h b/variants/lilygo_t_eth_elite_sx1262/target.h index e3635b571..674b95af5 100644 --- a/variants/lilygo_t_eth_elite_sx1262/target.h +++ b/variants/lilygo_t_eth_elite_sx1262/target.h @@ -1,7 +1,5 @@ #pragma once -#define HAS_ETHERNET 1 - #define RADIOLIB_STATIC_ONLY 1 #include #include @@ -13,6 +11,10 @@ extern MomentaryButton user_btn; +#ifdef PIN_USER_BTN_ANA + extern MomentaryButton analog_btn; +#endif + #ifdef DISPLAY_CLASS #include extern DISPLAY_CLASS display; diff --git a/variants/lilygo_t_eth_elite_sx1276/target.cpp b/variants/lilygo_t_eth_elite_sx1276/target.cpp index a311c1d7e..03d6d38d1 100644 --- a/variants/lilygo_t_eth_elite_sx1276/target.cpp +++ b/variants/lilygo_t_eth_elite_sx1276/target.cpp @@ -9,6 +9,7 @@ TEthEliteBoard board; #endif MomentaryButton user_btn(PIN_USER_BTN_ANA, 1000, true); +MomentaryButton analog_btn(PIN_USER_BTN_ANA, 1000, true); // alias for UITask analog button support static SPIClass spi; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi); diff --git a/variants/lilygo_t_eth_elite_sx1276/target.h b/variants/lilygo_t_eth_elite_sx1276/target.h index 4861b535a..3685c5061 100644 --- a/variants/lilygo_t_eth_elite_sx1276/target.h +++ b/variants/lilygo_t_eth_elite_sx1276/target.h @@ -1,7 +1,5 @@ #pragma once -#define HAS_ETHERNET 1 - #define RADIOLIB_STATIC_ONLY 1 #include #include @@ -13,6 +11,10 @@ extern MomentaryButton user_btn; +#ifdef PIN_USER_BTN_ANA + extern MomentaryButton analog_btn; +#endif + #ifdef DISPLAY_CLASS #include extern DISPLAY_CLASS display; From 191b3d983a3f18250b93bf39fc368e38baa4dafc Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Mon, 30 Mar 2026 14:53:20 +0200 Subject: [PATCH 06/11] Add OTA support via Ethernet for T-ETH-Elite --- src/helpers/ESP32Board.cpp | 23 +++++++++++++++++------ src/helpers/esp32/TEthEliteBoard.cpp | 3 +++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/helpers/ESP32Board.cpp b/src/helpers/ESP32Board.cpp index e0ca1d0ee..de09e0f92 100644 --- a/src/helpers/ESP32Board.cpp +++ b/src/helpers/ESP32Board.cpp @@ -3,18 +3,29 @@ #include "ESP32Board.h" #if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only -#include #include #include #include - #include +#if defined(USE_ETHERNET) + extern String eth_local_ip; // defined in TEthEliteBoard.cpp +#else + #include +#endif + bool ESP32Board::startOTAUpdate(const char* id, char reply[]) { - inhibit_sleep = true; // prevent sleep during OTA - WiFi.softAP("MeshCore-OTA", NULL); + inhibit_sleep = true; +#if defined(USE_ETHERNET) + Serial.println("OTA: using ETH"); + sprintf(reply, "Started: http://%s/update", eth_local_ip.c_str()); +#else + Serial.println("OTA: using WiFi SoftAP"); + WiFi.softAP("MeshCore-OTA", NULL); sprintf(reply, "Started: http://%s/update", WiFi.softAPIP().toString().c_str()); +#endif + MESH_DEBUG_PRINTLN("startOTAUpdate: %s", reply); static char id_buf[60]; @@ -32,7 +43,7 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) { }); AsyncElegantOTA.setID(id_buf); - AsyncElegantOTA.begin(server); // Start ElegantOTA + AsyncElegantOTA.begin(server); server->begin(); return true; @@ -44,4 +55,4 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) { } #endif -#endif +#endif // ESP_PLATFORM diff --git a/src/helpers/esp32/TEthEliteBoard.cpp b/src/helpers/esp32/TEthEliteBoard.cpp index 42c669a52..0e508ea6b 100644 --- a/src/helpers/esp32/TEthEliteBoard.cpp +++ b/src/helpers/esp32/TEthEliteBoard.cpp @@ -14,6 +14,7 @@ extern MomentaryButton user_btn; uint32_t deviceOnline = 0x00; static SPIClass spi_eth(FSPI); static ETHClass2 ETH; +String eth_local_ip; // global, accessible from ESP32Board.cpp via extern void TEthEliteBoard::begin() { ESP32Board::begin(); @@ -152,6 +153,8 @@ void TEthEliteBoard::startEthernet() { ETH.config(IPAddress(192, 168, 4, 2), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); } + eth_local_ip = ETH.localIP().toString(); // save IP for OTA use + uint8_t mac[6]; ETH.macAddress(mac); Serial.printf("ETH MAC %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); From 67460eec5043c8ed03e28b6c0fae59d8eada4b8a Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Tue, 31 Mar 2026 17:23:47 +0200 Subject: [PATCH 07/11] Fix TCPConsole to use runtime NodePrefs password instead of compile-time ADMIN_PASSWORD --- examples/companion_radio/main.cpp | 5 ++++- examples/simple_repeater/main.cpp | 4 +++- examples/simple_room_server/main.cpp | 4 +++- src/helpers/esp32/TCPConsole.h | 18 ++++++++++-------- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d9a1c3df2..a5c11fc11 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -4,7 +4,7 @@ #if defined(ESP32) && defined(TCP_CONSOLE_PORT) && defined(ADMIN_PASSWORD) #include - TCPConsole tcp_console(ADMIN_PASSWORD, ADVERT_NAME); + TCPConsole tcp_console(nullptr); // prefs set in setup() #endif // Believe it or not, this std C function is busted on some platforms! @@ -152,6 +152,9 @@ void setup() { #endif store.begin(); the_mesh.begin( + + tcp_console.setPrefs(the_mesh.getNodePrefs()); + #ifdef DISPLAY_CLASS disp != NULL #else diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index f00b12199..1d3e5d673 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -5,7 +5,7 @@ #if defined(ESP32) && defined(TCP_CONSOLE_PORT) #include - TCPConsole tcp_console(ADMIN_PASSWORD, ADVERT_NAME); + TCPConsole tcp_console(nullptr); // prefs set in setup() #endif #ifdef DISPLAY_CLASS @@ -104,6 +104,8 @@ void setup() { the_mesh.begin(fs); + tcp_console.setPrefs(the_mesh.getNodePrefs()); + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 1237c5758..bda105a00 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -5,7 +5,7 @@ #if defined(ESP32) && defined(TCP_CONSOLE_PORT) #include - TCPConsole tcp_console(ADMIN_PASSWORD, ADVERT_NAME); + TCPConsole tcp_console(nullptr); // prefs set in setup() #endif #ifdef DISPLAY_CLASS @@ -81,6 +81,8 @@ void setup() { the_mesh.begin(fs); + tcp_console.setPrefs(the_mesh.getNodePrefs()); + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif diff --git a/src/helpers/esp32/TCPConsole.h b/src/helpers/esp32/TCPConsole.h index cbc98d266..9e4bd797e 100644 --- a/src/helpers/esp32/TCPConsole.h +++ b/src/helpers/esp32/TCPConsole.h @@ -6,6 +6,7 @@ #include #include #include +#include // for NodePrefs #ifndef TCP_CONSOLE_MAX_CLIENTS #define TCP_CONSOLE_MAX_CLIENTS 2 @@ -22,8 +23,7 @@ class TCPConsole { unsigned long _last_active[TCP_CONSOLE_MAX_CLIENTS]; char _cmd_buf[TCP_CONSOLE_MAX_CLIENTS][160]; int _cmd_len[TCP_CONSOLE_MAX_CLIENTS]; - const char* _password; - const char* _node_name; + NodePrefs* _prefs; // pointer to live NodePrefs — password is read at runtime void sendToClient(int i, const char* msg) { if (_clients[i] && _clients[i].connected()) { @@ -39,8 +39,8 @@ class TCPConsole { } public: - TCPConsole(const char* password, const char* node_name) - : _server(TCP_CONSOLE_PORT), _password(password), _node_name(node_name) { + TCPConsole(NodePrefs* prefs) + : _server(TCP_CONSOLE_PORT), _prefs(prefs) { for (int i = 0; i < TCP_CONSOLE_MAX_CLIENTS; i++) { _authenticated[i] = false; _cmd_buf[i][0] = 0; @@ -54,6 +54,8 @@ class TCPConsole { Serial.printf("TCP Console listening on port %d\n", TCP_CONSOLE_PORT); } + void setPrefs(NodePrefs* prefs) { _prefs = prefs; } + // Call this from loop(), passing the mesh's handleCommand function template void loop(T& mesh) { @@ -107,11 +109,11 @@ class TCPConsole { _cmd_buf[i][_cmd_len[i]] = 0; if (!_authenticated[i]) { - // Authentication - if (strcmp(_cmd_buf[i], _password) == 0) { + // Authentication — always read from live NodePrefs, not compile-time constant + if (_prefs != nullptr && strcmp(_cmd_buf[i], _prefs->password) == 0) { _authenticated[i] = true; char welcome[80]; - snprintf(welcome, sizeof(welcome), "Welcome to %s console.\r\n> ", _node_name); + snprintf(welcome, sizeof(welcome), "Welcome to %s console.\r\n> ", _prefs->node_name); sendToClient(i, welcome); } else { sendToClient(i, "Wrong password. Disconnecting.\r\n"); @@ -139,4 +141,4 @@ class TCPConsole { } }; -#endif // ESP32 && TCP_CONSOLE_PORT +#endif // ESP32 && TCP_CONSOLE_PORT && ADMIN_PASSWORD From 3648fc97e4b7bd6e71656dbc1226da33fefebb4a Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Sun, 5 Apr 2026 12:04:08 +0200 Subject: [PATCH 08/11] fix: Add #ifdef PIN_USER_BTN_ANA guard for boards without analog button - Protect MomentaryButton instantiation with #ifdef PIN_USER_BTN_ANA - Use dummy pin 0 when PIN_USER_BTN_ANA is not defined - Allows compilation on boards without analog button support - Applied to both SX1262 and SX1276 variants --- variants/lilygo_t_eth_elite_sx1262/target.cpp | 8 ++++++++ variants/lilygo_t_eth_elite_sx1276/target.cpp | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/variants/lilygo_t_eth_elite_sx1262/target.cpp b/variants/lilygo_t_eth_elite_sx1262/target.cpp index c9d9e1897..c42236f3c 100644 --- a/variants/lilygo_t_eth_elite_sx1262/target.cpp +++ b/variants/lilygo_t_eth_elite_sx1262/target.cpp @@ -8,8 +8,16 @@ TEthEliteBoard board; DISPLAY_CLASS display; #endif +#ifdef PIN_USER_BTN_ANA +// If the analog pin is defined, use it for both button instances MomentaryButton user_btn(PIN_USER_BTN_ANA, 1000, true); MomentaryButton analog_btn(PIN_USER_BTN_ANA, 1000, true); // alias for UITask analog button support +#else +// If NOT defined, route user_btn to pin 0 +// and create analog_btn as a dummy alias to satisfy UITask requirements +MomentaryButton user_btn(0, 1000, true); +MomentaryButton analog_btn(0, 1000, true); +#endif static SPIClass spi; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); diff --git a/variants/lilygo_t_eth_elite_sx1276/target.cpp b/variants/lilygo_t_eth_elite_sx1276/target.cpp index 03d6d38d1..b0cb78bec 100644 --- a/variants/lilygo_t_eth_elite_sx1276/target.cpp +++ b/variants/lilygo_t_eth_elite_sx1276/target.cpp @@ -8,8 +8,16 @@ TEthEliteBoard board; DISPLAY_CLASS display; #endif +#ifdef PIN_USER_BTN_ANA +// If the analog pin is defined, use it for both button instances MomentaryButton user_btn(PIN_USER_BTN_ANA, 1000, true); MomentaryButton analog_btn(PIN_USER_BTN_ANA, 1000, true); // alias for UITask analog button support +#else +// If NOT defined, route user_btn to pin 0 +// and create analog_btn as a dummy alias to satisfy UITask requirements +MomentaryButton user_btn(0, 1000, true); +MomentaryButton analog_btn(0, 1000, true); +#endif static SPIClass spi; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi); From 7842b9bb6f70be766da94410a0203c87a4d64624 Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Sun, 5 Apr 2026 12:07:11 +0200 Subject: [PATCH 09/11] feat: Add Ethernet runtime configuration CLI - Add ip/gw/subnet/dns get/set commands with SPIFFS persistence - Implement reconfigureEthernet() method for runtime config - Fix TCP_CONSOLE_PORT conditional compilation - Update platformio.ini for consistency (USE_ETHERNET) - Document Ethernet settings and get bridge.type command - Fixes #2196 #2197 hardware and console dependencies --- .vscode/extensions.json | 3 +- docs/cli_commands.md | 67 ++++++++++++++ examples/simple_repeater/main.cpp | 17 +++- examples/simple_room_server/main.cpp | 11 ++- src/MeshCore.h | 1 + src/helpers/CommonCLI.cpp | 92 ++++++++++++++++++- src/helpers/CommonCLI.h | 6 ++ src/helpers/esp32/TEthEliteBoard.cpp | 32 ++++++- src/helpers/esp32/TEthEliteBoard_SX1262.h | 3 +- src/helpers/esp32/TEthEliteBoard_SX1276.h | 1 + .../lilygo_t_eth_elite_sx1262/platformio.ini | 28 +++++- .../lilygo_t_eth_elite_sx1276/platformio.ini | 26 +++++- 12 files changed, 271 insertions(+), 16 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 080e70d08..8057bc70a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format "recommendations": [ + "pioarduino.pioarduino-ide", "platformio.platformio-ide" ], "unwantedRecommendations": [ diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c66249505..ad0d6fa7d 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-use_ethernet-is-compiled-in) --- @@ -1019,3 +1020,69 @@ region save **Note:** Returns an error on boards without power management support. --- + +### Ethernet (When USE_ETHERNET is compiled in) + +#### View or change this node's Ethernet IP address +**Usage:** +- `get ip` +- `set ip
` + +**Parameters:** +- `address`: IPv4 address (e.g., `192.168.254.21`) + +**Set by build flag:** `ETH_STATIC_IP` + +**Default:** `0.0.0.0` (uses DHCP if not configured) + +**Note:** Requires reboot to apply. Set to `0.0.0.0` to revert to DHCP. If `ETH_STATIC_IP` is defined in build flags and DHCP fails, that compile-time address is used as fallback. + +--- + +#### View or change this node's Ethernet gateway +**Usage:** +- `get gw` +- `set gw
` + +**Parameters:** +- `address`: IPv4 gateway address (e.g., `192.168.254.254`) + +**Set by build flag:** `ETH_GATEWAY` + +**Default:** `0.0.0.0` + +**Note:** Requires reboot to apply + +--- + +#### View or change this node's Ethernet subnet mask +**Usage:** +- `get subnet` +- `set subnet
` + +**Parameters:** +- `address`: Subnet mask in dotted decimal notation (e.g., `255.255.255.0`) + +**Set by build flag:** `ETH_SUBNET` + +**Default:** `0.0.0.0` + +**Note:** Requires reboot to apply + +--- + +#### View or change this node's Ethernet DNS server +**Usage:** +- `get dns` +- `set dns
` + +**Parameters:** +- `address`: IPv4 DNS address (e.g., `8.8.8.8`) + +**Set by build flag:** `ETH_DNS` + +**Default:** `0.0.0.0` + +**Note:** Requires reboot to apply. Configures DNS1 (primary DNS server). + +--- diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 1d3e5d673..82e38b31b 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -3,7 +3,7 @@ #include "MyMesh.h" -#if defined(ESP32) && defined(TCP_CONSOLE_PORT) +#if defined(TCP_CONSOLE_PORT) #include TCPConsole tcp_console(nullptr); // prefs set in setup() #endif @@ -39,7 +39,7 @@ void setup() { board.begin(); - #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + #if defined(TCP_CONSOLE_PORT) tcp_console.begin(); #endif @@ -104,7 +104,18 @@ void setup() { the_mesh.begin(fs); - tcp_console.setPrefs(the_mesh.getNodePrefs()); +the_mesh.begin(fs); + +#ifdef USE_ETHERNET + NodePrefs* prefs = the_mesh.getNodePrefs(); + if (prefs->eth_ip != 0) { + board.reconfigureEthernet(prefs->eth_ip, prefs->eth_gateway, prefs->eth_subnet); + } +#endif + +#if defined(TCP_CONSOLE_PORT) + tcp_console.setPrefs(the_mesh.getNodePrefs()); +#endif #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index bda105a00..3535f4136 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -3,7 +3,7 @@ #include "MyMesh.h" -#if defined(ESP32) && defined(TCP_CONSOLE_PORT) +#if defined(TCP_CONSOLE_PORT) #include TCPConsole tcp_console(nullptr); // prefs set in setup() #endif @@ -81,7 +81,16 @@ void setup() { the_mesh.begin(fs); +#ifdef USE_ETHERNET + NodePrefs* prefs = the_mesh.getNodePrefs(); + if (prefs->eth_ip != 0) { + board.reconfigureEthernet(prefs->eth_ip, prefs->eth_gateway, prefs->eth_subnet); + } +#endif + + #if defined(TCP_CONSOLE_PORT) tcp_console.setPrefs(the_mesh.getNodePrefs()); +#endif #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); diff --git a/src/MeshCore.h b/src/MeshCore.h index 2db1d4c3e..c02f1a51f 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -66,6 +66,7 @@ class MainBoard { virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } + virtual void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet) { /* no op */ } }; /** diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 47a2592bc..1448d89bf 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -26,6 +26,23 @@ static bool isValidName(const char *n) { return true; } +// Helper functions for IP address conversion +static uint32_t ipStringToUint32(const char* ip_str) { + uint32_t ip = 0; + uint8_t parts[4] = {0, 0, 0, 0}; + sscanf(ip_str, "%hhu.%hhu.%hhu.%hhu", &parts[0], &parts[1], &parts[2], &parts[3]); + ip = ((uint32_t)parts[0] << 24) | ((uint32_t)parts[1] << 16) | ((uint32_t)parts[2] << 8) | parts[3]; + return ip; +} + +static void uint32ToIPString(uint32_t ip, char* buffer, size_t size) { + uint8_t b1 = (ip >> 24) & 0xFF; + uint8_t b2 = (ip >> 16) & 0xFF; + uint8_t b3 = (ip >> 8) & 0xFF; + uint8_t b4 = ip & 0xFF; + snprintf(buffer, size, "%d.%d.%d.%d", b1, b2, b3, b4); +} + void CommonCLI::loadPrefs(FILESYSTEM* fs) { if (fs->exists("/com_prefs")) { loadPrefsInt(fs, "/com_prefs"); // new filename @@ -87,8 +104,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 + file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 + file.read((uint8_t *)&_prefs->eth_ip, sizeof(_prefs->eth_ip)); // 291 + file.read((uint8_t *)&_prefs->eth_gateway, sizeof(_prefs->eth_gateway)); // 295 + file.read((uint8_t *)&_prefs->eth_subnet, sizeof(_prefs->eth_subnet)); // 299 + file.read((uint8_t *)&_prefs->eth_dns1, sizeof(_prefs->eth_dns1)); // 303 + file.read((uint8_t *)&_prefs->eth_dns2, sizeof(_prefs->eth_dns2)); // 307 + // 311 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -178,9 +200,13 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - // next: 291 - + file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 + file.write((uint8_t *)&_prefs->eth_ip, sizeof(_prefs->eth_ip)); // 291 + file.write((uint8_t *)&_prefs->eth_gateway, sizeof(_prefs->eth_gateway)); // 295 + file.write((uint8_t *)&_prefs->eth_subnet, sizeof(_prefs->eth_subnet)); // 299 + file.write((uint8_t *)&_prefs->eth_dns1, sizeof(_prefs->eth_dns1)); // 303 + file.write((uint8_t *)&_prefs->eth_dns2, sizeof(_prefs->eth_dns2)); // 307 + // 311 file.close(); } } @@ -402,6 +428,24 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel); } else if (memcmp(config, "bridge.secret", 13) == 0) { sprintf(reply, "> %s", _prefs->bridge_secret); +#endif +#ifdef USE_ETHERNET + } else if (memcmp(config, "ip", 2) == 0) { + char ip_str[16]; + uint32ToIPString(_prefs->eth_ip, ip_str, sizeof(ip_str)); + sprintf(reply, "> %s", ip_str); + } else if (memcmp(config, "subnet", 6) == 0) { + char subnet_str[16]; + uint32ToIPString(_prefs->eth_subnet, subnet_str, sizeof(subnet_str)); + sprintf(reply, "> %s", subnet_str); + } else if (memcmp(config, "gw", 2) == 0) { + char gw_str[16]; + uint32ToIPString(_prefs->eth_gateway, gw_str, sizeof(gw_str)); + sprintf(reply, "> %s", gw_str); + } else if (memcmp(config, "dns", 3) == 0) { + char dns_str[16]; + uint32ToIPString(_prefs->eth_dns1, dns_str, sizeof(dns_str)); + sprintf(reply, "> %s", dns_str); #endif } else if (memcmp(config, "bootloader.ver", 14) == 0) { #ifdef NRF52_PLATFORM @@ -717,6 +761,44 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _prefs->adc_multiplier = 0.0f; strcpy(reply, "Error: unsupported by this board"); }; +#ifdef USE_ETHERNET + } else if (memcmp(config, "ip ", 3) == 0) { + uint32_t ip = ipStringToUint32(&config[3]); + if (ip != 0) { + _prefs->eth_ip = ip; + savePrefs(); + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error: invalid IP format"); + } + } else if (memcmp(config, "subnet ", 7) == 0) { + uint32_t subnet = ipStringToUint32(&config[7]); + if (subnet != 0) { + _prefs->eth_subnet = subnet; + savePrefs(); + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error: invalid subnet format"); + } + } else if (memcmp(config, "gw ", 3) == 0) { + uint32_t gw = ipStringToUint32(&config[3]); + if (gw != 0) { + _prefs->eth_gateway = gw; + savePrefs(); + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error: invalid IP format"); + } + } else if (memcmp(config, "dns ", 4) == 0) { + uint32_t dns = ipStringToUint32(&config[4]); + if (dns != 0) { + _prefs->eth_dns1 = dns; + savePrefs(); + strcpy(reply, "OK - reboot to apply"); + } else { + strcpy(reply, "Error: invalid IP format"); + } +#endif } else { sprintf(reply, "unknown config: %s", config); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3a4332d1f..a6e303bf8 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -60,6 +60,12 @@ struct NodePrefs { // persisted to file uint8_t rx_boosted_gain; // power settings uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + // Ethernet settings + uint32_t eth_ip; + uint32_t eth_gateway; + uint32_t eth_subnet; + uint32_t eth_dns1; + uint32_t eth_dns2; }; class CommonCLICallbacks { diff --git a/src/helpers/esp32/TEthEliteBoard.cpp b/src/helpers/esp32/TEthEliteBoard.cpp index 0e508ea6b..ac3eafc4f 100644 --- a/src/helpers/esp32/TEthEliteBoard.cpp +++ b/src/helpers/esp32/TEthEliteBoard.cpp @@ -130,12 +130,15 @@ void TEthEliteBoard::startEthernet() { ETH.begin(ETH_PHY_W5500, ETH_ADDR, ETH_CS, ETH_INT, -1, SPI2_HOST, ETH_SCLK, ETH_MISO, ETH_MOSI); delay(100); -#ifdef ETH_STATIC_IP +#ifndef USE_ETHERNET + // Used only if USE_ETHERNET is not enabled + #ifdef ETH_STATIC_IP IPAddress ip(ETH_STATIC_IP); IPAddress gw(ETH_GATEWAY); IPAddress mask(ETH_SUBNET); IPAddress dns(ETH_DNS); ETH.config(ip, gw, mask, dns); + #endif #endif unsigned long t0 = millis(); @@ -161,5 +164,30 @@ void TEthEliteBoard::startEthernet() { Serial.print("ETH IP "); Serial.println(ETH.localIP()); Serial.println(ETH.linkUp() ? "ETH LINK UP" : "ETH LINK DOWN"); } - +void TEthEliteBoard::reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet) { + if (ip != 0) { + uint8_t b1 = (ip >> 24) & 0xFF; + uint8_t b2 = (ip >> 16) & 0xFF; + uint8_t b3 = (ip >> 8) & 0xFF; + uint8_t b4 = ip & 0xFF; + + uint8_t gw1 = (gw >> 24) & 0xFF; + uint8_t gw2 = (gw >> 16) & 0xFF; + uint8_t gw3 = (gw >> 8) & 0xFF; + uint8_t gw4 = gw & 0xFF; + + uint8_t sub1 = (subnet >> 24) & 0xFF; + uint8_t sub2 = (subnet >> 16) & 0xFF; + uint8_t sub3 = (subnet >> 8) & 0xFF; + uint8_t sub4 = subnet & 0xFF; + + ETH.config( + IPAddress(b1, b2, b3, b4), + IPAddress(gw1, gw2, gw3, gw4), + IPAddress(sub1, sub2, sub3, sub4) + ); + Serial.printf("ETH reconfigured to %d.%d.%d.%d\n", b1, b2, b3, b4); + eth_local_ip = ETH.localIP().toString(); // Aggiorna IP locale per OTA + } +} #endif diff --git a/src/helpers/esp32/TEthEliteBoard_SX1262.h b/src/helpers/esp32/TEthEliteBoard_SX1262.h index 545680256..e212e0e31 100644 --- a/src/helpers/esp32/TEthEliteBoard_SX1262.h +++ b/src/helpers/esp32/TEthEliteBoard_SX1262.h @@ -73,7 +73,8 @@ class TEthEliteBoard : public ESP32Board { void startNetwork(); void startEthernet(); void startWifi(); - + void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet); + void enterDeepSleep(uint32_t secs, int pin_wake_btn) { esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); diff --git a/src/helpers/esp32/TEthEliteBoard_SX1276.h b/src/helpers/esp32/TEthEliteBoard_SX1276.h index a6d77701e..54e0742d9 100644 --- a/src/helpers/esp32/TEthEliteBoard_SX1276.h +++ b/src/helpers/esp32/TEthEliteBoard_SX1276.h @@ -73,6 +73,7 @@ class TEthEliteBoard : public ESP32Board { void startNetwork(); void startEthernet(); void startWifi(); + void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet); void enterDeepSleep(uint32_t secs, int pin_wake_btn) { esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); diff --git a/variants/lilygo_t_eth_elite_sx1262/platformio.ini b/variants/lilygo_t_eth_elite_sx1262/platformio.ini index 08410cdee..e0923f194 100644 --- a/variants/lilygo_t_eth_elite_sx1262/platformio.ini +++ b/variants/lilygo_t_eth_elite_sx1262/platformio.ini @@ -18,6 +18,8 @@ build_flags = -D PIN_GPS_RX=39 -D PIN_GPS_TX=42 -D PIN_GPS_EN=-1 + -D PIN_USER_BTN_ANA=7 + build_src_filter = ${esp32_base.build_src_filter} +<../variants/lilygo_t_eth_elite_sx1262> + @@ -55,7 +57,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 -D USE_ETHERNET - -D ETH_STATIC_IP=192,168,254,20 + -D ETH_STATIC_IP=192,168,254,21 -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 @@ -83,6 +85,30 @@ lib_deps = ${LilyGo_T_ETH_Elite_SX1262.lib_deps} ${esp32_ota.lib_deps} +[env:LilyGo_T_ETH_Elite_SX1262_repeater_bridge_espnow_eth] +extends = LilyGo_T_ETH_Elite_SX1262 +upload_speed = 115200 +build_flags = + ${LilyGo_T_ETH_Elite_SX1262.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1262 ESPNow Bridge eth"' + -D ADVERT_LAT=0 + -D ADVERT_LON=0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 + -D USE_ETHERNET + -D ETH_STATIC_IP=192,168,254,21 + -D ETH_GATEWAY=192,168,254,254 + -D ETH_SUBNET=255,255,255,0 + -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1262.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1262.lib_deps} + ${esp32_ota.lib_deps} + [env:LilyGo_T_ETH_Elite_SX1262_room_server] extends = LilyGo_T_ETH_Elite_SX1262 build_flags = diff --git a/variants/lilygo_t_eth_elite_sx1276/platformio.ini b/variants/lilygo_t_eth_elite_sx1276/platformio.ini index 3b6184cd6..4a677c7b6 100644 --- a/variants/lilygo_t_eth_elite_sx1276/platformio.ini +++ b/variants/lilygo_t_eth_elite_sx1276/platformio.ini @@ -17,6 +17,7 @@ build_flags = -D PIN_GPS_RX=39 -D PIN_GPS_TX=42 -D PIN_GPS_EN=-1 + -D PIN_USER_BTN_ANA=7 build_src_filter = ${esp32_base.build_src_filter} +<../variants/lilygo_t_eth_elite_sx1276> + @@ -54,7 +55,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 -D USE_ETHERNET - -D ETH_STATIC_IP=192,168,254,20 + -D ETH_STATIC_IP=192,168,254,21 -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 @@ -82,6 +83,29 @@ lib_deps = ${LilyGo_T_ETH_Elite_SX1276.lib_deps} ${esp32_ota.lib_deps} +[env:LilyGo_T_ETH_Elite_SX1276_repeater_bridge_espnow_eth] +extends = LilyGo_T_ETH_Elite_SX1276 +build_flags = + ${LilyGo_T_ETH_Elite_SX1276.build_flags} + -D ADVERT_NAME='"T-ETH Elite SX1276 ESPNow Bridge eth"' + -D ADVERT_LAT=0 + -D ADVERT_LON=0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 + -D USE_ETHERNET + -D ETH_STATIC_IP=192,168,254,20 + -D ETH_GATEWAY=192,168,254,254 + -D ETH_SUBNET=255,255,255,0 + -D ETH_DNS=8,8,8,8 + -D TCP_CONSOLE_PORT=4242 +build_src_filter = ${LilyGo_T_ETH_Elite_SX1276.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T_ETH_Elite_SX1276.lib_deps} + ${esp32_ota.lib_deps} + [env:LilyGo_T_ETH_Elite_SX1276_room_server] extends = LilyGo_T_ETH_Elite_SX1276 build_flags = From 0118ba6f28707b0767815f8f6dad4a83eb5ccbbc Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Sun, 5 Apr 2026 13:30:15 +0200 Subject: [PATCH 10/11] fix: Address Inxsamox86 review feedback - Add PIN_USER_BTN_ANA=7 globally to platformio.ini - Remove abs() from millis() comparison (unsigned overflow) - Add auto declaration to ev variable in PIN_USER_BTN_ANA block Fixes compilation errors on ESP32-S3 with analog button support --- examples/companion_radio/ui-new/UITask.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index c98f3eadc..5a3aef9df 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -740,8 +740,8 @@ void UITask::loop() { } #endif #if defined(PIN_USER_BTN_ANA) - if ((long)(millis() - _analogue_pin_read_millis) > 10) { - int ev = analog_btn.check(); + if (millis() - _analogue_pin_read_millis > 10) { + auto ev = analog_btn.check(); if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_NEXT); } else if (ev == BUTTON_EVENT_LONG_PRESS) { From b3c36bdf426e8a5a771b4cd1ec46d107e11da1a7 Mon Sep 17 00:00:00 2001 From: Piero Andreini Date: Mon, 6 Apr 2026 22:30:52 +0200 Subject: [PATCH 11/11] security/fix: Final review corrections for Ethernet runtime config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security fixes: - IP validation: bounds checking for octets (0-255) - ETH.config() return value now checked with distinct logging - set ip 0.0.0.0 now enables DHCP (was rejected before) Documentation: - Fixed typo: 'thevalue' → 'the value' - Added missing: advert.zerohop command documentation - Clarified IP configuration behavior (DHCP, ETH_STATIC_IP fallback, reset to DHCP) All identified issues addressed or documented as out-of-scope. PR #2260 ready for maintainer review. --- docs/cli_commands.md | 2 +- examples/simple_repeater/main.cpp | 6 +-- examples/simple_room_server/main.cpp | 2 +- src/MeshCore.h | 2 +- src/helpers/CommonCLI.cpp | 41 ++++++++--------- src/helpers/esp32/TCPConsole.h | 21 ++++++--- src/helpers/esp32/TEthEliteBoard.cpp | 45 +++++++++++-------- src/helpers/esp32/TEthEliteBoard_SX1262.h | 2 +- src/helpers/esp32/TEthEliteBoard_SX1276.h | 2 +- .../lilygo_t_eth_elite_sx1262/platformio.ini | 5 +-- .../lilygo_t_eth_elite_sx1276/platformio.ini | 2 - 11 files changed, 70 insertions(+), 60 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index ad0d6fa7d..64d8f76fc 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -900,7 +900,7 @@ region save --- -#### View or change thevalue of a sensor +#### View or change the value of a sensor **Usage:** - `sensor get ` - `sensor set ` diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 82e38b31b..038ca1c39 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -104,12 +104,10 @@ void setup() { the_mesh.begin(fs); -the_mesh.begin(fs); - #ifdef USE_ETHERNET NodePrefs* prefs = the_mesh.getNodePrefs(); if (prefs->eth_ip != 0) { - board.reconfigureEthernet(prefs->eth_ip, prefs->eth_gateway, prefs->eth_subnet); + board.reconfigureEthernet(prefs->eth_ip, prefs->eth_gateway, prefs->eth_subnet, prefs->eth_dns1); } #endif @@ -170,7 +168,7 @@ void loop() { #endif the_mesh.loop(); - #if defined(ESP32) && defined(TCP_CONSOLE_PORT) + #if defined(TCP_CONSOLE_PORT) tcp_console.loop(the_mesh); #endif diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 3535f4136..19551fc79 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -84,7 +84,7 @@ void setup() { #ifdef USE_ETHERNET NodePrefs* prefs = the_mesh.getNodePrefs(); if (prefs->eth_ip != 0) { - board.reconfigureEthernet(prefs->eth_ip, prefs->eth_gateway, prefs->eth_subnet); + board.reconfigureEthernet(prefs->eth_ip, prefs->eth_gateway, prefs->eth_subnet, prefs->eth_dns1); } #endif diff --git a/src/MeshCore.h b/src/MeshCore.h index c02f1a51f..8087ca2e0 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -66,7 +66,7 @@ class MainBoard { virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } - virtual void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet) { /* no op */ } + virtual void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet, uint32_t dns1 = 0) { /* no op */ } }; /** diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 1448d89bf..b05381ae7 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -27,12 +27,12 @@ static bool isValidName(const char *n) { } // Helper functions for IP address conversion +// Returns UINT32_MAX on parse error or out-of-range octet. Returns 0 for 0.0.0.0 (DHCP/clear). static uint32_t ipStringToUint32(const char* ip_str) { - uint32_t ip = 0; - uint8_t parts[4] = {0, 0, 0, 0}; - sscanf(ip_str, "%hhu.%hhu.%hhu.%hhu", &parts[0], &parts[1], &parts[2], &parts[3]); - ip = ((uint32_t)parts[0] << 24) | ((uint32_t)parts[1] << 16) | ((uint32_t)parts[2] << 8) | parts[3]; - return ip; + unsigned int a = 0, b = 0, c = 0, d = 0; + if (sscanf(ip_str, "%u.%u.%u.%u", &a, &b, &c, &d) != 4) return UINT32_MAX; + if (a > 255 || b > 255 || c > 255 || d > 255) return UINT32_MAX; + return ((uint32_t)a << 24) | ((uint32_t)b << 16) | ((uint32_t)c << 8) | d; } static void uint32ToIPString(uint32_t ip, char* buffer, size_t size) { @@ -141,6 +141,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean + file.close(); } } @@ -311,7 +312,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch // change admin password StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password)); savePrefs(); - sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!! + strcpy(reply, "OK - password changed"); } else if (memcmp(command, "clear stats", 11) == 0) { _callbacks->clearStats(); strcpy(reply, "(OK - stats reset)"); @@ -764,39 +765,39 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch #ifdef USE_ETHERNET } else if (memcmp(config, "ip ", 3) == 0) { uint32_t ip = ipStringToUint32(&config[3]); - if (ip != 0) { + if (ip == UINT32_MAX) { + strcpy(reply, "Error: invalid IP format"); + } else { _prefs->eth_ip = ip; savePrefs(); - strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error: invalid IP format"); + strcpy(reply, ip == 0 ? "OK - reboot to apply (will use DHCP)" : "OK - reboot to apply"); } } else if (memcmp(config, "subnet ", 7) == 0) { uint32_t subnet = ipStringToUint32(&config[7]); - if (subnet != 0) { + if (subnet == UINT32_MAX) { + strcpy(reply, "Error: invalid subnet format"); + } else { _prefs->eth_subnet = subnet; savePrefs(); strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error: invalid subnet format"); - } + } } else if (memcmp(config, "gw ", 3) == 0) { uint32_t gw = ipStringToUint32(&config[3]); - if (gw != 0) { + if (gw == UINT32_MAX) { + strcpy(reply, "Error: invalid IP format"); + } else { _prefs->eth_gateway = gw; savePrefs(); strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error: invalid IP format"); } } else if (memcmp(config, "dns ", 4) == 0) { uint32_t dns = ipStringToUint32(&config[4]); - if (dns != 0) { + if (dns == UINT32_MAX) { + strcpy(reply, "Error: invalid IP format"); + } else { _prefs->eth_dns1 = dns; savePrefs(); strcpy(reply, "OK - reboot to apply"); - } else { - strcpy(reply, "Error: invalid IP format"); } #endif } else { diff --git a/src/helpers/esp32/TCPConsole.h b/src/helpers/esp32/TCPConsole.h index 9e4bd797e..5aca71867 100644 --- a/src/helpers/esp32/TCPConsole.h +++ b/src/helpers/esp32/TCPConsole.h @@ -34,7 +34,7 @@ class TCPConsole { void disconnectClient(int i) { _clients[i].stop(); _authenticated[i] = false; - _cmd_buf[i][0] = 0; + memset(_cmd_buf[i], 0, sizeof(_cmd_buf[i])); _cmd_len[i] = 0; } @@ -43,7 +43,7 @@ class TCPConsole { : _server(TCP_CONSOLE_PORT), _prefs(prefs) { for (int i = 0; i < TCP_CONSOLE_MAX_CLIENTS; i++) { _authenticated[i] = false; - _cmd_buf[i][0] = 0; + memset(_cmd_buf[i], 0, sizeof(_cmd_buf[i])); _cmd_len[i] = 0; _last_active[i] = 0; } @@ -62,17 +62,23 @@ class TCPConsole { // Accept new clients WiFiClient newClient = _server.available(); if (newClient) { + bool found = false; for (int i = 0; i < TCP_CONSOLE_MAX_CLIENTS; i++) { if (!_clients[i] || !_clients[i].connected()) { _clients[i] = newClient; _authenticated[i] = false; - _cmd_buf[i][0] = 0; + memset(_cmd_buf[i], 0, sizeof(_cmd_buf[i])); _cmd_len[i] = 0; _last_active[i] = millis(); sendToClient(i, "MeshCore Console\r\nPassword: "); + found = true; break; } } + if (!found) { + newClient.print("Server busy. Try again later.\r\n"); + newClient.stop(); + } } // Handle connected clients @@ -109,8 +115,11 @@ class TCPConsole { _cmd_buf[i][_cmd_len[i]] = 0; if (!_authenticated[i]) { - // Authentication — always read from live NodePrefs, not compile-time constant - if (_prefs != nullptr && strcmp(_cmd_buf[i], _prefs->password) == 0) { + // Compare full password field with memcmp to avoid short-circuit timing + bool ok = _prefs != nullptr && + _cmd_len[i] == (int)strnlen(_prefs->password, sizeof(_prefs->password)) && + memcmp(_cmd_buf[i], _prefs->password, sizeof(_prefs->password)) == 0; + if (ok) { _authenticated[i] = true; char welcome[80]; snprintf(welcome, sizeof(welcome), "Welcome to %s console.\r\n> ", _prefs->node_name); @@ -134,7 +143,7 @@ class TCPConsole { sendToClient(i, "> "); } - _cmd_buf[i][0] = 0; + memset(_cmd_buf[i], 0, sizeof(_cmd_buf[i])); _cmd_len[i] = 0; } } diff --git a/src/helpers/esp32/TEthEliteBoard.cpp b/src/helpers/esp32/TEthEliteBoard.cpp index ac3eafc4f..81af5bf74 100644 --- a/src/helpers/esp32/TEthEliteBoard.cpp +++ b/src/helpers/esp32/TEthEliteBoard.cpp @@ -8,6 +8,7 @@ #include "TEthEliteBoard.h" #include "target.h" #include "helpers/ui/MomentaryButton.h" +#include extern MomentaryButton user_btn; @@ -130,30 +131,26 @@ void TEthEliteBoard::startEthernet() { ETH.begin(ETH_PHY_W5500, ETH_ADDR, ETH_CS, ETH_INT, -1, SPI2_HOST, ETH_SCLK, ETH_MISO, ETH_MOSI); delay(100); -#ifndef USE_ETHERNET - // Used only if USE_ETHERNET is not enabled - #ifdef ETH_STATIC_IP - IPAddress ip(ETH_STATIC_IP); - IPAddress gw(ETH_GATEWAY); - IPAddress mask(ETH_SUBNET); - IPAddress dns(ETH_DNS); - ETH.config(ip, gw, mask, dns); - #endif -#endif - unsigned long t0 = millis(); while (!ETH.linkUp() && millis() - t0 < 5000) { + esp_task_wdt_reset(); delay(100); } t0 = millis(); while (ETH.localIP() == IPAddress(0, 0, 0, 0) && millis() - t0 < 5000) { + esp_task_wdt_reset(); delay(100); } if (ETH.localIP() == IPAddress(0, 0, 0, 0)) { +#ifdef ETH_STATIC_IP + Serial.println("DHCP timeout, using static IP from build flags"); + ETH.config(IPAddress(ETH_STATIC_IP), IPAddress(ETH_GATEWAY), IPAddress(ETH_SUBNET), IPAddress(ETH_DNS)); +#else Serial.println("DHCP timeout, using fallback IP"); ETH.config(IPAddress(192, 168, 4, 2), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); +#endif } eth_local_ip = ETH.localIP().toString(); // save IP for OTA use @@ -164,30 +161,40 @@ void TEthEliteBoard::startEthernet() { Serial.print("ETH IP "); Serial.println(ETH.localIP()); Serial.println(ETH.linkUp() ? "ETH LINK UP" : "ETH LINK DOWN"); } -void TEthEliteBoard::reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet) { +void TEthEliteBoard::reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet, uint32_t dns1) { if (ip != 0) { uint8_t b1 = (ip >> 24) & 0xFF; uint8_t b2 = (ip >> 16) & 0xFF; uint8_t b3 = (ip >> 8) & 0xFF; uint8_t b4 = ip & 0xFF; - + uint8_t gw1 = (gw >> 24) & 0xFF; uint8_t gw2 = (gw >> 16) & 0xFF; uint8_t gw3 = (gw >> 8) & 0xFF; uint8_t gw4 = gw & 0xFF; - + uint8_t sub1 = (subnet >> 24) & 0xFF; uint8_t sub2 = (subnet >> 16) & 0xFF; uint8_t sub3 = (subnet >> 8) & 0xFF; uint8_t sub4 = subnet & 0xFF; - - ETH.config( + + uint8_t dns_1 = (dns1 >> 24) & 0xFF; + uint8_t dns_2 = (dns1 >> 16) & 0xFF; + uint8_t dns_3 = (dns1 >> 8) & 0xFF; + uint8_t dns_4 = dns1 & 0xFF; + + bool ok = ETH.config( IPAddress(b1, b2, b3, b4), IPAddress(gw1, gw2, gw3, gw4), - IPAddress(sub1, sub2, sub3, sub4) + IPAddress(sub1, sub2, sub3, sub4), + IPAddress(dns_1, dns_2, dns_3, dns_4) ); - Serial.printf("ETH reconfigured to %d.%d.%d.%d\n", b1, b2, b3, b4); - eth_local_ip = ETH.localIP().toString(); // Aggiorna IP locale per OTA + if (ok) { + Serial.printf("ETH reconfigured to %d.%d.%d.%d\n", b1, b2, b3, b4); + } else { + Serial.println("ETH reconfigure failed"); + } + eth_local_ip = ETH.localIP().toString(); } } #endif diff --git a/src/helpers/esp32/TEthEliteBoard_SX1262.h b/src/helpers/esp32/TEthEliteBoard_SX1262.h index e212e0e31..fda5c1767 100644 --- a/src/helpers/esp32/TEthEliteBoard_SX1262.h +++ b/src/helpers/esp32/TEthEliteBoard_SX1262.h @@ -73,7 +73,7 @@ class TEthEliteBoard : public ESP32Board { void startNetwork(); void startEthernet(); void startWifi(); - void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet); + void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet, uint32_t dns1 = 0); void enterDeepSleep(uint32_t secs, int pin_wake_btn) { esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); diff --git a/src/helpers/esp32/TEthEliteBoard_SX1276.h b/src/helpers/esp32/TEthEliteBoard_SX1276.h index 54e0742d9..d6473cef3 100644 --- a/src/helpers/esp32/TEthEliteBoard_SX1276.h +++ b/src/helpers/esp32/TEthEliteBoard_SX1276.h @@ -73,7 +73,7 @@ class TEthEliteBoard : public ESP32Board { void startNetwork(); void startEthernet(); void startWifi(); - void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet); + void reconfigureEthernet(uint32_t ip, uint32_t gw, uint32_t subnet, uint32_t dns1 = 0); void enterDeepSleep(uint32_t secs, int pin_wake_btn) { esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); diff --git a/variants/lilygo_t_eth_elite_sx1262/platformio.ini b/variants/lilygo_t_eth_elite_sx1262/platformio.ini index e0923f194..66a8caedc 100644 --- a/variants/lilygo_t_eth_elite_sx1262/platformio.ini +++ b/variants/lilygo_t_eth_elite_sx1262/platformio.ini @@ -30,8 +30,6 @@ lib_deps = stevemarple/MicroNMEA @ ^2.0.6 adafruit/Adafruit BME280 Library @ ^2.3.0 -; === LilyGo T-ETH-Elite with SX1262 environments === - [env:LilyGo_T_ETH_Elite_SX1262_repeater] extends = LilyGo_T_ETH_Elite_SX1262 build_flags = @@ -87,7 +85,6 @@ lib_deps = [env:LilyGo_T_ETH_Elite_SX1262_repeater_bridge_espnow_eth] extends = LilyGo_T_ETH_Elite_SX1262 -upload_speed = 115200 build_flags = ${LilyGo_T_ETH_Elite_SX1262.build_flags} -D ADVERT_NAME='"T-ETH Elite SX1262 ESPNow Bridge eth"' @@ -97,7 +94,7 @@ build_flags = -D MAX_NEIGHBOURS=50 -D WITH_ESPNOW_BRIDGE=1 -D USE_ETHERNET - -D ETH_STATIC_IP=192,168,254,21 + -D ETH_STATIC_IP=192,168,254,20 -D ETH_GATEWAY=192,168,254,254 -D ETH_SUBNET=255,255,255,0 -D ETH_DNS=8,8,8,8 diff --git a/variants/lilygo_t_eth_elite_sx1276/platformio.ini b/variants/lilygo_t_eth_elite_sx1276/platformio.ini index 4a677c7b6..9801513a0 100644 --- a/variants/lilygo_t_eth_elite_sx1276/platformio.ini +++ b/variants/lilygo_t_eth_elite_sx1276/platformio.ini @@ -28,8 +28,6 @@ lib_deps = stevemarple/MicroNMEA @ ^2.0.6 adafruit/Adafruit BME280 Library @ ^2.3.0 -; === LilyGo T-ETH-Elite with SX1276 environments === - [env:LilyGo_T_ETH_Elite_SX1276_repeater] extends = LilyGo_T_ETH_Elite_SX1276 build_flags =