Skip to content

Commit b54c153

Browse files
committed
POC implementation for deferred settings
Add a basic function queue to the main loop, and have settings-set operations defer operation to it. Rejigger the JSON lock as a binary semaphrore, as it needs to pass ownership from one task to another. First step towards a global fix for wled#4779.
1 parent 93e011d commit b54c153

File tree

10 files changed

+141
-65
lines changed

10 files changed

+141
-65
lines changed

wled00/fcn_declare.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ size_t printSetFormIndex(Print& settingsScript, const char* key, int index);
508508
size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val);
509509
void prepareHostname(char* hostname);
510510
[[gnu::pure]] bool isAsterisksOnly(const char* str, byte maxLen);
511-
bool requestJSONBufferLock(uint8_t moduleID=255);
511+
bool requestJSONBufferLock(uint8_t moduleID=255, bool wait = true);
512512
void releaseJSONBufferLock();
513513
uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen);
514514
uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var = nullptr);

wled00/handler_queue.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#include "handler_queue.h"
2+
#include <deque>
3+
#include <Arduino.h>
4+
5+
#if defined(ARDUINO_ARCH_ESP32)
6+
7+
static StaticSemaphore_t handlerQueueMutexBuffer;
8+
static SemaphoreHandle_t handlerQueueMutex = xSemaphoreCreateMutexStatic(&handlerQueueMutexBuffer);
9+
struct guard_type {
10+
SemaphoreHandle_t _mtx;
11+
guard_type(SemaphoreHandle_t m) : _mtx(m) {
12+
xSemaphoreTake(_mtx, portMAX_DELAY); // todo: error check
13+
}
14+
~guard_type() {
15+
xSemaphoreGive(_mtx);
16+
}
17+
};
18+
#define guard() const guard_type guard(handlerQueueMutex)
19+
#else
20+
#define guard()
21+
#endif
22+
23+
static std::deque<std::function<void()>> handler_queue;
24+
25+
// Enqueue a function on the main task
26+
bool HandlerQueue::callOnMainTask(std::function<void()> func) {
27+
guard();
28+
handler_queue.push_back(std::move(func));
29+
return true; // TODO: queue limit
30+
}
31+
32+
// Run the next task in the queue, if any
33+
void HandlerQueue::runNext() {
34+
std::function<void()> f;
35+
{
36+
guard();
37+
if (handler_queue.size()) {
38+
f = std::move(handler_queue.front());
39+
handler_queue.pop_front();
40+
}
41+
}
42+
if (f) {
43+
auto t1 = millis();
44+
f();
45+
auto t2 = millis();
46+
Serial.printf("Handler took: %d\n", t2-t1);
47+
}
48+
}
49+

wled00/handler_queue.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// handler_queue.h
2+
// deferred execution handler queue. Used for making access to WLED globals safe.
3+
4+
#include <functional>
5+
6+
namespace HandlerQueue {
7+
8+
// Enqueue a function on the main task
9+
bool callOnMainTask(std::function<void()> func);
10+
11+
// Run the next task in the queue, if any
12+
void runNext();
13+
}

wled00/json.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,7 @@ void serveJson(AsyncWebServerRequest* request)
11231123
return;
11241124
}
11251125

