Skip to content

Commit 74d7f0f

Browse files
skialpineclaude
andcommitted
fix: review feedback — volatile cross-task globals, atomic mask replace
Code review on PR decentespresso#54 caught two real bugs in the deferred-action fix: 1. The pending-mask let opposing actions accumulate. If a client sent "display off" then "display on" before the main loop drained, both bits were set and processWsPendingCmds ran them in source order — final hardware ended up off while b_u8g2Sleep reported on. Add a wsReplacePending(set, clear) helper that atomically supersedes the opposing bit, and use it for the three mutually-exclusive pairs (display, low_power, sleep). 2. The new WS state globals (weightWebsocketNotifyInterval, b_websocketEventsEnabled, b_websocketLowPower/LedEnabled, i_websocketLedR/G/B, t_lastWebsocketStatusUpdate) and the pre- existing b_softSleep / b_u8g2Sleep / b_powerOff that are now written from the AsyncTCP task are missing `volatile`. On dual- core ESP32-S3 the compiler may cache stale values in a register on the loop core across the WS gate check. Mark them volatile. Also fold in smaller doc/format cleanups from the same review: * Cast stopWatch.elapsed() (uint32_t) to (unsigned long) for the %lu format spec in sendWebsocketStatus / sendWebsocketStatusAll. Works today on ESP32 (both 32-bit) but technically a type mismatch. * Correct the wsQueuePending doc-comment (StopWatch is safe to mutate inline; only u8g2 and power-rail GPIOs need deferring). * Correct the processWsPendingCmds comment to note that b_powerOff is set here on purpose. * README: document the JSON rate forms accepted by the parser that weren't listed (rate_hz, hz, interval_ms). Re-verified: * 60/60 WS feature regression PASS * 120s freeze stress: max weight gap 0.60s (was 4.18s pre-fix), 0 HTTP failures, 0 slow, 590 commands sent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1051681 commit 74d7f0f

3 files changed

