#pragma once #include #include #include #ifdef ARDUINO #include #include #include #include #include #include #else #include #endif #include #include #define DEFAULT_TCP_PORT 4965 /** * TCPClientInterface - TCP client interface for microReticulum. * * Connects to a Python RNS TCPServerInterface or another TCP server. * Uses HDLC framing for wire protocol compatibility with Python RNS. * * Features: * - Automatic reconnection with configurable retry interval * - TCP keepalive for connection health monitoring * - HDLC framing (0x7E flags with byte stuffing) * * Usage: * TCPClientInterface* tcp = new TCPClientInterface("tcp0"); * tcp->set_target_host("192.168.1.100"); * tcp->set_target_port(4965); * Interface interface(tcp); * interface.start(); * Transport::register_interface(interface); */ class TCPClientInterface : public RNS::InterfaceImpl { public: // Match Python RNS constants static const uint32_t BITRATE_GUESS = 10 * 1000 * 1000; // 10 Mbps static const uint32_t HW_MTU = 1064; // Match UDPInterface // Reconnection parameters. connect() runs on tcp_task (off the main loop), // but still bound the timeout and back off so a dead host doesn't busy-retry. static const uint32_t RECONNECT_WAIT_MS = 15000; // 15s between reconnect attempts static const uint32_t CONNECT_TIMEOUT_MS = 3000; // 3s connect timeout (task-side) static const uint8_t LOCAL_FAILURES_BEFORE_REASSOCIATE = 3; static const uint32_t REASSOCIATE_COOLDOWN_MS = 120000; static const uint32_t REASSOCIATE_MIN_INTERNAL_FREE = 24 * 1024; static const uint32_t REASSOCIATE_MIN_LARGEST_BLOCK = 12 * 1024; // TCP keepalive parameters (match Python RNS) static const int TCP_KEEPIDLE_SEC = 5; static const int TCP_KEEPINTVL_SEC = 2; static const int TCP_KEEPCNT_PROBES = 12; public: TCPClientInterface(const char* name = "TCPClient"); virtual ~TCPClientInterface(); // Configuration (call before start()) void set_target_host(const std::string& host) { _target_host = host; } void set_target_port(int port) { _target_port = port; } #ifdef ARDUINO void set_operation_active_callback(std::function callback) { _operation_active = std::move(callback); } #endif // InterfaceImpl overrides virtual bool start(); virtual void stop(); virtual void loop(); virtual inline std::string toString() const { return "TCPClientInterface[" + _name + "/" + _target_host + ":" + std::to_string(_target_port) + "]"; } protected: virtual bool send_outgoing(const RNS::Bytes& data); private: // Connection management bool connect(); void disconnect(); bool configure_socket(); void handle_disconnect(); #ifdef ARDUINO // Only the blocking connect()/DNS runs on tcp_task so it can't stall the // main loop (which froze the UI). read/write/frame all stay on the main loop // (low latency, unchanged data path). _conn_state hands _client ownership // between the two so they never touch the socket concurrently: // DISCONNECTED -> CONNECTING (task owns _client) -> CONNECTED (main loop // owns _client); main loop flips CONNECTED -> DISCONNECTED on a drop. enum ConnState : uint8_t { DISCONNECTED = 0, CONNECTING = 1, CONNECTED = 2 }; enum class TcpConnectStage : uint8_t { NONE, DNS, SOCKET, CONNECT, SELECT_TIMEOUT, SELECT_ERROR, SO_ERROR_READ, SOCKET_ERROR, SOCKET_OPTIONS }; static void tcp_task(void* arg); void task_loop(); void record_connect_failure(TcpConnectStage stage, int error, bool local_setup_failure); void maybe_reassociate_wifi(); static const char* connect_stage_name(TcpConnectStage stage); static bool local_lwip_failure(int error); TaskHandle_t _task_handle = nullptr; std::atomic _task_running{false}; std::atomic _task_done{false}; // task sets this right before exit; stop() joins on it std::atomic _conn_state{DISCONNECTED}; std::function _operation_active; uint8_t _consecutive_local_failures = 0; uint32_t _last_reassociate_ms = 0; #endif // HDLC frame processing void process_incoming(); void extract_and_process_frames(); // Target server std::string _target_host; int _target_port = DEFAULT_TCP_PORT; // Connection state bool _initiator = true; #ifdef ARDUINO // Touched by both task_loop() (core 0) and handle_disconnect() (core 1). std::atomic _last_connect_attempt{0}; #else uint32_t _last_connect_attempt = 0; #endif #ifdef ARDUINO std::atomic _reconnected{false}; // re-established after offline (task-set) #else bool _reconnected = false; // Set when connection re-established after being offline #endif uint32_t _last_data_received = 0; // Track last data receipt for stale connection detection static const uint32_t STALE_CONNECTION_MS = 120000; // Consider connection stale after 2 min no data public: // Check and clear reconnection flag (for announcing after reconnect) bool check_reconnected() { #ifdef ARDUINO return _reconnected.exchange(false); #else if (_reconnected) { _reconnected = false; return true; } return false; #endif } private: // HDLC frame buffer for partial frame reassembly RNS::Bytes _frame_buffer; // Read buffer for incoming data RNS::Bytes _read_buffer; // Platform-specific socket #ifdef ARDUINO WiFiClient _client; // WiFi credentials (for ESP32 WiFi connection) std::string _wifi_ssid; std::string _wifi_password; #else int _socket = -1; in_addr_t _target_address = INADDR_NONE; #endif };