From 5de857f8f830f27abb360cdb5814ae3be07e8b5d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 17 Jul 2026 23:59:27 +1200 Subject: [PATCH] initial ch390 ethernet support for thinknode m7 companion --- examples/companion_radio/main.cpp | 5 + src/helpers/ethernet/Ethernet.h | 9 + .../ethernet/ch390/CH390EthernetInterface.cpp | 207 ++++++++++++++++++ .../ethernet/ch390/CH390EthernetInterface.h | 80 +++++++ variants/thinknode_m7/platformio.ini | 36 +++ 5 files changed, 337 insertions(+) create mode 100644 src/helpers/ethernet/Ethernet.h create mode 100644 src/helpers/ethernet/ch390/CH390EthernetInterface.cpp create mode 100644 src/helpers/ethernet/ch390/CH390EthernetInterface.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d39aeef9..2fc00dbd 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -48,6 +48,9 @@ static uint32_t _atoi(const char* sp) { #include ArduinoSerialInterface serial_interface; HardwareSerial companion_serial(1); + #elif defined(ETHERNET_ENABLED) + #include + ETHERNET_CLASS serial_interface; #else #include ArduinoSerialInterface serial_interface; @@ -240,6 +243,8 @@ void setup() { companion_serial.setPins(SERIAL_RX, SERIAL_TX); companion_serial.begin(115200); serial_interface.begin(companion_serial); +#elif defined(ETHERNET_ENABLED) + serial_interface.begin(); #else serial_interface.begin(Serial); #endif diff --git a/src/helpers/ethernet/Ethernet.h b/src/helpers/ethernet/Ethernet.h new file mode 100644 index 00000000..4d889df4 --- /dev/null +++ b/src/helpers/ethernet/Ethernet.h @@ -0,0 +1,9 @@ +#pragma once + +#if defined(ETHERNET_ENABLED) + #if defined(ETHERNET_USE_CH390) + #include "helpers/ethernet/ch390/CH390EthernetInterface.h" + #else + #error "ETHERNET_ENABLED is defined, but no specific driver flag (e.g. ETHERNET_USE_CH390) was provided!" + #endif +#endif diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp new file mode 100644 index 00000000..f7ee5503 --- /dev/null +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp @@ -0,0 +1,207 @@ +#include "CH390EthernetInterface.h" + +#define RECV_STATE_IDLE 0 +#define RECV_STATE_HDR_FOUND 1 +#define RECV_STATE_LEN1_FOUND 2 +#define RECV_STATE_LEN2_FOUND 3 + +bool CH390EthernetInterface::begin() { + ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); + + // Init CH390 + ch390_config_t config = CH390_DEFAULT_CONFIG(); + config.spi_miso_gpio = ETH_MISO_PIN; + config.spi_mosi_gpio = ETH_MOSI_PIN; + config.spi_sck_gpio = ETH_SCLK_PIN; + config.spi_cs_gpio = ETH_CS_PIN; + config.int_gpio = ETH_INT_PIN; + if (!CH390.begin(config)) { + ETHERNET_DEBUG_PRINTLN("Failed to initialize CH390 hardware."); + return false; + } + + // Setup Static IP if build flags are present + #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) + IPAddress ip(ETHERNET_STATIC_IP); + IPAddress gw(ETHERNET_STATIC_GATEWAY); + IPAddress sn(ETHERNET_STATIC_SUBNET); + CH390.config(ip, gw, sn); + #endif + + // Start Server + server.begin(); + ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT); + + return true; +} + +void CH390EthernetInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); +} + +void CH390EthernetInterface::disable() { + _isEnabled = false; +} + +size_t CH390EthernetInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + return 0; + } + + if (deviceConnected && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + + send_queue[send_queue_len].len = len; // add to send queue + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + + return len; + } + return 0; +} + +bool CH390EthernetInterface::isWriteBusy() const { + return false; +} + +size_t CH390EthernetInterface::checkRecvFrame(uint8_t dest[]) { + if (server.hasClient()) { + auto newClient = server.available(); + if (newClient) { + IPAddress new_ip = newClient.remoteIP(); + uint16_t new_port = newClient.remotePort(); + ETHERNET_DEBUG_PRINTLN( + "New client accepted %u.%u.%u.%u:%u", + new_ip[0], new_ip[1], new_ip[2], new_ip[3], new_port); + + deviceConnected = false; + if (client) { + ETHERNET_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + _state = RECV_STATE_IDLE; + _frame_len = 0; + _rx_len = 0; + client = newClient; + ETHERNET_DEBUG_PRINTLN("Switched to new client"); + } + } + + if (client.connected()) { + if (!deviceConnected) { + ETHERNET_DEBUG_PRINTLN( + "Got connection %u.%u.%u.%u:%u", + client.remoteIP()[0], + client.remoteIP()[1], + client.remoteIP()[2], + client.remoteIP()[3], + client.remotePort()); + deviceConnected = true; + } + } else { + if (deviceConnected) { + deviceConnected = false; + ETHERNET_DEBUG_PRINTLN("Disconnected"); + } + } + + if (deviceConnected) { + if (send_queue_len > 0) { // first, check send queue + + _last_write = millis(); + int len = send_queue[0].len; + +#if ETHERNET_RAW_LINE + ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); + client.write(send_queue[0].buf, len); + client.write("\r\n", 2); +#else + uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames + pkt[0] = '>'; + pkt[1] = (len & 0xFF); // LSB + pkt[2] = (len >> 8); // MSB + memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); + ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); + #endif + client.write(pkt, 3 + len); +#endif + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) { // delete top item from queue + send_queue[i] = send_queue[i + 1]; + } + } else { + while (client.available()) { + int c = client.read(); + if (c < 0) break; + +#if ETHERNET_RAW_LINE + if (c == '\r' || c == '\n') { + if (_rx_len == 0) { + continue; + } + uint16_t out_len = _rx_len; + if (out_len > MAX_FRAME_SIZE) out_len = MAX_FRAME_SIZE; + memcpy(dest, _rx_buf, out_len); + _rx_len = 0; + return out_len; + } + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + _rx_len++; + } +#else + switch (_state) { + case RECV_STATE_IDLE: + if (c == '<') { + _state = RECV_STATE_HDR_FOUND; + } + break; + case RECV_STATE_HDR_FOUND: + _frame_len = (uint8_t)c; + _state = RECV_STATE_LEN1_FOUND; + break; + case RECV_STATE_LEN1_FOUND: + _frame_len |= ((uint16_t)c) << 8; + _rx_len = 0; + _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; + break; + default: + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + } + _rx_len++; + if (_rx_len >= _frame_len) { + if (_frame_len > MAX_FRAME_SIZE) { + _frame_len = MAX_FRAME_SIZE; + } + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); + #endif + memcpy(dest, _rx_buf, _frame_len); + _state = RECV_STATE_IDLE; + return _frame_len; + } + } +#endif + } + } + } + + return 0; +} + +bool CH390EthernetInterface::isConnected() const { + return deviceConnected; +} + +void CH390EthernetInterface::loop() { + +} diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.h b/src/helpers/ethernet/ch390/CH390EthernetInterface.h new file mode 100644 index 00000000..d638319b --- /dev/null +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.h @@ -0,0 +1,80 @@ +#pragma once + +#include "../../BaseSerialInterface.h" +#include +#include +#include +#include +#include + +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 5000 +#endif +// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets + +class CH390EthernetInterface : public BaseSerialInterface { + bool deviceConnected; + bool _isEnabled; + unsigned long _last_write; + uint8_t _state; + uint16_t _frame_len; + uint16_t _rx_len; + uint8_t _rx_buf[MAX_FRAME_SIZE]; + + WiFiServer server; + WiFiClient client; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 4 + int send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + + protected: + + public: + CH390EthernetInterface() : server(ETHERNET_TCP_PORT) { + deviceConnected = false; + _isEnabled = false; + _last_write = 0; + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + bool begin(); + void loop(); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override; + bool isWriteBusy() const override; + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; +}; + + +#if ETHERNET_DEBUG_LOGGING && ARDUINO + #include + #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) +#else + #define ETHERNET_DEBUG_PRINT(...) {} + #define ETHERNET_DEBUG_PRINTLN(...) {} + #define ETHERNET_DEBUG_PRINT_IP(...) {} +#endif diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index acd98c8c..43ff61c3 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -34,6 +34,22 @@ build_src_filter = ${esp32_base.build_src_filter} lib_deps = ${esp32_base.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 +[ThinkNode_M7_ethernet] +build_flags = + -D ETHERNET_ENABLED + -D ETHERNET_USE_CH390 + -D ETHERNET_CLASS=CH390EthernetInterface + -D ETH_MISO_PIN=14 + -D ETH_MOSI_PIN=48 + -D ETH_SCLK_PIN=47 + -D ETH_CS_PIN=21 + -D ETH_INT_PIN=45 + -D ETHERNET_DEBUG_LOGGING=1 +build_src_filter = + + +lib_deps = + https://github.com/liamcottle/ESP32-CH390.git#47b401f1de546118b03c18b5689dedec45871f2d + [env:ThinkNode_M7_repeater] extends = ThinkNode_M7 build_src_filter = ${ThinkNode_M7.build_src_filter} @@ -130,6 +146,26 @@ lib_deps = ${ThinkNode_M7.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:ThinkNode_M7_companion_radio_ethernet] +extends = ThinkNode_M7 +build_flags = + ${ThinkNode_M7.build_flags} + ${ThinkNode_M7_ethernet.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=NullDisplayDriver + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${ThinkNode_M7.build_src_filter} + ${ThinkNode_M7_ethernet.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M7.lib_deps} + ${ThinkNode_M7_ethernet.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:ThinkNode_M7_kiss_modem] extends = ThinkNode_M7 build_src_filter = ${ThinkNode_M7.build_src_filter}