Lines changed: 48 additions & 25 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,14 @@ rate 5k
6363
rate 10k
6464
```
6565

66-
or JSON:
66+
or JSON (any of these forms work):
6767

6868
```json
6969
{ "command": "rate", "value": "10k" }
7070
{ "rate": "10k" }
71+
{ "rate_hz": 10 }
72+
{ "hz": 10 }
73+
{ "interval_ms": 100 }
7174
```
7275

7376
The supported rates are 2 Hz, 5 Hz, and 10 Hz. The firmware sends back a

include/parameter.h

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ const unsigned long WEBSOCKET_5HZ_NOTIFY_INTERVAL_MS = 200;
1717
const unsigned long WEBSOCKET_10HZ_NOTIFY_INTERVAL_MS = 100;
1818
const unsigned long WEBSOCKET_DEFAULT_NOTIFY_INTERVAL_MS = WEBSOCKET_2HZ_NOTIFY_INTERVAL_MS;
1919
const unsigned long WEBSOCKET_STATUS_NOTIFY_INTERVAL_MS = 5000;
20-
unsigned long weightWebsocketNotifyInterval = WEBSOCKET_DEFAULT_NOTIFY_INTERVAL_MS;
21-
bool b_websocketEventsEnabled = false;
22-
bool b_websocketLowPowerEnabled = false;
23-
bool b_websocketLedEnabled = false;
24-
uint8_t i_websocketLedR = 0;
25-
uint8_t i_websocketLedG = 0;
26-
uint8_t i_websocketLedB = 0;
27-
unsigned long t_lastWebsocketStatusUpdate = 0;
20+
// volatile: written from the AsyncTCP task (WS event callback) and read
21+
// from the main loop. Without volatile, the compiler may keep these cached
22+
// in a register across the loop's WS gate check on the other core.
23+
volatile unsigned long weightWebsocketNotifyInterval = WEBSOCKET_DEFAULT_NOTIFY_INTERVAL_MS;
24+
volatile bool b_websocketEventsEnabled = false;
25+
volatile bool b_websocketLowPowerEnabled = false;
26+
volatile bool b_websocketLedEnabled = false;
27+
volatile uint8_t i_websocketLedR = 0;
28+
volatile uint8_t i_websocketLedG = 0;
29+
volatile uint8_t i_websocketLedB = 0;
30+
volatile unsigned long t_lastWebsocketStatusUpdate = 0;
2831

2932
// Websocket pending-command mask. Set on the AsyncTCP task by the WS event
3033
// callback; drained on the main loop. Defers hardware-touching ops (u8g2,
@@ -65,7 +68,8 @@ bool b_debug = false;
6568

6669
unsigned long t_batteryIcon = 0;
6770
bool b_showBatteryIcon = true;
68-
bool b_softSleep = false;
71+
// volatile: now also written from the AsyncTCP task (WS soft-sleep command).
72+
volatile bool b_softSleep = false;
6973
#if defined(ACC_MPU6050) || defined(ACC_BMA400)
7074
bool b_gyroEnabled = true;
7175
#endif
@@ -91,7 +95,8 @@ bool b_chargingOLED = true;
9195
bool b_heartBeatIcon = false; //debug ble heart icon
9296
unsigned long t_shutdownFailBle = 0; //for popping up shut down fail due to ble is connected.
9397
bool b_shutdownFailBle = false;
94-
bool b_u8g2Sleep = true;
98+
// volatile: now also written from the AsyncTCP task (WS display/sleep commands).
99+
volatile bool b_u8g2Sleep = true;
95100
unsigned long t_bootTare = 0;
96101
bool b_bootTare = false;
97102
int i_bootTareDelay = 1000;
@@ -106,7 +111,9 @@ bool b_tareByBle = false;
106111
portMUX_TYPE remoteTareMux = portMUX_INITIALIZER_UNLOCKED;
107112
unsigned long t_tareStatus = 0; //tare done time stamp
108113
unsigned long t_power_off; //关机倒计时
109-
bool b_powerOff = false;
114+
// volatile: set in processWsPendingCmds (main loop) and read in loop's top
115+
// guard; main loop also writes it from other paths. Keep volatile defensively.
116+
volatile bool b_powerOff = false;
110117
#if defined(ACC_MPU6050) || defined(ACC_BMA400)
111118
unsigned long t_power_off_gyro = 0; //侧放关机倒计时
112119
#endif

src/hds.ino

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -485,24 +485,37 @@ bool parseWebsocketRgb(String input, uint8_t &r, uint8_t &g, uint8_t &b) {
485485

486486
// Queue a pending hardware action so the main loop performs it on its own
487487
// task. The WS event callback runs on the AsyncTCP task and must not touch
488-
// u8g2, stopWatch, or power-rail GPIOs directly.
488+
// u8g2 (I2C bus) or peripheral power-rail GPIOs directly. StopWatch and
489+
// plain bool/uint32 flag writes are safe to do inline in the callback.
489490
inline void wsQueuePending(uint32_t bits) {
490491
portENTER_CRITICAL(&wsPendingMux);
491492
wsPendingMask |= bits;
492493
portEXIT_CRITICAL(&wsPendingMux);
493494
}
494495

496+
// Atomically set `setBits` and clear `clearBits` in the pending mask. Used
497+
// for mutually-exclusive action pairs (DISPLAY_ON/OFF, SLEEP_ON/OFF, ...) so
498+
// that if a previous opposing action is still queued, it's superseded by
499+
// the new one rather than running both in source order.
500+
inline void wsReplacePending(uint32_t setBits, uint32_t clearBits) {
501+
portENTER_CRITICAL(&wsPendingMux);
502+
wsPendingMask = (wsPendingMask & ~clearBits) | setBits;
503+
portEXIT_CRITICAL(&wsPendingMux);
504+
}
505+
495506
void processWsPendingCmds() {
496507
portENTER_CRITICAL(&wsPendingMux);
497508
uint32_t mask = wsPendingMask;
498509
wsPendingMask = 0;
499510
portEXIT_CRITICAL(&wsPendingMux);
500511
if (mask == 0) return;
501512

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.
513+
// u8g2 (I2C) and peripheral power-rail writes happen here because they
514+
// would race the main loop if called from the AsyncTCP task. State flags
515+
// touched by the user-visible status (b_u8g2Sleep, b_softSleep, ...) are
516+
// updated synchronously in the WS callback so the response is accurate;
517+
// b_powerOff is set here so it sequences after the centralized shutdown
518+
// chain in shut_down_now_nobeep().
506519
if (mask & WSP_DISPLAY_ON) { u8g2.setPowerSave(0); }
507520
if (mask & WSP_DISPLAY_OFF) { u8g2.setPowerSave(1); }
508521
if (mask & WSP_LOWPWR_ON) { u8g2.setContrast(0); }
@@ -571,7 +584,7 @@ void sendWebsocketStatus(AsyncWebSocketClient *client, const char *status) {
571584
f_batteryVoltage,
572585
websocketIsCharging() ? "true" : "false",
573586
stopWatch.isRunning() ? "true" : "false",
574-
stopWatch.elapsed(),
587+
(unsigned long)stopWatch.elapsed(),
575588
b_u8g2Sleep ? "false" : "true",
576589
b_websocketLowPowerEnabled ? "true" : "false",
577590
b_softSleep ? "true" : "false",
@@ -595,7 +608,7 @@ void sendWebsocketStatusAll(const char *status) {
595608
f_batteryVoltage,
596609
websocketIsCharging() ? "true" : "false",
597610
stopWatch.isRunning() ? "true" : "false",
598-
stopWatch.elapsed(),
611+
(unsigned long)stopWatch.elapsed(),
599612
b_u8g2Sleep ? "false" : "true",
600613
b_websocketLowPowerEnabled ? "true" : "false",
601614
b_softSleep ? "true" : "false",
@@ -738,14 +751,14 @@ bool handleWebsocketControlCommand(AsyncWebSocketClient *client, String command,
738751
if (action == "on") {
739752
Serial.println("Websocket LED/display on detected.");
740753
b_u8g2Sleep = false;
741-
wsQueuePending(WSP_DISPLAY_ON);
754+
wsReplacePending(WSP_DISPLAY_ON, WSP_DISPLAY_OFF);
742755
sendWebsocketStatus(client, "ok");
743756
return true;
744757
}
745758
if (action == "off") {
746759
Serial.println("Websocket display off detected.");
747760
b_u8g2Sleep = true;
748-
wsQueuePending(WSP_DISPLAY_OFF);
761+
wsReplacePending(WSP_DISPLAY_OFF, WSP_DISPLAY_ON);
749762
sendWebsocketStatus(client, "ok");
750763
return true;
751764
}
@@ -757,14 +770,14 @@ bool handleWebsocketControlCommand(AsyncWebSocketClient *client, String command,
757770
if (action == "on") {
758771
Serial.println("Websocket low power mode on detected.");
759772
b_websocketLowPowerEnabled = true;
760-
wsQueuePending(WSP_LOWPWR_ON);
773+
wsReplacePending(WSP_LOWPWR_ON, WSP_LOWPWR_OFF);
761774
sendWebsocketStatus(client, "ok");
762775
return true;
763776
}
764777
if (action == "off") {
765778
Serial.println("Websocket low power mode off detected.");
766779
b_websocketLowPowerEnabled = false;
767-
wsQueuePending(WSP_LOWPWR_OFF);
780+
wsReplacePending(WSP_LOWPWR_OFF, WSP_LOWPWR_ON);
768781
sendWebsocketStatus(client, "ok");
769782
return true;
770783
}
@@ -778,15 +791,15 @@ bool handleWebsocketControlCommand(AsyncWebSocketClient *client, String command,
778791
// Set state flags synchronously so status reflects the requested mode.
779792
// u8g2 + GPIO power-rail writes are deferred to the main loop.
780793
b_softSleep = true;
781-
wsQueuePending(WSP_SLEEP_ON);
794+
wsReplacePending(WSP_SLEEP_ON, WSP_SLEEP_OFF);
782795
sendWebsocketStatus(client, "ok");
783796
return true;
784797
}
785798
if (action == "off" || action == "wake") {
786799
Serial.println("Websocket soft sleep off detected.");
787800
b_softSleep = false;
788801
b_u8g2Sleep = false;
789-
wsQueuePending(WSP_SLEEP_OFF);
802+
wsReplacePending(WSP_SLEEP_OFF, WSP_SLEEP_ON);
790803
sendWebsocketStatus(client, "ok");
791804
return true;
792805
}

0 commit comments

Comments
 (0)