OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid commits / e84fa44

esp324xlight: serve multiple clients concurrently
The firmware kept a single LastClient and every new connection stop()ed the incumbent (the old line 701). Two controllers therefore could not coexist: the Blender addon and a telnet session evicted each other indefinitely, and with a fast-reconnecting client that became a tight flapping loop rather than an occasional drop. Nothing was wrong with either client; the server only had one seat. Now MAX_CLIENTS (4) slots. A new connection takes a free slot and leaves healthy sessions alone. Eviction happens only when all four are genuinely live, and then takes the least recently active -- the one most likely already dead -- instead of whoever happened to be there first. Each slot carries its own line buffer, which is required rather than tidy: CmdMgr is one SerialCommand with a single internal buffer, so handing it bytes as they arrive from two clients would splice two commands together and corrupt both. Bytes now accumulate per client and are dispatched only once a complete line has arrived, keeping each command atomic. An overlong line is discarded rather than dispatched truncated, since a cut-off command parses as a different one. Replies follow the request. ActiveClient is set for the duration of one dispatch so printAck/diagPrint reach whoever asked; NULL means the command came from the serial console and replies go to Serial alone. Reboot and other unsolicited notices broadcast to every connected client. Idle timeouts are now per slot, replacing the single global lastClientByteMs. Verified over OTA on showlight02 with two simultaneous sessions: both greeted, neither evicted, replies correctly routed to the issuer and not the other, and -- the case the per-client buffers exist for -- client A's command split around a complete command from client B still parsed correctly: A sends "pw" -> B sends "hostname\n" -> A sends "m 0 0 0 ...\n" A got 'GOT pwm / OK pwm 12' B got 'hostname: showlight02' Costs 612 bytes of flash and 824 bytes of RAM (16% total, from 15%). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e84fa44bb6ffcf64d9f8781f0a7c0cb98e3b7732
by plankatron <fleeky@gmail.com> · 2026-08-30T20:00:50.000Z

Changed files (1) +117 -39