1126-
if (!requestJSONBufferLock(17)) {
1126+
if (!requestJSONBufferLock(17, false)) {
11271127
request->deferResponse();
11281128
return;
11291129
}

wled00/set.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
652652
//USERMODS
653653
if (subPage == SUBPAGE_UM)
654654
{
655-
if (!requestJSONBufferLock(5)) {
655+
if (!requestJSONBufferLock(5, false)) {
656656
request->deferResponse();
657657
return;
658658
}

wled00/util.cpp

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,18 +151,19 @@ bool isAsterisksOnly(const char* str, byte maxLen)
151151

152152

153153
//threading/network callback details: https://github.com/wled-dev/WLED/pull/2336#discussion_r762276994
154-
bool requestJSONBufferLock(uint8_t moduleID)
154+
bool requestJSONBufferLock(uint8_t moduleID, bool wait)
155155
{
156156
if (pDoc == nullptr) {
157157
DEBUG_PRINTLN(F("ERROR: JSON buffer not allocated!"));
158158
return false;
159159
}
160160

161161
#if defined(ARDUINO_ARCH_ESP32)
162-
// Use a recursive mutex type in case our task is the one holding the JSON buffer.
163-
// This can happen during large JSON web transactions. In this case, we continue immediately
164-
// and then will return out below if the lock is still held.
165-
if (xSemaphoreTakeRecursive(jsonBufferLockMutex, 250) == pdFALSE) return false; // timed out waiting
162+
if (xSemaphoreTake(jsonBufferLockMutex, wait ? 250 : 0) == pdFALSE) {
163+
// TODO, coaelesce with below
164+
DEBUG_PRINTF_P(PSTR("ERROR: Locking JSON buffer mutex (%d) failed! (locked by %d??)\n"), moduleID, jsonBufferLock);
165+
return false; // timed out waiting
166+
}
166167
#elif defined(ARDUINO_ARCH_ESP8266)
167168
// If we're in system context, delay() won't return control to the user context, so there's
168169
// no point in waiting.
@@ -176,9 +177,6 @@ bool requestJSONBufferLock(uint8_t moduleID)
176177
// If the lock is still held - by us, or by another task
177178
if (jsonBufferLock) {
178179
DEBUG_PRINTF_P(PSTR("ERROR: Locking JSON buffer (%d) failed! (still locked by %d)\n"), moduleID, jsonBufferLock);
179-
#ifdef ARDUINO_ARCH_ESP32
180-
xSemaphoreGiveRecursive(jsonBufferLockMutex);
181-
#endif
182180
return false;
183181
}
184182

@@ -194,7 +192,7 @@ void releaseJSONBufferLock()
194192
DEBUG_PRINTF_P(PSTR("JSON buffer released. (%d)\n"), jsonBufferLock);
195193
jsonBufferLock = 0;
196194
#ifdef ARDUINO_ARCH_ESP32
197-
xSemaphoreGiveRecursive(jsonBufferLockMutex);
195+
xSemaphoreGive(jsonBufferLockMutex);
198196
#endif
199197
}
200198

wled00/wled.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#define WLED_DEFINE_GLOBAL_VARS //only in one source file, wled.cpp!
22
#include "wled.h"
33
#include "wled_ethernet.h"
4+
#include "handler_queue.h"
45
#ifdef WLED_ENABLE_AOTA
56
#define NO_OTA_PORT
67
#include <ArduinoOTA.h>
@@ -184,6 +185,9 @@ void WLED::loop()
184185
heapTime = millis();
185186
}
186187

188+
// Run next deferred request
189+
HandlerQueue::runNext();
190+
187191
//LED settings have been saved, re-init busses
188192
//This code block causes severe FPS drop on ESP32 with the original "if (busConfigs[0] != nullptr)" conditional. Investigate!
189193
if (doInitBusses) {
@@ -320,6 +324,7 @@ void WLED::setup()
320324

321325
#ifdef ARDUINO_ARCH_ESP32
322326
pinMode(hardwareRX, INPUT_PULLDOWN); delay(1); // suppress noise in case RX pin is floating (at low noise energy) - see issue #3128
327+
xSemaphoreGive(jsonBufferLockMutex); // Init JSON buffer lock
323328
#endif
324329
#ifdef WLED_BOOTUPDELAY
325330
delay(WLED_BOOTUPDELAY); // delay to let voltage stabilize, helps with boot issues on some setups

wled00/wled.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -970,7 +970,7 @@ WLED_GLOBAL int8_t spi_sclk _INIT(SPISCLKPIN);
970970
// global ArduinoJson buffer
971971
#if defined(ARDUINO_ARCH_ESP32)
972972
WLED_GLOBAL JsonDocument *pDoc _INIT(nullptr);
973-
WLED_GLOBAL SemaphoreHandle_t jsonBufferLockMutex _INIT(xSemaphoreCreateRecursiveMutex());
973+
WLED_GLOBAL SemaphoreHandle_t jsonBufferLockMutex _INIT(xSemaphoreCreateBinary());
974974
#else
975975
WLED_GLOBAL StaticJsonDocument<JSON_BUFFER_SIZE> gDoc;
976976
WLED_GLOBAL JsonDocument *pDoc _INIT(&gDoc);

wled00/wled_server.cpp

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "wled.h"
2+
#include "handler_queue.h"
23

34
#ifndef WLED_DISABLE_OTA
45
#ifdef ESP8266
@@ -299,7 +300,7 @@ void initServer()
299300
});
300301

301302
server.on(F("/settings"), HTTP_POST, [](AsyncWebServerRequest *request){
302-
serveSettings(request, true);
303+
HandlerQueue::callOnMainTask([=]{ serveSettings(request, true); });
303304
});
304305

305306
const static char _json[] PROGMEM = "/json";
@@ -308,10 +309,7 @@ void initServer()
308309
});
309310

