Skip to content
2 changes: 1 addition & 1 deletion firmware/esp32-s3.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build_flags =
${HP_ALL_DRIVERS.build_flags}
-D CONFIG_IDF_TARGET_ESP32S3=1
; -D ARDUINO_USB_MODE=1 ; which USB device classes are enabled on your ESP32 at boot. default 1 in board definition (serial only)
; -D ARDUINO_USB_CDC_ON_BOOT=1 ;Communications Device Class: controls whether the ESP32's USB serial port is enabled automatically at boot, default 1 in board definition
; -D ARDUINO_USB_CDC_ON_BOOT=1 ;Communications Device Class: controls whether the ESP32's USB serial port is enabled automatically at boot, not set in board definition!
; -D ARDUINO_USB_MSC_ON_BOOT=0 ;Mass Storage Class, disable
; -D ARDUINO_USB_DFU_ON_BOOT=0 ;download firmware update, disable
; -D ML_LIVE_MAPPING
Expand Down
10 changes: 6 additions & 4 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ 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.8.1\" ; semver compatible version string
-D APP_DATE=\"20260212\" ; 🌙
-D APP_DATE=\"20260213\" ; 🌙

-D PLATFORM_VERSION=\"pioarduino-55.03.37\" ; 🌙 make sure it matches with above plaftform

Expand Down Expand Up @@ -141,7 +141,9 @@ extra_scripts =
scripts/save_elf.py
lib_deps =
ArduinoJson@>=7.0.0
elims/PsychicMqttClient@^0.2.4
elims/PsychicMqttClient@^0.2.4
ElectronicCats/MPU6050 @ 1.3.0 ; for D_IMU.h driver
; https://github.com/hanyazou/BMI160-Arduino.git ; hanyazou/BMI160-Arduino#057f36e002bee0473a54fcedf41b93acad059568 ; @ ^1.0.0 ; for BMI160

;💫
[moonlight]
Expand All @@ -152,9 +154,9 @@ build_flags =
-D DRIVERS_STACK_SIZE=4096 ; psramFound() ? 4 * 1024 : 3 * 1024, 4096 is sufficient for now

; -D FASTLED_TESTING ; causes duplicate definition of initSpiHardware(); - workaround: removed implementation in spi_hw_manager_esp32.cpp.hpp
-D FASTLED_BUILD=\"20260212\"
-D FASTLED_BUILD=\"20260214\"
lib_deps =
https://github.com/FastLED/FastLED#ea5d2d7aadcd5697f912a1c32bb3b7e9891f949b ; master 20260212
https://github.com/FastLED/FastLED#878d87d39545228d266e07c6c8c91586b2037894 ; master 20260214
https://github.com/ewowi/WLED-sync#25f280b5e8e47e49a95282d0b78a5ce5301af4fe ; sourceIP + fftUdp.clear() if arduino >=3 (20251104)

