Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,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=\"20260224\" ; 🌙
-D APP_DATE=\"20260301\" ; 🌙

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

Expand Down Expand Up @@ -204,9 +204,20 @@ build_flags =
-D DRIVERS_STACK_SIZE=6144 ; psramFound() ? 4 * 1024 : 3 * 1024, 4096 is sufficient for now. Update: due to FastLED audio I had to increase to 6144 (might want to move audio to a separate task)

; -D FASTLED_TESTING ; causes duplicate definition of initSpiHardware(); - workaround: removed implementation in spi_hw_manager_esp32.cpp.hpp
-D FASTLED_BUILD=\"20260224\"
-D FASTLED_BUILD=\"20260226\"
lib_deps =
https://github.com/FastLED/FastLED#56d49b14796fa14e4fcc3f394256ae0bbe509b65 ; master 20260224
https://github.com/FastLED/FastLED#809744a9738f7e390f345d597ea59700648de906 ;
; https://github.com/FastLED/FastLED#7e133f8f39480cac7ca5876fc78c980f874b527d ; 20260225 10:03 AM bass/mid/high looks nervous / all or nothing, doens't align with audio sweep (https://www.youtube.com/watch?v=PAsMlDptjx8)
; https://github.com/FastLED/FastLED#5d7ce6f1fcd1cf3cccdc641636c66dd46ade44fc ; 20260224 9:16 PM FastLED driver websocket issue
; https://github.com/FastLED/FastLED#8adee9130ea1849da6f9d01578171016e69f18c1 ; 20260224 8:35 PM FastLED driver websocket issue
; https://github.com/FastLED/FastLED#56d49b14796fa14e4fcc3f394256ae0bbe509b65 ; master 20260224 8:49 AM (fix fl_* functions) ; FastLED driver websocket issue
; https://github.com/FastLED/FastLED#6017ce6b94552fd57f5c0be60592902d8111b470 ; 20260224 5:15 fix c6... not working ❌
; https://github.com/FastLED/FastLED#629a1e468fd823a6834c1bd6430fedea522089ed ; 20260224 4:17 fix spam ... compile error
; https://github.com/FastLED/FastLED#8c0015af87908cb641907df69061321f6a306f0e ; 20260224 4:15 Channel engine ... compile error
; https://github.com/FastLED/FastLED#4cef007214bdf70039576ccf60c9384981c06a0d ; 20260224 4:13 isqrt32 ... working ✅
; https://github.com/FastLED/FastLED#595150340cb0034b50328bee8eaaaceaeb570dc7 ; 20260224 2:48 AM add filtering to audio detectors working
; https://github.com/FastLED/FastLED#d03ffd69c68f1a00f883243f78b2a0e9bfb66298 ; master 2026023 6:15AM (fix no preamble for the UCS led chipset) websocket working!

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
64 changes: 42 additions & 22 deletions src/MoonBase/Modules/ModuleIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -675,36 +675,47 @@ class ModuleIO : public Module {
// on update triggers another onUpdates on 2 occasions: 1) newState modded (directly) and 2) setBoardPresetDefaults (via main loop)
// each will trigger the updateHandler of this module sending readpins again ...
void onUpdate(const UpdatedItem& updatedItem, const String& originId) override {
// Below updates only triggered from UI (not from backend updates)
if (!originId.toInt()) return;

JsonDocument doc;
JsonObject newState = doc.to<JsonObject>();

// Handle boardPreset changes
if (updatedItem.name == "boardPreset") {
// if booting and modded is false or ! booting
if ((updatedItem.oldValue == "" && _state.data["modded"] == false) || updatedItem.oldValue != "") { // only update unmodded
// Only load board defaults if modded is false
if (_state.data["modded"] == false) {
EXT_LOGD(MB_TAG, "newBoardID %s %s[%d]%s[%d].%s = %s -> %s", originId.c_str(), 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<String>().c_str());
newBoardID = updatedItem.value; // run in sveltekit task
newBoardID = updatedItem.value; // Will be processed in loop20ms
} else {
EXT_LOGD(MB_TAG, "boardPreset change ignored - modded=true");
}
return; // Don't process further for boardPreset changes
}

if (!originId.toInt()) return; // below updates only triggered from UI

if (updatedItem.name == "modded") {
// set pins to default if modded is turned off
// When modded is set to false, reload the current board preset defaults
if (updatedItem.value == false) {
// EXT_LOGD(MB_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<String>().c_str());
newBoardID = _state.data["boardPreset"]; // run in sveltekit task
EXT_LOGD(MB_TAG, "modded set to false - reloading board defaults");
newBoardID = _state.data["boardPreset"]; // Reload current board
}
} else if (updatedItem.name == "switch1" || updatedItem.name == "switch2") {
// rebuild with new switch setting
newBoardID = _state.data["boardPreset"]; // run in sveltekit task
// Rebuild pins with new switch settings
EXT_LOGD(MB_TAG, "switch changed - rebuilding board defaults");
newBoardID = _state.data["boardPreset"];
} else if (updatedItem.name == "maxPower") {
// Manual maxPower change = user is customizing
newState["modded"] = true;
} else if (updatedItem.name == "usage" || updatedItem.name == "index") { // usage / index of any of the pins
} else if (updatedItem.name == "usage" || updatedItem.name == "index") {
// Manual pin usage change = user is customizing
newState["modded"] = true;
} else if (updatedItem.name == "i2cFreq") {
Wire.setClock(updatedItem.value.as<uint32_t>() * 1000); // uint32_t instead of uint16_t to multiply in 32-bit arithmetic
Wire.setClock(updatedItem.value.as<uint32_t>() * 1000);
}

if (newState.size()) update(newState, ModuleState::update, _moduleName); // if changes made then update
if (newState.size()) {
update(newState, ModuleState::update, _moduleName);
}
}

// Function to convert drive capability to string
Expand All @@ -723,26 +734,35 @@ class ModuleIO : public Module {
}
}

bool _initialPinReadDone = false;
bool _initialUpdateDone = false;

void loop20ms() override {
// run in sveltekit task
Module::loop20ms();

// update board presets
// Update board presets
if (newBoardID != UINT8_MAX) {
setBoardPresetDefaults(newBoardID); // run from sveltekit task
setBoardPresetDefaults(newBoardID);
newBoardID = UINT8_MAX;
_initialPinReadDone = true;
_initialUpdateDone = true;
}

// during boot, the IO module is unchanged, not triggering updates, so need to do it manually
if (!_initialPinReadDone) {
callUpdateHandlers(_moduleName); // calls readPins for all subscribed handlers
_initialPinReadDone = true;
// During boot, handle boardPreset from file if modded=false
// This runs AFTER file load completes and all values (including modded) are restored
if (!_initialUpdateDone) {
if (_state.data["modded"] == false) {
uint8_t boardPreset = _state.data["boardPreset"];
EXT_LOGD(MB_TAG, "Applying board preset %d defaults from file (modded=false)", boardPreset);
newBoardID = boardPreset;
// Will be processed in next loop20ms iteration at top, setting _initialUpdateDone
} else {
EXT_LOGD(MB_TAG, "Skipping board preset defaults - using custom pins from file (modded=true)");
callUpdateHandlers(_moduleName);
_initialUpdateDone = true;
}
}

// update I2C devices
// Update I2C devices
if (_triggerUpdateI2C != UINT8_MAX) {
_updateI2CDevices();
_triggerUpdateI2C = UINT8_MAX;
Expand Down
11 changes: 7 additions & 4 deletions src/MoonBase/NodeManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,8 @@ class NodeManager : public Module {
// 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<String>().c_str());
if (updatedItem.index[0] < nodes->size()) {
EXT_LOGD(ML_TAG, "%s on: %s (#%d)", nodeState["name"].as<const char*>(), updatedItem.value.as<String>().c_str(), nodes->size());
const char* name = nodeState["name"];
EXT_LOGD(ML_TAG, "%s on: %s (#%d)", name ? name : "", updatedItem.value.as<String>().c_str(), nodes->size());
Node* nodeClass = (*nodes)[updatedItem.index[0]];
if (nodeClass != nullptr) {
nodeClass->on = updatedItem.value.as<bool>(); // set nodeclass on/off
Expand All @@ -269,7 +270,7 @@ class NodeManager : public Module {
xSemaphoreGive(*nodeClass->layerMutex);
nodeClass->requestMappings();
} else
EXT_LOGW(ML_TAG, "Nodeclass %s not found", nodeState["name"].as<const char*>());
EXT_LOGW(ML_TAG, "Nodeclass %s not found", name ? name : "");
}
} // nodes[i].on

Expand All @@ -287,8 +288,10 @@ class NodeManager : public Module {
xSemaphoreGive(*nodeClass->layerMutex);

nodeClass->requestMappings();
} else
EXT_LOGW(ML_TAG, "nodeClass not found %s", nodeState["name"].as<const char*>());
} else {
const char* name = nodeState["name"];
EXT_LOGW(ML_TAG, "nodeClass not found %s", name ? name : "");
}
}

} // nodes[i].controls[j].value
Expand Down
2 changes: 1 addition & 1 deletion src/MoonLight/Modules/ModuleLightsControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ class ModuleLightsControl : public Module {
};
layerP.lights.header.brightness = newBri;
} else if (updatedItem.name == "palette") {
const size_t nrOfPaletteEntries = sizeof(layerP.palette.entries) / sizeof(CRGB);
// const size_t nrOfPaletteEntries = sizeof(layerP.palette.entries) / sizeof(CRGB);
layerP.palette = getGradientPalette(updatedItem.value);
} else if (updatedItem.name == "preset") {
// copy /.config/effects.json to the hidden folder /.config/presets/preset[x].json
Expand Down
46 changes: 23 additions & 23 deletions src/MoonLight/Modules/palettes.h
Original file line number Diff line number Diff line change
Expand Up @@ -535,29 +535,29 @@ const uint8_t* const gGradientPalettes[] = {
retro2_16_gp, // Yellowout
};

const char* const palette_names[] = {"Cloud⚡️", "Forest⚡️", "Heat⚡️", "Lava⚡️",
"Ocean⚡️", "Party⚡️", "Rainbow⚡️", "Rainbow Bands⚡️",
"MoonLight💫", "Random💫", "Red", "Green",
"Blue", "Orange", "Purple", "Cyan",
"Warm White", "Cold White", "Audio Hue🌙", "Audio Ramp🌙",
"Audio Ratio🌙", "Analogous", "April Night", "Aqua Flash",
"Atlantica", "Aurora", "Aurora 2", "Autumn",
"Beach", //
"Beech", "Blink Red", "Breeze", "C9",
"C9 2", "C9 New", "Candy", "Candy2",
"Cyane", "Departure",
"Drywet", //
"Fairy Reaf", "Fire", "Grintage", "Hult",
"Hult 64", "Icefire", "Jul", "Landscape",
"Light Pink", //
"Lite Light", "Magenta", "Magred", "Orange & Teal",
"Orangery", "Pastel", "Pink Candy", "Red & Blue",
"Red Flash", "Red Reaf", "Red Shift", "Red Tide",
"Retro Clown", "Rewhi", "Rivendell", "Sakura",
"Semi Blue", "Sherbet", "Splash", "Sunset",
"Sunset 2", "Temperature", "Tertiary", "Tiamat",
"Toxy Reaf", "Vintage", "Yelblu Hot", "Yelblu",
"Yelmag", "Yellowout"};
const char* const palette_names[] = {"Cloud⚡️💙", "Forest⚡️💚", "Heat⚡️💛", "Lava⚡️❤️",
"Ocean⚡️🩵", "Party⚡️💛", "Rainbow⚡️🩵", "Rainbow Bands⚡️🩵",
"MoonLight💫💜", "Random💫", "Red❤️", "Green💚",
"Blue💙", "Orange🧡", "Purple💜", "Cyan🩵",
"Warm White🤍", "Cold White🤍", "Audio Hue🌙", "Audio Ramp🌙",
"Audio Ratio🌙", "Analogous💙", "April Night💙-", "Aqua Flash🤍",
"Atlantica💚", "Aurora💚", "Aurora 2💙", "Autumn💛",
"Beach💚", //
"Beech💙", "Blink Red🩷", "Breeze💙", "C9🧡",
"C9 2❤️", "C9 New🧡", "Candy💜", "Candy2💛",
"Cyane🩷", "Departure💚-",
"Drywet💙", //
"Fairy Reaf💜", "Fire❤️-", "Grintage🩷", "Hult💜",
"Hult 64💚", "Icefire💙", "Jul💚", "Landscape💚-",
"Light Pink🤍-", //
"Lite Light💜-", "Magenta💙", "Magred💜-", "Orange & Teal🤍",
"Orangery💛", "Pastel🤍", "Pink Candy🩷", "Red & Blue💙",
"Red Flash🤍", "Red Reaf🤍", "Red Shift💛", "Red Tide🧡",
"Retro Clown🩷", "Rewhi🩷", "Rivendell🧟‍♂️", "Sakura🩷",
"Semi Blue💙", "Sherbet❤️", "Splash🩷", "Sunset💜",
"Sunset 2🤍", "Temperature💛", "Tertiary💚", "Tiamat🩵",
"Toxy Reaf🩵", "Vintage🧡-", "Yelblu Hot🧡-", "Yelblu🩵",
"Yelmag🩷", "Yellowout💛"};

static_assert(sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]) == sizeof(palette_names) / sizeof(palette_names[0]), "gGradientPalettes and palette_names must have the same number of entries");