esp324xlight/esp324xlight.ino +117 -39
……@@ -90,10 +90,41 @@ const char *MsgPWMTupleFormatError = "Error: takes up to 6 arguments between 0 a
9090// Works around arduino-esp32 issues #1527 (connected() lies after FIN) and #4773
9191// (server.accept() can leave stale clients in place).
9292/////////////////////////////////////////////////////////////////////////////////////////
93WiFiClient LastClient;
94unsigned long lastClientByteMs = 0;
93// Several clients may be connected at once (e.g. the Blender addon driving the
94// rig while a telnet session is open to inspect it). Previously a single
95// LastClient was kept and every new connection stop()ed the incumbent, so two
96// controllers evicted each other indefinitely rather than coexisting.
97//
98// Each slot carries its OWN line buffer. That is required, not cosmetic:
99// CmdMgr is one SerialCommand with a single internal buffer, so feeding it
100// bytes from two clients as they arrive would splice two commands together and
101// corrupt both. Bytes are accumulated per client and only handed over once a
102// complete line has arrived, which keeps every command atomic.
103const int MAX_CLIENTS = 4;
104const int CLIENT_BUF_LEN = 160; // longest real command is a 12-value pwm2
105
106struct ClientSlot {
107 WiFiClient sock;
108 unsigned long lastByteMs = 0;
109 char buf[CLIENT_BUF_LEN];
110 int len = 0;
111};
112ClientSlot clients[MAX_CLIENTS];
113
114// Set for the duration of one command dispatch so replies reach whoever asked.
115// NULL means the command came from the serial console, so replies go to Serial
116// alone.
117WiFiClient *ActiveClient = NULL;
118
95119const unsigned long CLIENT_IDLE_TIMEOUT_MS = 300000; // 5 min — drop a dead session
96120
121// Write to every connected client, for messages not tied to one request.
122void broadcastLine(const char *s) {
123 for (int i = 0; i < MAX_CLIENTS; i++) {
124 if (clients[i].sock && clients[i].sock.connected()) clients[i].sock.println(s);
125 }
126}
127
97128// When true, command handlers also write GOT/OK/ERR markers to the active telnet
98129// client so you can see whether commands are reaching the parser. Toggle at runtime
99130// with "verbose 0" or "verbose 1". Default on for debugging.
……@@ -108,19 +139,19 @@ float channelGamma[3] = {1.0f, 1.0f, 1.0f};
108139
109140void diagPrint(const char *s) {
110141 Serial.print(s);
111 if (diagVerbose && LastClient && LastClient.connected()) LastClient.print(s);
142 if (diagVerbose && ActiveClient && ActiveClient->connected()) ActiveClient->print(s);
112143}
113144void diagPrint(long v) {
114145 Serial.print(v);
115 if (diagVerbose && LastClient && LastClient.connected()) LastClient.print(v);
146 if (diagVerbose && ActiveClient && ActiveClient->connected()) ActiveClient->print(v);
116147}
117148void diagPrintln(const char *s) {
118149 Serial.println(s);
119 if (diagVerbose && LastClient && LastClient.connected()) LastClient.println(s);
150 if (diagVerbose && ActiveClient && ActiveClient->connected()) ActiveClient->println(s);
120151}
121152void diagPrintln(long v) {
122153 Serial.println(v);
123 if (diagVerbose && LastClient && LastClient.connected()) LastClient.println(v);
154 if (diagVerbose && ActiveClient && ActiveClient->connected()) ActiveClient->println(v);
124155}
125156
126157/////////////////////////////////////////////////////////////////////////////////////////
……@@ -287,7 +318,7 @@ void cmdSetFrequency() {
287318 char buf[80];
288319 snprintf(buf, sizeof(buf), "OK freq %d Hz at %d-bit (dutyMax=%d)", freq, res, dutyMax);
289320 Serial.println(buf);
290 if (diagVerbose && LastClient && LastClient.connected()) LastClient.println(buf);
321 if (diagVerbose && ActiveClient && ActiveClient->connected()) ActiveClient->println(buf);
291322}
292323
293324// pwm2 <numPeriods> <periodLengthMs> <v1> ... <v12>
……@@ -460,9 +491,9 @@ void cmdVerbose() {
460491 }
461492 Serial.print("verbose ");
462493 Serial.println(diagVerbose ? "1" : "0");
463 if (LastClient && LastClient.connected()) {
464 LastClient.print("verbose ");
465 LastClient.println(diagVerbose ? "1" : "0");
494 if (ActiveClient && ActiveClient->connected()) {
495 ActiveClient->print("verbose ");
496 ActiveClient->println(diagVerbose ? "1" : "0");
466497 }
467498}
468499
……@@ -480,7 +511,7 @@ void cmdGamma() {
480511 snprintf(buf, sizeof(buf), "gamma B=%.2f G=%.2f R=%.2f",
481512 channelGamma[0], channelGamma[1], channelGamma[2]);
482513 Serial.println(buf);
483 if (LastClient && LastClient.connected()) LastClient.println(buf);
514 if (ActiveClient && ActiveClient->connected()) ActiveClient->println(buf);
484515}
485516
486517// hostname [name] — get or set the mDNS/OTA hostname. Set persists to NVS and
……@@ -510,12 +541,12 @@ void cmdHostname() {
510541 snprintf(buf, sizeof(buf),
511542 "hostname stored as '%s' — `reboot` to apply", arg);
512543 Serial.println(buf);
513 if (LastClient && LastClient.connected()) LastClient.println(buf);
544 if (ActiveClient && ActiveClient->connected()) ActiveClient->println(buf);
514545 } else {
515546 char buf[128];
516547 snprintf(buf, sizeof(buf), "hostname: %s", currentHostname);
517548 Serial.println(buf);
518 if (LastClient && LastClient.connected()) LastClient.println(buf);
549 if (ActiveClient && ActiveClient->connected()) ActiveClient->println(buf);
519550 }
520551}
521552
……@@ -523,10 +554,11 @@ void cmdHostname() {
523554// from a stuck state.
524555void cmdReboot() {
525556 Serial.println("Rebooting...");
526 if (LastClient && LastClient.connected()) {
527 LastClient.println("Rebooting...");
528 LastClient.flush();
529 LastClient.stop();
557 broadcastLine("Rebooting...");
558 for (int i = 0; i < MAX_CLIENTS; i++) {
559 if (!clients[i].sock) continue;
560 clients[i].sock.flush();
561 clients[i].sock.stop();
530562 }
531563 delay(200); // let buffers flush
532564 ESP.restart();
……@@ -590,7 +622,7 @@ InitState initState = FADE_TEST;
590622int wifiWaitCount = 0;
591623bool serverStarted = false;
592624unsigned long fadeStartMs = 0;
593// LastClient declared at the top with the diag helpers.
625// Client slots declared at the top with the diag helpers.
594626
595627// 10-second boot self-test: fade all pins off→on→off so we can see the chip
596628// is alive. Runs before WiFi so it's visible on every cold boot.
……@@ -696,31 +728,77 @@ void loop() {
696728 }
697729
698730 if (serverStarted) {
699 WiFiClient newClient = server.accept();
700 if (newClient) {
701 if (LastClient) LastClient.stop(); // drop the old session cleanly
702 LastClient = newClient;
703 lastClientByteMs = millis();
704 Serial.println("New Client.");
705 LastClient.print(currentHostname);
706 LastClient.println(" ready");
731 // --- accept into a free slot; never evict a healthy client ---
732 WiFiClient incoming = server.accept();
733 if (incoming) {
734 int slot = -1;
735 for (int i = 0; i < MAX_CLIENTS; i++) {
736 if (!clients[i].sock || !clients[i].sock.connected()) { slot = i; break; }
737 }
738 if (slot < 0) {
739 // Genuinely full: drop the least recently active, which is the one
740 // most likely to be dead. Only reachable with MAX_CLIENTS live
741 // sessions, not on every new connection as before.
742 unsigned long oldest = ~0UL; // avoids depending on <limits.h>
743 slot = 0;
744 for (int i = 0; i < MAX_CLIENTS; i++) {
745 if (clients[i].lastByteMs < oldest) { oldest = clients[i].lastByteMs; slot = i; }
746 }
747 Serial.printf("Client slots full — evicting slot %d\n", slot);
748 clients[slot].sock.stop();
749 }
750 clients[slot].sock = incoming;
751 clients[slot].lastByteMs = millis();
752 clients[slot].len = 0;
753 Serial.printf("New client in slot %d\n", slot);
754 clients[slot].sock.print(currentHostname);
755 clients[slot].sock.println(" ready");
707756 }
708 if (LastClient && LastClient.connected()) {
709 while (LastClient.available()) {
710 char c = LastClient.read();
711 CmdMgr.handleChar(c);
712 Serial.write(c);
713 lastClientByteMs = millis();
757
758 // --- service every slot ---
759 for (int i = 0; i < MAX_CLIENTS; i++) {
760 ClientSlot &c = clients[i];
761 if (!c.sock) continue;
762
763 if (!c.sock.connected()) {
764 c.sock.stop();
765 c.sock = WiFiClient();
766 c.len = 0;
767 continue;
714768 }
769
770 while (c.sock.available()) {
771 char ch = c.sock.read();
772 c.lastByteMs = millis();
773 Serial.write(ch);
774
775 if (ch == '\n' || ch == '\r') {
776 if (c.len > 0) {
777 // Hand over one whole command, with this client marked active
778 // so its replies come back to it and not to some other session.
779 ActiveClient = &c.sock;
780 for (int k = 0; k < c.len; k++) CmdMgr.handleChar(c.buf[k]);
781 CmdMgr.handleChar('\n');
782 ActiveClient = NULL;
783 c.len = 0;
784 }
785 } else if (c.len < CLIENT_BUF_LEN - 1) {
786 c.buf[c.len++] = ch;
787 } else {
788 // Overlong line: discard it rather than dispatch a truncated
789 // command that would be parsed as something else.
790 c.len = 0;
791 Serial.printf("Client %d: line too long, discarded\n", i);
792 }
793 }
794
715795 // Idle-timeout: connected() can lie after FIN. Drop stale sessions.
716 if (millis() - lastClientByteMs > CLIENT_IDLE_TIMEOUT_MS) {
717 Serial.println("Client idle — dropping");
718 LastClient.stop();
719 LastClient = WiFiClient();
796 if (millis() - c.lastByteMs > CLIENT_IDLE_TIMEOUT_MS) {
797 Serial.printf("Client %d idle — dropping\n", i);
798 c.sock.stop();
799 c.sock = WiFiClient();
800 c.len = 0;
720801 }
721 } else if (LastClient) {
722 LastClient.stop();
723 LastClient = WiFiClient();
724802 }
725803 }
726804 break;