Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 7 additions & 7 deletions interface/src/routes/system/update/GithubFirmwareManager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,23 @@
}
}

function confirmGithubUpdate(assets: any) {
function confirmGithubUpdate(release: any) { // 🌙 use release instead of assets
let url = '';
// iterate over assets and find the correct one
for (let i = 0; i < assets.length; i++) {
for (let i = 0; i < release.assets.length; i++) {
// check if the asset is of type *.bin
if (
assets[i].name.includes('.bin') &&
assets[i].name.includes(page.data.features.firmware_built_target)
release.assets[i].name.includes('.bin') &&
release.assets[i].name.includes(page.data.features.firmware_built_target)
) {
url = assets[i].browser_download_url;
url = release.assets[i].browser_download_url;
}
}
Comment on lines +59 to 70
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against releases with no assets.

If a GitHub release has no uploaded assets (e.g., a draft), release.assets could be undefined or an empty array. Accessing .length on undefined will throw a TypeError. Consider adding an early return.

Also, includes() on Line 66 is a loose substring match — if target names overlap (e.g., "esp32" matching a binary named "firmware-esp32s3.bin"), the wrong asset could be selected. Consider a more precise match (e.g., exact segment match or a regex with word boundaries).

Proposed guard
 function confirmGithubUpdate(release: any) {
 	let url = '';
+	if (!release.assets || release.assets.length === 0) {
+		modals.open(InfoDialog as unknown as ModalComponent<any>, {
+			title: 'No matching firmware found',
+			message: 'No assets found in release ' + release.name,
+			dismiss: { label: 'OK', icon: Check },
+			onDismiss: () => modals.close()
+		});
+		return;
+	}
 	for (let i = 0; i < release.assets.length; i++) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/system/update/GithubFirmwareManager.svelte` around lines
59 - 70, In confirmGithubUpdate(release: any) guard against missing or empty
release.assets by checking if release.assets is truthy and has length and return
early (or show an error) if not; then replace the loose substring check that
uses release.assets[i].name.includes(page.data.features.firmware_built_target)
with a stricter match (for example use a regex with word boundaries anchored
around page.data.features.firmware_built_target or split the asset name into
segments and compare the exact segment) so you only pick the correct .bin asset,
and keep the existing .bin check and browser_download_url assignment.

if (url === '') {
modals.open(InfoDialog as unknown as ModalComponent<any>, {
title: 'No matching firmware found',
message:
'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',
'No matching firmware was found in ' + release.name + ' for ' + page.data.features.firmware_built_target, // 🌙
dismiss: { label: 'OK', icon: Check },
onDismiss: () => modals.close()
});
Expand Down Expand Up @@ -157,7 +157,7 @@
<button
class="btn btn-ghost btn-circle btn-sm"
onclick={() => {
confirmGithubUpdate(release.assets);
confirmGithubUpdate(release); // 🌙 use release instead of assets
}}
>
<CloudDown class="text-secondary h-6 w-6" />
Expand Down
1 change: 1 addition & 0 deletions lib/framework/SystemStatus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ esp_err_t SystemStatus::systemStatus(PsychicRequest *request)
root["core_temp"] = temperatureRead();
root["cpu_reset_reason"] = verbosePrintResetReason(esp_reset_reason());
root["uptime"] = millis() / 1000;
root["lps"] = 100;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded "lps" value is misleading in a dynamic status endpoint.

Every other field in this response is read from hardware or runtime state, but "lps" is a magic constant 100. If this is meant to report actual loops-per-second, it should be measured (e.g., increment a counter in the main loop and compute the rate). If it's a target value, consider using a named constant and making the field name clearer (e.g., "target_lps"). Also, if the main loop runs every 20 ms (per the new loop20ms hook), the true rate would be ~50, not 100.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/framework/SystemStatus.cpp` at line 200, The hardcoded root["lps"] = 100
is misleading; either measure real loops-per-second or make it a named
constant/target. Fix by replacing the magic 100: if you want real LPS, add a
counter (e.g., lpsCounter) incremented in the main loop hook (e.g., loop20ms or
mainLoop) and compute a rate over a short interval (resetting the counter or
using a rolling window) then assign that computed value to root["lps"];
alternatively, if it is a target, introduce a descriptive constant (e.g.,
constexpr int TARGET_LPS) and set root["target_lps"] = TARGET_LPS and remove the
misleading root["lps"] assignment. Ensure you modify SystemStatus.cpp where
root["lps"] is set and update any callers/consumers accordingly.

#ifdef CONFIG_IDF_TARGET_ESP32P4
esp_hosted_coprocessor_fwver_t c6_fw_version;
esp_hosted_get_coprocessor_fwversion(&c6_fw_version);
Expand Down
2 changes: 1 addition & 1 deletion lib/framework/UploadFirmwareService.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ constexpr uint8_t ESP_MAGIC_BYTE = 0xE9; // ESP binary magic byte
#elif CONFIG_IDF_TARGET_ESP32S3
constexpr uint8_t ESP_CHIP_ID = 9;
#elif CONFIG_IDF_TARGET_ESP32P4
constexpr uint8_t ESP_CHIP_ID = 19; // 🌙 P4 support
constexpr uint8_t ESP_CHIP_ID = 18; // 🌙 P4 support
#else
#error "Unsupported ESP32 target"
#endif
Expand Down
25,651 changes: 12,825 additions & 12,826 deletions lib/framework/WWWData.h

Large diffs are not rendered by default.

6 changes: 3 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=\"20260214\" ; 🌙
-D APP_DATE=\"20260215\" ; 🌙

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

Expand Down Expand Up @@ -155,9 +155,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=\"20260217\"
lib_deps =
https://github.com/FastLED/FastLED#fcdbb572b3d84394845209f2bcd8fa77c2cb4ee2 ; master 20260215
https://github.com/FastLED/FastLED#99e55a02ebf54ff89aa687972f0589870540cb2a ; master 20260217
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
12 changes: 6 additions & 6 deletions src/MoonBase/Module.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,9 @@ class Module : public StatefulService<ModuleState> {
// any Module that overrides begin() must continue to call Module::begin() (e.g., at the start of its own begin()
virtual void begin();

// any Module that overrides loop() must continue to call Module::loop() (e.g., at the start of its own loop()
virtual void loop() {
// run in sveltekit task

// run in sveltekit task
virtual void loop() {}
virtual void loop20ms() { // any Module that overrides loop20ms() must continue to call Module::loop20ms()
if (requestUIUpdate) {
requestUIUpdate = false; // reset the flag
EXT_LOGD(ML_TAG, "requestUIUpdate %s", _moduleName);
Expand All @@ -124,12 +123,13 @@ class Module : public StatefulService<ModuleState> {
_moduleName);
}
}
virtual void loop1s() {}
virtual void loop10s() {}

void processUpdatedItem(const UpdatedItem& updatedItem, const String& originId) {
if (updatedItem.name == "swap") {
onReOrderSwap(updatedItem.index[0], updatedItem.index[1]);
if (originId.toInt())
saveNeeded = true;
if (originId.toInt()) saveNeeded = true;
} else {
// if (updatedItem.parent[0] != "devices" && updatedItem.parent[0] != "tasks" && updatedItem.name != "core0") 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<String>().c_str());
if (updatedItem.name != "channel") { // todo: fix the problem at channel, not here...
Expand Down
8 changes: 5 additions & 3 deletions src/MoonBase/Modules/ModuleDevices.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class ModuleDevices : public Module {

_moduleControl->addUpdateHandler(
[this](const String& originId) {
EXT_LOGD(MB_TAG, "control update origin %s", originId.c_str());
EXT_LOGD(MB_TAG, "control update %s", originId.c_str());
sendUDP(originId != "group"); // sendUDP control yes / no
},
false);
Expand Down Expand Up @@ -116,15 +116,17 @@ class ModuleDevices : public Module {
}
}

void loop20ms() {
void loop20ms() override {
Module::loop20ms();

if (!WiFi.localIP() && !ETH.localIP()) return;

if (!deviceUDPConnected) return;

receiveUDP(); // and updateDevices
}

void loop10s() {
void loop10s() override {
if (!WiFi.localIP() && !ETH.localIP()) return;

if (!deviceUDPConnected) {
Expand Down
48 changes: 30 additions & 18 deletions src/MoonBase/Modules/ModuleIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ enum IO_PinUsageEnum {
pin_Dig_Input, // Digital Input pin type. May contains some protection circuit
pin_Exposed,
pin_Reserved,
pin_PIR, // support for PIR (passive infrared) sensor
pin_count
};

Expand Down Expand Up @@ -171,8 +172,8 @@ class ModuleIO : public Module {
addControlValue(control, "I2S WS");
addControlValue(control, "I2S SCK");
addControlValue(control, "I2S MCLK");
addControlValue(control, "I2C SDA");
addControlValue(control, "I2C SCL");
addControlValue(control, "I2C SDA 🔌");
addControlValue(control, "I2C SCL 🔌");
addControlValue(control, "Button 🛎️");
addControlValue(control, "Button 𓐟");
addControlValue(control, "Button LightOn 🛎️");
Expand Down Expand Up @@ -209,6 +210,7 @@ class ModuleIO : public Module {
addControlValue(control, "Digital Input");
addControlValue(control, "Exposed");
addControlValue(control, "Reserved");
addControlValue(control, "PIR ♨️");

control = addControl(rows, "index", "number", 1, 32); // max 32 of one type, e.g 32 led pins
control["default"] = UINT8_MAX;
Expand Down Expand Up @@ -259,6 +261,7 @@ class ModuleIO : public Module {
JsonDocument doc;
JsonObject newState = doc.to<JsonObject>();
newState["modded"] = false;
newState["I2CReady"] = false;

JsonArray pins = newState["pins"].to<JsonArray>();

Expand Down Expand Up @@ -491,10 +494,10 @@ class ModuleIO : public Module {
pinAssigner.assignPin(0, pin_I2S_MCLK);
uint8_t exposedPins[] = {4, 5, 17, 19, 21, 22, 23, 25, 26, 27, 33};
for (uint8_t gpio : exposedPins) pinAssigner.assignPin(gpio, pin_Exposed); // Ethernet Pins
} else if (boardID == board_MHCV57PRO) { // https://shop.myhome-control.de/ABC-WLED-Controller-PRO-V57-mit-iMOSFET/HW10030
newState["maxPower"] = 75; // 15A Fuse @ 5V
uint8_t ledPins[] = {12, 13, 18, 32}; // 4 LED_PINS

} else if (boardID == board_MHCV57PRO) { // https://shop.myhome-control.de/ABC-WLED-Controller-PRO-V57-mit-iMOSFET/HW10030
newState["maxPower"] = 75; // 15A Fuse @ 5V
uint8_t ledPins[] = {12, 13, 18, 32}; // 4 LED_PINS
for (uint8_t gpio : ledPins) pinAssigner.assignPin(gpio, pin_LED);
pinAssigner.assignPin(4, pin_Relay_LightsOn);
pinAssigner.assignPin(35, pin_I2S_SD);
Expand All @@ -504,8 +507,8 @@ class ModuleIO : public Module {
uint8_t exposedPins[] = {4, 5, 17, 19, 21, 22, 23, 25, 26, 27, 33};
for (uint8_t gpio : exposedPins) pinAssigner.assignPin(gpio, pin_Exposed); // Ethernet Pins

} else if (boardID == board_MHCP4NanoV1) { // https://shop.myhome-control.de/ABC-WLED-ESP32-P4-Shield/HW10027
newState["maxPower"] = 100; // Assuming decent LED power!!
} else if (boardID == board_MHCP4NanoV1) { // https://shop.myhome-control.de/ABC-WLED-ESP32-P4-Shield/HW10027
newState["maxPower"] = 100; // Assuming decent LED power!!

if (_state.data["switch1"]) { // on: 8 LED Pins + RS485 + Dig Input
uint8_t ledPins[] = {21, 20, 25, 5, 7, 23, 8, 27}; // 8 LED pins in this order
Expand Down Expand Up @@ -536,11 +539,11 @@ class ModuleIO : public Module {
pinAssigner.assignPin(12, pin_I2S_SCK);
pinAssigner.assignPin(13, pin_I2S_MCLK);
}
} else if (boardID == board_MHCP4NanoV2) { // https://shop.myhome-control.de/ABC-WLED-ESP32-P4-Shield/HW10027
newState["maxPower"] = 100; // Assuming decent LED power!!
pinAssigner.assignPin(7, pin_I2C_SDA); // on V2 these are I2C Pins
pinAssigner.assignPin(8, pin_I2C_SCL); // on V2 these are I2C Pins
if (_state.data["switch1"]) { // on: 8 LED Pins + RS485 + Dig Input
} else if (boardID == board_MHCP4NanoV2) { // https://shop.myhome-control.de/ABC-WLED-ESP32-P4-Shield/HW10027
newState["maxPower"] = 100; // Assuming decent LED power!!
pinAssigner.assignPin(7, pin_I2C_SDA); // on V2 these are I2C Pins
pinAssigner.assignPin(8, pin_I2C_SCL); // on V2 these are I2C Pins
if (_state.data["switch1"]) { // on: 8 LED Pins + RS485 + Dig Input
uint8_t ledPins[] = {21, 20, 25, 5, 22, 23, 24, 27}; // 8 LED pins in this order
for (uint8_t gpio : ledPins) pinAssigner.assignPin(gpio, pin_LED);
pinAssigner.assignPin(3, pin_RS485_TX);
Expand All @@ -551,7 +554,7 @@ class ModuleIO : public Module {
pinAssigner.assignPin(46, pin_Dig_Input);
pinAssigner.assignPin(47, pin_Dig_Input);
pinAssigner.assignPin(48, pin_Dig_Input);
} else { // off / default: 16 LED pins
} else { // off / default: 16 LED pins
uint8_t ledPins[] = {21, 20, 25, 5, 22, 23, 24, 27, 3, 6, 53, 4, 46, 47, 2, 48}; // 16 LED_PINS in this order
for (uint8_t gpio : ledPins) pinAssigner.assignPin(gpio, pin_LED);
}
Expand Down Expand Up @@ -609,8 +612,9 @@ class ModuleIO : public Module {
for (uint8_t gpio : ledPins) pinAssigner.assignPin(gpio, pin_LED);
} else if (boardID == board_LuxceoMood1XiaoMod) {
newState["maxPower"] = 50;
uint8_t ledPins[] = {1, 2, 3, 4};
uint8_t ledPins[] = {1, 2, 3};
for (uint8_t gpio : ledPins) pinAssigner.assignPin(gpio, pin_LED);
pinAssigner.assignPin(4, pin_PIR);
pinAssigner.assignPin(5, pin_I2C_SDA);
pinAssigner.assignPin(6, pin_I2C_SCL);
pinAssigner.assignPin(7, pin_SPI_SCK);
Expand Down Expand Up @@ -713,14 +717,22 @@ class ModuleIO : public Module {
}
}

void loop() override {
bool called = false;

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

if (newBoardID != UINT8_MAX) {
setBoardPresetDefaults(newBoardID); // run from sveltekit task
newBoardID = UINT8_MAX;
}

// during boot, the IO module is unchanged , not triggering updates, so need to do it manually
if (!called) {
callUpdateHandlers(_moduleName); // calls readPins for all subscribed handlers
called = true;
}
Comment on lines +720 to +735
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Double handler fan-out on boot when a board preset is active.

When newBoardID != UINT8_MAX on the first loop20ms() call (normal path — the saved board preset triggers onUpdate → sets newBoardID), setBoardPresetDefaultsupdate() already fires callUpdateHandlers internally. Then the explicit callUpdateHandlers on line 733 fires them all a second time.

Consider either:

  • Moving the one-shot callUpdateHandlers inside the newBoardID == UINT8_MAX branch (i.e., only when no preset change is pending), or
  • Setting called = true before setBoardPresetDefaults so the explicit call is skipped when the preset path already triggered handlers.

Also, called is a very generic name for a class-level flag — something like _initialPinReadDone would be self-documenting.

Proposed fix
-  bool called = false;
+  bool _initialPinReadDone = false;

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

     if (newBoardID != UINT8_MAX) {
       setBoardPresetDefaults(newBoardID);  // run from sveltekit task
       newBoardID = UINT8_MAX;
+      _initialPinReadDone = true;          // setBoardPresetDefaults→update already notified handlers
     }

     // during boot, the IO module is unchanged , not triggering updates, so need to do it manually
-    if (!called) {
+    if (!_initialPinReadDone) {
       callUpdateHandlers(_moduleName);  // calls readPins for all subscribed handlers
-      called = true;
+      _initialPinReadDone = true;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonBase/Modules/ModuleIO.h` around lines 720 - 735, The one-shot boot
handler is firing twice because loop20ms() calls
setBoardPresetDefaults(newBoardID) which itself triggers callUpdateHandlers(),
then your subsequent guarded callUpdateHandlers(_moduleName) runs again; to fix,
rename the flag from called to a clearer _initialPinReadDone and either (A) set
_initialPinReadDone = true before invoking setBoardPresetDefaults(newBoardID) so
the explicit callUpdateHandlers(_moduleName) is skipped when a preset change
occurs, or (B) move the one-shot callUpdateHandlers(_moduleName) into the branch
guarded by newBoardID == UINT8_MAX so it only runs when no preset change is
pending; update all references to the renamed flag accordingly.

}

void readPins() {
Expand Down Expand Up @@ -938,7 +950,7 @@ class ModuleIO : public Module {
adc_attenuation_t current_readout_current_adc_attenuation = ADC_11db;
#endif

void loop1s() {
void loop1s() override {
if (_triggerUpdateI2C != UINT8_MAX) {
_updateI2CDevices();
_triggerUpdateI2C = UINT8_MAX;
Expand Down
2 changes: 1 addition & 1 deletion src/MoonBase/Modules/ModuleTasks.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ModuleTasks : public Module {
}
}

void loop1s() {
void loop1s() override {
if (!_socket->getConnectedClients()) return; // 🌙 No need for UI tasks
if (!WiFi.localIP() && !ETH.localIP()) return;

Expand Down
17 changes: 3 additions & 14 deletions src/MoonLight/Modules/ModuleDrivers.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class ModuleDrivers : public NodeManager {
ModuleDrivers(PsychicHttpServer* server, ESP32SvelteKit* sveltekit, FileManager* fileManager, ModuleLightsControl* moduleLightsControl, ModuleIO* moduleIO) : NodeManager("drivers", server, sveltekit, fileManager) {
_moduleLightsControl = moduleLightsControl;
_moduleIO = moduleIO;
EXT_LOGV(ML_TAG, "constructor");

_moduleIO->addUpdateHandler([this](const String& originId) { readPins(); }, false);
}

void readPins() {
Expand Down Expand Up @@ -72,14 +73,13 @@ class ModuleDrivers : public NodeManager {
layerP.requestMapVirtual = true;
},
_moduleName);
}
} // readPins

void begin() override {
defaultNodeName = ""; // getNameAndTags<PanelLayout>();
nodes = &layerP.nodes;
NodeManager::begin();

_moduleIO->addUpdateHandler([this](const String& originId) { readPins(); }, false);
}

void addNodes(const JsonObject& control) override {
Expand Down Expand Up @@ -186,17 +186,6 @@ class ModuleDrivers : public NodeManager {
return node;
}

bool initPins = false;

void loop() override {
NodeManager::loop();

if (!initPins) {
readPins(); // initially
initPins = true;
}
}

}; // class ModuleDrivers

#endif
Expand Down
13 changes: 11 additions & 2 deletions src/MoonLight/Modules/ModuleEffects.h
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@ class ModuleEffects : public NodeManager {
return node;
}

void loop() override {
NodeManager::loop();
void loop20ms() override {
NodeManager::loop20ms();

if (triggerResetPreset) {
triggerResetPreset = false;
Expand All @@ -316,6 +316,15 @@ class ModuleEffects : public NodeManager {
}
}

void loop1s() override {
// set shared data (eg used in scrolling text effect), every second
sharedData.fps = esp32sveltekit.getAnalyticsService()->lps;
sharedData.connectionStatus = (uint8_t)esp32sveltekit.getConnectionStatus();
sharedData.clientListSize = esp32sveltekit.getServer()->getClientList().size();
sharedData.connectedClients = esp32sveltekit.getSocket()->getConnectedClients();
sharedData.activeClients = esp32sveltekit.getSocket()->getActiveClients();
}

bool triggerResetPreset = false;
void onUpdate(const UpdatedItem& updatedItem, const String& originId) override {
NodeManager::onUpdate(updatedItem, originId);
Expand Down
Loading