Skip to content

Commit 1051681

Browse files
skialpineclaude
andcommitted
fix: defer WS hardware ops to main loop; clean up webserver init
The /snapshot WebSocket handler runs on the AsyncTCP task. Several commands (display, low_power, sleep, power_off) were calling u8g2.setPowerSave / u8g2.setContrast / digitalWrite(PWR_CTRL,...) directly from that task while the main loop hammered the same peripherals — under feature-test load at 10Hz this caused the device to stall for ~4s and HTTP responses to time out mid-body. * Add a portMUX-protected pending-action mask. The WS callback queues the hardware op and updates the matching state flag (b_u8g2Sleep / b_softSleep) synchronously so the status frame is accurate; the main loop drains the mask early in loop() (before the b_softSleep guard so SLEEP_OFF can wake the device). * StopWatch.* is just bool/uint32 writes, so timer ops stay synchronous in the callback. * webserver.h: register handlers only once across stop/start cycles, drop the duplicate addHandler(&websocket), and move server.begin() to the end of startWebServer() so handlers are in place before the server accepts requests. * WS DATA callback: build the String with the (buf,len) constructor instead of O(N²) char-by-char concatenation, and only process complete unfragmented text frames. * Use unsigned long for the WS broadcast lastUpdate to avoid signed-subtraction wrap at the millis() 49.7-day rollover. * Document why WiFi.setSleep(false) is the wrong call here: tested it, caused 8% loss and HTTP all-timeout when BLE is active because BT/WiFi share the 2.4GHz radio and the coex layer needs WiFi sleep windows for BT slots. Stress test (10Hz WS + HTTP every 1s + 5 cmd/s feature chaos, 120s): pre-fix post-fix max gap 4.18 s 0.90 s HTTP fail 1 0 HTTP slow 2 0 Feature regression (60 WS commands) still 60/60 PASS. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c0eb374 commit 1051681

4 files changed

Lines changed: 126 additions & 58 deletions

File tree

include/parameter.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ uint8_t i_websocketLedR = 0;
2525
uint8_t i_websocketLedG = 0;
2626
uint8_t i_websocketLedB = 0;
2727
unsigned long t_lastWebsocketStatusUpdate = 0;
28+
29+
// Websocket pending-command mask. Set on the AsyncTCP task by the WS event
30+
// callback; drained on the main loop. Defers hardware-touching ops (u8g2,
31+
// stopWatch, power-rail GPIOs) so they never race the main loop.
32+
const uint32_t WSP_DISPLAY_ON = 1u << 0;
33+
const uint32_t WSP_DISPLAY_OFF = 1u << 1;
34+
const uint32_t WSP_LOWPWR_ON = 1u << 2;
35+
const uint32_t WSP_LOWPWR_OFF = 1u << 3;
36+
const uint32_t WSP_SLEEP_ON = 1u << 4;
37+
const uint32_t WSP_SLEEP_OFF = 1u << 5;
38+
const uint32_t WSP_POWER_OFF = 1u << 6;
39+
portMUX_TYPE wsPendingMux = portMUX_INITIALIZER_UNLOCKED;
40+
volatile uint32_t wsPendingMask = 0;
41+
2842
int i_onWrite_counter = 0;
2943
unsigned long t_heartBeat = 0;
3044
unsigned long t_firstConnect = 0;

include/webserver.h

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,49 +14,53 @@ static AsyncWebServer server(80);
1414
static AsyncWebSocket websocket("/snapshot");
1515

