v1.0.5: Heltec V3 support, heap stability fix, TCP reconnection improvements

- Add Heltec WiFi LoRa 32 V3 board support (8MB flash, 8MB PSRAM)
  - New heltec_V3_boundary build environment in platformio.ini
  - Board auto-detection in flash.py (8MB=V3, 16MB=V4)
  - V3 board definition in Boards.h
- Fix heap exhaustion causing watchdog reboots every ~70 min
  - Lower boundary_mentioned_addresses cap from 512 to 200
  - Heap now stable at ~38KB free (was draining to 0)
- TCP reconnection improvements in TcpInterface.h
  - SO_LINGER(0) for clean socket teardown
  - 10-minute read timeout prevents zombie connections
  - Defensive client cleanup on accept
- Add heap telemetry instrumentation (HEAP-TEL) for monitoring
- Add level guards on TRACE/DEBUG macros in Log.h
- Update README for dual V3/V4 board support
This commit is contained in:
James L
2026-02-26 14:32:18 -05:00
parent 990649d810
commit 5e0e3f538a
8 changed files with 424 additions and 111 deletions

View File

@@ -17,6 +17,7 @@
#ifdef BOUNDARY_MODE
#include <WiFi.h>
#include <lwip/sockets.h> // SO_LINGER — force RST to free lwIP PCBs immediately
#include <Interface.h>
#include <Transport.h>
#include <Bytes.h>
@@ -75,6 +76,7 @@ public:
_last_reconnect(0),
_last_keepalive(0),
_reconnect_interval(TCP_IF_RECONNECT_MIN),
_read_timeout(TCP_IF_READ_TIMEOUT),
_resolved_ip((uint32_t)0),
_consecutive_failures(0),
_started(false)
@@ -128,7 +130,16 @@ public:
void stop() {
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
if (_clients[i].active) {
// Force RST to free lwIP PCBs immediately (no TIME_WAIT)
int fd = _clients[i].client.fd();
if (fd >= 0) {
struct linger lin;
lin.l_onoff = 1;
lin.l_linger = 0;
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
}
_clients[i].client.stop();
_clients[i].client = WiFiClient();
_clients[i].active = false;
}
}
@@ -185,25 +196,15 @@ public:
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--;
_cleanup_client(i, "disconnected");
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--;
// Check read timeout (0 = disabled)
if (_read_timeout > 0 &&
_clients[i].last_activity > 0 &&
(millis() - _clients[i].last_activity) > _read_timeout) {
_cleanup_client(i, "read timeout");
continue;
}
@@ -220,6 +221,7 @@ public:
int clientCount() const { return _num_clients; }
bool isStarted() const { return _started; }
bool isConnected() const { return _num_clients > 0; }
void setReadTimeout(uint32_t timeout_ms) { _read_timeout = timeout_ms; }
protected:
// ─── RNS InterfaceImpl: outgoing packet from RNS Transport ───────────────
@@ -248,12 +250,7 @@ protected:
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--;
_cleanup_client(i, "write failed");
}
}
}
@@ -270,6 +267,38 @@ protected:
}
private:
// ─── Cleanup a client slot, freeing all lwIP resources ───────────────────
void _cleanup_client(int idx, const char* reason) {
TcpClient& c = _clients[idx];
if (!c.active) return;
uint32_t heap_before = ESP.getFreeHeap();
// Set SO_LINGER with timeout 0: forces RST instead of FIN,
// which skips TIME_WAIT and immediately frees the lwIP PCB
// and all associated TCP send/receive buffers (~2-4 KB each).
int fd = c.client.fd();
if (fd >= 0) {
struct linger lin;
lin.l_onoff = 1;
lin.l_linger = 0;
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
}
c.client.stop();
c.client = WiFiClient(); // Release any residual shared_ptr state
c.active = false;
c.in_frame = false;
c.escape = false;
c.rxlen = 0;
_num_clients--;
uint32_t heap_after = ESP.getFreeHeap();
Serial.printf("[TcpIF] Client %d %s (heap: %u -> %u, delta: %+d)\r\n",
idx, reason, heap_before, heap_after,
(int)(heap_after - heap_before));
}
// ─── HDLC byte-level deframing ──────────────────────────────────────────
void _hdlc_deframe(int idx, uint8_t byte) {
TcpClient& c = _clients[idx];
@@ -306,6 +335,18 @@ private:
// Find a free slot
for (int i = 0; i < TCP_IF_MAX_CLIENTS; i++) {
if (!_clients[i].active) {
// Defensive: force-release any residual lwIP resources in this slot
// before assigning the new client (prevents PCB/buffer leaks)
int fd = _clients[i].client.fd();
if (fd >= 0) {
struct linger lin;
lin.l_onoff = 1;
lin.l_linger = 0;
setsockopt(fd, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
_clients[i].client.stop();
}
_clients[i].client = WiFiClient(); // Reset to clean state
_clients[i].client = newClient;
_clients[i].client.setNoDelay(true);
_clients[i].client.setTimeout(TCP_IF_WRITE_TIMEOUT / 1000);
@@ -399,6 +440,7 @@ private:
uint32_t _last_reconnect;
uint32_t _last_keepalive;
uint32_t _reconnect_interval;
uint32_t _read_timeout;
IPAddress _resolved_ip;
uint16_t _consecutive_failures;
bool _started;