From b15d69f0effe3133e0dd33019b8601ad434a9f98 Mon Sep 17 00:00:00 2001 From: ewowi Date: Fri, 26 Dec 2025 20:56:43 +0100 Subject: [PATCH 01/11] Highlight double buffering in docs, main.cpp and lights control --- docs/develop/architecture.md | 49 ++++++++++++--------- src/MoonLight/Modules/ModuleLightsControl.h | 2 + src/main.cpp | 4 +- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/docs/develop/architecture.md b/docs/develop/architecture.md index b1b388e5..c96f7155 100644 --- a/docs/develop/architecture.md +++ b/docs/develop/architecture.md @@ -161,19 +161,22 @@ Synchronization Flow void effectTask(void* param) { while (true) { - if (useDoubleBuffer) { - // Step 1: Copy front → back (NO LOCK) - memcpy(channelsBack, channels, nrOfChannels); - - // Step 2: Compute effects on back buffer (NO LOCK, 5-15ms) - uint8_t* temp = channels; - channels = channelsBack; - computeEffects(); // Reads and writes channelsBack - - // Step 3: BRIEF LOCK - Swap pointers (10µs) + if (layerP.lights.useDoubleBuffer) { + layerP.lights.channels = layerP.lights.channelsBack; + layerP.loop(); // getRGB and setRGB both use channelsBack + + // Atomic swap channels + xSemaphoreTake(swapMutex, portMAX_DELAY); + uint8_t* temp = layerP.lights.channelsBack; + layerP.lights.channelsBack = layerP.lights.channels; + layerP.lights.channels = temp; + newFrameReady = true; + xSemaphoreGive(swapMutex); + + } else { xSemaphoreTake(swapMutex, portMAX_DELAY); - channelsBack = channels; - channels = temp; + layerP.loop(); + xSemaphoreGive(swapMutex); } vTaskDelay(1); @@ -182,14 +185,20 @@ void effectTask(void* param) { void driverTask(void* param) { while (true) { - if (useDoubleBuffer) { - // Step 4: BRIEF LOCK - Capture pointer (10µs) - xSemaphoreTake(swapMutex, portMAX_DELAY); - uint8_t* currentFrame = channels; + xSemaphoreTake(swapMutex, portMAX_DELAY); + esp32sveltekit.lps++; + + if (layerP.lights.useDoubleBuffer) { + if (newFrameReady) { + newFrameReady = false; + xSemaphoreGive(swapMutex); + layerP.loopDrivers(); // ✅ No lock needed + } else { + xSemaphoreGive(swapMutex); + } + } else { + layerP.loopDrivers(); // ✅ Protected by lock xSemaphoreGive(swapMutex); - - // Step 5: Send to LEDs (NO LOCK, 1-5ms) - sendViaDMA(currentFrame); } vTaskDelay(1); } @@ -387,7 +396,7 @@ xTaskCreateUniversal(effectTask, "AppEffectTask", psramFound() ? 4 * 1024 : 3 * 1024, NULL, - 3, // Priority + 10, // Priority &effectTaskHandle, 0 // Core 0 (PRO_CPU) ); diff --git a/src/MoonLight/Modules/ModuleLightsControl.h b/src/MoonLight/Modules/ModuleLightsControl.h index b760d767..bd932ac3 100644 --- a/src/MoonLight/Modules/ModuleLightsControl.h +++ b/src/MoonLight/Modules/ModuleLightsControl.h @@ -367,6 +367,8 @@ class ModuleLightsControl : public Module { read([&](ModuleState& _state) { if (_socket->getConnectedClients() && _state.data["monitorOn"]) { + + //protect emit by swapMutex, see main.cpp extern SemaphoreHandle_t swapMutex; xSemaphoreTake(swapMutex, portMAX_DELAY); diff --git a/src/main.cpp b/src/main.cpp index 6e37a9a7..5171a57f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -126,7 +126,7 @@ void effectTask(void* pvParameters) { layerP.setup(); // setup virtual layers (no node setup here as done in addNode) static unsigned long last20ms = 0; - for (;;) { + while (true) { if (layerP.lights.useDoubleBuffer) { // effectTask always writes to channelsBack, reads previous channelsBack layerP.lights.channels = layerP.lights.channelsBack; @@ -167,7 +167,7 @@ void driverTask(void* pvParameters) { // layerP.setup() done in effectTask - for (;;) { + while (true) { xSemaphoreTake(swapMutex, portMAX_DELAY); esp32sveltekit.lps++; From 601fb8a48aa231e65fc30d17a59bdd5277dfd7bc Mon Sep 17 00:00:00 2001 From: ewowi Date: Sat, 27 Dec 2025 10:58:56 +0100 Subject: [PATCH 02/11] channelsE and channelsP (swap for double buffering) Effects and modifiers use channelsE (virtual layer, Art-Net In) Drivers use channelsD (FastLED, parallel, Art-Net out) main swaps between them --- docs/develop/architecture.md | 15 +++++++------- docs/develop/layers.md | 2 +- src/MoonBase/Nodes.cpp | 4 ++-- src/MoonLight/Layers/PhysicalLayer.cpp | 20 ++++++++++--------- src/MoonLight/Layers/PhysicalLayer.h | 4 ++-- src/MoonLight/Layers/VirtualLayer.cpp | 20 +++++++++---------- src/MoonLight/Layers/VirtualLayer.h | 2 +- src/MoonLight/Modules/ModuleChannels.h | 6 +++--- src/MoonLight/Modules/ModuleLightsControl.h | 8 ++++---- src/MoonLight/Nodes/Drivers/D_ArtnetIn.h | 4 ++-- src/MoonLight/Nodes/Drivers/D_ArtnetOut.h | 10 +++++----- src/MoonLight/Nodes/Drivers/D_FastLED.h | 2 +- .../Nodes/Drivers/D_ParallelLEDDriver.h | 6 +++--- src/MoonLight/Nodes/Drivers/parlio.cpp | 2 +- src/main.cpp | 8 ++++---- 15 files changed, 58 insertions(+), 55 deletions(-) diff --git a/docs/develop/architecture.md b/docs/develop/architecture.md index c96f7155..82c7ba28 100644 --- a/docs/develop/architecture.md +++ b/docs/develop/architecture.md @@ -162,14 +162,15 @@ Synchronization Flow void effectTask(void* param) { while (true) { if (layerP.lights.useDoubleBuffer) { - layerP.lights.channels = layerP.lights.channelsBack; + layerP.lights.channelsE = layerP.lights.channelsD; + layerP.loop(); // getRGB and setRGB both use channelsBack // Atomic swap channels xSemaphoreTake(swapMutex, portMAX_DELAY); - uint8_t* temp = layerP.lights.channelsBack; - layerP.lights.channelsBack = layerP.lights.channels; - layerP.lights.channels = temp; + uint8_t* temp = layerP.lights.channelsD; + layerP.lights.channelsD = layerP.lights.channelsE; + layerP.lights.channelsE = temp; newFrameReady = true; xSemaphoreGive(swapMutex); @@ -366,11 +367,11 @@ Double buffering is **automatically enabled** when PSRAM is detected: // In PhysicalLayer::setup() if (psramFound()) { lights.useDoubleBuffer = true; - lights.channels = allocMB(maxChannels); - lights.channelsBack = allocMB(maxChannels); + lights.channelsE = allocMB(maxChannels); + lights.channelsD = allocMB(maxChannels); } else { lights.useDoubleBuffer = false; - lights.channels = allocMB(maxChannels); + lights.channelsE = allocMB(maxChannels); } ``` diff --git a/docs/develop/layers.md b/docs/develop/layers.md index 89e3d6c5..39d2be22 100644 --- a/docs/develop/layers.md +++ b/docs/develop/layers.md @@ -35,7 +35,7 @@ * A Virtual Layer mapping gets updated if a layout, mapping or dimensions change 🚧 * An effect uses a virtual layer. One Virtual layer can have multiple effects. ✅ * Physical layer - * Lights.header and Lights.channels. CRGB leds[] is using lights.channels (acting like leds[] in FASTLED) ✅ + * Lights.header and lights.channelsE/D. CRGB leds[] is using lights.channelsE/D (acting like leds[] in FASTLED) ✅ * A Physical layer has one or more virtual layers and a virtual layer has one or more effects using it. ✅ * Presets/playlist: change (part of) the nodes model diff --git a/src/MoonBase/Nodes.cpp b/src/MoonBase/Nodes.cpp index befc4d5d..970883be 100644 --- a/src/MoonBase/Nodes.cpp +++ b/src/MoonBase/Nodes.cpp @@ -183,7 +183,7 @@ void LiveScriptNode::setup() { // addExternal( "uint8_t dmaBuffer", &layerP.ledsDriver.dmaBuffer); addExternal("void fadeToBlackBy(uint8_t)", (void*)_fadeToBlackBy); - addExternal("CRGB* leds", (void*)(CRGB*)layerP.lights.channels); + addExternal("CRGB* leds", (void*)(CRGB*)layerP.lights.channelsE); addExternal("void setRGB(uint16_t,CRGB)", (void*)_setRGB); addExternal("void setRGBPal(uint16_t,uint8_t,uint8_t)", (void*)_setRGBPal); addExternal("void setPan(uint16_t,uint8_t)", (void*)_setPan); @@ -370,7 +370,7 @@ void DriverNode::loop() { if (brightness != brightnessSaved || layerP.maxPower != maxPowerSaved) { // Use FastLED for setMaxPowerInMilliWatts stuff - uint8_t correctedBrightness = calculate_max_brightness_for_power_mW((CRGB*)&layerP.lights.channels, layerP.lights.header.nrOfLights, brightness, layerP.maxPower * 1000); + uint8_t correctedBrightness = calculate_max_brightness_for_power_mW((CRGB*)&layerP.lights.channelsD, layerP.lights.header.nrOfLights, brightness, layerP.maxPower * 1000); // EXT_LOGD(ML_TAG, "setBrightness b:%d + p:%d -> cb:%d", brightness, layerP.maxPower, correctedBrightness); ledsDriver.setBrightness(correctedBrightness); brightnessSaved = brightness; diff --git a/src/MoonLight/Layers/PhysicalLayer.cpp b/src/MoonLight/Layers/PhysicalLayer.cpp index ebf305d5..a5749882 100644 --- a/src/MoonLight/Layers/PhysicalLayer.cpp +++ b/src/MoonLight/Layers/PhysicalLayer.cpp @@ -33,10 +33,10 @@ PhysicalLayer::PhysicalLayer() { // heap-optimization: request heap optimization review // on boards without PSRAM, heap is only 60 KB (30KB max alloc) available, need to find out how to increase the heap -// goal is to have lights.channels as large as possible, preferable 12288 at least for boards without PSRAM +// goal is to have lights.channelsE/D as large as possible, preferable 12288 at least for boards without PSRAM void PhysicalLayer::setup() { - // allocate lights.channels + // allocate lights.channelsE/D if (psramFound()) { lights.maxChannels = MIN(ESP.getPsramSize() / 4, 61440 * 3); // fill halve with channels, max 120 pins * 512 LEDs, still addressable with uint16_t @@ -46,17 +46,19 @@ void PhysicalLayer::setup() { lights.useDoubleBuffer = false; // Single buffer mode } - lights.channels = allocMB(lights.maxChannels); + lights.channelsE = allocMB(lights.maxChannels); - if (lights.channels) { - EXT_LOGD(ML_TAG, "allocated %d bytes in %s", lights.maxChannels, isInPSRAM(lights.channels) ? "PSRAM" : "RAM"); + if (lights.channelsE) { + EXT_LOGD(ML_TAG, "allocated %d bytes in %s", lights.maxChannels, isInPSRAM(lights.channelsE) ? "PSRAM" : "RAM"); // Allocate back buffer only if PSRAM available if (lights.useDoubleBuffer) { - lights.channelsBack = allocMB(lights.maxChannels); - if (!lights.channelsBack) { + lights.channelsD = allocMB(lights.maxChannels); + if (!lights.channelsD) { EXT_LOGW(ML_TAG, "Failed to allocate back buffer, disabling double buffering"); lights.useDoubleBuffer = false; } + } else { + lights.channelsD = lights.channelsE; // share the same array } } else { EXT_LOGE(ML_TAG, "failed to allocated %d bytes of RAM or PSRAM", lights.maxChannels); @@ -141,7 +143,7 @@ void PhysicalLayer::onLayoutPre() { lights.header.isPositions = 1; // in progress... delay(100); // wait to stop effects // set all channels to 0 (e.g for multichannel to not activate unused channels, e.g. fancy modes on MHs) - memset(lights.channels, 0, lights.maxChannels); // set all the channels to 0 + memset(lights.channelsE, 0, lights.maxChannels); // set all the channels to 0, positions in channelsE // dealloc pins if (!monitorPass) { memset(ledsPerPin, 0xFF, sizeof(ledsPerPin)); // UINT16_MAX is 2 * 0xFF @@ -171,7 +173,7 @@ void PhysicalLayer::addLight(Coord3D position) { if (pass == 1) { // EXT_LOGD(ML_TAG, "%d,%d,%d", position.x, position.y, position.z); if (lights.header.nrOfLights < lights.maxChannels / 3) { - packCoord3DInto3Bytes(&lights.channels[lights.header.nrOfLights * 3], position); + packCoord3DInto3Bytes(&lights.channelsE[lights.header.nrOfLights * 3], position); // positions in channelsE } lights.header.size = lights.header.size.maximum(position); diff --git a/src/MoonLight/Layers/PhysicalLayer.h b/src/MoonLight/Layers/PhysicalLayer.h index e92a4ba6..c8b9f6af 100644 --- a/src/MoonLight/Layers/PhysicalLayer.h +++ b/src/MoonLight/Layers/PhysicalLayer.h @@ -88,8 +88,8 @@ struct LightsHeader { struct Lights { LightsHeader header; - uint8_t* channels = nullptr; // pka leds, created in constructor - uint8_t* channelsBack = nullptr; // Back buffer (being written by effects) + uint8_t* channelsE = nullptr; // channels used by effects and modifiers (double buffering) + uint8_t* channelsD = nullptr; // channels used by drivers (double buffering) size_t maxChannels = 0; bool useDoubleBuffer = false; // Only when PSRAM available diff --git a/src/MoonLight/Layers/VirtualLayer.cpp b/src/MoonLight/Layers/VirtualLayer.cpp index 3520cf3f..6522d330 100644 --- a/src/MoonLight/Layers/VirtualLayer.cpp +++ b/src/MoonLight/Layers/VirtualLayer.cpp @@ -139,7 +139,7 @@ void VirtualLayer::setLight(const uint16_t indexV, const uint8_t* channels, uint if (layerP->lights.header.lightPreset == lightPreset_RGB2040) { // RGB2040 has empty channels: Skip the 20..39 range, so adjust group mapping indexP += (indexP / 20) * 20; } - memcpy(&layerP->lights.channels[indexP * layerP->lights.header.channelsPerLight + offset], channels, length); + memcpy(&layerP->lights.channelsE[indexP * layerP->lights.header.channelsPerLight + offset], channels, length); break; } @@ -149,7 +149,7 @@ void VirtualLayer::setLight(const uint16_t indexV, const uint8_t* channels, uint if (layerP->lights.header.lightPreset == lightPreset_RGB2040) { // RGB2040 has empty channels: Skip the 20..39 range, so adjust group mapping indexP += (indexP / 20) * 20; } - memcpy(&layerP->lights.channels[indexP * layerP->lights.header.channelsPerLight + offset], channels, length); + memcpy(&layerP->lights.channelsE[indexP * layerP->lights.header.channelsPerLight + offset], channels, length); } else EXT_LOGW(ML_TAG, "dev setLightColor i:%d m:%d s:%d", indexV, mappingTable[indexV].indexes, mappingTableIndexes.size()); @@ -157,7 +157,7 @@ void VirtualLayer::setLight(const uint16_t indexV, const uint8_t* channels, uint default:; } } else if (indexV * layerP->lights.header.channelsPerLight + offset + length < layerP->lights.maxChannels) { // no mapping - memcpy(&layerP->lights.channels[indexV * layerP->lights.header.channelsPerLight + offset], channels, length); + memcpy(&layerP->lights.channelsE[indexV * layerP->lights.header.channelsPerLight + offset], channels, length); } } @@ -170,7 +170,7 @@ T VirtualLayer::getLight(const uint16_t indexV, uint8_t offset) const { if (layerP->lights.header.lightPreset == lightPreset_RGB2040) { // RGB2040 has empty channels: Skip the 20..39 range, so adjust group mapping indexP += (indexP / 20) * 20; } - T* result = (T*)&layerP->lights.channels[indexP * layerP->lights.header.channelsPerLight + offset]; + T* result = (T*)&layerP->lights.channelsE[indexP * layerP->lights.header.channelsPerLight + offset]; return *result; // return the color as CRGB break; } @@ -179,7 +179,7 @@ T VirtualLayer::getLight(const uint16_t indexV, uint8_t offset) const { if (layerP->lights.header.lightPreset == lightPreset_RGB2040) { // RGB2040 has empty channels indexP += (indexP / 20) * 20; } - T* result = (T*)&layerP->lights.channels[indexP * layerP->lights.header.channelsPerLight + offset]; + T* result = (T*)&layerP->lights.channelsE[indexP * layerP->lights.header.channelsPerLight + offset]; return *result; // return the color as CRGB break; } @@ -195,7 +195,7 @@ T VirtualLayer::getLight(const uint16_t indexV, uint8_t offset) const { break; } } else if (indexV * layerP->lights.header.channelsPerLight + offset + 3 < layerP->lights.maxChannels) { // no mapping - T* result = (T*)&layerP->lights.channels[indexV * layerP->lights.header.channelsPerLight + offset]; + T* result = (T*)&layerP->lights.channelsE[indexV * layerP->lights.header.channelsPerLight + offset]; return *result; // return the color as CRGB } else { // some operations will go out of bounds e.g. VUMeter, uncomment below lines if you wanna test on a specific effect @@ -221,7 +221,7 @@ void VirtualLayer::fadeToBlackMin() { // } // } else if (layerP->lights.header.channelsPerLight == 3 && layerP->layers.size() == 1) { // CRGB lights - fastled_fadeToBlackBy((CRGB*)layerP->lights.channels, layerP->lights.header.nrOfChannels / sizeof(CRGB), fadeBy); + fastled_fadeToBlackBy((CRGB*)layerP->lights.channelsE, layerP->lights.header.nrOfChannels / sizeof(CRGB), fadeBy); } else { // multichannel lights for (uint16_t index = 0; index < nrOfLights; index++) { CRGB color = getRGB(index); // direct access to the channels @@ -263,7 +263,7 @@ void VirtualLayer::fill_solid(const CRGB& color) { // } // } else if (layerP->lights.header.channelsPerLight == 3 && layerP->layers.size() == 1) { // faster, else manual - fastled_fill_solid((CRGB*)layerP->lights.channels, layerP->lights.header.nrOfChannels / sizeof(CRGB), color); + fastled_fill_solid((CRGB*)layerP->lights.channelsE, layerP->lights.header.nrOfChannels / sizeof(CRGB), color); } else { for (uint16_t index = 0; index < nrOfLights; index++) setRGB(index, color); } @@ -283,7 +283,7 @@ void VirtualLayer::fill_rainbow(const uint8_t initialhue, const uint8_t deltahue // } // } else if (layerP->lights.header.channelsPerLight == 3 && layerP->layers.size() == 1) { // faster, else manual - fastled_fill_rainbow((CRGB*)layerP->lights.channels, layerP->lights.header.nrOfChannels / sizeof(CRGB), initialhue, deltahue); + fastled_fill_rainbow((CRGB*)layerP->lights.channelsE, layerP->lights.header.nrOfChannels / sizeof(CRGB), initialhue, deltahue); } else { CHSV hsv; hsv.hue = initialhue; @@ -354,7 +354,7 @@ void VirtualLayer::addLight(Coord3D position) { } } else { // set unmapped lights to 0, e.g. needed by checkerboard modifier - memset(&layerP->lights.channels[layerP->indexP * layerP->lights.header.channelsPerLight], 0, layerP->lights.header.channelsPerLight); + memset(&layerP->lights.channelsE[layerP->indexP * layerP->lights.header.channelsPerLight], 0, layerP->lights.header.channelsPerLight); } } diff --git a/src/MoonLight/Layers/VirtualLayer.h b/src/MoonLight/Layers/VirtualLayer.h index d3f6455e..a1e166b3 100644 --- a/src/MoonLight/Layers/VirtualLayer.h +++ b/src/MoonLight/Layers/VirtualLayer.h @@ -63,7 +63,7 @@ class VirtualLayer { // heap-optimization: request heap optimization review // on boards without PSRAM, heap is only 60 KB (30KB max alloc) available, need to find out how to increase the heap // for virtual mapping mappingTable and mappingTableIndexes is used - // mappingTable is per default same size as the number of LEDs/lights (stored in lights.channels), see Physical layer, goal is also here to support 12288 LEDs on non PSRAM boards at least for non PSRAM board + // mappingTable is per default same size as the number of LEDs/lights (stored in lights.channelsE/D), see Physical layer, goal is also here to support 12288 LEDs on non PSRAM boards at least for non PSRAM board // mappingTableIndexes is used of the mapping of effects to lights.channel is not 1:1 but 1:M // they will be reused to avoid fragmentation diff --git a/src/MoonLight/Modules/ModuleChannels.h b/src/MoonLight/Modules/ModuleChannels.h index ebc1007e..0ac7beb5 100644 --- a/src/MoonLight/Modules/ModuleChannels.h +++ b/src/MoonLight/Modules/ModuleChannels.h @@ -74,15 +74,15 @@ class ModuleChannels : public Module { // EXT_LOGD(ML_TAG, "%s[%d]%s[%d].%s = %s -> %s", updatedItem.parent[0].c_str(), updatedItem.index[0], updatedItem.parent[1].c_str(), updatedItem.index[1], updatedItem.name.c_str(), updatedItem.oldValue.c_str(), updatedItem.value.as().c_str()); // copy the file to the hidden folder... if (updatedItem.oldValue != "" && !updatedItem.value["action"].isNull() && updatedItem.value["action"] != "") { - if (!layerP.lights.channels) return; // to avoid crash during init + if (!layerP.lights.channelsE) return; // to avoid crash during init EXT_LOGD(ML_TAG, "handle %s[%d]%s[%d].%s = %s -> %s", updatedItem.parent[0].c_str(), updatedItem.index[0], updatedItem.parent[1].c_str(), updatedItem.index[1], updatedItem.name.c_str(), updatedItem.oldValue.c_str(), updatedItem.value.as().c_str()); uint16_t select = updatedItem.value["select"]; uint8_t value = updatedItem.value["action"] == "mouseenter" ? 255 : 0; if (view == 0) { // physical layer if (group) - for (uint8_t i = 0; i < layerP.lights.header.channelsPerLight; i++) layerP.lights.channels[select * layerP.lights.header.channelsPerLight + i] = value; + for (uint8_t i = 0; i < layerP.lights.header.channelsPerLight; i++) layerP.lights.channelsE[select * layerP.lights.header.channelsPerLight + i] = value; else - layerP.lights.channels[select] = value; + layerP.lights.channelsE[select] = value; } else { if (group) for (uint8_t i = 0; i < layerP.lights.header.channelsPerLight; i++) layerP.layers[view - 1]->setLight(select, &value, i, 1); diff --git a/src/MoonLight/Modules/ModuleLightsControl.h b/src/MoonLight/Modules/ModuleLightsControl.h index bd932ac3..4698cf4c 100644 --- a/src/MoonLight/Modules/ModuleLightsControl.h +++ b/src/MoonLight/Modules/ModuleLightsControl.h @@ -41,7 +41,7 @@ class ModuleLightsControl : public Module { EXT_LOGI(ML_TAG, "Lights:%d(Header:%d) L-H:%d Node:%d PL:%d(PL-L:%d) VL:%d PM:%d C3D:%d", sizeof(Lights), sizeof(LightsHeader), sizeof(Lights) - sizeof(LightsHeader), sizeof(Node), sizeof(PhysicalLayer), sizeof(PhysicalLayer) - sizeof(Lights), sizeof(VirtualLayer), sizeof(PhysMap), sizeof(Coord3D)); - EXT_LOGI(ML_TAG, "isInPSRAM: mt:%d mti:%d ch:%d", isInPSRAM(layerP.layers[0]->mappingTable), isInPSRAM(layerP.layers[0]->mappingTableIndexes.data()), isInPSRAM(layerP.lights.channels)); + EXT_LOGI(ML_TAG, "isInPSRAM: mt:%d mti:%d ch:%d", isInPSRAM(layerP.layers[0]->mappingTable), isInPSRAM(layerP.layers[0]->mappingTableIndexes.data()), isInPSRAM(layerP.lights.channelsE)); setPresetsFromFolder(); // set the right values during boot @@ -356,9 +356,9 @@ class ModuleLightsControl : public Module { read([&](ModuleState& _state) { if (_socket->getConnectedClients() && _state.data["monitorOn"]) { _socket->emitEvent("monitor", (char*)&layerP.lights.header, 37); // sizeof(LightsHeader)); //sizeof(LightsHeader), nearest prime nr above 32 to avoid monitor data to be seen as header - _socket->emitEvent("monitor", (char*)layerP.lights.channels, MIN(layerP.lights.header.nrOfLights * 3, layerP.lights.maxChannels)); //*3 is for 3 bytes position + _socket->emitEvent("monitor", (char*)layerP.lights.channelsE, MIN(layerP.lights.header.nrOfLights * 3, layerP.lights.maxChannels)); //*3 is for 3 bytes position } - memset(layerP.lights.channels, 0, layerP.lights.maxChannels); // set all the channels to 0 //cleaning the positions + memset(layerP.lights.channelsE, 0, layerP.lights.maxChannels); // set all the channels to 0 //cleaning the positions EXT_LOGD(ML_TAG, "positions sent to monitor (2 -> 3, #L:%d maxC:%d)", layerP.lights.header.nrOfLights, layerP.lights.maxChannels); layerP.lights.header.isPositions = 3; }); @@ -372,7 +372,7 @@ class ModuleLightsControl : public Module { extern SemaphoreHandle_t swapMutex; xSemaphoreTake(swapMutex, portMAX_DELAY); - _socket->emitEvent("monitor", (char*)layerP.lights.channels, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); + _socket->emitEvent("monitor", (char*)layerP.lights.channelsE, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); xSemaphoreGive(swapMutex); } }); diff --git a/src/MoonLight/Nodes/Drivers/D_ArtnetIn.h b/src/MoonLight/Nodes/Drivers/D_ArtnetIn.h index 87e00e60..d78e4339 100644 --- a/src/MoonLight/Nodes/Drivers/D_ArtnetIn.h +++ b/src/MoonLight/Nodes/Drivers/D_ArtnetIn.h @@ -130,7 +130,7 @@ class ArtNetInDriver : public Node { int ledIndex = startPixel + i; if (ledIndex < layerP.lights.header.nrOfLights) { if (layer == 0) { // Physical layer - memcpy(&layerP.lights.channels[ledIndex * layerP.lights.header.channelsPerLight], &dmxData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight); + memcpy(&layerP.lights.channelsE[ledIndex * layerP.lights.header.channelsPerLight], &dmxData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight); } else { // Virtual layer layerP.layers[layer - 1]->setLight(ledIndex, &dmxData[i * layerP.lights.header.channelsPerLight], 0, layerP.lights.header.channelsPerLight); } @@ -160,7 +160,7 @@ class ArtNetInDriver : public Node { int ledIndex = startPixel + i; if (ledIndex < layerP.lights.header.nrOfLights) { if (layer == 0) { // Physical layer - memcpy(&layerP.lights.channels[ledIndex * layerP.lights.header.channelsPerLight], &pixelData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight); + memcpy(&layerP.lights.channelsE[ledIndex * layerP.lights.header.channelsPerLight], &pixelData[i * layerP.lights.header.channelsPerLight], layerP.lights.header.channelsPerLight); } else { // Virtual layer layerP.layers[layer - 1]->setLight(ledIndex, &pixelData[i * layerP.lights.header.channelsPerLight], 0, layerP.lights.header.channelsPerLight); } diff --git a/src/MoonLight/Nodes/Drivers/D_ArtnetOut.h b/src/MoonLight/Nodes/Drivers/D_ArtnetOut.h index 9f44a8c0..3d906b60 100644 --- a/src/MoonLight/Nodes/Drivers/D_ArtnetOut.h +++ b/src/MoonLight/Nodes/Drivers/D_ArtnetOut.h @@ -137,14 +137,14 @@ class ArtNetOutDriver : public DriverNode { // send all the leds to artnet for (int indexP = 0; indexP < header->nrOfLights; indexP++) { // fill a package - memcpy(&packet_buffer[packetSize + 18], &layerP.lights.channels[indexP * header->channelsPerLight], header->channelsPerLight); // set all the channels + memcpy(&packet_buffer[packetSize + 18], &layerP.lights.channelsD[indexP * header->channelsPerLight], header->channelsPerLight); // set all the channels // correct the RGB channels for color order and brightness - reOrderAndDimRGBW(&packet_buffer[packetSize + 18 + header->offsetRGB], &layerP.lights.channels[indexP * header->channelsPerLight + header->offsetRGB]); + reOrderAndDimRGBW(&packet_buffer[packetSize + 18 + header->offsetRGB], &layerP.lights.channelsD[indexP * header->channelsPerLight + header->offsetRGB]); - if (header->offsetRGB1 != UINT8_MAX) reOrderAndDimRGBW(&packet_buffer[packetSize + 18 + header->offsetRGB1], &layerP.lights.channels[indexP * header->channelsPerLight + header->offsetRGB1]); - if (header->offsetRGB2 != UINT8_MAX) reOrderAndDimRGBW(&packet_buffer[packetSize + 18 + header->offsetRGB2], &layerP.lights.channels[indexP * header->channelsPerLight + header->offsetRGB2]); - if (header->offsetRGB3 != UINT8_MAX) reOrderAndDimRGBW(&packet_buffer[packetSize + 18 + header->offsetRGB3], &layerP.lights.channels[indexP * header->channelsPerLight + header->offsetRGB3]); + if (header->offsetRGB1 != UINT8_MAX) reOrderAndDimRGBW(&packet_buffer[packetSize + 18 + header->offsetRGB1], &layerP.lights.channelsD[indexP * header->channelsPerLight + header->offsetRGB1]); + if (header->offsetRGB2 != UINT8_MAX) reOrderAndDimRGBW(&packet_buffer[packetSize + 18 + header->offsetRGB2], &layerP.lights.channelsD[indexP * header->channelsPerLight + header->offsetRGB2]); + if (header->offsetRGB3 != UINT8_MAX) reOrderAndDimRGBW(&packet_buffer[packetSize + 18 + header->offsetRGB3], &layerP.lights.channelsD[indexP * header->channelsPerLight + header->offsetRGB3]); if (header->lightPreset == 9 && indexP < 72) // RGBWYP this config assumes a mix of 4 channels and 6 channels per light !!!! packetSize += 4; diff --git a/src/MoonLight/Nodes/Drivers/D_FastLED.h b/src/MoonLight/Nodes/Drivers/D_FastLED.h index 828f7711..d39fd402 100644 --- a/src/MoonLight/Nodes/Drivers/D_FastLED.h +++ b/src/MoonLight/Nodes/Drivers/D_FastLED.h @@ -128,7 +128,7 @@ class FastLEDDriver : public Node { uint16_t nrOfLights = layerP.ledsPerPin[pinIndex]; - CRGB* leds = (CRGB*)layerP.lights.channels; + CRGB* leds = (CRGB*)layerP.lights.channelsD; switch (pins[pinIndex]) { #if CONFIG_IDF_TARGET_ESP32 diff --git a/src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h b/src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h index ac3027e6..12ae3d4e 100644 --- a/src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h +++ b/src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h @@ -60,7 +60,7 @@ class ParallelLEDDriver : public DriverNode { uint8_t nrOfPins = min(layerP.nrOfLedPins, layerP.nrOfAssignedPins); // LUTs are accessed directly within show_parlio via extern ledsDriver // No brightness parameter needed - show_parlio(pins, layerP.lights.header.nrOfLights, layerP.lights.channels, layerP.lights.header.channelsPerLight == 4, nrOfPins, layerP.ledsPerPin[0], layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue); + show_parlio(pins, layerP.lights.header.nrOfLights, layerP.lights.channelsD, layerP.lights.header.channelsPerLight == 4, nrOfPins, layerP.ledsPerPin[0], layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue); #endif } #else // ESP32_LEDSDRIVER @@ -122,7 +122,7 @@ class ParallelLEDDriver : public DriverNode { uint8_t savedBrightness = ledsDriver._brightness; //(initLed sets it to 255 and thats not what we want) EXT_LOGD(ML_TAG, "init Parallel LED Driver %d %d %d %d", layerP.lights.header.channelsPerLight, layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue); - ledsDriver.initled(layerP.lights.channels, pins, layerP.ledsPerPin, nrOfPins, layerP.lights.header.channelsPerLight, layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue); + ledsDriver.initled(layerP.lights.channelsD, pins, layerP.ledsPerPin, nrOfPins, layerP.lights.header.channelsPerLight, layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue); ledsDriver.setBrightness(savedBrightness); //(initLed sets it to 255 and thats not what we want) @@ -171,7 +171,7 @@ class ParallelLEDDriver : public DriverNode { } if (numPins > 0) { - ledsDriver.initLeds(layerP.lights.channels, pinConfig, numPins, layerP.lights.header.channelsPerLight, layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue, layerP.lights.header.offsetWhite); // 102 is GRB + ledsDriver.initLeds(layerP.lights.channelsD, pinConfig, numPins, layerP.lights.header.channelsPerLight, layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue, layerP.lights.header.offsetWhite); // 102 is GRB #if ML_LIVE_MAPPING driver.setMapLed(&mapLed); diff --git a/src/MoonLight/Nodes/Drivers/parlio.cpp b/src/MoonLight/Nodes/Drivers/parlio.cpp index 45c365e4..f2bb834f 100644 --- a/src/MoonLight/Nodes/Drivers/parlio.cpp +++ b/src/MoonLight/Nodes/Drivers/parlio.cpp @@ -206,7 +206,7 @@ void create_transposed_led_output_optimized(const uint8_t* input_buffer, uint16_ uint32_t transposed_slices[32]; - // Select LUT based on INPUT component (which color we're reading from lights.channels) + // Select LUT based on INPUT component (which color we're reading from lights.channelsD) const uint8_t* brightness_cache; switch (input_component) { // ← Change 1: Use input_component case 0: diff --git a/src/main.cpp b/src/main.cpp index 5171a57f..7f036b2c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -129,7 +129,7 @@ void effectTask(void* pvParameters) { while (true) { if (layerP.lights.useDoubleBuffer) { // effectTask always writes to channelsBack, reads previous channelsBack - layerP.lights.channels = layerP.lights.channelsBack; + layerP.lights.channelsE = layerP.lights.channelsD; layerP.loop(); // getRGB and setRGB both use channelsBack if (millis() - last20ms >= 20) { @@ -139,9 +139,9 @@ void effectTask(void* pvParameters) { // Atomic swap channels xSemaphoreTake(swapMutex, portMAX_DELAY); - uint8_t* temp = layerP.lights.channelsBack; - layerP.lights.channelsBack = layerP.lights.channels; - layerP.lights.channels = temp; + uint8_t* temp = layerP.lights.channelsD; + layerP.lights.channelsD = layerP.lights.channelsE; + layerP.lights.channelsE = temp; newFrameReady = true; xSemaphoreGive(swapMutex); From 6b17669f5c55959c5ba0a91cfdc3f189ac308bc7 Mon Sep 17 00:00:00 2001 From: ewowi Date: Sat, 27 Dec 2025 12:15:42 +0100 Subject: [PATCH 03/11] Bugfix channelsE assignment + monitor using channelsD --- docs/develop/architecture.md | 6 ++---- src/MoonLight/Modules/ModuleLightsControl.h | 2 +- src/main.cpp | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/develop/architecture.md b/docs/develop/architecture.md index 82c7ba28..af722535 100644 --- a/docs/develop/architecture.md +++ b/docs/develop/architecture.md @@ -10,11 +10,11 @@ MoonLight uses a multi-core, multi-task architecture on ESP32 to achieve smooth |------|------|----------|------------|-----------|---------| | **WiFi/BT** | 0 (PRO_CPU) | 23 | System | Event-driven | System networking stack | | **lwIP TCP/IP** | 0 (PRO_CPU) | 18 | System | Event-driven | TCP/IP protocol processing | -| **Effect Task** | 0 (PRO_CPU) | 3 | 3-4KB | ~60 fps | Calculate LED colors and effects | +| **Effect Task** | 0 (PRO_CPU) | 10 | 3-4KB | ~60 fps | Calculate LED colors and effects | | **ESP32SvelteKit** | 1 (APP_CPU) | 2 | System | 10ms | HTTP/WebSocket UI framework | | **Driver Task** | 1 (APP_CPU) | 3 | 3-4KB | ~60 fps | Output data to LEDs via DMA/I2S/LCD/PARLIO | -Effect Task (Core 0, Priority 3) +Effect Task (Core 0, Priority 10) - **Function**: Pure computation - calculates pixel colors based on effect algorithms - **Operations**: Reads/writes to `channels` array, performs mathematical calculations @@ -162,7 +162,6 @@ Synchronization Flow void effectTask(void* param) { while (true) { if (layerP.lights.useDoubleBuffer) { - layerP.lights.channelsE = layerP.lights.channelsD; layerP.loop(); // getRGB and setRGB both use channelsBack @@ -177,7 +176,6 @@ void effectTask(void* param) { } else { xSemaphoreTake(swapMutex, portMAX_DELAY); layerP.loop(); - xSemaphoreGive(swapMutex); } vTaskDelay(1); diff --git a/src/MoonLight/Modules/ModuleLightsControl.h b/src/MoonLight/Modules/ModuleLightsControl.h index 4698cf4c..482038a6 100644 --- a/src/MoonLight/Modules/ModuleLightsControl.h +++ b/src/MoonLight/Modules/ModuleLightsControl.h @@ -372,7 +372,7 @@ class ModuleLightsControl : public Module { extern SemaphoreHandle_t swapMutex; xSemaphoreTake(swapMutex, portMAX_DELAY); - _socket->emitEvent("monitor", (char*)layerP.lights.channelsE, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); + _socket->emitEvent("monitor", (char*)layerP.lights.channelsD, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); xSemaphoreGive(swapMutex); } }); diff --git a/src/main.cpp b/src/main.cpp index 7f036b2c..09d297c2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -129,7 +129,6 @@ void effectTask(void* pvParameters) { while (true) { if (layerP.lights.useDoubleBuffer) { // effectTask always writes to channelsBack, reads previous channelsBack - layerP.lights.channelsE = layerP.lights.channelsD; layerP.loop(); // getRGB and setRGB both use channelsBack if (millis() - last20ms >= 20) { From 28eceabf90022dc2e0fbd75b8e95ab098f738d0d Mon Sep 17 00:00:00 2001 From: ewowi Date: Sat, 27 Dec 2025 21:11:35 +0100 Subject: [PATCH 04/11] No need to produce more frames then consumed --- src/MoonLight/Modules/ModuleLightsControl.h | 2 +- src/main.cpp | 36 ++++++++++++--------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/MoonLight/Modules/ModuleLightsControl.h b/src/MoonLight/Modules/ModuleLightsControl.h index 482038a6..4698cf4c 100644 --- a/src/MoonLight/Modules/ModuleLightsControl.h +++ b/src/MoonLight/Modules/ModuleLightsControl.h @@ -372,7 +372,7 @@ class ModuleLightsControl : public Module { extern SemaphoreHandle_t swapMutex; xSemaphoreTake(swapMutex, portMAX_DELAY); - _socket->emitEvent("monitor", (char*)layerP.lights.channelsD, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); + _socket->emitEvent("monitor", (char*)layerP.lights.channelsE, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); xSemaphoreGive(swapMutex); } }); diff --git a/src/main.cpp b/src/main.cpp index 09d297c2..c71e0713 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -128,22 +128,28 @@ void effectTask(void* pvParameters) { while (true) { if (layerP.lights.useDoubleBuffer) { - // effectTask always writes to channelsBack, reads previous channelsBack - layerP.loop(); // getRGB and setRGB both use channelsBack - - if (millis() - last20ms >= 20) { - last20ms = millis(); - layerP.loop20ms(); - } - - // Atomic swap channels xSemaphoreTake(swapMutex, portMAX_DELAY); - uint8_t* temp = layerP.lights.channelsD; - layerP.lights.channelsD = layerP.lights.channelsE; - layerP.lights.channelsE = temp; - newFrameReady = true; + bool canProduce = !newFrameReady; xSemaphoreGive(swapMutex); + if (canProduce) { + // effectTask always writes to channelsBack, reads previous channelsBack + layerP.loop(); // getRGB and setRGB both use channelsBack + + if (millis() - last20ms >= 20) { + last20ms = millis(); + layerP.loop20ms(); + } + + // Atomic swap channels + xSemaphoreTake(swapMutex, portMAX_DELAY); + uint8_t* temp = layerP.lights.channelsD; + layerP.lights.channelsD = layerP.lights.channelsE; + layerP.lights.channelsE = temp; + newFrameReady = true; + xSemaphoreGive(swapMutex); + } + } else { // Single buffer mode xSemaphoreTake(swapMutex, portMAX_DELAY); @@ -167,9 +173,9 @@ void driverTask(void* pvParameters) { // layerP.setup() done in effectTask while (true) { - xSemaphoreTake(swapMutex, portMAX_DELAY); esp32sveltekit.lps++; - + + xSemaphoreTake(swapMutex, portMAX_DELAY); if (layerP.lights.useDoubleBuffer) { if (newFrameReady) { newFrameReady = false; From 77cc503d03da7ad886c1aa2f2fb29c957d2007d0 Mon Sep 17 00:00:00 2001 From: ewowi Date: Sun, 28 Dec 2025 10:53:06 +0100 Subject: [PATCH 05/11] Refactor task parallelism (monitorMutex, isPositions, memcpy) back end ======= - main: add monitorMutex, use isPositions, add effect loop memcpy - node manager: delay(100) before deleting a node so the loop can finish - Physical layer, ArtnetOut, PLEDDriver: remove isPositions from loop and loopDriver (done in main) - Lights control: add monitorMutex and monitorMillis --- docs/develop/architecture.md | 34 +------ src/MoonBase/NodeManager.h | 3 +- src/MoonLight/Layers/PhysicalLayer.cpp | 28 ++---- src/MoonLight/Modules/ModuleDrivers.h | 3 - src/MoonLight/Modules/ModuleLightsControl.h | 19 ++-- src/MoonLight/Nodes/Drivers/D_ArtnetOut.h | 2 +- .../Nodes/Drivers/D_ParallelLEDDriver.h | 20 ++-- src/main.cpp | 99 ++++++++++--------- 8 files changed, 87 insertions(+), 121 deletions(-) diff --git a/docs/develop/architecture.md b/docs/develop/architecture.md index af722535..9edf46f7 100644 --- a/docs/develop/architecture.md +++ b/docs/develop/architecture.md @@ -161,44 +161,14 @@ Synchronization Flow void effectTask(void* param) { while (true) { - if (layerP.lights.useDoubleBuffer) { - - layerP.loop(); // getRGB and setRGB both use channelsBack - - // Atomic swap channels - xSemaphoreTake(swapMutex, portMAX_DELAY); - uint8_t* temp = layerP.lights.channelsD; - layerP.lights.channelsD = layerP.lights.channelsE; - layerP.lights.channelsE = temp; - newFrameReady = true; - xSemaphoreGive(swapMutex); - - } else { - xSemaphoreTake(swapMutex, portMAX_DELAY); - layerP.loop(); - xSemaphoreGive(swapMutex); - } + // tbd ... vTaskDelay(1); } } void driverTask(void* param) { while (true) { - xSemaphoreTake(swapMutex, portMAX_DELAY); - esp32sveltekit.lps++; - - if (layerP.lights.useDoubleBuffer) { - if (newFrameReady) { - newFrameReady = false; - xSemaphoreGive(swapMutex); - layerP.loopDrivers(); // ✅ No lock needed - } else { - xSemaphoreGive(swapMutex); - } - } else { - layerP.loopDrivers(); // ✅ Protected by lock - xSemaphoreGive(swapMutex); - } + // tbd ... vTaskDelay(1); } } diff --git a/src/MoonBase/NodeManager.h b/src/MoonBase/NodeManager.h index d794e244..fd2cb603 100644 --- a/src/MoonBase/NodeManager.h +++ b/src/MoonBase/NodeManager.h @@ -220,7 +220,8 @@ class NodeManager : public Module { } oldNode->requestMappings(); - + + delay(100); // to allow the node to finish its last loop EXT_LOGD(ML_TAG, "remove oldNode: %d p:%p", nodes->size(), oldNode); // delete node; //causing assert failed: multi_heap_free multi_heap_poisoning.c:259 (head != NULL) ATM // EXT_LOGD(MB_TAG, "destructing object (inPR:%d)", isInPSRAM(node)); diff --git a/src/MoonLight/Layers/PhysicalLayer.cpp b/src/MoonLight/Layers/PhysicalLayer.cpp index a5749882..b1c574a8 100644 --- a/src/MoonLight/Layers/PhysicalLayer.cpp +++ b/src/MoonLight/Layers/PhysicalLayer.cpp @@ -42,7 +42,7 @@ void PhysicalLayer::setup() { lights.maxChannels = MIN(ESP.getPsramSize() / 4, 61440 * 3); // fill halve with channels, max 120 pins * 512 LEDs, still addressable with uint16_t lights.useDoubleBuffer = true; // Enable double buffering } else { - lights.maxChannels = 4096 * 3; // esp32-d0: max 1024->2048->4096 Leds ATM + lights.maxChannels = 4096 * 3; // esp32-d0: max 1024->2048->4096 Leds ATM lights.useDoubleBuffer = false; // Single buffer mode } @@ -71,12 +71,9 @@ void PhysicalLayer::setup() { } void PhysicalLayer::loop() { - if (lights.header.isPositions == 0 || lights.header.isPositions == 3) { // otherwise lights is used for positions etc. - - // runs the loop of all effects / nodes in the layer - for (VirtualLayer* layer : layers) { - if (layer) layer->loop(); // if (layer) needed when deleting rows ... - } + // runs the loop of all effects / nodes in the layer + for (VirtualLayer* layer : layers) { + if (layer) layer->loop(); // if (layer) needed when deleting rows ... } } @@ -108,19 +105,14 @@ void PhysicalLayer::loopDrivers() { requestMapVirtual = false; } - if (lights.header.isPositions == 3) { - EXT_LOGD(ML_TAG, "positions done (3 -> 0)"); - lights.header.isPositions = 0; // now driver can show again - } + if (prevSize != lights.header.size) EXT_LOGD(ML_TAG, "onSizeChanged P %d,%d,%d -> %d,%d,%d", prevSize.x, prevSize.y, prevSize.z, lights.header.size.x, lights.header.size.y, lights.header.size.z); - if (lights.header.isPositions == 0) { // otherwise lights is used for positions etc. - if (prevSize != lights.header.size) EXT_LOGD(ML_TAG, "onSizeChanged P %d,%d,%d -> %d,%d,%d", prevSize.x, prevSize.y, prevSize.z, lights.header.size.x, lights.header.size.y, lights.header.size.z); - for (Node* node : nodes) { - if (prevSize != lights.header.size) node->onSizeChanged(prevSize); - if (node->on) node->loop(); - } - prevSize = lights.header.size; + for (Node* node : nodes) { + if (prevSize != lights.header.size) node->onSizeChanged(prevSize); + if (node->on) node->loop(); } + + prevSize = lights.header.size; } void PhysicalLayer::mapLayout() { diff --git a/src/MoonLight/Modules/ModuleDrivers.h b/src/MoonLight/Modules/ModuleDrivers.h index 146a2814..d8a0194a 100644 --- a/src/MoonLight/Modules/ModuleDrivers.h +++ b/src/MoonLight/Modules/ModuleDrivers.h @@ -159,9 +159,6 @@ class ModuleDrivers : public NodeManager { bool initPins = false; void loop() override { - // if (layerP.lights.header.isPositions == 0) //otherwise lights is used for positions etc. - // layerP.loop(); //run all the effects of all virtual layers (currently only one) - NodeManager::loop(); if (!initPins) { diff --git a/src/MoonLight/Modules/ModuleLightsControl.h b/src/MoonLight/Modules/ModuleLightsControl.h index 4698cf4c..ae3313ab 100644 --- a/src/MoonLight/Modules/ModuleLightsControl.h +++ b/src/MoonLight/Modules/ModuleLightsControl.h @@ -352,10 +352,11 @@ class ModuleLightsControl : public Module { } #if FT_ENABLED(FT_MONITOR) + extern SemaphoreHandle_t monitorMutex; // defined in main if (layerP.lights.header.isPositions == 2) { // send to UI read([&](ModuleState& _state) { if (_socket->getConnectedClients() && _state.data["monitorOn"]) { - _socket->emitEvent("monitor", (char*)&layerP.lights.header, 37); // sizeof(LightsHeader)); //sizeof(LightsHeader), nearest prime nr above 32 to avoid monitor data to be seen as header + _socket->emitEvent("monitor", (char*)&layerP.lights.header, 37); // sizeof(LightsHeader)); //sizeof(LightsHeader), nearest prime nr above 32 to avoid monitor data to be seen as header _socket->emitEvent("monitor", (char*)layerP.lights.channelsE, MIN(layerP.lights.header.nrOfLights * 3, layerP.lights.maxChannels)); //*3 is for 3 bytes position } memset(layerP.lights.channelsE, 0, layerP.lights.maxChannels); // set all the channels to 0 //cleaning the positions @@ -363,20 +364,18 @@ class ModuleLightsControl : public Module { layerP.lights.header.isPositions = 3; }); } else if (layerP.lights.header.isPositions == 0 && layerP.lights.header.nrOfLights) { // send to UI - EVERY_N_MILLIS(layerP.lights.header.nrOfLights / 12) { - + static unsigned long monitorMillis = 0; + if (millis() - monitorMillis >= layerP.lights.header.nrOfLights / 12) { + monitorMillis = millis(); + read([&](ModuleState& _state) { if (_socket->getConnectedClients() && _state.data["monitorOn"]) { - - //protect emit by swapMutex, see main.cpp - extern SemaphoreHandle_t swapMutex; - - xSemaphoreTake(swapMutex, portMAX_DELAY); + // protect emit by monitorMutex, see main.cpp + xSemaphoreTake(monitorMutex, portMAX_DELAY); _socket->emitEvent("monitor", (char*)layerP.lights.channelsE, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); - xSemaphoreGive(swapMutex); + xSemaphoreGive(monitorMutex); } }); - } } #endif diff --git a/src/MoonLight/Nodes/Drivers/D_ArtnetOut.h b/src/MoonLight/Nodes/Drivers/D_ArtnetOut.h index 3d906b60..5d6699e6 100644 --- a/src/MoonLight/Nodes/Drivers/D_ArtnetOut.h +++ b/src/MoonLight/Nodes/Drivers/D_ArtnetOut.h @@ -106,7 +106,7 @@ class ArtNetOutDriver : public DriverNode { LightsHeader* header = &layerP.lights.header; - if (header->isPositions != 0 || nrOfIPAddresses == 0) return; // don't sent if positions are sent or no IP addresses found (to do broadcast if no addresses specified...!) + if (nrOfIPAddresses == 0) return; // don't sent if no IP addresses found (to do broadcast if no addresses specified...!) // continue with Art-Net code uint8_t actualIPIndex = 0; diff --git a/src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h b/src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h index 12ae3d4e..4332b0b2 100644 --- a/src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h +++ b/src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h @@ -51,26 +51,22 @@ class ParallelLEDDriver : public DriverNode { #if HP_ALL_DRIVERS if (!initDone) return; - if (layerP.lights.header.isPositions == 0) { - DriverNode::loop(); // This populates the LUT tables! + DriverNode::loop(); // This populates the LUT tables! #ifndef CONFIG_IDF_TARGET_ESP32P4 - if (ledsDriver.total_leds > 0) ledsDriver.showPixels(WAIT); + if (ledsDriver.total_leds > 0) ledsDriver.showPixels(WAIT); #else - uint8_t nrOfPins = min(layerP.nrOfLedPins, layerP.nrOfAssignedPins); - // LUTs are accessed directly within show_parlio via extern ledsDriver - // No brightness parameter needed - show_parlio(pins, layerP.lights.header.nrOfLights, layerP.lights.channelsD, layerP.lights.header.channelsPerLight == 4, nrOfPins, layerP.ledsPerPin[0], layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue); + uint8_t nrOfPins = min(layerP.nrOfLedPins, layerP.nrOfAssignedPins); + // LUTs are accessed directly within show_parlio via extern ledsDriver + // No brightness parameter needed + show_parlio(pins, layerP.lights.header.nrOfLights, layerP.lights.channelsD, layerP.lights.header.channelsPerLight == 4, nrOfPins, layerP.ledsPerPin[0], layerP.lights.header.offsetRed, layerP.lights.header.offsetGreen, layerP.lights.header.offsetBlue); #endif - } #else // ESP32_LEDSDRIVER if (!ledsDriver.initLedsDone) return; - if (layerP.lights.header.isPositions == 0) { - DriverNode::loop(); + DriverNode::loop(); - ledsDriver.show(); - } + ledsDriver.show(); #endif } diff --git a/src/main.cpp b/src/main.cpp index c71e0713..3bce000f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -114,7 +114,8 @@ ModuleLiveScripts moduleLiveScripts = ModuleLiveScripts(&server, &esp32sveltekit ModuleChannels moduleChannels = ModuleChannels(&server, &esp32sveltekit); ModuleMoonLightInfo moduleMoonLightInfo = ModuleMoonLightInfo(&server, &esp32sveltekit); -SemaphoreHandle_t swapMutex = NULL; +SemaphoreHandle_t swapMutex = xSemaphoreCreateMutex(); +SemaphoreHandle_t monitorMutex = xSemaphoreCreateMutex(); volatile bool newFrameReady = false; TaskHandle_t effectTaskHandle = NULL; @@ -127,40 +128,46 @@ void effectTask(void* pvParameters) { static unsigned long last20ms = 0; while (true) { - if (layerP.lights.useDoubleBuffer) { - xSemaphoreTake(swapMutex, portMAX_DELAY); - bool canProduce = !newFrameReady; - xSemaphoreGive(swapMutex); + if (layerP.lights.header.isPositions == 0) { // driver task can change this + if (layerP.lights.useDoubleBuffer) { + xSemaphoreTake(swapMutex, portMAX_DELAY); + bool canProduce = !newFrameReady; + xSemaphoreGive(swapMutex); + + if (canProduce) { + // Copy previous frame (channelsD) to working buffer (channelsE) + memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); - if (canProduce) { - // effectTask always writes to channelsBack, reads previous channelsBack - layerP.loop(); // getRGB and setRGB both use channelsBack + layerP.loop(); + + if (millis() - last20ms >= 20) { + last20ms = millis(); + layerP.loop20ms(); + } + + // Atomic swap channels + xSemaphoreTake(swapMutex, portMAX_DELAY); + xSemaphoreTake(monitorMutex, portMAX_DELAY); + uint8_t* temp = layerP.lights.channelsD; + layerP.lights.channelsD = layerP.lights.channelsE; + layerP.lights.channelsE = temp; + newFrameReady = true; + xSemaphoreGive(monitorMutex); + xSemaphoreGive(swapMutex); + } + + } else { + // Single buffer mode + xSemaphoreTake(swapMutex, portMAX_DELAY); + layerP.loop(); if (millis() - last20ms >= 20) { last20ms = millis(); layerP.loop20ms(); } - // Atomic swap channels - xSemaphoreTake(swapMutex, portMAX_DELAY); - uint8_t* temp = layerP.lights.channelsD; - layerP.lights.channelsD = layerP.lights.channelsE; - layerP.lights.channelsE = temp; - newFrameReady = true; xSemaphoreGive(swapMutex); } - - } else { - // Single buffer mode - xSemaphoreTake(swapMutex, portMAX_DELAY); - layerP.loop(); - - if (millis() - last20ms >= 20) { - last20ms = millis(); - layerP.loop20ms(); - } - - xSemaphoreGive(swapMutex); } vTaskDelay(1); // yield to other tasks, 1 tick (~1ms) @@ -173,24 +180,30 @@ void driverTask(void* pvParameters) { // layerP.setup() done in effectTask while (true) { - esp32sveltekit.lps++; - - xSemaphoreTake(swapMutex, portMAX_DELAY); - if (layerP.lights.useDoubleBuffer) { - if (newFrameReady) { - newFrameReady = false; - // Double buffer: release lock, then send - xSemaphoreGive(swapMutex); - layerP.loopDrivers(); // ✅ No lock needed + if (layerP.lights.header.isPositions == 3) { + EXT_LOGD(ML_TAG, "positions done (3 -> 0)"); + layerP.lights.header.isPositions = 0; // now driver can show again + } + + if (layerP.lights.header.isPositions == 0) { + xSemaphoreTake(swapMutex, portMAX_DELAY); + if (layerP.lights.useDoubleBuffer) { + if (newFrameReady) { + newFrameReady = false; + esp32sveltekit.lps++; + // Double buffer: release lock, then send + xSemaphoreGive(swapMutex); + + layerP.loopDrivers(); // ✅ No lock needed + } else { + xSemaphoreGive(swapMutex); + } } else { + // Single buffer: keep lock while sending + layerP.loopDrivers(); // ✅ Protected by lock xSemaphoreGive(swapMutex); } - } else { - // Single buffer: keep lock while sending - layerP.loopDrivers(); // ✅ Protected by lock - xSemaphoreGive(swapMutex); } - vTaskDelay(1); } } @@ -358,8 +371,6 @@ void setup() { false); #endif - swapMutex = xSemaphoreCreateMutex(); - // 🌙 xTaskCreateUniversal(effectTask, // task function "AppEffectTask", // name @@ -367,7 +378,7 @@ void setup() { NULL, // parameter 10, // priority (between 5 and 10: ASYNC_WORKER_TASK_PRIORITY and Restart/Sleep), don't set it higher then 10... &effectTaskHandle, // task handle - 0 // core (0 or 1) + 0 // core ); xTaskCreateUniversal(driverTask, // task function @@ -376,7 +387,7 @@ void setup() { NULL, // parameter 3, // priority (between 5 and 10: ASYNC_WORKER_TASK_PRIORITY and Restart/Sleep), don't set it higher then 10... &driverTaskHandle, // task handle - 1 // core (0 or 1) + 1 // core ); #endif From 34afb7398b29c340e28dc34e2f30738455cbe564 Mon Sep 17 00:00:00 2001 From: ewowi Date: Sun, 28 Dec 2025 11:20:42 +0100 Subject: [PATCH 06/11] bugfix lps++ and monitorMutex around effects loop --- src/main.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 3bce000f..f46b2fb6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -138,21 +138,21 @@ void effectTask(void* pvParameters) { // Copy previous frame (channelsD) to working buffer (channelsE) memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); + xSemaphoreTake(monitorMutex, portMAX_DELAY); // don't change channelsE while the monitor display data is sent layerP.loop(); if (millis() - last20ms >= 20) { last20ms = millis(); layerP.loop20ms(); } + xSemaphoreGive(monitorMutex); // Atomic swap channels xSemaphoreTake(swapMutex, portMAX_DELAY); - xSemaphoreTake(monitorMutex, portMAX_DELAY); uint8_t* temp = layerP.lights.channelsD; layerP.lights.channelsD = layerP.lights.channelsE; layerP.lights.channelsE = temp; newFrameReady = true; - xSemaphoreGive(monitorMutex); xSemaphoreGive(swapMutex); } @@ -190,16 +190,17 @@ void driverTask(void* pvParameters) { if (layerP.lights.useDoubleBuffer) { if (newFrameReady) { newFrameReady = false; - esp32sveltekit.lps++; // Double buffer: release lock, then send xSemaphoreGive(swapMutex); + esp32sveltekit.lps++; layerP.loopDrivers(); // ✅ No lock needed } else { xSemaphoreGive(swapMutex); } } else { // Single buffer: keep lock while sending + esp32sveltekit.lps++; layerP.loopDrivers(); // ✅ Protected by lock xSemaphoreGive(swapMutex); } From 3cef7857085096b25b05eb478144e70d93eb0f20 Mon Sep 17 00:00:00 2001 From: ewowi Date: Sun, 28 Dec 2025 11:52:32 +0100 Subject: [PATCH 07/11] isPosition guarded by mutex, monitor using channelsD --- src/MoonLight/Layers/PhysicalLayer.cpp | 12 ++++++++++-- src/MoonLight/Modules/ModuleLightsControl.h | 19 ++++++++++++++----- src/main.cpp | 19 ++++++++++++++----- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/MoonLight/Layers/PhysicalLayer.cpp b/src/MoonLight/Layers/PhysicalLayer.cpp index b1c574a8..2bdcb2e5 100644 --- a/src/MoonLight/Layers/PhysicalLayer.cpp +++ b/src/MoonLight/Layers/PhysicalLayer.cpp @@ -111,7 +111,7 @@ void PhysicalLayer::loopDrivers() { if (prevSize != lights.header.size) node->onSizeChanged(prevSize); if (node->on) node->loop(); } - + prevSize = lights.header.size; } @@ -131,9 +131,14 @@ void PhysicalLayer::onLayoutPre() { if (pass == 1) { lights.header.nrOfLights = 0; // for pass1 and pass2 as in pass2 virtual layer needs it lights.header.size = {0, 0, 0}; + extern SemaphoreHandle_t swapMutex; + xSemaphoreTake(swapMutex, portMAX_DELAY); EXT_LOGD(ML_TAG, "positions in progress (%d -> 1)", lights.header.isPositions); lights.header.isPositions = 1; // in progress... - delay(100); // wait to stop effects + xSemaphoreGive(swapMutex); + + delay(100); // wait to stop effects + // set all channels to 0 (e.g for multichannel to not activate unused channels, e.g. fancy modes on MHs) memset(lights.channelsE, 0, lights.maxChannels); // set all the channels to 0, positions in channelsE // dealloc pins @@ -206,8 +211,11 @@ void PhysicalLayer::onLayoutPost() { lights.header.nrOfChannels = lights.header.nrOfLights * lights.header.channelsPerLight * ((lights.header.lightPreset == lightPreset_RGB2040) ? 2 : 1); // RGB2040 has empty channels EXT_LOGD(ML_TAG, "pass %d mp:%d #:%d / %d s:%d,%d,%d", pass, monitorPass, lights.header.nrOfLights, lights.header.nrOfChannels, lights.header.size.x, lights.header.size.y, lights.header.size.z); // send the positions to the UI _socket_emit + extern SemaphoreHandle_t swapMutex; + xSemaphoreTake(swapMutex, portMAX_DELAY); EXT_LOGD(ML_TAG, "positions stored (%d -> %d)", lights.header.isPositions, lights.header.nrOfLights ? 2 : 3); lights.header.isPositions = lights.header.nrOfLights ? 2 : 3; // filled with positions, set back to 3 in ModuleEffects, or direct to 3 if no lights (effects will move it to 0) + xSemaphoreGive(swapMutex); // initLightsToBlend(); diff --git a/src/MoonLight/Modules/ModuleLightsControl.h b/src/MoonLight/Modules/ModuleLightsControl.h index ae3313ab..def7fb90 100644 --- a/src/MoonLight/Modules/ModuleLightsControl.h +++ b/src/MoonLight/Modules/ModuleLightsControl.h @@ -352,18 +352,27 @@ class ModuleLightsControl : public Module { } #if FT_ENABLED(FT_MONITOR) - extern SemaphoreHandle_t monitorMutex; // defined in main - if (layerP.lights.header.isPositions == 2) { // send to UI + extern SemaphoreHandle_t monitorMutex; // defined in main + extern SemaphoreHandle_t swapMutex; + + // Check and transition under lock + xSemaphoreTake(swapMutex, portMAX_DELAY); + uint8_t isPositions = layerP.lights.header.isPositions; + xSemaphoreGive(swapMutex); + + if (isPositions == 2) { // send to UI read([&](ModuleState& _state) { if (_socket->getConnectedClients() && _state.data["monitorOn"]) { _socket->emitEvent("monitor", (char*)&layerP.lights.header, 37); // sizeof(LightsHeader)); //sizeof(LightsHeader), nearest prime nr above 32 to avoid monitor data to be seen as header _socket->emitEvent("monitor", (char*)layerP.lights.channelsE, MIN(layerP.lights.header.nrOfLights * 3, layerP.lights.maxChannels)); //*3 is for 3 bytes position } memset(layerP.lights.channelsE, 0, layerP.lights.maxChannels); // set all the channels to 0 //cleaning the positions - EXT_LOGD(ML_TAG, "positions sent to monitor (2 -> 3, #L:%d maxC:%d)", layerP.lights.header.nrOfLights, layerP.lights.maxChannels); + xSemaphoreTake(swapMutex, portMAX_DELAY); + EXT_LOGD(ML_TAG, "positions sent to monitor (2 -> 3)"); layerP.lights.header.isPositions = 3; + xSemaphoreGive(swapMutex); }); - } else if (layerP.lights.header.isPositions == 0 && layerP.lights.header.nrOfLights) { // send to UI + } else if (isPositions == 0 && layerP.lights.header.nrOfLights) { // send to UI static unsigned long monitorMillis = 0; if (millis() - monitorMillis >= layerP.lights.header.nrOfLights / 12) { monitorMillis = millis(); @@ -372,7 +381,7 @@ class ModuleLightsControl : public Module { if (_socket->getConnectedClients() && _state.data["monitorOn"]) { // protect emit by monitorMutex, see main.cpp xSemaphoreTake(monitorMutex, portMAX_DELAY); - _socket->emitEvent("monitor", (char*)layerP.lights.channelsE, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); + _socket->emitEvent("monitor", (char*)layerP.lights.channelsD, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); // use channelsD as it won't be overwritten by effects during loop xSemaphoreGive(monitorMutex); } }); diff --git a/src/main.cpp b/src/main.cpp index f46b2fb6..9968a3a2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -128,7 +128,12 @@ void effectTask(void* pvParameters) { static unsigned long last20ms = 0; while (true) { - if (layerP.lights.header.isPositions == 0) { // driver task can change this + // Check state under lock + xSemaphoreTake(swapMutex, portMAX_DELAY); + uint8_t isPositions = layerP.lights.header.isPositions; + xSemaphoreGive(swapMutex); + + if (isPositions == 0) { // driver task can change this if (layerP.lights.useDoubleBuffer) { xSemaphoreTake(swapMutex, portMAX_DELAY); bool canProduce = !newFrameReady; @@ -138,21 +143,21 @@ void effectTask(void* pvParameters) { // Copy previous frame (channelsD) to working buffer (channelsE) memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); - xSemaphoreTake(monitorMutex, portMAX_DELAY); // don't change channelsE while the monitor display data is sent layerP.loop(); if (millis() - last20ms >= 20) { last20ms = millis(); layerP.loop20ms(); } - xSemaphoreGive(monitorMutex); // Atomic swap channels xSemaphoreTake(swapMutex, portMAX_DELAY); + xSemaphoreTake(monitorMutex, portMAX_DELAY); uint8_t* temp = layerP.lights.channelsD; layerP.lights.channelsD = layerP.lights.channelsE; layerP.lights.channelsE = temp; newFrameReady = true; + xSemaphoreGive(monitorMutex); xSemaphoreGive(swapMutex); } @@ -180,12 +185,16 @@ void driverTask(void* pvParameters) { // layerP.setup() done in effectTask while (true) { + // Check and transition state under lock + xSemaphoreTake(swapMutex, portMAX_DELAY); if (layerP.lights.header.isPositions == 3) { EXT_LOGD(ML_TAG, "positions done (3 -> 0)"); - layerP.lights.header.isPositions = 0; // now driver can show again + layerP.lights.header.isPositions = 0; } + uint8_t isPositions = layerP.lights.header.isPositions; + xSemaphoreGive(swapMutex); - if (layerP.lights.header.isPositions == 0) { + if (isPositions == 0) { xSemaphoreTake(swapMutex, portMAX_DELAY); if (layerP.lights.useDoubleBuffer) { if (newFrameReady) { From 18d9401f0cff042ef63cc4a514ae705932ff847b Mon Sep 17 00:00:00 2001 From: ewowi Date: Sun, 28 Dec 2025 12:15:59 +0100 Subject: [PATCH 08/11] Update architecture --- docs/develop/architecture.md | 145 +++++++++---------------- src/MoonLight/Layers/PhysicalLayer.cpp | 4 +- src/main.cpp | 4 +- 3 files changed, 52 insertions(+), 101 deletions(-) diff --git a/docs/develop/architecture.md b/docs/develop/architecture.md index 9edf46f7..b4c98511 100644 --- a/docs/develop/architecture.md +++ b/docs/develop/architecture.md @@ -45,18 +45,16 @@ sequenceDiagram participant EffectTask participant DriverTask participant LEDs - participant FileSystem Note over EffectTask,DriverTask: Both tasks synchronized via mutex User->>WebUI: Adjust effect parameter WebUI->>SvelteKit: WebSocket message SvelteKit->>SvelteKit: Update in-memory state - SvelteKit->>SvelteKit: Queue deferred write Note over EffectTask: Core 0 (PRO_CPU) EffectTask->>EffectTask: Take mutex (10µs) - EffectTask->>EffectTask: memcpy front→back buffer + EffectTask->>EffectTask: memcpy channelsD → channelsE EffectTask->>EffectTask: Release mutex EffectTask->>EffectTask: Compute effects (5-15ms) EffectTask->>EffectTask: Take mutex (10µs) @@ -69,11 +67,6 @@ sequenceDiagram DriverTask->>DriverTask: Release mutex DriverTask->>DriverTask: Send via DMA (1-5ms) DriverTask->>LEDs: Pixel data - - User->>WebUI: Click "Save Config" - WebUI->>SvelteKit: POST /rest/saveConfig - SvelteKit->>FileSystem: Execute all deferred writes - FileSystem-->>SvelteKit: Write complete (10-50ms) ``` ## Core Assignments @@ -139,19 +132,19 @@ Buffer Architecture (PSRAM Only) ```mermaid graph LR subgraph MemoryBuffers["Memory Buffers"] - Front[Front Buffer
channels*] - Back[Back Buffer
channelsBack*] + Effects[Effects Buffer
channelsE*] + Drivers[Drivers Buffer
channelsD*] end - EffectTask[Effect Task
Core 0] -.->|1. memcpy| Back - EffectTask -.->|2. Compute effects| Back - EffectTask -.->|3. Swap pointers
MUTEX 10µs| Front + EffectTask[Effect Task
Core 0] -.->|1. memcpy| Drivers + EffectTask -.->|2. Compute effects| Drivers + EffectTask -.->|3. Swap pointers
MUTEX 10µs| Effects - DriverTask[Driver Task
Core 1] -->|4. Read pixels| Front + DriverTask[Driver Task
Core 1] -->|4. Read pixels| Effects DriverTask -->|5. Send via DMA| LEDs[LEDs] - style Front fill:#898f89 - style Back fill:#898c8f + style Effects fill:#898f89 + style Drivers fill:#898c8f ``` Synchronization Flow @@ -161,14 +154,50 @@ Synchronization Flow void effectTask(void* param) { while (true) { - // tbd ... - vTaskDelay(1); + uint8_t isPositions = layerP.lights.header.isPositions; + bool canProduce = !newFrameReady; + + if (isPositions == 0) { // driver task can change this + if (layerP.lights.useDoubleBuffer) { + + if (canProduce) { + // Copy previous frame (channelsD) to working buffer (channelsE) + memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); + + layerP.loop(); + + // Atomic swap channels + uint8_t* temp = layerP.lights.channelsD; + layerP.lights.channelsD = layerP.lights.channelsE; + layerP.lights.channelsE = temp; + newFrameReady = true; + } + + } else { + // Single buffer mode + layerP.loop(); + } + vTaskDelay(1); } } void driverTask(void* param) { while (true) { - // tbd ... + if (isPositions == 0) { + if (layerP.lights.useDoubleBuffer) { + if (newFrameReady) { + newFrameReady = false; + // Double buffer: release lock, then send + + esp32sveltekit.lps++; + layerP.loopDrivers(); // ✅ No lock needed + } + } else { + // Single buffer: keep lock while sending + esp32sveltekit.lps++; + layerP.loopDrivers(); // ✅ Protected by lock + } + } vTaskDelay(1); } } @@ -187,79 +216,6 @@ Performance Impact **Conclusion**: Double buffering overhead is negligible (<1% for typical setups). -## State Persistence & Deferred Writes - -Why Deferred Writes? - -Flash write operations (LittleFS) **block all CPU cores** for 10-50ms, causing: - -- ❌ Dropped frames (2-6 frames at 60fps) -- ❌ Visible LED stutter -- ❌ Poor user experience during settings changes - -Solution: Deferred Write Queue - -```mermaid -sequenceDiagram - participant User - participant UI - participant Module - participant WriteQueue - participant FileSystem - - User->>UI: Move slider - UI->>Module: Update state (in-memory) - Module->>WriteQueue: Queue write operation - Note over WriteQueue: Changes accumulate
in memory - - User->>UI: Move slider again - UI->>Module: Update state (in-memory) - Note over WriteQueue: Previous write replaced
No flash access yet - - User->>UI: Click "Save Config" - UI->>WriteQueue: Execute all queued writes - WriteQueue->>FileSystem: Write all changes (10-50ms) - Note over FileSystem: Single flash write
for all changes - FileSystem-->>UI: Complete -``` - -Implementation - -**When UI updates state:** -```cpp -// File: SharedFSPersistence.h -void writeToFS(const String& moduleName) { - if (delayedWriting) { - // Add to global queue (no flash write yet) - sharedDelayedWrites.push_back([this, module](char writeOrCancel) { - if (writeOrCancel == 'W') { - this->writeToFSNow(moduleName); // Actual flash write - } - }); - } -} -``` - -**When user clicks "Save Config":** -```cpp -// File: FileManager.cpp -_server->on("/rest/saveConfig", HTTP_POST, [](PsychicRequest* request) { - // Execute all queued writes in a single batch - FSPersistence::writeToFSDelayed('W'); - return ESP_OK; -}); -``` - -Benefits - -| Aspect | Without Deferred Writes | With Deferred Writes | -|--------|-------------------------|----------------------| -| **Flash writes per slider move** | 1 (10-50ms) | 0 | -| **LED stutter during UI use** | Constant | None | -| **Flash writes per session** | 100+ | 1 | -| **User experience** | Laggy, stuttering | Smooth | -| **Flash wear** | High | Minimal | - ## Performance Budget at 60fps Per-Frame Time Budget (16.66ms) @@ -315,15 +271,13 @@ Overhead Analysis | SvelteKit | 0.5-2ms (on Core 1) | 2-3ms (on Core 1) | 5ms | | Double buffer memcpy | 0.1ms (0.6%) | 0.1ms (0.6%) | 0.1ms | | Mutex locks | 0.02ms (0.1%) | 0.02ms (0.1%) | 0.02ms | -| Flash writes | **0ms** (deferred) | **0ms** (deferred) | 10-50ms (on save) | | **Total** | **1-3ms (6-18%)** | **4-8ms (24-48%)** | **Flash: user-triggered** | **Result**: - ✅ 60fps sustained during normal operation - ✅ 52-60fps during heavy WiFi/UI activity -- ✅ No stutter during UI interaction (deferred writes) -- ✅ Only brief stutter when user explicitly saves config (acceptable) +- ✅ No stutter during UI interaction ## Configuration @@ -389,6 +343,5 @@ This architecture achieves optimal performance through: 2. **Priority Hierarchy**: Driver > SvelteKit ensures LED timing is never compromised 3. **Minimal Locking**: 10µs mutex locks enable 99% parallel execution 4. **Double Buffering**: Eliminates tearing with <1% overhead -5. **Deferred Writes**: Eliminates UI stutter by batching flash operations **Result**: Smooth 60fps LED effects with responsive UI and stable networking. 🚀 \ No newline at end of file diff --git a/src/MoonLight/Layers/PhysicalLayer.cpp b/src/MoonLight/Layers/PhysicalLayer.cpp index 2bdcb2e5..46c69666 100644 --- a/src/MoonLight/Layers/PhysicalLayer.cpp +++ b/src/MoonLight/Layers/PhysicalLayer.cpp @@ -19,6 +19,8 @@ #include "MoonBase/Utilities.h" #include "VirtualLayer.h" +extern SemaphoreHandle_t swapMutex; + PhysicalLayer layerP; // global declaration of the physical layer PhysicalLayer::PhysicalLayer() { @@ -131,7 +133,6 @@ void PhysicalLayer::onLayoutPre() { if (pass == 1) { lights.header.nrOfLights = 0; // for pass1 and pass2 as in pass2 virtual layer needs it lights.header.size = {0, 0, 0}; - extern SemaphoreHandle_t swapMutex; xSemaphoreTake(swapMutex, portMAX_DELAY); EXT_LOGD(ML_TAG, "positions in progress (%d -> 1)", lights.header.isPositions); lights.header.isPositions = 1; // in progress... @@ -211,7 +212,6 @@ void PhysicalLayer::onLayoutPost() { lights.header.nrOfChannels = lights.header.nrOfLights * lights.header.channelsPerLight * ((lights.header.lightPreset == lightPreset_RGB2040) ? 2 : 1); // RGB2040 has empty channels EXT_LOGD(ML_TAG, "pass %d mp:%d #:%d / %d s:%d,%d,%d", pass, monitorPass, lights.header.nrOfLights, lights.header.nrOfChannels, lights.header.size.x, lights.header.size.y, lights.header.size.z); // send the positions to the UI _socket_emit - extern SemaphoreHandle_t swapMutex; xSemaphoreTake(swapMutex, portMAX_DELAY); EXT_LOGD(ML_TAG, "positions stored (%d -> %d)", lights.header.isPositions, lights.header.nrOfLights ? 2 : 3); lights.header.isPositions = lights.header.nrOfLights ? 2 : 3; // filled with positions, set back to 3 in ModuleEffects, or direct to 3 if no lights (effects will move it to 0) diff --git a/src/main.cpp b/src/main.cpp index 9968a3a2..322e0048 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -131,13 +131,11 @@ void effectTask(void* pvParameters) { // Check state under lock xSemaphoreTake(swapMutex, portMAX_DELAY); uint8_t isPositions = layerP.lights.header.isPositions; + bool canProduce = !newFrameReady; xSemaphoreGive(swapMutex); if (isPositions == 0) { // driver task can change this if (layerP.lights.useDoubleBuffer) { - xSemaphoreTake(swapMutex, portMAX_DELAY); - bool canProduce = !newFrameReady; - xSemaphoreGive(swapMutex); if (canProduce) { // Copy previous frame (channelsD) to working buffer (channelsE) From 7d86405ecf2a55c5b559b5da1e34755244bc3f16 Mon Sep 17 00:00:00 2001 From: ewowi Date: Sun, 28 Dec 2025 13:22:18 +0100 Subject: [PATCH 09/11] changes in architecture and main simplify double buffering --- docs/develop/architecture.md | 10 +++--- src/main.cpp | 68 ++++++++++++++---------------------- 2 files changed, 31 insertions(+), 47 deletions(-) diff --git a/docs/develop/architecture.md b/docs/develop/architecture.md index b4c98511..614409c3 100644 --- a/docs/develop/architecture.md +++ b/docs/develop/architecture.md @@ -78,7 +78,7 @@ graph TB subgraph Core0["Core 0 (PRO_CPU)"] WiFi[WiFi/BT
Priority 23] lwIP[lwIP TCP/IP
Priority 18] - Effect[Effect Task
Priority 3
Computation Only] + Effect[Effect Task
Priority 10
Computation Only] end subgraph Core1["Core 1 (APP_CPU)"] @@ -136,11 +136,11 @@ graph LR Drivers[Drivers Buffer
channelsD*] end - EffectTask[Effect Task
Core 0] -.->|1. memcpy| Drivers - EffectTask -.->|2. Compute effects| Drivers - EffectTask -.->|3. Swap pointers
MUTEX 10µs| Effects + EffectTask[Effect Task
Core 0] -.->|1. memcpy| Effects + EffectTask -.->|2. Compute effects| Effects + EffectTask -.->|3. Swap pointers
MUTEX 10µs| Drivers - DriverTask[Driver Task
Core 1] -->|4. Read pixels| Effects + DriverTask[Driver Task
Core 1] -->|4. Read pixels| Drivers DriverTask -->|5. Send via DMA| LEDs[LEDs] style Effects fill:#898f89 diff --git a/src/main.cpp b/src/main.cpp index 322e0048..9a2916ad 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -135,33 +135,11 @@ void effectTask(void* pvParameters) { xSemaphoreGive(swapMutex); if (isPositions == 0) { // driver task can change this - if (layerP.lights.useDoubleBuffer) { - - if (canProduce) { - // Copy previous frame (channelsD) to working buffer (channelsE) - memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); - - layerP.loop(); - - if (millis() - last20ms >= 20) { - last20ms = millis(); - layerP.loop20ms(); - } - - // Atomic swap channels - xSemaphoreTake(swapMutex, portMAX_DELAY); - xSemaphoreTake(monitorMutex, portMAX_DELAY); - uint8_t* temp = layerP.lights.channelsD; - layerP.lights.channelsD = layerP.lights.channelsE; - layerP.lights.channelsE = temp; - newFrameReady = true; - xSemaphoreGive(monitorMutex); - xSemaphoreGive(swapMutex); + if (canProduce) { + if (layerP.lights.useDoubleBuffer) { + memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); // Copy previous frame (channelsD) to working buffer (channelsE) } - } else { - // Single buffer mode - xSemaphoreTake(swapMutex, portMAX_DELAY); layerP.loop(); if (millis() - last20ms >= 20) { @@ -169,7 +147,18 @@ void effectTask(void* pvParameters) { layerP.loop20ms(); } + xSemaphoreTake(monitorMutex, portMAX_DELAY); + + xSemaphoreTake(swapMutex, portMAX_DELAY); + if (layerP.lights.useDoubleBuffer) { // Atomic swap channels + uint8_t* temp = layerP.lights.channelsD; + layerP.lights.channelsD = layerP.lights.channelsE; + layerP.lights.channelsE = temp; + } + newFrameReady = true; xSemaphoreGive(swapMutex); + + xSemaphoreGive(monitorMutex); } } @@ -189,29 +178,24 @@ void driverTask(void* pvParameters) { EXT_LOGD(ML_TAG, "positions done (3 -> 0)"); layerP.lights.header.isPositions = 0; } - uint8_t isPositions = layerP.lights.header.isPositions; - xSemaphoreGive(swapMutex); - if (isPositions == 0) { - xSemaphoreTake(swapMutex, portMAX_DELAY); - if (layerP.lights.useDoubleBuffer) { - if (newFrameReady) { - newFrameReady = false; - // Double buffer: release lock, then send - xSemaphoreGive(swapMutex); + bool mutexGiven = false; - esp32sveltekit.lps++; - layerP.loopDrivers(); // ✅ No lock needed - } else { - xSemaphoreGive(swapMutex); + if (layerP.lights.header.isPositions == 0) { + if (newFrameReady) { + newFrameReady = false; + if (layerP.lights.useDoubleBuffer) { + xSemaphoreGive(swapMutex); // Double buffer: release lock, then send + mutexGiven = true; } - } else { - // Single buffer: keep lock while sending + esp32sveltekit.lps++; - layerP.loopDrivers(); // ✅ Protected by lock - xSemaphoreGive(swapMutex); + layerP.loopDrivers(); // ✅ No lock needed } } + + if (!mutexGiven) xSemaphoreGive(swapMutex); + vTaskDelay(1); } } From 091e60c13037f55f51773b6c4394532338967812 Mon Sep 17 00:00:00 2001 From: ewowi Date: Mon, 29 Dec 2025 10:55:09 +0100 Subject: [PATCH 10/11] High speed tuning! Up to 64K LEDs !!! Docs - Update architecture Backend - Main: simplify effects and drivers task loop - Physical layer: move remapping from driver to effects, mutex isPositions only if useDoubleBuffer - lights control: no monitor mutex, increase monitor framerate for large fixtures (10K plus) --- docs/develop/architecture.md | 58 ++++++++++----------- platformio.ini | 4 +- src/MoonLight/Layers/PhysicalLayer.cpp | 40 +++++++------- src/MoonLight/Modules/ModuleLightsControl.h | 6 +-- src/main.cpp | 50 +++++++----------- 5 files changed, 71 insertions(+), 87 deletions(-) diff --git a/docs/develop/architecture.md b/docs/develop/architecture.md index 614409c3..00218e82 100644 --- a/docs/develop/architecture.md +++ b/docs/develop/architecture.md @@ -154,50 +154,48 @@ Synchronization Flow void effectTask(void* param) { while (true) { - uint8_t isPositions = layerP.lights.header.isPositions; - bool canProduce = !newFrameReady; + xSemaphoreTake(swapMutex, portMAX_DELAY); - if (isPositions == 0) { // driver task can change this + if (layerP.lights.header.isPositions == 0 && !newFrameReady) { // within mutex as driver task can change this if (layerP.lights.useDoubleBuffer) { + xSemaphoreGive(swapMutex); + memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); // Copy previous frame (channelsD) to working buffer (channelsE) + } - if (canProduce) { - // Copy previous frame (channelsD) to working buffer (channelsE) - memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); - - layerP.loop(); + layerP.loop(); - // Atomic swap channels - uint8_t* temp = layerP.lights.channelsD; - layerP.lights.channelsD = layerP.lights.channelsE; - layerP.lights.channelsE = temp; - newFrameReady = true; - } - - } else { - // Single buffer mode - layerP.loop(); + if (layerP.lights.useDoubleBuffer) { // Atomic swap channels + xSemaphoreTake(swapMutex, portMAX_DELAY); + uint8_t* temp = layerP.lights.channelsD; + layerP.lights.channelsD = layerP.lights.channelsE; + layerP.lights.channelsE = temp; } - vTaskDelay(1); + newFrameReady = true; + } + + xSemaphoreGive(swapMutex); + vTaskDelay(1); } } void driverTask(void* param) { while (true) { - if (isPositions == 0) { - if (layerP.lights.useDoubleBuffer) { - if (newFrameReady) { - newFrameReady = false; - // Double buffer: release lock, then send - - esp32sveltekit.lps++; - layerP.loopDrivers(); // ✅ No lock needed + xSemaphoreTake(swapMutex, portMAX_DELAY); + + if (layerP.lights.header.isPositions == 0) { + if (newFrameReady) { + newFrameReady = false; + if (layerP.lights.useDoubleBuffer) { + xSemaphoreGive(swapMutex); // Double buffer: release lock, then send + mutexGiven = true; } - } else { - // Single buffer: keep lock while sending + esp32sveltekit.lps++; - layerP.loopDrivers(); // ✅ Protected by lock + layerP.loopDrivers(); } } + + if (!mutexGiven) xSemaphoreGive(swapMutex); // not double buffer or if conditions not met vTaskDelay(1); } } diff --git a/platformio.ini b/platformio.ini index d85c2633..66e34acc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -55,8 +55,8 @@ build_flags = ${features.build_flags} -D BUILD_TARGET=\"$PIOENV\" -D APP_NAME=\"MoonLight\" ; 🌙 Must only contain characters from [a-zA-Z0-9-_] as this is converted into a filename - -D APP_VERSION=\"0.7.0\" ; semver compatible version string - -D APP_DATE=\"20251226\" ; 🌙 + -D APP_VERSION=\"0.7.1\" ; semver compatible version string + -D APP_DATE=\"20251229\" ; 🌙 -D PLATFORM_VERSION=\"pioarduino-55.03.35\" ; 🌙 make sure it matches with above plaftform diff --git a/src/MoonLight/Layers/PhysicalLayer.cpp b/src/MoonLight/Layers/PhysicalLayer.cpp index 46c69666..df9f3dc4 100644 --- a/src/MoonLight/Layers/PhysicalLayer.cpp +++ b/src/MoonLight/Layers/PhysicalLayer.cpp @@ -42,7 +42,7 @@ void PhysicalLayer::setup() { if (psramFound()) { lights.maxChannels = MIN(ESP.getPsramSize() / 4, 61440 * 3); // fill halve with channels, max 120 pins * 512 LEDs, still addressable with uint16_t - lights.useDoubleBuffer = true; // Enable double buffering + lights.useDoubleBuffer = false; // Enable double buffering } else { lights.maxChannels = 4096 * 3; // esp32-d0: max 1024->2048->4096 Leds ATM lights.useDoubleBuffer = false; // Single buffer mode @@ -73,21 +73,7 @@ void PhysicalLayer::setup() { } void PhysicalLayer::loop() { - // runs the loop of all effects / nodes in the layer - for (VirtualLayer* layer : layers) { - if (layer) layer->loop(); // if (layer) needed when deleting rows ... - } -} - -void PhysicalLayer::loop20ms() { - // runs the loop of all effects / nodes in the layer - for (VirtualLayer* layer : layers) { - if (layer) layer->loop20ms(); // if (layer) needed when deleting rows ... - } -} - -void PhysicalLayer::loopDrivers() { - // run mapping in the driver task + // run mapping in the effects task if (requestMapPhysical) { EXT_LOGD(ML_TAG, "mapLayout physical requested"); @@ -107,6 +93,20 @@ void PhysicalLayer::loopDrivers() { requestMapVirtual = false; } + // runs the loop of all effects / nodes in the layer + for (VirtualLayer* layer : layers) { + if (layer) layer->loop(); // if (layer) needed when deleting rows ... + } +} + +void PhysicalLayer::loop20ms() { + // runs the loop of all effects / nodes in the layer + for (VirtualLayer* layer : layers) { + if (layer) layer->loop20ms(); // if (layer) needed when deleting rows ... + } +} + +void PhysicalLayer::loopDrivers() { if (prevSize != lights.header.size) EXT_LOGD(ML_TAG, "onSizeChanged P %d,%d,%d -> %d,%d,%d", prevSize.x, prevSize.y, prevSize.z, lights.header.size.x, lights.header.size.y, lights.header.size.z); for (Node* node : nodes) { @@ -133,10 +133,10 @@ void PhysicalLayer::onLayoutPre() { if (pass == 1) { lights.header.nrOfLights = 0; // for pass1 and pass2 as in pass2 virtual layer needs it lights.header.size = {0, 0, 0}; - xSemaphoreTake(swapMutex, portMAX_DELAY); + if (layerP.lights.useDoubleBuffer) xSemaphoreTake(swapMutex, portMAX_DELAY); EXT_LOGD(ML_TAG, "positions in progress (%d -> 1)", lights.header.isPositions); lights.header.isPositions = 1; // in progress... - xSemaphoreGive(swapMutex); + if (layerP.lights.useDoubleBuffer) xSemaphoreGive(swapMutex); delay(100); // wait to stop effects @@ -212,10 +212,10 @@ void PhysicalLayer::onLayoutPost() { lights.header.nrOfChannels = lights.header.nrOfLights * lights.header.channelsPerLight * ((lights.header.lightPreset == lightPreset_RGB2040) ? 2 : 1); // RGB2040 has empty channels EXT_LOGD(ML_TAG, "pass %d mp:%d #:%d / %d s:%d,%d,%d", pass, monitorPass, lights.header.nrOfLights, lights.header.nrOfChannels, lights.header.size.x, lights.header.size.y, lights.header.size.z); // send the positions to the UI _socket_emit - xSemaphoreTake(swapMutex, portMAX_DELAY); + if (layerP.lights.useDoubleBuffer) xSemaphoreTake(swapMutex, portMAX_DELAY); EXT_LOGD(ML_TAG, "positions stored (%d -> %d)", lights.header.isPositions, lights.header.nrOfLights ? 2 : 3); lights.header.isPositions = lights.header.nrOfLights ? 2 : 3; // filled with positions, set back to 3 in ModuleEffects, or direct to 3 if no lights (effects will move it to 0) - xSemaphoreGive(swapMutex); + if (layerP.lights.useDoubleBuffer) xSemaphoreGive(swapMutex); // initLightsToBlend(); diff --git a/src/MoonLight/Modules/ModuleLightsControl.h b/src/MoonLight/Modules/ModuleLightsControl.h index def7fb90..86cc9fe1 100644 --- a/src/MoonLight/Modules/ModuleLightsControl.h +++ b/src/MoonLight/Modules/ModuleLightsControl.h @@ -352,7 +352,6 @@ class ModuleLightsControl : public Module { } #if FT_ENABLED(FT_MONITOR) - extern SemaphoreHandle_t monitorMutex; // defined in main extern SemaphoreHandle_t swapMutex; // Check and transition under lock @@ -374,15 +373,12 @@ class ModuleLightsControl : public Module { }); } else if (isPositions == 0 && layerP.lights.header.nrOfLights) { // send to UI static unsigned long monitorMillis = 0; - if (millis() - monitorMillis >= layerP.lights.header.nrOfLights / 12) { + if (millis() - monitorMillis >= MAX(20, layerP.lights.header.nrOfLights / 300)) { // 12K lights -> 40ms monitorMillis = millis(); read([&](ModuleState& _state) { if (_socket->getConnectedClients() && _state.data["monitorOn"]) { - // protect emit by monitorMutex, see main.cpp - xSemaphoreTake(monitorMutex, portMAX_DELAY); _socket->emitEvent("monitor", (char*)layerP.lights.channelsD, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels)); // use channelsD as it won't be overwritten by effects during loop - xSemaphoreGive(monitorMutex); } }); } diff --git a/src/main.cpp b/src/main.cpp index 9a2916ad..e57dfa8c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -115,7 +115,6 @@ ModuleChannels moduleChannels = ModuleChannels(&server, &esp32sveltekit); ModuleMoonLightInfo moduleMoonLightInfo = ModuleMoonLightInfo(&server, &esp32sveltekit); SemaphoreHandle_t swapMutex = xSemaphoreCreateMutex(); -SemaphoreHandle_t monitorMutex = xSemaphoreCreateMutex(); volatile bool newFrameReady = false; TaskHandle_t effectTaskHandle = NULL; @@ -130,38 +129,30 @@ void effectTask(void* pvParameters) { while (true) { // Check state under lock xSemaphoreTake(swapMutex, portMAX_DELAY); - uint8_t isPositions = layerP.lights.header.isPositions; - bool canProduce = !newFrameReady; - xSemaphoreGive(swapMutex); - - if (isPositions == 0) { // driver task can change this - if (canProduce) { - if (layerP.lights.useDoubleBuffer) { - memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); // Copy previous frame (channelsD) to working buffer (channelsE) - } - layerP.loop(); + if (layerP.lights.header.isPositions == 0 && !newFrameReady) { // within mutex as driver task can change this + if (layerP.lights.useDoubleBuffer) { + xSemaphoreGive(swapMutex); + memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); // Copy previous frame (channelsD) to working buffer (channelsE) + } - if (millis() - last20ms >= 20) { - last20ms = millis(); - layerP.loop20ms(); - } + layerP.loop(); - xSemaphoreTake(monitorMutex, portMAX_DELAY); + if (millis() - last20ms >= 20) { + last20ms = millis(); + layerP.loop20ms(); + } + if (layerP.lights.useDoubleBuffer) { // Atomic swap channels xSemaphoreTake(swapMutex, portMAX_DELAY); - if (layerP.lights.useDoubleBuffer) { // Atomic swap channels - uint8_t* temp = layerP.lights.channelsD; - layerP.lights.channelsD = layerP.lights.channelsE; - layerP.lights.channelsE = temp; - } - newFrameReady = true; - xSemaphoreGive(swapMutex); - - xSemaphoreGive(monitorMutex); + uint8_t* temp = layerP.lights.channelsD; + layerP.lights.channelsD = layerP.lights.channelsE; + layerP.lights.channelsE = temp; } + newFrameReady = true; } + xSemaphoreGive(swapMutex); vTaskDelay(1); // yield to other tasks, 1 tick (~1ms) } } @@ -173,14 +164,14 @@ void driverTask(void* pvParameters) { while (true) { // Check and transition state under lock + bool mutexGiven = false; xSemaphoreTake(swapMutex, portMAX_DELAY); + if (layerP.lights.header.isPositions == 3) { EXT_LOGD(ML_TAG, "positions done (3 -> 0)"); layerP.lights.header.isPositions = 0; } - bool mutexGiven = false; - if (layerP.lights.header.isPositions == 0) { if (newFrameReady) { newFrameReady = false; @@ -190,12 +181,11 @@ void driverTask(void* pvParameters) { } esp32sveltekit.lps++; - layerP.loopDrivers(); // ✅ No lock needed + layerP.loopDrivers(); } } - if (!mutexGiven) xSemaphoreGive(swapMutex); - + if (!mutexGiven) xSemaphoreGive(swapMutex); // not double buffer or if conditions not met vTaskDelay(1); } } From 8c22dff1c9000882215433bcfad9acac71141c58 Mon Sep 17 00:00:00 2001 From: ewowi Date: Mon, 29 Dec 2025 11:31:39 +0100 Subject: [PATCH 11/11] =?UTF-8?q?Use=20=E2=9A=99=EF=B8=8F=20instead=20of?= =?UTF-8?q?=20=F0=9F=94=8E=20for=20node=20details=20+=20doublebuffer=20bug?= =?UTF-8?q?fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/develop/architecture.md | 1 + .../components/moonbase/RowRenderer.svelte | 4 +- lib/framework/WWWData.h | 25446 ++++++++-------- src/MoonLight/Layers/PhysicalLayer.cpp | 2 +- src/main.cpp | 5 +- 5 files changed, 12725 insertions(+), 12733 deletions(-) diff --git a/docs/develop/architecture.md b/docs/develop/architecture.md index 00218e82..7b67b234 100644 --- a/docs/develop/architecture.md +++ b/docs/develop/architecture.md @@ -180,6 +180,7 @@ void effectTask(void* param) { void driverTask(void* param) { while (true) { + bool mutexGiven = false; xSemaphoreTake(swapMutex, portMAX_DELAY); if (layerP.lights.header.isPositions == 0) { diff --git a/interface/src/lib/components/moonbase/RowRenderer.svelte b/interface/src/lib/components/moonbase/RowRenderer.svelte index 0e00e912..b51a40bc 100644 --- a/interface/src/lib/components/moonbase/RowRenderer.svelte +++ b/interface/src/lib/components/moonbase/RowRenderer.svelte @@ -18,7 +18,7 @@ import { page } from '$app/state'; import { slide } from 'svelte/transition'; import { cubicOut } from 'svelte/easing'; - import SearchIcon from '~icons/tabler/search'; + import SettingsIcon from '~icons/tabler/settings'; import Delete from '~icons/tabler/trash'; import { initCap } from '$lib/stores/moonbase_utilities'; @@ -246,7 +246,7 @@ handleEdit(property.name, itemWrapper.item); }} > - {#if findItemInDefinition?.crud == null || findItemInDefinition?.crud?.includes('d')}