Initial commit: RNodeTHV4 boundary mode firmware for Heltec V4
Bridges LoRa mesh and TCP/WiFi backbone networks using microReticulum. Based on microReticulum_Firmware with boundary mode additions: - BoundaryMode.h: State management and EEPROM persistence - BoundaryConfig.h: WiFi captive portal for configuration - TcpInterface.h: TCP backbone interface with HDLC framing - Display.h: Custom OLED layout with network status indicators - Transport/Identity library patches for embedded memory constraints
This commit is contained in:
404
TcpInterface.h
Normal file
404
TcpInterface.h
Normal file
@@ -0,0 +1,404 @@
|
||||
// Copyright (C) 2026, Boundary Mode Extension
|
||||
// Based on microReticulum_Firmware by Mark Qvist
|
||||
//
|
||||
// TcpInterface — An RNS InterfaceImpl that bridges the WiFi TCP
|
||||
// connection as a second RNS transport interface, enabling
|
||||
// Boundary mode operation between LoRa and TCP/IP backbone.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
#ifndef TCP_INTERFACE_H
|
||||
#define TCP_INTERFACE_H
|
||||
|
||||
#ifdef HAS_RNS
|
||||
#ifdef BOUNDARY_MODE
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <Interface.h>
|
||||
#include <Transport.h>
|
||||
#include <Bytes.h>
|
||||
|
||||
// ─── TCP Interface Configuration ─────────────────────────────────────────────
|
||||
#define TCP_IF_DEFAULT_PORT 4242
|
||||
#define TCP_IF_MAX_CLIENTS 4
|
||||
#define TCP_IF_HW_MTU 1064
|
||||
#define TCP_IF_CONNECT_TIMEOUT 6000 // ms
|
||||
#define TCP_IF_WRITE_TIMEOUT 2000 // ms — short to avoid WDT
|
||||
#define TCP_IF_READ_TIMEOUT 120000 // ms — 2 minutes (backbone can go quiet)
|
||||
#define TCP_IF_RECONNECT_MIN 10000 // ms — initial reconnect interval
|
||||
#define TCP_IF_RECONNECT_MAX 120000 // ms — max backoff (2 minutes)
|
||||
#define TCP_IF_KEEPALIVE_INTERVAL 30000 // ms — send empty HDLC frames to keep link alive
|
||||
#define TCP_IF_POLL_INTERVAL 10 // ms
|
||||
|
||||
// HDLC-like framing for TCP (matches Reticulum-rust tcp_interface)
|
||||
#define HDLC_FLAG 0x7E
|
||||
#define HDLC_ESC 0x7D
|
||||
#define HDLC_ESC_MASK 0x20
|
||||
|
||||
// ─── TCP Interface Mode ──────────────────────────────────────────────────────
|
||||
enum TcpIfMode {
|
||||
TCP_IF_MODE_SERVER = 0, // Listen for incoming connections (from backbone rnsd)
|
||||
TCP_IF_MODE_CLIENT = 1, // Connect out to a backbone rnsd TCP server
|
||||
};
|
||||
|
||||
// ─── Client connection state ─────────────────────────────────────────────────
|
||||
struct TcpClient {
|
||||
WiFiClient client;
|
||||
uint32_t last_activity;
|
||||
bool active;
|
||||
// HDLC deframe state
|
||||
bool in_frame;
|
||||
bool escape;
|
||||
uint8_t rxbuf[TCP_IF_HW_MTU];
|
||||
uint16_t rxlen;
|
||||
};
|
||||
|
||||
// ─── TcpInterface Class ─────────────────────────────────────────────────────
|
||||
class TcpInterface : public RNS::InterfaceImpl {
|
||||
public:
|
||||
TcpInterface(TcpIfMode mode, uint16_t port = TCP_IF_DEFAULT_PORT,
|
||||
const char* target_host = nullptr, uint16_t target_port = 0)
|
||||
: RNS::InterfaceImpl("TcpInterface"),
|
||||
_mode(mode),
|
||||
_port(port),
|
||||
_target_port(target_port),
|
||||
_server(nullptr),
|
||||
_num_clients(0),
|
||||
_last_reconnect(0),
|
||||
_last_keepalive(0),
|
||||
_reconnect_interval(TCP_IF_RECONNECT_MIN),
|
||||
_resolved_ip((uint32_t)0),
|
||||
_consecutive_failures(0),
|
||||
_started(false)
|
||||
{
|
||||
_IN = true;
|
||||
_OUT = true;
|
||||
_HW_MTU = TCP_IF_HW_MTU;
|
||||
// Report low bitrate + small announce_cap so that Transport
|
||||
// rate-limits announce forwarding through this interface.
|
||||
// Without this the backbone floods the ESP32 with announces.
|
||||
// 500 bps ≈ LoRa-class throughput; announce_cap = 2% max bandwidth.
|
||||
_bitrate = 500;
|
||||
_announce_cap = 2.0;
|
||||
if (target_host != nullptr) {
|
||||
strncpy(_target_host, target_host, sizeof(_target_host) - 1);
|
||||
_target_host[sizeof(_target_host) - 1] = '\0';
|
||||
} else {
|
||||
_target_host[0] = '\0';
|
||||
}
|
||||
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
|
||||
_clients[i].active = false;
|
||||
_clients[i].in_frame = false;
|
||||
_clients[i].escape = false;
|
||||
_clients[i].rxlen = 0;
|
||||
_clients[i].last_activity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
virtual ~TcpInterface() {
|
||||
stop();
|
||||
}
|
||||
|
||||
// ─── Lifecycle ───────────────────────────────────────────────────────────
|
||||
bool start() {
|
||||
if (_started) return true;
|
||||
|
||||
if (_mode == TCP_IF_MODE_SERVER) {
|
||||
_server = new WiFiServer(_port, TCP_IF_MAX_CLIENTS);
|
||||
_server->begin();
|
||||
_server->setNoDelay(true);
|
||||
Serial.printf("[TcpIF] Server listening on port %d\r\n", _port);
|
||||
_started = true;
|
||||
} else {
|
||||
// Client mode — try initial connection
|
||||
_started = true;
|
||||
_connect_client();
|
||||
}
|
||||
return _started;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
|
||||
if (_clients[i].active) {
|
||||
_clients[i].client.stop();
|
||||
_clients[i].active = false;
|
||||
}
|
||||
}
|
||||
if (_server) {
|
||||
_server->end();
|
||||
delete _server;
|
||||
_server = nullptr;
|
||||
}
|
||||
_started = false;
|
||||
_num_clients = 0;
|
||||
}
|
||||
|
||||
// ─── Main loop — call from Arduino loop() ────────────────────────────────
|
||||
void loop() {
|
||||
if (!_started) return;
|
||||
|
||||
// Accept new connections in server mode
|
||||
if (_mode == TCP_IF_MODE_SERVER && _server) {
|
||||
WiFiClient newClient = _server->available();
|
||||
if (newClient) {
|
||||
_accept_client(newClient);
|
||||
}
|
||||
}
|
||||
|
||||
// Client mode reconnection (with WiFi check + exponential backoff)
|
||||
if (_mode == TCP_IF_MODE_CLIENT && _num_clients == 0) {
|
||||
uint32_t now = millis();
|
||||
if (now - _last_reconnect >= _reconnect_interval) {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
_connect_client();
|
||||
} else {
|
||||
// WiFi not connected — skip TCP attempt, just update timer
|
||||
_last_reconnect = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send keepalive (empty HDLC frames) to prevent read timeout on both sides
|
||||
if (_num_clients > 0) {
|
||||
uint32_t now = millis();
|
||||
if (now - _last_keepalive >= TCP_IF_KEEPALIVE_INTERVAL) {
|
||||
_last_keepalive = now;
|
||||
uint8_t ka[] = { HDLC_FLAG, HDLC_FLAG };
|
||||
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
|
||||
if (_clients[i].active && _clients[i].client.connected()) {
|
||||
_clients[i].client.write(ka, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process incoming data from all active clients
|
||||
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
|
||||
if (!_clients[i].active) continue;
|
||||
|
||||
if (!_clients[i].client.connected()) {
|
||||
Serial.printf("[TcpIF] Client %d disconnected\r\n", i);
|
||||
_clients[i].client.stop();
|
||||
_clients[i].active = false;
|
||||
_clients[i].in_frame = false;
|
||||
_clients[i].escape = false;
|
||||
_clients[i].rxlen = 0;
|
||||
_num_clients--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check read timeout
|
||||
if (_clients[i].last_activity > 0 &&
|
||||
(millis() - _clients[i].last_activity) > TCP_IF_READ_TIMEOUT) {
|
||||
Serial.printf("[TcpIF] Client %d read timeout\r\n", i);
|
||||
_clients[i].client.stop();
|
||||
_clients[i].active = false;
|
||||
_clients[i].in_frame = false;
|
||||
_clients[i].rxlen = 0;
|
||||
_num_clients--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read available bytes and deframe
|
||||
while (_clients[i].client.available()) {
|
||||
uint8_t byte = _clients[i].client.read();
|
||||
_clients[i].last_activity = millis();
|
||||
_hdlc_deframe(i, byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stats ───────────────────────────────────────────────────────────────
|
||||
int clientCount() const { return _num_clients; }
|
||||
bool isStarted() const { return _started; }
|
||||
bool isConnected() const { return _num_clients > 0; }
|
||||
|
||||
protected:
|
||||
// ─── RNS InterfaceImpl: outgoing packet from RNS Transport ───────────────
|
||||
virtual void send_outgoing(const RNS::Bytes& data) override {
|
||||
if (!_started || _num_clients == 0) return;
|
||||
|
||||
// HDLC frame the data
|
||||
uint8_t frame_buf[TCP_IF_HW_MTU * 2 + 4]; // worst case: every byte escaped + 2 flags
|
||||
uint16_t flen = 0;
|
||||
|
||||
frame_buf[flen++] = HDLC_FLAG;
|
||||
for (size_t i = 0; i < data.size(); i++) {
|
||||
uint8_t b = data.data()[i];
|
||||
if (b == HDLC_FLAG || b == HDLC_ESC) {
|
||||
frame_buf[flen++] = HDLC_ESC;
|
||||
frame_buf[flen++] = b ^ HDLC_ESC_MASK;
|
||||
} else {
|
||||
frame_buf[flen++] = b;
|
||||
}
|
||||
if (flen >= sizeof(frame_buf) - 4) break; // safety
|
||||
}
|
||||
frame_buf[flen++] = HDLC_FLAG;
|
||||
|
||||
// Send to all connected clients (non-blocking: no flush)
|
||||
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
|
||||
if (_clients[i].active && _clients[i].client.connected()) {
|
||||
size_t written = _clients[i].client.write(frame_buf, flen);
|
||||
if (written == 0) {
|
||||
Serial.printf("[TcpIF] Write failed on client %d, dropping\r\n", i);
|
||||
_clients[i].client.stop();
|
||||
_clients[i].active = false;
|
||||
_clients[i].in_frame = false;
|
||||
_clients[i].rxlen = 0;
|
||||
_num_clients--;
|
||||
}
|
||||
}
|
||||
}
|
||||
yield(); // feed WDT between TCP writes and RNS processing
|
||||
|
||||
// Post-send housekeeping
|
||||
InterfaceImpl::handle_outgoing(data);
|
||||
}
|
||||
|
||||
// ─── RNS InterfaceImpl: incoming packet to RNS Transport ─────────────────
|
||||
virtual void handle_incoming(const RNS::Bytes& data) override {
|
||||
TRACEF("TcpInterface.handle_incoming: (%u bytes)", data.size());
|
||||
InterfaceImpl::handle_incoming(data);
|
||||
}
|
||||
|
||||
private:
|
||||
// ─── HDLC byte-level deframing ──────────────────────────────────────────
|
||||
void _hdlc_deframe(int idx, uint8_t byte) {
|
||||
TcpClient& c = _clients[idx];
|
||||
|
||||
if (byte == HDLC_FLAG) {
|
||||
if (c.in_frame && c.rxlen > 0) {
|
||||
// End of frame — deliver to RNS
|
||||
RNS::Bytes data(c.rxbuf, c.rxlen);
|
||||
handle_incoming(data);
|
||||
c.rxlen = 0;
|
||||
}
|
||||
c.in_frame = true;
|
||||
c.escape = false;
|
||||
c.rxlen = 0;
|
||||
} else if (c.in_frame) {
|
||||
if (c.escape) {
|
||||
byte ^= HDLC_ESC_MASK;
|
||||
c.escape = false;
|
||||
if (c.rxlen < TCP_IF_HW_MTU) {
|
||||
c.rxbuf[c.rxlen++] = byte;
|
||||
}
|
||||
} else if (byte == HDLC_ESC) {
|
||||
c.escape = true;
|
||||
} else {
|
||||
if (c.rxlen < TCP_IF_HW_MTU) {
|
||||
c.rxbuf[c.rxlen++] = byte;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Accept a new server-mode client ─────────────────────────────────────
|
||||
void _accept_client(WiFiClient& newClient) {
|
||||
// Find a free slot
|
||||
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
|
||||
if (!_clients[i].active) {
|
||||
_clients[i].client = newClient;
|
||||
_clients[i].client.setNoDelay(true);
|
||||
_clients[i].client.setTimeout(TCP_IF_WRITE_TIMEOUT / 1000);
|
||||
_clients[i].active = true;
|
||||
_clients[i].in_frame = false;
|
||||
_clients[i].escape = false;
|
||||
_clients[i].rxlen = 0;
|
||||
_clients[i].last_activity = millis();
|
||||
_num_clients++;
|
||||
Serial.printf("[TcpIF] Client %d connected from %s\r\n",
|
||||
i, _clients[i].client.remoteIP().toString().c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
// No free slots — reject
|
||||
Serial.println("[TcpIF] Max clients reached, rejecting connection");
|
||||
newClient.stop();
|
||||
}
|
||||
|
||||
// ─── Client-mode outbound connection ─────────────────────────────────────
|
||||
void _connect_client() {
|
||||
if (_target_host[0] == '\0') {
|
||||
Serial.println("[TcpIF] No target host configured for client mode");
|
||||
return;
|
||||
}
|
||||
|
||||
WiFiClient client;
|
||||
client.setTimeout(TCP_IF_CONNECT_TIMEOUT / 1000);
|
||||
|
||||
bool connected = false;
|
||||
|
||||
// Try cached IP first (avoids DNS lookup on every reconnect)
|
||||
if (_resolved_ip != (uint32_t)0) {
|
||||
Serial.printf("[TcpIF] Connecting to %s:%d (cached IP)...\r\n", _target_host, _target_port);
|
||||
connected = client.connect(_resolved_ip, _target_port);
|
||||
if (!connected) {
|
||||
// Cached IP failed — clear cache and try fresh DNS
|
||||
_resolved_ip = (uint32_t)0;
|
||||
Serial.println("[TcpIF] Cached IP failed, retrying with DNS");
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
Serial.printf("[TcpIF] Connecting to %s:%d (DNS)...\r\n", _target_host, _target_port);
|
||||
IPAddress resolved;
|
||||
if (WiFi.hostByName(_target_host, resolved)) {
|
||||
_resolved_ip = resolved;
|
||||
Serial.printf("[TcpIF] Resolved %s -> %s\r\n", _target_host, resolved.toString().c_str());
|
||||
connected = client.connect(resolved, _target_port);
|
||||
} else {
|
||||
Serial.printf("[TcpIF] DNS failed for %s\r\n", _target_host);
|
||||
}
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
client.setNoDelay(true);
|
||||
client.setTimeout(TCP_IF_WRITE_TIMEOUT / 1000);
|
||||
_clients[0].client = client;
|
||||
_clients[0].active = true;
|
||||
_clients[0].in_frame = false;
|
||||
_clients[0].escape = false;
|
||||
_clients[0].rxlen = 0;
|
||||
_clients[0].last_activity = millis();
|
||||
_num_clients = 1;
|
||||
_consecutive_failures = 0;
|
||||
_reconnect_interval = TCP_IF_RECONNECT_MIN;
|
||||
Serial.printf("[TcpIF] Connected to backbone at %s:%d\r\n",
|
||||
_target_host, _target_port);
|
||||
} else {
|
||||
_consecutive_failures++;
|
||||
// Exponential backoff: 10s -> 20s -> 40s -> 80s -> 120s (max)
|
||||
_reconnect_interval = _reconnect_interval * 2;
|
||||
if (_reconnect_interval > TCP_IF_RECONNECT_MAX) {
|
||||
_reconnect_interval = TCP_IF_RECONNECT_MAX;
|
||||
}
|
||||
Serial.printf("[TcpIF] Failed to connect to %s:%d (attempt %d, next retry in %ds)\r\n",
|
||||
_target_host, _target_port, _consecutive_failures,
|
||||
_reconnect_interval / 1000);
|
||||
}
|
||||
_last_reconnect = millis();
|
||||
}
|
||||
|
||||
// ─── Member variables ────────────────────────────────────────────────────
|
||||
TcpIfMode _mode;
|
||||
uint16_t _port;
|
||||
char _target_host[64];
|
||||
uint16_t _target_port;
|
||||
WiFiServer* _server;
|
||||
TcpClient _clients[TCP_IF_MAX_CLIENTS];
|
||||
int _num_clients;
|
||||
uint32_t _last_reconnect;
|
||||
uint32_t _last_keepalive;
|
||||
uint32_t _reconnect_interval;
|
||||
IPAddress _resolved_ip;
|
||||
uint16_t _consecutive_failures;
|
||||
bool _started;
|
||||
};
|
||||
|
||||
#endif // BOUNDARY_MODE
|
||||
#endif // HAS_RNS
|
||||
#endif // TCP_INTERFACE_H
|
||||
Reference in New Issue
Block a user