1616
void startWebServer() {
17-
server.begin();
18-
Serial.println("HTTP server started");
19-
AsyncCallbackJsonWebHandler *wifiHandler = new AsyncCallbackJsonWebHandler(
20-
"/setup/wifi", [](AsyncWebServerRequest *request, JsonVariant &json) {
21-
JsonObject jsonObj = json.as<JsonObject>();
22-
if (jsonObj.isNull()) {
23-
request->send(400);
24-
return;
25-
}
26-
if (jsonObj["ssid"] == NULL) {
27-
request->send(400);
28-
return;
29-
}
30-
String ssid = jsonObj["ssid"];
31-
String pass = jsonObj["pass"];
17+
// Handlers must be registered before server.begin(), and only once across
18+
// stop/start cycles — server.end() doesn't clear the handler list.
19+
static bool handlersRegistered = false;
20+
if (!handlersRegistered) {
21+
AsyncCallbackJsonWebHandler *wifiHandler = new AsyncCallbackJsonWebHandler(
22+
"/setup/wifi", [](AsyncWebServerRequest *request, JsonVariant &json) {
23+
JsonObject jsonObj = json.as<JsonObject>();
24+
if (jsonObj.isNull()) {
25+
request->send(400);
26+
return;
27+
}
28+
if (jsonObj["ssid"] == NULL) {
29+
request->send(400);
30+
return;
31+
}
32+
String ssid = jsonObj["ssid"];
33+
String pass = jsonObj["pass"];
3234

33-
saveCredentials(ssid, pass);
34-
Serial.println("new ssid saved");
35-
request->send(200);
36-
esp_restart();
37-
});
38-
server.addHandler(wifiHandler);
35+
saveCredentials(ssid, pass);
36+
Serial.println("new ssid saved");
37+
request->send(200);
38+
esp_restart();
39+
});
40+
server.addHandler(wifiHandler);
3941

40-
server.addHandler(&websocket).addMiddleware([](AsyncWebServerRequest *request, ArMiddlewareNext next) {
41-
// ws.count() is the current count of WS clients: this one is trying to upgrade its HTTP connection
42-
if (websocket.count() > 0) {
43-
// if we have 1 clients or more, prevent the next one to connect
44-
request->send(503, "text/plain", "Server is busy");
42+
server.addHandler(&websocket).addMiddleware([](AsyncWebServerRequest *request, ArMiddlewareNext next) {
43+
// ws.count() is the current count of WS clients: this one is trying to upgrade its HTTP connection
44+
if (websocket.count() > 0) {
45+
// if we have 1 clients or more, prevent the next one to connect
46+
request->send(503, "text/plain", "Server is busy");
47+
} else {
48+
// process next middleware and at the end the handler
49+
next();
50+
}
51+
});
52+
53+
if (!LittleFS.begin()) {
54+
Serial.println("LittleFS mount failed");
4555
} else {
46-
// process next middleware and at the end the handler
47-
next();
56+
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
57+
Serial.println("Serving web-apps");
4858
}
49-
});
50-
server.addHandler(&websocket);
51-
52-
if (!LittleFS.begin()) {
53-
Serial.println("SPIFFS failed");
54-
return;
59+
handlersRegistered = true;
5560
}
5661

57-
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
58-
59-
Serial.println("Serving web-apps");
62+
server.begin();
63+
Serial.println("HTTP server started");
6064
}
6165

6266
void stopWebServer() {

src/hds.ino

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,45 @@ bool parseWebsocketRgb(String input, uint8_t &r, uint8_t &g, uint8_t &b) {
483483
return true;
484484
}
485485

486+
// Queue a pending hardware action so the main loop performs it on its own
487+
// task. The WS event callback runs on the AsyncTCP task and must not touch
488+
// u8g2, stopWatch, or power-rail GPIOs directly.
489+
inline void wsQueuePending(uint32_t bits) {
490+
portENTER_CRITICAL(&wsPendingMux);
491+
wsPendingMask |= bits;
492+
portEXIT_CRITICAL(&wsPendingMux);
493+
}
494+
495+
void processWsPendingCmds() {
496+
portENTER_CRITICAL(&wsPendingMux);
497+
uint32_t mask = wsPendingMask;
498+
wsPendingMask = 0;
499+
portEXIT_CRITICAL(&wsPendingMux);
500+
if (mask == 0) return;
501+
502+
// Hardware ops only — touching u8g2 (I2C) or peripheral power rails from
503+
// the AsyncTCP task can race the main loop. State flags (b_u8g2Sleep,
504+
// b_softSleep, etc.) are already updated synchronously in the WS callback
505+
// so status frames stay accurate.
506+
if (mask & WSP_DISPLAY_ON) { u8g2.setPowerSave(0); }
507+
if (mask & WSP_DISPLAY_OFF) { u8g2.setPowerSave(1); }
508+
if (mask & WSP_LOWPWR_ON) { u8g2.setContrast(0); }
509+
if (mask & WSP_LOWPWR_OFF) { u8g2.setContrast(255); }
510+
if (mask & WSP_SLEEP_OFF) {
511+
digitalWrite(PWR_CTRL, HIGH);
512+
digitalWrite(ACC_PWR_CTRL, HIGH);
513+
u8g2.setPowerSave(0);
514+
}
515+
if (mask & WSP_SLEEP_ON) {
516+
u8g2.setPowerSave(1);
517+
digitalWrite(PWR_CTRL, LOW);
518+
digitalWrite(ACC_PWR_CTRL, LOW);
519+
}
520+
if (mask & WSP_POWER_OFF) {
521+
b_powerOff = true;
522+
}
523+
}
524+
486525
void setWebsocketLedRgb(uint8_t r, uint8_t g, uint8_t b) {
487526
i_websocketLedR = r;
488527
i_websocketLedG = g;
@@ -649,6 +688,8 @@ bool handleWebsocketControlCommand(AsyncWebSocketClient *client, String command,
649688
}
650689

651690
if (command == "timer") {
691+
// StopWatch is just bool/uint32 state — safe to mutate from the AsyncTCP
692+
// task, no hardware bus involved.
652693
if (action == "start") {
653694
Serial.println("Websocket timer start detected.");
654695
stopWatch.reset();
@@ -691,17 +732,20 @@ bool handleWebsocketControlCommand(AsyncWebSocketClient *client, String command,
691732
}
692733

693734
if (command == "display") {
735+
// Update the state flag synchronously so the status frame we send next
736+
// reflects the requested state. The u8g2 call (I2C, not thread-safe) is
737+
// deferred and runs on the main loop one tick later.
694738
if (action == "on") {
695739
Serial.println("Websocket LED/display on detected.");
696-
u8g2.setPowerSave(0);
697740
b_u8g2Sleep = false;
741+
wsQueuePending(WSP_DISPLAY_ON);
698742
sendWebsocketStatus(client, "ok");
699743
return true;
700744
}
701745
if (action == "off") {
702746
Serial.println("Websocket display off detected.");
703-
u8g2.setPowerSave(1);
704747
b_u8g2Sleep = true;
748+
wsQueuePending(WSP_DISPLAY_OFF);
705749
sendWebsocketStatus(client, "ok");
706750
return true;
707751
}
@@ -712,15 +756,15 @@ bool handleWebsocketControlCommand(AsyncWebSocketClient *client, String command,
712756
if (command == "low_power") {
713757
if (action == "on") {
714758
Serial.println("Websocket low power mode on detected.");
715-
u8g2.setContrast(0);
716759
b_websocketLowPowerEnabled = true;
760+
wsQueuePending(WSP_LOWPWR_ON);
717761
sendWebsocketStatus(client, "ok");
718762
return true;
719763
}
720764
if (action == "off") {
721765
Serial.println("Websocket low power mode off detected.");
722-
u8g2.setContrast(255);
723766
b_websocketLowPowerEnabled = false;
767+
wsQueuePending(WSP_LOWPWR_OFF);
724768
sendWebsocketStatus(client, "ok");
725769
return true;
726770
}
@@ -731,20 +775,18 @@ bool handleWebsocketControlCommand(AsyncWebSocketClient *client, String command,
731775
if (command == "sleep" || command == "soft_sleep") {
732776
if (action == "on") {
733777
Serial.println("Websocket soft sleep on detected.");
734-
u8g2.setPowerSave(1);
778+
// Set state flags synchronously so status reflects the requested mode.
779+
// u8g2 + GPIO power-rail writes are deferred to the main loop.
735780
b_softSleep = true;
736-
digitalWrite(PWR_CTRL, LOW);
737-
digitalWrite(ACC_PWR_CTRL, LOW);
781+
wsQueuePending(WSP_SLEEP_ON);
738782
sendWebsocketStatus(client, "ok");
739783
return true;
740784
}
741785
if (action == "off" || action == "wake") {
742786
Serial.println("Websocket soft sleep off detected.");
743-
digitalWrite(PWR_CTRL, HIGH);
744-
digitalWrite(ACC_PWR_CTRL, HIGH);
745-
u8g2.setPowerSave(0);
746-
b_u8g2Sleep = false;
747787
b_softSleep = false;
788+
b_u8g2Sleep = false;
789+
wsQueuePending(WSP_SLEEP_OFF);
748790
sendWebsocketStatus(client, "ok");
749791
return true;
750792
}
@@ -755,7 +797,7 @@ bool handleWebsocketControlCommand(AsyncWebSocketClient *client, String command,
755797
if (command == "power" && action == "off") {
756798
Serial.println("Websocket power off detected.");
757799
sendWebsocketPowerOff(0);
758-
b_powerOff = true;
800+
wsQueuePending(WSP_POWER_OFF);
759801
sendWebsocketStatus(client, "ok");
760802
return true;
761803
}
@@ -912,16 +954,15 @@ void _wifi_init(void *args) {
912954
Serial.printf("Pong received from client %u\n", client->id());
913955
}
914956
if (type == WS_EVT_DATA) {
915-
916957
AwsFrameInfo *info = (AwsFrameInfo *)arg;
917-
String msg = "";
918-
919-
for (size_t i = 0; i < info->len; i++) {
920-
msg += (char)data[i];
958+
// Only handle complete, unfragmented text frames. Fragmented or binary
959+
// frames would corrupt the parsers below.
960+
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
961+
String msg((const char *)data, len);
962+
Serial.print("Websocket recv: ");
963+
Serial.println(msg);
964+
handleWebsocketRateCommand(client, msg);
921965
}
922-
Serial.print("Websocket recv: ");
923-
Serial.println(msg);
924-
handleWebsocketRateCommand(client, msg);
925966
}
926967
});
927968
vTaskDelete(NULL);
@@ -1806,6 +1847,11 @@ void setManualStableValue(float value) {
18061847

18071848

18081849
void loop() {
1850+
// Drain any deferred WS hardware actions before checking shutdown/sleep,
1851+
// so a SLEEP_OFF / POWER_OFF queued from the AsyncTCP task takes effect
1852+
// here on the loop task rather than racing peripheral drivers.
1853+
processWsPendingCmds();
1854+
18091855
if (b_powerOff){
18101856
shut_down_now_nobeep();
18111857
return;
@@ -1930,7 +1976,7 @@ void loop() {
19301976
if (b_wifiEnabled) {
19311977
websocket.cleanupClients(1);
19321978
ElegantOTA.loop();
1933-
static long lastUpdate = 0;
1979+
static unsigned long lastUpdate = 0;
19341980
unsigned long current = millis();
19351981
if (current - lastUpdate >= weightWebsocketNotifyInterval) {
19361982
if (websocket.availableForWriteAll() > 0) {

src/wifi_setup.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ void connectToWifi() {
5151

5252
WiFi.begin(params.getSSID(), params.getPass());
5353
WiFi.setTxPower(WIFI_POWER_18_5dBm);
54+
// Leaving WiFi modem-sleep at the Arduino default (WIFI_PS_MIN_MODEM):
55+
// forcing setSleep(false) caused severe packet loss and HTTP stalls when
56+
// BLE was active, because BT/WiFi share one 2.4GHz radio and the coex
57+
// layer needs WiFi's sleep windows for BT slots.
5458
int wifiCounter = 0;
5559
while (WiFi.status() != WL_CONNECTED) {
5660
if (WiFi.status() == WL_CONNECT_FAILED) {

0 commit comments

Comments
 (0)