Expand Down
20 changes: 14 additions & 6 deletions src/MoonLight/Nodes/Drivers/D_ArtnetOut.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class ArtNetOutDriver : public DriverNode {
}
}

totalChannels = layerP.lights.header.nrOfLights * layerP.lights.header.channelsPerLight / (nrOfIPAddresses?nrOfIPAddresses:1);
totalChannels = layerP.lights.header.nrOfLights * layerP.lights.header.channelsPerLight / (nrOfIPAddresses ? nrOfIPAddresses : 1);
usedChannelsPerUniverse = universeSize / layerP.lights.header.channelsPerLight * layerP.lights.header.channelsPerLight; // calculated
totalUniverses = (totalChannels + usedChannelsPerUniverse - 1) / usedChannelsPerUniverse; // ceiling //calculated

Expand Down Expand Up @@ -137,10 +137,20 @@ class ArtNetOutDriver : public DriverNode {
}

uint8_t processedOutputs = 0;
TickType_t xLastWakeTime = xTaskGetTickCount();
unsigned long lastSendTime = 0;

void loop() override {
DriverNode::loop(); // this populates the LUT tables
DriverNode::loop(); // this populates the LUT tables when needed

// Non-blocking FPS limiter - check if enough time has elapsed
unsigned long currentTime = millis();
unsigned long frameInterval = 1000 / FPSLimiter; // e.g., 20ms for 50 FPS

if (currentTime - lastSendTime < frameInterval) {
return; // Not enough time elapsed, skip this frame without blocking
}

lastSendTime = currentTime; // Update last send time

LightsHeader* header = &layerP.lights.header;

Expand Down Expand Up @@ -196,7 +206,7 @@ class ArtNetOutDriver : public DriverNode {

if (!writePackage()) return; // resets packagesize

// addYield(10);
addYield(10); // Yield every 10 packets to allow AsyncUDP to actually transmit

if (channels_remaining < header->channelsPerLight) { // jump to next output
channels_remaining = channelsPerOutput; // reset for a new output
Expand All @@ -220,8 +230,6 @@ class ArtNetOutDriver : public DriverNode {
writePackage(); // remaining
}
// EXT_LOGD(ML_TAG, "Universes send %d %d", universe, packages);

vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(1000.0 / FPSLimiter)); // Yields AND feeds watchdog
} // loop
};