; 💫 currently only enabled on s3 as esp32dev runs over 100%
Expand Down
11 changes: 10 additions & 1 deletion src/MoonBase/Modules/ModuleDevices.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,16 @@ class ModuleDevices : public Module {
for (JsonObject dev : devices) {
if (time(nullptr) - dev["lastSync"].as<time_t>() < 86400) devicesVector.push_back(dev); // max 1 day
}
std::sort(devicesVector.begin(), devicesVector.end(), [](JsonObject a, JsonObject b) { return a["name"] < b["name"]; });

std::sort(devicesVector.begin(), devicesVector.end(), [](JsonObject a, JsonObject b) {
// Primary sort: by name
// int nameCompare = strcmp(a["name"].as<const char*>(), b["name"].as<const char*>());
// if (nameCompare != 0) return nameCompare < 0;
if (a["name"] != b["name"]) return a["name"] < b["name"];

// Tie-breaker: by IP address (ensures stable sort)
return strcmp(a["ip"].as<const char*>(), b["ip"].as<const char*>()) < 0;
});

doc2["devices"].to<JsonArray>();
for (JsonObject device : devicesVector) {
Expand Down
94 changes: 93 additions & 1 deletion src/MoonBase/Modules/ModuleIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#if FT_MOONBASE == 1

#include <Wire.h> // for i2C

#include "MoonBase/Module.h"
#include "driver/uart.h"

Expand Down Expand Up @@ -216,6 +218,16 @@ class ModuleIO : public Module {
addControl(rows, "Level", "text", 0, 32, true); // ro
addControl(rows, "DriveCap", "text", 0, 32, true); // ro
}

control = addControl(controls, "i2cFreq", "number", 10, 1000, false, "kHz");
control["default"] = 100; // 100 kHz standard mode

control = addControl(controls, "i2cBus", "rows");
control["crud"] = "r";
rows = control["n"].to<JsonArray>();
{
addControl(rows, "address", "number", 0, 255, true); // ro
}
}

class PinAssigner {
Expand Down Expand Up @@ -565,6 +577,23 @@ class ModuleIO : public Module {
pinAssigner.assignPin(16, pin_LED);
#endif

#ifdef CONFIG_IDF_TARGET_ESP32
pinAssigner.assignPin(21, pin_I2C_SDA); // ESP32 classic
pinAssigner.assignPin(22, pin_I2C_SCL);
#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)
pinAssigner.assignPin(8, pin_I2C_SDA); // ESP32-C3
pinAssigner.assignPin(9, pin_I2C_SCL);
#elif defined(CONFIG_IDF_TARGET_ESP32C6)
pinAssigner.assignPin(23, pin_I2C_SDA); // ESP32-C6
pinAssigner.assignPin(22, pin_I2C_SCL);
#elif defined(CONFIG_IDF_TARGET_ESP32P4)
pinAssigner.assignPin(7, pin_I2C_SDA); // ESP32-P4 (common board default)
pinAssigner.assignPin(8, pin_I2C_SCL);
#else
pinAssigner.assignPin(21, pin_I2C_SDA); // Fallback
pinAssigner.assignPin(22, pin_I2C_SCL);
#endif
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// trying to add more pins, but these pins not liked by esp32-d0-16MB ... 🚧
// pinAssigner.assignPin(4, pin_LED_02;
// pinAssigner.assignPin(5, pin_LED_03;
Expand Down Expand Up @@ -609,6 +638,8 @@ class ModuleIO : public Module {
newState["modded"] = true;
} else if (updatedItem.name == "usage") {
newState["modded"] = true;
} else if (updatedItem.name == "i2cFreq") {
Wire.setClock(updatedItem.value.as<uint16_t>() * 1000);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

if (newState.size()) update(newState, ModuleState::update, _moduleName); // if changes made then update
Expand Down Expand Up @@ -701,6 +732,9 @@ class ModuleIO : public Module {
#endif // ethernet

#if FT_BATTERY
pinVoltage = UINT8_MAX;
pinCurrent = UINT8_MAX;
pinBattery = UINT8_MAX;
for (JsonObject pinObject : _state.data["pins"].as<JsonArray>()) {
uint8_t usage = pinObject["usage"];
if (usage == pin_Voltage) {
Expand Down Expand Up @@ -790,7 +824,39 @@ class ModuleIO : public Module {
}
#endif
}
}

pinI2CSDA = UINT8_MAX;
pinI2CSCL = UINT8_MAX;
for (JsonObject pinObject : _state.data["pins"].as<JsonArray>()) {
uint8_t usage = pinObject["usage"];
if (usage == pin_I2C_SDA) {
pinI2CSDA = pinObject["GPIO"];
EXT_LOGD(ML_TAG, "I2CSDA found %d", pinI2CSDA);
}
if (usage == pin_I2C_SCL) {
pinI2CSCL = pinObject["GPIO"];
EXT_LOGD(ML_TAG, "I2CSCL found %d", pinI2CSCL);
}
}

if (pinI2CSCL != UINT8_MAX && pinI2CSDA != UINT8_MAX) {
Wire.end(); // Clean up any previous I2C initialization
delay(100);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
uint16_t frequency = _state.data["i2cFreq"];
if (Wire.begin(pinI2CSDA, pinI2CSCL, frequency * 1000)) {
EXT_LOGI(ML_TAG, "initI2C Wire sda:%d scl:%d freq:%d kHz", pinI2CSDA, pinI2CSCL, frequency);
// delay(200); // Give I2C bus time to stabilize
// Wire.setClock(50000); // Explicitly set to 100kHz
_state.data["I2CReady"] = true;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
updateDevices();
} else
_state.data["I2CReady"] = false;
EXT_LOGE(ML_TAG, "initI2C Wire failed");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} // readPins

uint8_t pinI2CSDA = UINT8_MAX;
uint8_t pinI2CSCL = UINT8_MAX;

#if FT_BATTERY
uint8_t pinVoltage = UINT8_MAX;
Expand Down Expand Up @@ -867,6 +933,32 @@ class ModuleIO : public Module {
#endif
}

void updateDevices() {
JsonDocument doc;
doc["i2cBus"].to<JsonArray>();
JsonObject newState = doc.as<JsonObject>();

EXT_LOGI(ML_TAG, "Scanning I2C bus...");
byte count = 0;
for (byte i = 1; i < 127; i++) {
Wire.beginTransmission(i);
if (Wire.endTransmission() == 0) {
JsonObject i2cDevice = newState["i2cBus"].as<JsonArray>().add<JsonObject>();
i2cDevice["address"] = i;

EXT_LOGI(ML_TAG, "Found I2C device at address 0x%02X", i);
count++;
}
}
EXT_LOGI(ML_TAG, "Found %d device(s)", count);
JsonObject i2cDevice = newState["i2cBus"].as<JsonArray>().add<JsonObject>();
i2cDevice["address"] = 255;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Sentinel entry with address 255 pollutes the device list.

A fake device at address 255 is appended after the real scan results. This is not a valid I2C address (7-bit range is 0–127) and will appear as a spurious device in the UI. If this is meant to ensure the i2cBus array is non-empty or signal end-of-list, consider handling that in the UI layer instead.

Proposed fix: remove the sentinel
     EXT_LOGI(ML_TAG, "Found %d device(s)", count);
-    JsonObject i2cDevice = newState["i2cBus"].as<JsonArray>().add<JsonObject>();
-    i2cDevice["address"] = 255;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JsonObject i2cDevice = newState["i2cBus"].as<JsonArray>().add<JsonObject>();
i2cDevice["address"] = 255;
// Lines 928-929 removed - sentinel entry deleted
🤖 Prompt for AI Agents
In `@src/MoonBase/Modules/ModuleIO.h` around lines 928 - 929, Remove the sentinel
fake I2C device being appended with address 255: stop creating the JsonObject
via newState["i2cBus"].as<JsonArray>().add<JsonObject>() and assigning
i2cDevice["address"] = 255 so the i2cBus array only contains real scan results;
if a non-empty-array indicator is needed, handle that in the UI layer instead of
adding a sentinel entry (look for usages of i2cDevice/newState["i2cBus"] in
ModuleIO.h and related scan/serialize functions to ensure no other code depends
on the sentinel).


doc["i2cFreq"] = Wire.getClock() / 1000;

update(newState, ModuleState::update, _moduleName);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

private:
ESP32SvelteKit* _sveltekit;
uint8_t current_board_id = UINT8_MAX;
Expand Down
3 changes: 3 additions & 0 deletions src/MoonBase/NodeManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ class NodeManager : public Module {
if (nodeClass != nullptr) {
nodeClass->on = updatedItem.value.as<bool>(); // set nodeclass on/off
// EXT_LOGD(ML_TAG, " nodeclass 🔘:%d 🚥:%d 💎:%d", nodeClass->on, nodeClass->hasOnLayout(), nodeClass->hasModifier());
xSemaphoreTake(*nodeClass->layerMutex, portMAX_DELAY);
nodeClass->onUpdate(updatedItem.oldValue, nodeState); // custom onUpdate for the node
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent mutex handling and argument semantics for onUpdate.

Two observations:

  1. No mutex: The controls-value path (line 282–285) holds layerMutex around onUpdate, but this new on-toggle path does not. For MPU6050Driver the hardware init may not need it, but other nodes' onUpdate could mutate shared layer state. Consider wrapping consistently:
+            xSemaphoreTake(*nodeClass->layerMutex, portMAX_DELAY);
             nodeClass->onUpdate(updatedItem.oldValue, nodeState);  // custom onUpdate for the node
+            xSemaphoreGive(*nodeClass->layerMutex);
  1. Argument type mismatch: Here nodeState (full node JSON with name, on, controls) is passed as the control parameter, while at line 284 a single control object is passed. The MPU6050Driver relies on checking control["on"] to distinguish, but this convention is implicit and fragile — a future node's onUpdate could misinterpret the JSON shape.
🤖 Prompt for AI Agents
In `@src/MoonBase/NodeManager.h` at line 267, The call to nodeClass->onUpdate
currently passes the full nodeState without holding layerMutex and thus is
inconsistent with the controls-value path; wrap the on-toggle path's
nodeClass->onUpdate call with layerMutex (same scope used in the controls-value
path) and normalize the argument semantics by passing a consistent control
object (e.g., construct a single-control JSON containing only the on field or an
explicit {"on": ...} object) instead of the full nodeState so onUpdate
implementations always receive the same JSON shape.

xSemaphoreGive(*nodeClass->layerMutex);
nodeClass->requestMappings();
} else
EXT_LOGW(ML_TAG, "Nodeclass %s not found", nodeState["name"].as<const char*>());
Expand Down
3 changes: 3 additions & 0 deletions src/MoonBase/Nodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ static struct SharedData {
size_t connectedClients;
size_t activeClients;
size_t clientListSize;

Coord3D gravity;
Comment on lines +347 to +348
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Coord3D gravity may not be the right type for gravity data.

As noted in the D_IMU.h review, the MPU6050 gravity vector contains float values in the range [–1.0, 1.0]. If Coord3D uses integer fields (e.g., int16_t), this will silently truncate to 0/±1. Consider using a float-based struct (e.g., VectorFloat) or documenting the expected scale/encoding if you intend to pre-scale the values.

🤖 Prompt for AI Agents
In `@src/MoonBase/Nodes.h` around lines 347 - 348, The field Coord3D gravity
likely uses integer components and will truncate the MPU6050 float gravity
vector; update the type or handling: replace Coord3D gravity with a float-based
struct (e.g., VectorFloat or a new Coord3DFloat) or store gravity as an array of
floats and ensure any code using gravity performs the proper float
reads/conversions (see related D_IMU.h handling), or if you must keep Coord3D,
add clear documentation and explicit scaling/quantization conversions where
gravity is assigned so values in [−1.0,1.0] are preserved correctly.

} sharedData;

/**
Expand All @@ -360,6 +362,7 @@ static struct SharedData {
#include "MoonLight/Nodes/Drivers/D_FastLED.h"
#include "MoonLight/Nodes/Drivers/D_Hub75.h"
#include "MoonLight/Nodes/Drivers/D_Infrared.h"
#include "MoonLight/Nodes/Drivers/D_IMU.h"
#include "MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h"
#include "MoonLight/Nodes/Drivers/D__Sandbox.h"
#include "MoonLight/Nodes/Effects/E_FastLED.h"
Expand Down
23 changes: 16 additions & 7 deletions src/MoonBase/SharedFSPersistence.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ class SharedFSPersistence {
info.delayedWriting = delayedWriting;
info.hasDelayedWrite = false;

// Register update handler
info.updateHandlerId = module->addUpdateHandler([this, module](const String& originId) { writeToFS(module->_moduleName); }, false);

_modules[module->_moduleName] = info;
}

Expand All @@ -59,7 +56,16 @@ class SharedFSPersistence {
for (auto& pair : _modules) {
readFromFS(pair.first);
}
// All setup happens in registerModule

// Register update handlers for modules that requested delayed writing
for (auto& pair : _modules) {
if (pair.second.delayedWriting) {
enableUpdateHandler(pair.first);
EXT_LOGD(ML_TAG, "Enabled update handler for %s after file read", pair.first);
}
}

EXT_LOGI(ML_TAG, "SharedFSPersistence initialization complete");
}

// ADDED: Enable/disable update handler for specific module
Expand Down Expand Up @@ -157,17 +163,20 @@ class SharedFSPersistence {

serializeJson(doc, file);
file.close();

return true;
}

// ADDED: Static method to process all delayed writes
static void writeToFSDelayed(char writeOrCancel) {
ESP_LOGD(SVK_TAG, "calling %d writeFuncs from delayedWrites", sharedDelayedWrites.size());
ESP_LOGD(SVK_TAG, "calling %u writeFuncs from delayedWrites", sharedDelayedWrites.size());

for (auto& writeFunc : sharedDelayedWrites) {
// writeFunc("C") calls readFromFS and module->update, which will call SharedFSPersistence.h onUpdate which will send any state change to writeToFS which add to sharedDelayedWrites
auto pending = std::move(sharedDelayedWrites);
sharedDelayedWrites.clear(); // leave in valid-but-empty state
for (auto& writeFunc : pending) {
writeFunc(writeOrCancel);
}
sharedDelayedWrites.clear();
}

private:
Expand Down
2 changes: 1 addition & 1 deletion src/MoonBase/SharedWebSocketServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class SharedWebSocketServer {
// Handle incoming frame data
if (frame->type == HTTPD_WS_TYPE_TEXT) {
Module* module = findModule(request->url());
EXT_LOGD(ML_TAG, "search module %s: %p", request->path().c_str(), module);
EXT_LOGD(ML_TAG, "search module %s: %p", request->url().c_str(), module);
if (module) {
JsonDocument doc;
DeserializationError error = deserializeJson(doc, (char*)frame->payload, frame->len);
Expand Down
2 changes: 1 addition & 1 deletion src/MoonLight/Layers/PhysicalLayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ void PhysicalLayer::setup() {
lights.maxChannels = MIN(ESP.getPsramSize() / 4, 128 * 64 * 16 * 3); // fill max 2 * 25% of PSRAM with channels, supporting Virtual driver which is 120 pins * 512..1024 LEDs, max 16 Hub75 128x64 panels
lights.useDoubleBuffer = true; // Enable double buffering
} else {
lights.maxChannels = 4096 * 3; // esp32-d0: max 1024->2048->4096 Leds ATM
lights.maxChannels = 2048 * 3; // esp32-d0: max 1024->2048->4096->2048 Leds ATM (4096 is too bleeding edge still - hope is replacing PhysicHTTP by Async webserver solves this)
lights.useDoubleBuffer = false; // Single buffer mode
}

Expand Down
2 changes: 2 additions & 0 deletions src/MoonLight/Modules/ModuleDrivers.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class ModuleDrivers : public NodeManager {
addControlValue(control, getNameAndTags<ArtNetOutDriver>());
addControlValue(control, getNameAndTags<AudioSyncDriver>());
addControlValue(control, getNameAndTags<IRDriver>());
addControlValue(control, getNameAndTags<IMUDriver>());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
addControlValue(control, getNameAndTags<HUB75Driver>());

// board preset specific
Expand Down Expand Up @@ -143,6 +144,7 @@ class ModuleDrivers : public NodeManager {
if (!node) node = checkAndAlloc<ArtNetOutDriver>(name);
if (!node) node = checkAndAlloc<AudioSyncDriver>(name);
if (!node) node = checkAndAlloc<IRDriver>(name);
if (!node) node = checkAndAlloc<IMUDriver>(name);
if (!node) node = checkAndAlloc<HUB75Driver>(name);

// board preset specific
Expand Down
7 changes: 6 additions & 1 deletion src/MoonLight/Nodes/Drivers/D_FastLED.h
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,12 @@ class FastLEDDriver : public DriverNode {
// FastLED.setMaxPowerInMilliWatts(1000 * layerP.maxPower); // 5v, 2000mA, to protect usb while developing
}

~FastLEDDriver() override { FastLED.reset(ResetFlags::CHANNELS); }
~FastLEDDriver() override {
auto& events = FastLED.channelEvents();
events.onChannelCreated.clear();
events.onChannelEnqueued.clear();
FastLED.reset(ResetFlags::CHANNELS); // global: ensure only one FastLEDDriver instance exists. If multiple driver nodes are possible, this destructor will tear down channels for all of them. If singleton is guaranteed by design, consider documenting that assumption at the class level.
}
Comment on lines +347 to +353
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, examine the FastLEDDriver class definition to check for singleton enforcement
# (deleted copy constructor, private constructor, etc.)
grep -n "class FastLEDDriver" src/MoonLight/Nodes/Drivers/D_FastLED.h

Repository: MoonModules/MoonLight

Length of output: 109


🏁 Script executed:

#!/bin/bash
# Check if there are explicit singleton guards or design patterns in the class
head -100 src/MoonLight/Nodes/Drivers/D_FastLED.h | tail -50

Repository: MoonModules/MoonLight

Length of output: 1977


🏁 Script executed:

#!/bin/bash
# Look at the setup() method where listeners are added to understand the pattern
sed -n '60,100p' src/MoonLight/Nodes/Drivers/D_FastLED.h

Repository: MoonModules/MoonLight

Length of output: 1707


🏁 Script executed:

#!/bin/bash
# Search for how many FastLEDDriver instances are created in the codebase
rg "FastLEDDriver" --type=cpp -B2 -A2 | head -50

Repository: MoonModules/MoonLight

Length of output: 2211


🏁 Script executed:

#!/bin/bash
# Check the full destructor context and any comments about singleton design
sed -n '340,360p' src/MoonLight/Nodes/Drivers/D_FastLED.h

Repository: MoonModules/MoonLight

Length of output: 680


.clear() removes all listeners and offers no protection against multiple driver instances.

Lines 349–350 clear every subscriber on onChannelCreated and onChannelEnqueued. While the comment on line 351 acknowledges the singleton assumption for FastLED.reset(), the same caveat applies to the event clearing above it. However, the class provides no enforcement of singleton semantics—there are no deleted copy/move constructors, and the allocation pattern in ModuleDrivers.h uses checkAndAlloc<FastLEDDriver>(name), which permits multiple instances.

If a second FastLEDDriver is instantiated and then destroyed, its destructor will wipe all event listeners (including those from the first instance), breaking the first driver's operation.

Store the listener handles returned by .add() and remove only those in the destructor, or enforce singleton semantics explicitly (deleted copy/move constructors, or static instance guard).

🤖 Prompt for AI Agents
In `@src/MoonLight/Nodes/Drivers/D_FastLED.h` around lines 347 - 352, The
destructor ~FastLEDDriver() currently calls events.onChannelCreated.clear() and
events.onChannelEnqueued.clear() and invokes FastLED.reset(...), which removes
all listeners and assumes a singleton; to fix, either (A) track the listener
handles returned when adding listeners to FastLED.channelEvents() (store them as
members, e.g. onChannelCreatedHandle/onChannelEnqueuedHandle) and in
~FastLEDDriver() call the corresponding remove/unregister using those handles
instead of clear(), or (B) enforce singleton semantics on FastLEDDriver by
deleting copy/move constructors and adding a static instance guard (and document
it) so checkAndAlloc<FastLEDDriver>(name) cannot create multiple instances;
choose one approach and update the destructor and class declaration accordingly,
leaving FastLED.reset(ResetFlags::CHANNELS) only if singleton is guaranteed.

};

#endif
Loading
Loading