From d5b8acecda2fc52eb8c1f5932ccb159513d155df Mon Sep 17 00:00:00 2001 From: cjkas Date: Wed, 25 Mar 2026 16:09:08 +0100 Subject: [PATCH 1/2] fix race condition with multiple simultaneus requests --- src/MQTT.cpp | 5 +- src/Somfy.cpp | 14 ++++- src/Web.cpp | 171 +++++++++++++++++++++++++++++++++++++++++++++----- src/Web.h | 34 ++++++++++ 4 files changed, 203 insertions(+), 21 deletions(-) diff --git a/src/MQTT.cpp b/src/MQTT.cpp index 72c5607..244bbca 100644 --- a/src/MQTT.cpp +++ b/src/MQTT.cpp @@ -101,8 +101,10 @@ void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) { if (shade) { int val = atoi(value); if(strncmp(command, "target", sizeof(command)) == 0) { - if(val >= 0 && val <= 100) + if(val >= 0 && val <= 100) { + ESP_LOGI(TAG, "MQTT shade %s target=%d", entityId, val); shade->moveToTarget(shade->transformPosition(atoi(value))); + } } if(strncmp(command, "tiltTarget", sizeof(command)) == 0) { if(val >= 0 && val <= 100) @@ -152,6 +154,7 @@ void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) { SomfyGroup* group = somfy.getGroupById(atoi(entityId)); if (group) { int val = atoi(value); + ESP_LOGI(TAG, "MQTT group %s command=%s value=%d", entityId, command, val); if(strncmp(command, "direction", sizeof(command)) == 0) { if(val < 0) group->sendCommand(somfy_commands::Up); diff --git a/src/Somfy.cpp b/src/Somfy.cpp index 59284b6..1a3e34e 100644 --- a/src/Somfy.cpp +++ b/src/Somfy.cpp @@ -2926,11 +2926,12 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz } void SomfyGroup::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); } void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { + ESP_LOGI(TAG, "[Group %u] sendCommand cmd=%s repeat=%u", this->getGroupId(), translateSomfyCommand(cmd).c_str(), repeat); // This sendCommand function will always be called externally. sendCommand at the remote level // is expected to be called internally when the motor needs commanded. if(this->bitLength == 0) this->bitLength = somfy.transceiver.config.type; SomfyRemote::sendCommand(cmd, repeat, stepSize); - + switch(cmd) { case somfy_commands::My: this->p_direction(0); @@ -2949,6 +2950,7 @@ void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz if(this->linkedShades[i] != 0) { SomfyShade *shade = somfy.getShadeById(this->linkedShades[i]); if(shade) { + ESP_LOGI(TAG, "[Group %u] processInternalCommand on shade %u cmd=%s", this->getGroupId(), shade->getShadeId(), translateSomfyCommand(cmd).c_str()); shade->processInternalCommand(cmd, repeat); shade->emitCommand(cmd, "group", this->getRemoteAddress()); } @@ -2994,6 +2996,8 @@ void SomfyShade::moveToTiltTarget(float target) { if(cmd != somfy_commands::My) this->settingTiltPos = true; } void SomfyShade::moveToTarget(float pos, float tilt) { + ESP_LOGI(TAG, "[Shade %u] moveToTarget(pos=%.2f, tilt=%.2f) settingPos=%d direction=%d currentTarget=%.2f currentPos=%.2f", + this->getShadeId(), pos, tilt, this->settingPos, this->direction, this->target, this->currentPos); somfy_commands cmd = somfy_commands::My; if(this->isToggle()) { // Overload this as we cannot seek a position on a garage door or single button device. @@ -3042,7 +3046,9 @@ bool SomfyShade::save() { if(somfy.useNVS()) { char shadeKey[15]; snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->getShadeId()); - pref.begin(shadeKey); + if(!pref.begin(shadeKey)) { + ESP_LOGE(TAG, "[Shade %u] save() pref.begin(%s) FAILED", this->getShadeId(), shadeKey); + } pref.clear(); pref.putChar("shadeType", static_cast(this->shadeType)); pref.putUInt("remoteAddress", this->getRemoteAddress()); @@ -3915,7 +3921,9 @@ bool SomfyShadeController::deleteGroup(uint8_t groupId) { bool SomfyShadeController::loadShadesFile(const char *filename) { return ShadeConfigFile::load(this, filename); } uint16_t SomfyRemote::getNextRollingCode() { - pref.begin("ShadeCodes"); + if(!pref.begin("ShadeCodes")) { + ESP_LOGE(TAG, "getNextRollingCode() pref.begin(ShadeCodes) FAILED"); + } uint16_t code = pref.getUShort(this->m_remotePrefId, 0); code++; pref.putUShort(this->m_remotePrefId, code); diff --git a/src/Web.cpp b/src/Web.cpp index 9a5f6f7..84889a5 100644 --- a/src/Web.cpp +++ b/src/Web.cpp @@ -43,10 +43,15 @@ static const char _encoding_json[] = "application/json"; static const char *TAG = "Web"; +static QueueHandle_t webCmdQueue = nullptr; +static SemaphoreHandle_t webCmdDone = nullptr; + AsyncWebServer asyncServer(80); AsyncWebServer asyncApiServer(8081); void Web::startup() { ESP_LOGI(TAG, "Launching web server..."); + if(!webCmdQueue) webCmdQueue = xQueueCreate(WEB_CMD_QUEUE_SIZE, sizeof(web_command_t)); + if(!webCmdDone) webCmdDone = xSemaphoreCreateBinary(); asyncServer.on("/loginContext", HTTP_GET, [](AsyncWebServerRequest *request) { AsyncJsonResponse *response = new AsyncJsonResponse(); @@ -64,8 +69,96 @@ void Web::startup() { ESP_LOGI(TAG, "Async API server started on port 8081"); } void Web::loop() { + this->processQueue(); delay(1); } +bool Web::queueCommand(const web_command_t &cmd) { + if(!webCmdQueue || !webCmdDone) return false; + // Clear any stale signal + xSemaphoreTake(webCmdDone, 0); + if(xQueueSend(webCmdQueue, &cmd, pdMS_TO_TICKS(100)) != pdTRUE) { + ESP_LOGE(TAG, "Command queue full, dropping command"); + return false; + } + // Wait for main loop to process it + if(xSemaphoreTake(webCmdDone, pdMS_TO_TICKS(WEB_CMD_TIMEOUT_MS)) != pdTRUE) { + ESP_LOGW(TAG, "Command queue timeout waiting for processing"); + return false; + } + return true; +} +void Web::processQueue() { + if(!webCmdQueue || !webCmdDone) return; + web_command_t cmd; + while(xQueueReceive(webCmdQueue, &cmd, 0) == pdTRUE) { + switch(cmd.type) { + case web_cmd_t::shade_command: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + if(cmd.target <= 100) shade->moveToTarget(shade->transformPosition(cmd.target)); + else shade->sendCommand(cmd.command, cmd.repeat > 0 ? cmd.repeat : shade->repeats, cmd.stepSize); + } + break; + } + case web_cmd_t::group_command: { + SomfyGroup *group = somfy.getGroupById(cmd.groupId); + if(group) group->sendCommand(cmd.command, cmd.repeat >= 0 ? cmd.repeat : group->repeats, cmd.stepSize); + break; + } + case web_cmd_t::tilt_command: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + if(cmd.target <= 100) shade->moveToTiltTarget(shade->transformPosition(cmd.target)); + else shade->sendTiltCommand(cmd.command); + } + break; + } + case web_cmd_t::shade_repeat: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + if(shade->shadeType == shade_types::garage1 && cmd.command == somfy_commands::Prog) cmd.command = somfy_commands::Toggle; + if(!shade->isLastCommand(cmd.command)) shade->sendCommand(cmd.command, cmd.repeat >= 0 ? cmd.repeat : shade->repeats, cmd.stepSize); + else shade->repeatFrame(cmd.repeat >= 0 ? cmd.repeat : shade->repeats); + } + break; + } + case web_cmd_t::group_repeat: { + SomfyGroup *group = somfy.getGroupById(cmd.groupId); + if(group) { + if(!group->isLastCommand(cmd.command)) group->sendCommand(cmd.command, cmd.repeat >= 0 ? cmd.repeat : group->repeats, cmd.stepSize); + else group->repeatFrame(cmd.repeat >= 0 ? cmd.repeat : group->repeats); + } + break; + } + case web_cmd_t::set_positions: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + if(cmd.position >= 0) shade->target = shade->currentPos = cmd.position; + if(cmd.tiltPosition >= 0 && shade->tiltType != tilt_types::none) shade->tiltTarget = shade->currentTiltPos = cmd.tiltPosition; + shade->emitState(); + } + break; + } + case web_cmd_t::shade_sensor: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + shade->sendSensorCommand(cmd.windy, cmd.sunny, cmd.repeat >= 0 ? (uint8_t)cmd.repeat : shade->repeats); + shade->emitState(); + } + break; + } + case web_cmd_t::group_sensor: { + SomfyGroup *group = somfy.getGroupById(cmd.groupId); + if(group) { + group->sendSensorCommand(cmd.windy, cmd.sunny, cmd.repeat >= 0 ? (uint8_t)cmd.repeat : group->repeats); + group->emitState(); + } + break; + } + } + xSemaphoreGive(webCmdDone); + } +} bool Web::isAuthenticated(AsyncWebServerRequest *request, bool cfg) { ESP_LOGD(TAG, "Checking async authentication"); if(settings.Security.type == security_types::None) return true; @@ -521,8 +614,15 @@ void Web::handleShadeCommand(AsyncWebServerRequest *request, JsonVariant &json) else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; } SomfyShade *shade = somfy.getShadeById(shadeId); if(shade) { - if(target <= 100) shade->moveToTarget(shade->transformPosition(target)); - else shade->sendCommand(command, repeat > 0 ? repeat : shade->repeats, stepSize); + ESP_LOGI(TAG, "handleShadeCommand shade=%u target=%u command=%s", shadeId, target, translateSomfyCommand(command).c_str()); + web_command_t cmd = {}; + cmd.type = web_cmd_t::shade_command; + cmd.shadeId = shadeId; + cmd.target = target; + cmd.command = command; + cmd.repeat = repeat; + cmd.stepSize = stepSize; + this->queueCommand(cmd); AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -556,7 +656,14 @@ void Web::handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json) else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); return; } SomfyGroup *group = somfy.getGroupById(groupId); if(group) { - group->sendCommand(command, repeat >= 0 ? repeat : group->repeats, stepSize); + ESP_LOGI(TAG, "handleGroupCommand group=%u command=%s", groupId, translateSomfyCommand(command).c_str()); + web_command_t cmd = {}; + cmd.type = web_cmd_t::group_command; + cmd.groupId = groupId; + cmd.command = command; + cmd.repeat = repeat; + cmd.stepSize = stepSize; + this->queueCommand(cmd); AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -587,8 +694,13 @@ void Web::handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json) { else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; } SomfyShade *shade = somfy.getShadeById(shadeId); if(shade) { - if(target <= 100) shade->moveToTiltTarget(shade->transformPosition(target)); - else shade->sendTiltCommand(command); + ESP_LOGI(TAG, "handleTiltCommand shade=%u target=%u command=%s", shadeId, target, translateSomfyCommand(command).c_str()); + web_command_t cmd = {}; + cmd.type = web_cmd_t::tilt_command; + cmd.shadeId = shadeId; + cmd.target = target; + cmd.command = command; + this->queueCommand(cmd); AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -620,11 +732,16 @@ void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json) if(!obj["repeat"].isNull()) repeat = obj["repeat"].as(); } if(shadeId != 255) { + ESP_LOGI(TAG, "handleRepeatCommand shade=%u command=%s", shadeId, translateSomfyCommand(command).c_str()); SomfyShade *shade = somfy.getShadeById(shadeId); if(!shade) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade reference could not be found.\"}")); return; } - if(shade->shadeType == shade_types::garage1 && command == somfy_commands::Prog) command = somfy_commands::Toggle; - if(!shade->isLastCommand(command)) shade->sendCommand(command, repeat >= 0 ? repeat : shade->repeats, stepSize); - else shade->repeatFrame(repeat >= 0 ? repeat : shade->repeats); + web_command_t cmd = {}; + cmd.type = web_cmd_t::shade_repeat; + cmd.shadeId = shadeId; + cmd.command = command; + cmd.repeat = repeat; + cmd.stepSize = stepSize; + this->queueCommand(cmd); AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginArray(); @@ -633,10 +750,16 @@ void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json) resp.endResponse(); } else if(groupId != 255) { + ESP_LOGI(TAG, "handleRepeatCommand group=%u command=%s", groupId, translateSomfyCommand(command).c_str()); SomfyGroup *group = somfy.getGroupById(groupId); if(!group) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group reference could not be found.\"}")); return; } - if(!group->isLastCommand(command)) group->sendCommand(command, repeat >= 0 ? repeat : group->repeats, stepSize); - else group->repeatFrame(repeat >= 0 ? repeat : group->repeats); + web_command_t cmd = {}; + cmd.type = web_cmd_t::group_repeat; + cmd.groupId = groupId; + cmd.command = command; + cmd.repeat = repeat; + cmd.stepSize = stepSize; + this->queueCommand(cmd); AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -722,11 +845,15 @@ void Web::handleSetPositions(AsyncWebServerRequest *request, JsonVariant &json) if(!obj["tiltPosition"].isNull()) tiltPos = obj["tiltPosition"]; } if(shadeId != 255) { + ESP_LOGI(TAG, "handleSetPositions shade=%u pos=%d tiltPos=%d", shadeId, pos, tiltPos); SomfyShade *shade = somfy.getShadeById(shadeId); if(shade) { - if(pos >= 0) shade->target = shade->currentPos = pos; - if(tiltPos >= 0 && shade->tiltType != tilt_types::none) shade->tiltTarget = shade->currentTiltPos = tiltPos; - shade->emitState(); + web_command_t cmd = {}; + cmd.type = web_cmd_t::set_positions; + cmd.shadeId = shadeId; + cmd.position = pos; + cmd.tiltPosition = tiltPos; + this->queueCommand(cmd); AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -763,8 +890,13 @@ void Web::handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json) { if(shadeId != 255) { SomfyShade *shade = somfy.getShadeById(shadeId); if(shade) { - shade->sendSensorCommand(windy, sunny, repeat >= 0 ? (uint8_t)repeat : shade->repeats); - shade->emitState(); + web_command_t cmd = {}; + cmd.type = web_cmd_t::shade_sensor; + cmd.shadeId = shadeId; + cmd.sunny = sunny; + cmd.windy = windy; + cmd.repeat = repeat; + this->queueCommand(cmd); AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); @@ -777,8 +909,13 @@ void Web::handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json) { else if(groupId != 255) { SomfyGroup *group = somfy.getGroupById(groupId); if(group) { - group->sendSensorCommand(windy, sunny, repeat >= 0 ? (uint8_t)repeat : group->repeats); - group->emitState(); + web_command_t cmd = {}; + cmd.type = web_cmd_t::group_sensor; + cmd.groupId = groupId; + cmd.sunny = sunny; + cmd.windy = windy; + cmd.repeat = repeat; + this->queueCommand(cmd); AsyncJsonResp resp; resp.beginResponse(request, g_async_content, sizeof(g_async_content)); resp.beginObject(); diff --git a/src/Web.h b/src/Web.h index 7cf3150..c764dd1 100644 --- a/src/Web.h +++ b/src/Web.h @@ -1,8 +1,39 @@ #include #include +#include +#include #include "Somfy.h" #ifndef webserver_h #define webserver_h + +#define WEB_CMD_QUEUE_SIZE 8 +#define WEB_CMD_TIMEOUT_MS 3000 + +enum class web_cmd_t : uint8_t { + shade_command, // moveToTarget or sendCommand + group_command, // group sendCommand + tilt_command, // moveToTiltTarget or sendTiltCommand + shade_repeat, // shade sendCommand/repeatFrame + group_repeat, // group sendCommand/repeatFrame + set_positions, // set shade position directly + shade_sensor, // shade sensor command + group_sensor, // group sensor command +}; + +struct web_command_t { + web_cmd_t type; + uint8_t shadeId; + uint8_t groupId; + uint8_t target; // 0-100 or 255 (none) + somfy_commands command; + int8_t repeat; + uint8_t stepSize; + int8_t position; // for setPositions + int8_t tiltPosition; // for setPositions/tilt + int8_t sunny; // for sensor + int8_t windy; // for sensor +}; + class Web { public: bool uploadSuccess = false; @@ -36,5 +67,8 @@ class Web { void handleBackup(AsyncWebServerRequest *request); void handleReboot(AsyncWebServerRequest *request); void handleNotFound(AsyncWebServerRequest *request); + private: + void processQueue(); + bool queueCommand(const web_command_t &cmd); }; #endif From 9c17aa4642089c33a4d113a4cd49ec709d2615cf Mon Sep 17 00:00:00 2001 From: cjkas Date: Fri, 27 Mar 2026 08:24:08 +0100 Subject: [PATCH 2/2] added c3,s3 builds --- platformio.ini | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 6d0d8de..ed31843 100644 --- a/platformio.ini +++ b/platformio.ini @@ -25,7 +25,7 @@ extra_scripts = post:archive_elf.py board_build.partitions = huge_app.csv board_build.filesystem = littlefs -build_flags = +build_flags = -DCORE_DEBUG_LEVEL=3 -DCONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=1 -DCONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=1 @@ -44,3 +44,9 @@ board = esp32dev [env:esp32devdbg] board = esp32dev build_type = debug + +[env:esp32c3] +board = esp32-c3-devkitm-1 + +[env:esp32s3] +board = esp32-s3-devkitm-1