Expand Down
7 changes: 6 additions & 1 deletion src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ class FastLEDAudioDriver : public Node {
bool autoGain = false;
bool noiseFloorTracking = false;
uint8_t channel = fl::Left;
uint8_t gain = 128;

void setup() override {
addControl(signalConditioning, "signalConditioning", "checkbox");
addControl(gain, "gain", "slider");
addControl(autoGain, "autoGain", "checkbox");
addControl(noiseFloorTracking, "noiseFloorTracking", "checkbox");
addControl(channel, "channel", "select");
Expand Down Expand Up @@ -102,7 +104,10 @@ class FastLEDAudioDriver : public Node {
audioProcessor.setSignalConditioningEnabled(signalConditioning);
}
if (control["name"] == "autoGain") {
audioProcessor.setAutoGainEnabled(autoGain);
// audioProcessor.setAutoGainEnabled(autoGain);
}
if (control["name"] == "gain") {
audioProcessor.setGain(gain / 128.0f);
}
Comment on lines 106 to 111
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

Orphaned autoGain control: checkbox has no effect.

The autoGain checkbox control is still added at Line 46, but its handler is now commented out. Users will see a working checkbox that does nothing when toggled, which could be confusing.

Consider either:

  1. Re-enabling the auto-gain functionality alongside manual gain
  2. Removing/hiding the autoGain control if it's intentionally disabled
Option A: Remove orphaned control
 void setup() override {
   addControl(signalConditioning, "signalConditioning", "checkbox");
   addControl(gain, "gain", "slider");
-  addControl(autoGain, "autoGain", "checkbox");
   addControl(noiseFloorTracking, "noiseFloorTracking", "checkbox");
Option B: Re-enable auto-gain
   if (control["name"] == "autoGain") {
-    // audioProcessor.setAutoGainEnabled(autoGain);
+    audioProcessor.setAutoGainEnabled(autoGain);
   }
📝 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
if (control["name"] == "autoGain") {
audioProcessor.setAutoGainEnabled(autoGain);
// audioProcessor.setAutoGainEnabled(autoGain);
}
if (control["name"] == "gain") {
audioProcessor.setGain(gain / 128.0f);
}
if (control["name"] == "autoGain") {
audioProcessor.setAutoGainEnabled(autoGain);
}
if (control["name"] == "gain") {
audioProcessor.setGain(gain / 128.0f);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h` around lines 106 - 111, The
checkbox labeled "autoGain" is added but its handler is commented out in the
control dispatch (the branch matching control["name"] == "autoGain"), leaving
the UI control orphaned; either re-enable auto-gain by restoring the call to
audioProcessor.setAutoGainEnabled(...) and ensure the audioProcessor respects
auto-gain when audioProcessor.setGain(...) is called (e.g., skip or store manual
gain when autoGain is true), or remove the "autoGain" control creation so the
checkbox is not shown; locate the control creation code (the place that adds the
"autoGain" checkbox) and the control handling branch (control["name"] ==
"autoGain") and apply one of these two fixes consistently.

if (control["name"] == "noiseFloorTracking") {
audioProcessor.setNoiseFloorTrackingEnabled(noiseFloorTracking);
Expand Down
2 changes: 2 additions & 0 deletions src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ class FastLEDDriver : public DriverNode {
uint16_t startLed = 0;

FastLED.clear(ClearFlags::CHANNELS);
// FastLED.reset(ResetFlags::CHANNELS);

for (uint8_t pinIndex = 0; pinIndex < nrOfPins; pinIndex++) {
EXT_LOGD(ML_TAG, "ledPin p:%d #:%d rgb:%d aff:%s", pins[pinIndex], layerP.ledsPerPin[pinIndex], rgbOrder, options.mAffinity.c_str());
Expand Down Expand Up @@ -348,6 +349,7 @@ class FastLEDDriver : public DriverNode {
events.onChannelCreated.clear();
events.onChannelEnqueued.clear();
FastLED.clear(ClearFlags::CHANNELS);
// FastLED.reset(ResetFlags::CHANNELS);

moduleIO->removeUpdateHandler(ioUpdateHandler);
moduleControl->removeUpdateHandler(controlUpdateHandler);
Expand Down
2 changes: 1 addition & 1 deletion src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class ParallelLEDDriver : public DriverNode {
#if HP_ALL_DRIVERS
if (!initDone) return;

DriverNode::loop(); // This populates the LUT tables!
DriverNode::loop(); // This populates the LUT tables when needed

#ifndef CONFIG_IDF_TARGET_ESP32P4
if (ledsDriver.total_leds > 0) ledsDriver.showPixels(WAIT);
Expand Down
Loading
Loading