diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp index 027581e3039..d94d82c7fb8 100644 --- a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp @@ -71,39 +71,145 @@ bool RAK13800EthernetInterface::begin() { return true; } -int RAK13800EthernetInterface::available() { - return client.available(); +void RAK13800EthernetInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); } -int RAK13800EthernetInterface::read() { - return client.read(); +void RAK13800EthernetInterface::disable() { + _isEnabled = false; } -size_t RAK13800EthernetInterface::write(const uint8_t *buf, size_t size) { - return client.write(buf, size); -} +size_t RAK13800EthernetInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(): frame too big, len=%d", (int)len); + return 0; + } + if (!_connected || len == 0) return 0; + + if (send_queue_len >= ETH_FRAME_QUEUE_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(): send_queue full (dropping code=0x%02x)", src[0]); + return 0; + } + + // PUSH codes (>= 0x80) go to all clients; command responses go to the + // client that issued the most recent command. + int8_t target = (src[0] >= 0x80) ? -1 : (int8_t)_last_rx; -bool RAK13800EthernetInterface::isConnected() const { - return _isConnected; + send_queue[send_queue_len].target = target; + send_queue[send_queue_len].len = (uint8_t)len; + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + return len; } -void RAK13800EthernetInterface::loop() { +size_t RAK13800EthernetInterface::checkRecvFrame(uint8_t dest[]) { - Ethernet.maintain(); - - auto newClient = server.accept(); - if (newClient) { - IPAddress remoteIp = newClient.remoteIP(); - uint16_t remotePort = newClient.remotePort(); - ETHERNET_DEBUG_PRINTLN("New client accepted %u.%u.%u.%u:%u", remoteIp[0], remoteIp[1], remoteIp[2], remoteIp[3], remotePort); - if (client) { - ETHERNET_DEBUG_PRINTLN("Closing previous client"); - client.stop(); + // ---- accept a new connection into a free slot -------------------------- + // accept() returns each new connection once and maintains the listen socket, + // so it must be called every loop. + EthernetClient nc = server.accept(); + if (nc) { + int slot = -1; + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { + if (!clients[i].connected()) { slot = i; break; } + } + if (slot >= 0) { + clients[slot].stop(); // free any lingering socket in this slot + clients[slot] = nc; + rx_header[slot].type = 0; + rx_header[slot].length = 0; + ETHERNET_DEBUG_PRINTLN("Got connection (slot %d)", slot); + } else { + nc.stop(); // all slots busy — reject + ETHERNET_DEBUG_PRINTLN("Rejected connection (all %d slots busy)", MAX_ETH_CLIENTS); } - client = newClient; - onClientConnected(); } - _isConnected = client.connected(); + // ---- refresh connected state, free dropped sockets --------------------- + bool any = false; + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { + if (clients[i].connected()) { + any = true; + } else if (rx_header[i].type || rx_header[i].length) { + // a client that was active just dropped — reset its parse state + rx_header[i].type = 0; + rx_header[i].length = 0; + clients[i].stop(); + ETHERNET_DEBUG_PRINTLN("Disconnected (slot %d)", i); + } + } + _connected = any; + + // ---- drain the outbound queue ------------------------------------------ + while (send_queue_len > 0) { + Frame &f = send_queue[0]; + uint8_t pkt[3 + MAX_FRAME_SIZE]; + pkt[0] = '>'; + pkt[1] = (f.len & 0xFF); + pkt[2] = (f.len >> 8); + memcpy(&pkt[3], f.buf, f.len); + + if (f.target < 0) { // broadcast (push) + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { + if (clients[i].connected()) clients[i].write(pkt, 3 + f.len); + } + } else if (f.target < MAX_ETH_CLIENTS && clients[f.target].connected()) { + clients[f.target].write(pkt, 3 + f.len); // response to the requester + } + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) send_queue[i] = send_queue[i + 1]; + } + + // ---- read ONE inbound frame (round-robin across clients) --------------- + for (int k = 0; k < MAX_ETH_CLIENTS; k++) { + int i = (_rr + k) % MAX_ETH_CLIENTS; + EthernetClient &c = clients[i]; + if (!c.connected()) continue; + + // frame header = [type][len_lo][len_hi] + if (rx_header[i].type == 0 || rx_header[i].length == 0) { + if (c.available() >= 3) { + c.readBytes(&rx_header[i].type, 1); + c.readBytes((uint8_t *)&rx_header[i].length, 2); + } + } + + if (rx_header[i].type != 0 && rx_header[i].length != 0) { + int avail = c.available(); + int frame_type = rx_header[i].type; + int frame_length = rx_header[i].length; + + if (frame_length > avail) continue; // wait for the rest + + if (frame_length > MAX_FRAME_SIZE || frame_type != '<') { + // oversized or unexpected type — discard + while (frame_length > 0) { + uint8_t skip[1]; + int n = c.read(skip, 1); + if (n <= 0) break; + frame_length -= n; + } + rx_header[i].type = 0; + rx_header[i].length = 0; + continue; + } + + c.readBytes(dest, frame_length); + rx_header[i].type = 0; + rx_header[i].length = 0; + _last_rx = i; // route responses back here + _rr = (i + 1) % MAX_ETH_CLIENTS; // fairness + ETHERNET_DEBUG_PRINTLN("RX[%d] cmd=0x%02x len=%d", i, dest[0], frame_length); + return frame_length; + } + } + + return 0; +} + +void RAK13800EthernetInterface::loop() { + Ethernet.maintain(); } diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h index 34bb7587775..2239f1c1263 100644 --- a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h @@ -4,24 +4,85 @@ #include #include +// Multi-client TCP companion interface over a W5100S Ethernet module (RAK13800). +// Lets several clients (e.g. Home Assistant AND the phone app) stay connected +// at once — a single-client model has them kicking each other off the one +// socket, causing an endless reconnect loop. +// +// This fully overrides SerialEthernetInterface's writeFrame()/checkRecvFrame() +// (and enable/disable/isWriteBusy) rather than using its single-client queue + +// framing state, since routing frames to/from several concurrent clients needs +// per-client state the shared base doesn't track. available()/read()/write() +// are still implemented (trivially) only to satisfy the base's pure-virtual +// contract — they are unused here. +// +// Routing of outbound frames (the companion protocol isn't natively +// multi-client, so we route by frame code): +// - PUSH frames (code >= 0x80, e.g. LoRa-RX log, adverts) -> ALL clients +// - command RESPONSES (code < 0x80) -> the client +// that issued +// the last command + +#ifndef MAX_ETH_CLIENTS + #define MAX_ETH_CLIENTS 3 // W5100S has 4 sockets: up to 3 clients + 1 listen +#endif + class RAK13800EthernetInterface : public SerialEthernetInterface { - - bool _isConnected; + + bool _isEnabled; + bool _connected; // true if at least one client is connected + EthernetServer server; - EthernetClient client; + EthernetClient clients[MAX_ETH_CLIENTS]; + + struct FrameHeader { uint8_t type; uint16_t length; }; + FrameHeader rx_header[MAX_ETH_CLIENTS]; // per-client inbound parse state + + struct Frame { + int8_t target; // -1 = broadcast, else client index + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define ETH_FRAME_QUEUE_SIZE 16 + int send_queue_len; + Frame send_queue[ETH_FRAME_QUEUE_SIZE]; + + int _last_rx; // client index of the most recent inbound command + int _rr; // round-robin cursor for fair inbound polling + + void clearBuffers() { + send_queue_len = 0; + _last_rx = -1; + _rr = 0; + for (int i = 0; i < MAX_ETH_CLIENTS; i++) { rx_header[i].type = 0; rx_header[i].length = 0; } + } public: RAK13800EthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { - _isConnected = false; + _isEnabled = false; + _connected = false; + clearBuffers(); } - + bool begin(); void loop() override; // BaseSerialInterface methods - bool isConnected() const override; + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override { return _connected; } + bool isWriteBusy() const override { return false; } + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; - int available() override; - int read() override; - size_t write(const uint8_t *buf, size_t size) override; + // Unused — checkRecvFrame()/writeFrame() are fully overridden above for + // multi-client routing. Only implemented to satisfy SerialEthernetInterface's + // pure-virtual contract. + int available() override { return 0; } + int read() override { return -1; } + size_t write(const uint8_t *buf, size_t size) override { (void)buf; (void)size; return 0; } };