310311
AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler(FPSTR(_json), [](AsyncWebServerRequest *request) {
311-
bool verboseResponse = false;
312-
bool isConfig = false;
313-
314-
if (!requestJSONBufferLock(14)) {
312+
if (!requestJSONBufferLock(14, false)) {
315313
request->deferResponse();
316314
return;
317315
}
@@ -325,37 +323,42 @@ void initServer()
325323
}
326324
if (root.containsKey("pin")) checkSettingsPIN(root["pin"].as<const char*>());
327325

328-
const String& url = request->url();
329-
isConfig = url.indexOf(F("cfg")) > -1;
330-
if (!isConfig) {
331-
/*
332-
#ifdef WLED_DEBUG
333-
DEBUG_PRINTLN(F("Serialized HTTP"));
334-
serializeJson(root,Serial);
335-
DEBUG_PRINTLN();
336-
#endif
337-
*/
338-
verboseResponse = deserializeState(root);
339-
} else {
340-
if (!correctPIN && strlen(settingsPIN)>0) {
341-
releaseJSONBufferLock();
342-
serveJsonError(request, 401, ERR_DENIED);
343-
return;
344-
}
345-
verboseResponse = deserializeConfig(root); //use verboseResponse to determine whether cfg change should be saved immediately
346-
}
347-
releaseJSONBufferLock();
326+
HandlerQueue::callOnMainTask([=](){
327+
bool verboseResponse = false;
328+
bool isConfig = false;
348329

349-
if (verboseResponse) {
330+
const String& url = request->url();
331+
isConfig = url.indexOf(F("cfg")) > -1;
350332
if (!isConfig) {
351-
lastInterfaceUpdate = millis(); // prevent WS update until cooldown
352-
interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update
353-
serveJson(request); return; //if JSON contains "v"
333+
/*
334+
#ifdef WLED_DEBUG
335+
DEBUG_PRINTLN(F("Serialized HTTP"));
336+
serializeJson(root,Serial);
337+
DEBUG_PRINTLN();
338+
#endif
339+
*/
340+
verboseResponse = deserializeState(root);
354341
} else {
355-
configNeedsWrite = true; //Save new settings to FS
342+
if (!correctPIN && strlen(settingsPIN)>0) {
343+
releaseJSONBufferLock();
344+
serveJsonError(request, 401, ERR_DENIED);
345+
return;
346+
}
347+
verboseResponse = deserializeConfig(root); //use verboseResponse to determine whether cfg change should be saved immediately
356348
}
357-
}
358-
request->send(200, CONTENT_TYPE_JSON, F("{\"success\":true}"));
349+
releaseJSONBufferLock();
350+
351+
if (verboseResponse) {
352+
if (!isConfig) {
353+
lastInterfaceUpdate = millis(); // prevent WS update until cooldown
354+
interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update
355+
serveJson(request); return; //if JSON contains "v"
356+
} else {
357+
configNeedsWrite = true; //Save new settings to FS
358+
}
359+
}
360+
request->send(200, CONTENT_TYPE_JSON, F("{\"success\":true}"));
361+
});
359362
}, JSON_BUFFER_SIZE);
360363
server.addHandler(handler);
361364

@@ -509,7 +512,10 @@ void initServer()
509512
return;
510513
}
511514

512-
if(handleSet(request, request->url())) return;
515+
if (request->url().indexOf("win") >= 0) {
516+
HandlerQueue::callOnMainTask([=]() { handleSet(request, request->url()); });
517+
return;
518+
}
513519
#ifndef WLED_DISABLE_ALEXA
514520
if(espalexa.handleAlexaApiCall(request)) return;
515521
#endif

wled00/ws.cpp

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "wled.h"
2+
#include "handler_queue.h"
23

34
/*
45
* WebSockets server for bidirectional communication
@@ -34,8 +35,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
3435
client->text(F("pong"));
3536
return;
3637
}
37-
38-
bool verboseResponse = false;
38+
3939
if (!requestJSONBufferLock(11)) {
4040
client->text(F("{\"error\":3}")); // ERR_NOBUF
4141
return;
@@ -47,26 +47,31 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
4747
releaseJSONBufferLock();
4848
return;
4949
}
50-
if (root["v"] && root.size() == 1) {
51-
//if the received value is just "{"v":true}", send only to this client
52-
verboseResponse = true;
53-
} else if (root.containsKey("lv")) {
54-
wsLiveClientId = root["lv"] ? client->id() : 0;
55-
} else {
56-
verboseResponse = deserializeState(root);
57-
}
58-
releaseJSONBufferLock();
5950

60-
if (!interfaceUpdateCallMode) { // individual client response only needed if no WS broadcast soon
61-
if (verboseResponse) {
62-
sendDataWs(client);
51+
// Handle the rest on the main task
52+
HandlerQueue::callOnMainTask([=](){
53+
bool verboseResponse = false;
54+
if (root["v"] && root.size() == 1) {
55+
//if the received value is just "{"v":true}", send only to this client
56+
verboseResponse = true;
57+
} else if (root.containsKey("lv")) {
58+
wsLiveClientId = root["lv"] ? client->id() : 0;
6359
} else {
64-
// we have to send something back otherwise WS connection closes
65-
client->text(F("{\"success\":true}"));
60+
verboseResponse = deserializeState(root);
6661
}
67-
// force broadcast in 500ms after updating client
68-
//lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this
69-
}
62+
releaseJSONBufferLock();
63+
64+
if (!interfaceUpdateCallMode) { // individual client response only needed if no WS broadcast soon
65+
if (verboseResponse) {
66+
sendDataWs(client);
67+
} else {
68+
// we have to send something back otherwise WS connection closes
69+
client->text(F("{\"success\":true}"));
70+
}
71+
// force broadcast in 500ms after updating client
72+
//lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this
73+
}
74+
});
7075
}
7176
} else {
7277
//message is comprised of multiple frames or the frame is split into multiple packets

0 commit comments

Comments
 (0)