Files
wadamesh/src/helpers/esp32/TCPCompanionServer.cpp
T
Kaj SchittecatandClaude Opus 4.8 2353c56adc p4: T-Display P4 port — web VNC/remote, time-sync hardening, brightness + sound
LilyGo T-Display P4 (ESP32-P4 + factory ESP-AT C6) reaches POC: LoRa mesh, Wi-Fi
companion (TCP:5000), GPS, SD, battery gauge, chat persistence, and now:

- Web VNC/REMOTE/terminal via a first-byte router on the single ESP-AT listener
  (HTTP verbs -> WS server, '<' frames -> phone companion; both live at once).
  C6Server gains a _begun gate so never-listening instances stay inert.
- VNC freeze fixed: bounded one-CIPSEND-chunk drains under the WS client mutex
  (a whole-band blocking write starved the main loop), fast-drop of stalled
  mirror clients, SEND-OK wait 8s->4s.
- Wi-Fi robustness: CWJAP? miss-streak tolerance, CIPSERVER liveness verify +
  re-arm (a re-association silently killed the listener until reboot).
- Time sync: HTTP-Date fallback when SNTP is blocked; ClockFloorRTC gains a
  MAX_PLAUSIBLE_EPOCH guard (all boards) after a garbage-future hardware RTC
  read (2043) latched the ratchet.
- Brightness: RM69A10 DCS 0x51 driven live (CC slider + persisted pref).
- Sound: ES8311 + NS4150B notification chimes (P4Audio, esp-bsp-derived
  register sequence on Wire1), full HAS_UI_SOUND surface + CC Screen/Sound
  sliders; ccVolumeReleaseCb's hardcoded tanBeep -> uiSoundPreview.
- tdisplay_p4/: IDF project (build.sh, c6_at AT-over-SDIO driver, sdkconfigs,
  fetch-deps.sh vendoring, .gitignore for local payloads).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:13:23 +02:00

233 lines
7.7 KiB
C++

#include "TCPCompanionServer.h"
#include <lwip/sockets.h>
#define RECV_STATE_IDLE 0
#define RECV_STATE_HDR_FOUND 1
#define RECV_STATE_LEN1_FOUND 2
#define RECV_STATE_LEN2_FOUND 3
#define TCP_WRITE_TIMEOUT_MS 120
#define TCP_WEDGED_DROP_MS 10000
// True when lwIP can accept bytes for this socket right now. select() only reports
// writable once at least TCP_SNDLOWAT (~2.8 KB) of send buffer is free, which holds a
// whole companion frame (MAX_FRAME_SIZE + header), so a write issued after this probe
// completes without blocking. Without the probe, WiFiClient::write() sits in 1 s
// select() waits (up to 10) against a peer that stopped ACKing — a phone asleep in a
// pocket keeps its socket "connected" but unwritable, and every mesh-RX push then
// stalls the loop thread (and the UI) for seconds.
static bool socketWritableNow(WiFiClient& client) {
#if defined(HAS_TDISPLAY_P4)
// C6/AT transport: no lwIP fd to select() on (fd() is -1 by design). Writes go through the AT
// worker whose CIPSEND exchange is itself bounded (the C6 buffers TX), so liveness is the only
// cheap pre-check here.
return client.connected();
#else
int fd = client.fd();
if (fd < 0) return false;
fd_set wset;
FD_ZERO(&wset);
FD_SET(fd, &wset);
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 0;
return select(fd + 1, NULL, &wset, NULL, &tv) > 0 && FD_ISSET(fd, &wset);
#endif
}
static bool writeAllBytes(WiFiClient& client, const uint8_t* buf, size_t len, uint32_t timeout_ms) {
size_t sent = 0;
uint32_t start = millis();
while (sent < len) {
if (!client.connected()) return false;
if (socketWritableNow(client)) {
size_t n = client.write(buf + sent, len - sent);
if (n > 0) {
sent += n;
continue;
}
}
if (millis() - start >= timeout_ms) return false;
delay(1);
}
return true;
}
TCPCompanionServer::TCPCompanionServer() : _server(WiFiServer()), _port(0), _poll_start_idx(0) {
for (int i = 0; i < TCP_COMPANION_MAX_CLIENTS; i++) {
_clients[i].state = RECV_STATE_IDLE;
_clients[i].in_use = false;
_clients[i].stall_ms = 0;
}
}
void TCPCompanionServer::begin(uint16_t port) {
_port = port;
_server.begin(port);
}
void TCPCompanionServer::stop() {
_server.stop();
for (int i = 0; i < TCP_COMPANION_MAX_CLIENTS; i++) {
if (_clients[i].in_use) {
_clients[i].client.stop();
_clients[i].in_use = false;
}
}
}
void TCPCompanionServer::adoptClient(WiFiClient& incoming) {
// Slot-insert an already-accepted connection. Used by our own accept loop below AND by the
// T-Display P4's first-byte router (one shared AT listener dispatches companion vs web).
int slot = -1;
for (int i = 0; i < TCP_COMPANION_MAX_CLIENTS; i++) {
if (!_clients[i].in_use) {
slot = i;
break;
}
}
if (slot >= 0) {
_clients[slot].client = incoming;
_clients[slot].in_use = true;
_clients[slot].state = RECV_STATE_IDLE;
_clients[slot].rx_len = 0;
_clients[slot].stall_ms = 0;
} else {
incoming.stop();
}
}
void TCPCompanionServer::acceptNewClients() {
while (_server.hasClient()) {
WiFiClient incoming = _server.accept();
if (!incoming) continue;
adoptClient(incoming);
}
}
void TCPCompanionServer::pruneDisconnected() {
for (int i = 0; i < TCP_COMPANION_MAX_CLIENTS; i++) {
if (_clients[i].in_use && !_clients[i].client.connected()) {
_clients[i].client.stop();
_clients[i].in_use = false;
_clients[i].state = RECV_STATE_IDLE;
}
}
}
void TCPCompanionServer::disconnectClient(int client_index) {
if (client_index >= 0 && client_index < TCP_COMPANION_MAX_CLIENTS && _clients[client_index].in_use) {
_clients[client_index].client.stop();
_clients[client_index].in_use = false;
_clients[client_index].state = RECV_STATE_IDLE;
_clients[client_index].stall_ms = 0;
}
}
bool TCPCompanionServer::isClientConnected(int client_index) const {
if (client_index < 0 || client_index >= TCP_COMPANION_MAX_CLIENTS) return false;
return _clients[client_index].in_use && _clients[client_index].client.connected();
}
int TCPCompanionServer::connectedCount() const {
int n = 0;
for (int i = 0; i < TCP_COMPANION_MAX_CLIENTS; i++) {
if (_clients[i].in_use && _clients[i].client.connected()) n++;
}
return n;
}
size_t TCPCompanionServer::pollRecvFrame(uint8_t dest[], int* client_index_out) {
acceptNewClients();
pruneDisconnected();
// Fair polling across active TCP clients: rotate the first slot we inspect
// so one busy client cannot starve other connected clients.
int start = _poll_start_idx;
if (start < 0 || start >= TCP_COMPANION_MAX_CLIENTS) start = 0;
for (int off = 0; off < TCP_COMPANION_MAX_CLIENTS; off++) {
int idx = (start + off) % TCP_COMPANION_MAX_CLIENTS;
if (!_clients[idx].in_use || !_clients[idx].client.connected()) continue;
TCPClientState* c = &_clients[idx];
WiFiClient* cl = &c->client;
while (cl->available()) {
int b = cl->read();
if (b < 0) break;
switch (c->state) {
case RECV_STATE_IDLE:
if (b == '<') c->state = RECV_STATE_HDR_FOUND;
break;
case RECV_STATE_HDR_FOUND:
c->frame_len = (uint8_t)b;
c->state = RECV_STATE_LEN1_FOUND;
break;
case RECV_STATE_LEN1_FOUND:
c->frame_len |= ((uint16_t)b) << 8;
c->rx_len = 0;
c->state = (c->frame_len > 0) ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE;
break;
default:
if (c->rx_len < MAX_FRAME_SIZE) c->rx_buf[c->rx_len] = (uint8_t)b;
c->rx_len++;
if (c->rx_len >= c->frame_len) {
size_t copy_len = c->frame_len;
if (copy_len > MAX_FRAME_SIZE) copy_len = MAX_FRAME_SIZE;
memcpy(dest, c->rx_buf, copy_len);
c->state = RECV_STATE_IDLE;
if (client_index_out) *client_index_out = idx;
_poll_start_idx = (idx + 1) % TCP_COMPANION_MAX_CLIENTS;
return copy_len;
}
break;
}
}
}
_poll_start_idx = (start + 1) % TCP_COMPANION_MAX_CLIENTS;
return 0;
}
size_t TCPCompanionServer::writeToClient(int client_index, const uint8_t src[], size_t len) {
if (client_index < 0 || client_index >= TCP_COMPANION_MAX_CLIENTS || len > MAX_FRAME_SIZE) return 0;
if (!_clients[client_index].in_use || !_clients[client_index].client.connected()) return 0;
uint8_t hdr[3];
hdr[0] = '>';
hdr[1] = (len & 0xFF);
hdr[2] = (len >> 8);
WiFiClient* cl = &_clients[client_index].client;
if (!writeAllBytes(*cl, hdr, 3, TCP_WRITE_TIMEOUT_MS) ||
!writeAllBytes(*cl, src, len, TCP_WRITE_TIMEOUT_MS)) {
// Return 0 so caller can retry (e.g. contact list). A transient full buffer must
// not disconnect — the companion layer retries and completes the sequence. But a
// peer that stopped ACKing (phone asleep, walked out of range) keeps a half-open
// socket "connected" indefinitely while staying unwritable; drop it once the
// failure has been continuous for TCP_WEDGED_DROP_MS so it stops eating the
// write-timeout budget on every pushed frame.
TCPClientState* c = &_clients[client_index];
if (c->stall_ms == 0) {
c->stall_ms = millis() | 1;
} else if (millis() - c->stall_ms >= TCP_WEDGED_DROP_MS) {
disconnectClient(client_index);
}
return 0;
}
_clients[client_index].stall_ms = 0;
return len;
}
size_t TCPCompanionServer::writeToAllClients(const uint8_t src[], size_t len) {
if (len == 0 || len > MAX_FRAME_SIZE) return 0;
int connected = 0;
int sent = 0;
for (int i = 0; i < TCP_COMPANION_MAX_CLIENTS; i++) {
if (_clients[i].in_use && _clients[i].client.connected()) {
connected++;
if (writeToClient(i, src, len) == len) sent++;
}
}
return (sent == connected) ? len : 0;
}