-
-
Notifications
You must be signed in to change notification settings - Fork 9
add heap-optimization: request heap optimization review #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Note
|
| Cohort / File(s) | Summary |
|---|---|
Module core & update flow src/MoonBase/Module.h, src/MoonBase/Module.cpp |
Replace execOnUpdate/runInAppTask with postUpdate/getUpdate semaphores; UpdatedItem uses fixed-size Char; update(...) now takes originId; add processUpdatedItem callback and global gModulesDoc; loop()/begin() adjusted to new flow and semaphores. |
Shared routers & persistence src/MoonBase/SharedHttpEndpoint.h, src/MoonBase/SharedWebSocketServer.h, src/MoonBase/SharedEventEndpoint.h, src/MoonBase/SharedFSPersistence.h |
Add header-only SharedHttpEndpoint, SharedWebSocketServer, SharedEventEndpoint and SharedFSPersistence for per-module FS JSON, delayed-write queue, registerModule wiring, and centralized emit/sync logic. |
State updater / settings services lib/framework/StatefulService.h, lib/framework/*SettingsService.h, src/MoonBase/Modules/FileManager.h |
Propagate originId into JsonStateUpdater and many settings update(...) signatures (AP, MQTT, NTP, WiFi, Ethernet, Files); ETHERNET_SETTINGS_FILE path adjusted. |
Main app modularization src/main.cpp |
Add global std::vector<Module*> modules, create one-time shared router instances (HTTP/WS/Event), register modules centrally, unify begin/loop invocation and stack sizing adjustments. |
Event socket / emission buffering lib/framework/EventSocket.h, lib/framework/EventSocket.cpp |
New emitEvent(const JsonDocument&) overload; make emitEvent signatures const-correct; replace per-call heap buffers with static buffers/vectors to reduce heap allocations. |
FS persistence & FileManager integration src/MoonBase/SharedFSPersistence.h, src/MoonBase/Modules/FileManager.h, src/MoonBase/Modules/FileManager.cpp |
New SharedFSPersistence class (registerModule, read/write per-module JSON, delayed writes); FilesState::update gains originId; FileManager.begin starts WebSocket in sequence. |
Heap/stack notes & build flags lib/framework/EventEndpoint.h, lib/framework/HttpEndpoint.h, lib/framework/WebSocketServer.h, src/MoonLight/Layers/PhysicalLayer.cpp, firmware/*.ini, lib/framework/ESP32SvelteKit.cpp |
Add PSRAM/heap optimization comments across headers; adjust some layer defaults; increase HTTPD and loop-task stack sizes via build flags and task creation. |
Utilities & Char type improvements src/MoonBase/Utilities.h, src/MoonBase/Utilities.cpp |
Remove runInAppTask globals; add const-correct Coord3D converters, Char constructors/assignments/operators and helpers; add fastDiv255. |
Nodes / Drivers / Effects signatures & fixes src/MoonBase/Nodes.h, src/MoonBase/Nodes.cpp, src/MoonLight/Nodes/Drivers/*, src/MoonLight/Nodes/Effects/* |
Wide signature tightening: String& → const Char<20>&, JsonObject const-correctness; fix =→== bugs; use WiFi OR ETH connectivity checks; initialization/memset/memcpy fixes. |
Module subclasses lifecycle & onUpdate changes src/MoonBase/Modules/*, src/MoonLight/Modules/* |
Many modules: setupDefinition now const JsonArray&, onUpdate takes const UpdatedItem&, many begin()/loop() marked override; some modules add local queuing (e.g., ModuleIO newBoardID) and change "null" checks to "". |
System status & heap diagnostics lib/framework/SystemStatus.h, lib/framework/SystemStatus.cpp, interface/src/* |
Add SystemStatus::heapHealth to report app/DMA heap diagnostics; frontend types/UI updated to display heap_info_app and heap_info_dma; proxy target changed in vite config. |
Shared endpoint header comments lib/framework/EventEndpoint.h, lib/framework/HttpEndpoint.h, lib/framework/WebSocketServer.h |
Add heap/PSRAM commentary blocks above endpoint classes; no control-flow changes. |
Misc: logging, config, docs & build lib/PsychicHttp/src/PsychicHttpServer.cpp, platformio.ini, firmware/*.ini, docs/*, interface/*, .coderabbit.yaml |
Improve endpoint error logging, bump APP_DATE, remove deprecated envs, docs updated for new signatures, UI tweaks, proxy host update, and minor formatting changes. |
Sequence Diagram(s)
sequenceDiagram
participant Client as HTTP/WS Client
participant Shared as SharedEndpoint (HTTP/WS/Event)
participant Module as Module / ModuleState
participant App as SvelteKit/App Task (loop)
participant FS as SharedFSPersistence
Client->>Shared: POST /rest or WebSocket frame (JSON + originId)
Shared->>Module: ModuleState::update(root, state, originId)
Module->>Module: build UpdatedItem and call postUpdate(const UpdatedItem&)
Module->>App: release semaphore (update ready)
App->>Module: getUpdate() → processUpdatedItem(const UpdatedItem&)
Module->>FS: writeToFS(moduleName) (delayed or immediate)
Module->>Shared: emit events / broadcast updated state (originId preserved)
Shared->>Client: send updated state
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
- Files/areas needing extra attention:
- src/MoonBase/Module.{h,cpp}: semaphore producer/consumer correctness, UpdatedItem lifetimes and thread-safety.
- src/MoonBase/SharedFSPersistence.h: delayed-write queue semantics, file/dir creation, concurrent read/write and update handler registration.
- Shared endpoints: originId propagation, registration/unregistration races and per-client state handling.
- Global gModulesDoc allocation/cleanup and JSON lifetime across tasks.
- EventSocket static buffers and MsgPack path concurrency.
- Widespread signature changes: ensure all callers updated to new update(..., originId) signatures.
Possibly related PRs
- Module: support button, new rows, nested tables #1 — touches module update plumbing and UpdatedItem/onUpdate handling; likely related changes to update flow and signatures.
Suggested labels
enhancement
"I hopped through bytes and trimmed the heap,
I queued the updates, not too deep,
Routers now share, semaphores hum,
Files saved slow — the burrow's done,
Nibble a carrot, code takes a leap."
Pre-merge checks and finishing touches
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 21.50% which is insufficient. The required threshold is 80.00%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The title 'add heap-optimization: request heap optimization review' accurately summarizes the primary goal of the PR, which is to implement heap optimization improvements for ESP32. |
✨ Finishing touches
- 📝 Generate docstrings
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
heap-optimize
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
|
See also #60 |
|
Warning Docstrings generation - IN PROGRESS Generating docstrings for this pull request |
|
Note Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
@CodeRabbit-ai to be more specific: Module.h defines class variables which are template classess: HttpEndpoint _httpEndpoint; // sizeof() = 152 And sizeof(Module) = 1144 Plus each of these template classes also might do things on the heap. So I think this might not be the best setup in terms of minimizing heap use. @CodeRabbit-ai, can you review this, check heap usage of these template classes and propose changes to do this in more heap-optimal way? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 26546 @MoonModules, excellent question! I've analyzed the template classes and their heap usage. Here's what I found: Current Heap Usage AnalysisPer Module (×15 modules):
Total Heap Impact (15 modules):
Biggest Memory Consumer: Proposed Optimizations1. Replace String with const char (Quick Win)*Since paths are typically static strings, eliminate heap allocations: // In HttpEndpoint.h
class HttpEndpoint {
protected:
// String _servicePath; // OLD: heap allocation
const char* _servicePath; // NEW: no heap allocation
public:
HttpEndpoint(..., const char *servicePath, ...)
: _servicePath(servicePath) { }
};
// Similar changes for EventEndpoint, WebSocketServer, FSPersistenceSavings: ~80 bytes heap per module × 15 = 1,200 bytes 2. Pointer-Based Lazy Initialization (Medium Impact)Only allocate components when needed: // In Module.h
class Module : public StatefulService<ModuleState> {
private:
HttpEndpoint<ModuleState>* _httpEndpoint = nullptr;
EventEndpoint<ModuleState>* _eventEndpoint = nullptr;
WebSocketServer<ModuleState>* _webSocketServer = nullptr;
FSPersistence<ModuleState>* _fsPersistence = nullptr;
public:
Module(...) {
// Allocate in constructor or begin()
_httpEndpoint = new HttpEndpoint<ModuleState>(...);
_eventEndpoint = new EventEndpoint<ModuleState>(...);
_webSocketServer = new WebSocketServer<ModuleState>(...);
_fsPersistence = new FSPersistence<ModuleState>(...);
}
~Module() {
delete _httpEndpoint;
delete _eventEndpoint;
delete _webSocketServer;
delete _fsPersistence;
}
};Savings: Moves 880 bytes per module from global memory to heap (controlled allocation timing) 3. Shared WebSocketServer (Biggest Win)Use a single WebSocketServer with multiplexing: // In a shared location
class SharedWebSocketServer {
private:
PsychicWebSocketHandler _webSocket;
std::map<String, StatefulService<ModuleState>*> _modules;
public:
void registerModule(const char* moduleName, StatefulService<ModuleState>* service) {
_modules[moduleName] = service;
}
void transmitData(const char* moduleName) {
auto it = _modules.find(moduleName);
if (it != _modules.end()) {
// Send data for specific module
}
}
};
// In Module.h - remove WebSocketServer member
class Module {
private:
// WebSocketServer<ModuleState> _webSocketServer; // REMOVED
static SharedWebSocketServer* _sharedWS; // Shared across all modules
};Savings: (488 bytes × 14) + heap buffers = ~6,832 bytes + significant heap 4. JsonDocument PoolingReuse JsonDocument instances instead of creating temporary ones: // In WebSocketServer.h
template <class T>
class WebSocketServer {
private:
static JsonDocument _sharedJsonDoc; // Shared across instances
void transmitData(PsychicWebSocketClient *client, const String &originId) {
// Reuse _sharedJsonDoc instead of creating new JsonDocument
_sharedJsonDoc.clear();
JsonObject root = _sharedJsonDoc.to<JsonObject>();
_statefulService->read(root, _stateReader);
String buffer;
serializeJson(_sharedJsonDoc, buffer);
// ... send
}
};Savings: Eliminates temporary allocations, saves ~500-1000 bytes per transmit operation 5. Recommended Implementation PlanPhase 1 (Easy, Low Risk):
Phase 2 (Medium Impact): Phase 3 (High Impact, Higher Complexity): Expected Total Savings
Total potential savings: ~8,000-10,000 bytes which increases your effective heap from 60KB to ~70KB. Would you like me to generate the complete code changes for Phase 1 (the quick wins)? |
@CodeRabbit-ai, yeah - why not - go ahead |
|
Tip For best results, initiate chat on the files or code changes. @ewowi, here are the complete Phase 1 code changes for heap optimization! 🚀 Phase 1: Quick Wins Implementation1. HttpEndpoint.hChanges:
// lib/framework/HttpEndpoint.h
#ifndef HttpEndpoint_h
#define HttpEndpoint_h
#include <StatefulService.h>
#include <PsychicHttp.h>
template <class T>
class HttpEndpoint {
public:
HttpEndpoint(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
PsychicHttpServer* server,
const char* servicePath, // CHANGED: was String
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
_stateReader(stateReader),
_stateUpdater(stateUpdater),
_statefulService(statefulService),
_server(server),
_servicePath(servicePath), // CHANGED: now const char*
_securityManager(securityManager),
_authenticationPredicate(authenticationPredicate),
_bufferSize(bufferSize) {
}
void begin() {
_server->on(_servicePath, HTTP_GET, _securityManager->wrapRequest(std::bind(&HttpEndpoint::fetchConfig, this, std::placeholders::_1), _authenticationPredicate));
_updateHandler = &_server->on(_servicePath, HTTP_POST, _securityManager->wrapRequest(std::bind(&HttpEndpoint::updateConfig, this, std::placeholders::_1, std::placeholders::_2), _authenticationPredicate));
}
protected:
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T>* _statefulService;
PsychicHttpServer* _server;
const char* _servicePath; // CHANGED: was String _servicePath
SecurityManager* _securityManager;
AuthenticationPredicate _authenticationPredicate;
size_t _bufferSize;
PsychicWebHandler* _updateHandler;
esp_err_t fetchConfig(PsychicRequest *request) {
PsychicJsonResponse response = PsychicJsonResponse(request, false, _bufferSize);
JsonObject jsonObject = response.getRoot();
_statefulService->read(jsonObject, _stateReader);
return response.send();
}
esp_err_t updateConfig(PsychicRequest *request, JsonVariant &json) {
if (!json.is<JsonObject>()) {
return request->reply(400);
}
JsonObject jsonObject = json.as<JsonObject>();
StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
if (outcome == StateUpdateResult::ERROR) {
return request->reply(400);
}
response.setLength();
return response.send();
}
};
#endif2. EventEndpoint.hChanges:
// lib/framework/EventEndpoint.h
#ifndef EventEndpoint_h
#define EventEndpoint_h
#include <StatefulService.h>
#include <EventSocket.h>
template <class T>
class EventEndpoint {
public:
EventEndpoint(JsonStateReader<T> stateReader,
StatefulService<T>* statefulService,
EventSocket* socket,
const char* event) : // CHANGED: was String
_stateReader(stateReader), _statefulService(statefulService), _socket(socket), _event(event) { // CHANGED: now const char*
_statefulService->addUpdateHandler([&](const String& originId) { transmitData(nullptr, originId); }, false);
}
void begin() {
_socket->registerEvent(_event);
}
protected:
JsonStateReader<T> _stateReader;
StatefulService<T>* _statefulService;
EventSocket* _socket;
const char* _event; // CHANGED: was String _event
void transmitData(PsychicWebSocketClient* client, const String& originId) {
JsonDocument jsonDocument = JsonDocument();
JsonObject root = jsonDocument.to<JsonObject>();
_statefulService->read(root, _stateReader);
size_t len = measureJson(jsonDocument);
AsyncWebSocketMessageBuffer* buffer = _socket->makeBuffer(len);
if (buffer) {
serializeJson(jsonDocument, buffer->get(), len);
if (client) {
client->text(buffer);
} else {
_socket->emitEvent(_event, buffer->get(), len, originId.c_str());
}
}
}
};
#endif3. WebSocketServer.hChanges:
// lib/framework/WebSocketServer.h
#ifndef WebSocketServer_h
#define WebSocketServer_h
#include <StatefulService.h>
#include <PsychicHttp.h>
template <class T>
class WebSocketServer {
public:
WebSocketServer(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
PsychicHttpServer* server,
const char* webSocketPath, // CHANGED: was String
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN,
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
_stateReader(stateReader),
_stateUpdater(stateUpdater),
_statefulService(statefulService),
_server(server),
_webSocketPath(webSocketPath), // CHANGED: now const char*
_securityManager(securityManager),
_authenticationPredicate(authenticationPredicate),
_bufferSize(bufferSize) {
_statefulService->addUpdateHandler([&](const String& originId) { transmitData(nullptr, originId); }, false);
}
void begin() {
_webSocket.onOpen([&](PsychicWebSocketClient* client) {
transmitData(client, NONE_ORIGIN);
});
_webSocket.onFrame([&](PsychicWebSocketRequest *request, httpd_ws_frame *frame) {
// Handle incoming WebSocket messages
String message = String((char*)frame->payload);
JsonDocument jsonDocument;
DeserializationError error = deserializeJson(jsonDocument, message);
if (!error && jsonDocument.is<JsonObject>()) {
JsonObject jsonObject = jsonDocument.as<JsonObject>();
_statefulService->update(jsonObject, _stateUpdater, LOCAL_ORIGIN);
}
return ESP_OK;
});
_server->on(_webSocketPath, &_webSocket);
_securityManager->registerWebSocket(&_webSocket, _authenticationPredicate);
}
protected:
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T>* _statefulService;
PsychicHttpServer* _server;
const char* _webSocketPath; // CHANGED: was String _webSocketPath
SecurityManager* _securityManager;
AuthenticationPredicate _authenticationPredicate;
size_t _bufferSize;
PsychicWebSocketHandler _webSocket;
// ADDED: Shared JsonDocument for pooling (saves ~500-1000 bytes per transmit)
static JsonDocument _sharedJsonDoc;
void transmitData(PsychicWebSocketClient* client, const String& originId) {
// CHANGED: Reuse shared JsonDocument instead of creating new one
_sharedJsonDoc.clear();
JsonObject root = _sharedJsonDoc.to<JsonObject>();
_statefulService->read(root, _stateReader);
String buffer;
serializeJson(_sharedJsonDoc, buffer);
if (client) {
client->sendMessage(buffer.c_str());
} else {
_webSocket.sendAll(buffer.c_str());
}
}
};
// ADDED: Define static member (add this at the end of the header)
template <class T>
JsonDocument WebSocketServer<T>::_sharedJsonDoc;
#endif4. FSPersistence.hChanges:
// lib/framework/FSPersistence.h
#ifndef FSPersistence_h
#define FSPersistence_h
#include <StatefulService.h>
#include <FS.h>
template <class T>
class FSPersistence {
public:
FSPersistence(JsonStateReader<T> stateReader,
JsonStateUpdater<T> stateUpdater,
StatefulService<T>* statefulService,
FS* fs,
const char* filePath, // CHANGED: was String
size_t bufferSize = DEFAULT_BUFFER_SIZE) :
_stateReader(stateReader),
_stateUpdater(stateUpdater),
_statefulService(statefulService),
_fs(fs),
_filePath(filePath), // CHANGED: now const char*
_bufferSize(bufferSize) {
_statefulService->addUpdateHandler([&](const String& originId) { writeToFS(); }, false);
}
void readFromFS() {
File file = _fs->open(_filePath, "r");
if (file) {
JsonDocument jsonDocument;
DeserializationError error = deserializeJson(jsonDocument, file);
file.close();
if (!error && jsonDocument.is<JsonObject>()) {
JsonObject jsonObject = jsonDocument.as<JsonObject>();
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
}
}
}
protected:
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T>* _statefulService;
FS* _fs;
const char* _filePath; // CHANGED: was String _filePath
size_t _bufferSize;
void writeToFS() {
File file = _fs->open(_filePath, "w");
if (file) {
JsonDocument jsonDocument;
JsonObject root = jsonDocument.to<JsonObject>();
_statefulService->read(root, _stateReader);
serializeJson(jsonDocument, file);
file.close();
}
}
};
#endif5. Update Module.cpp constructor callsChanges needed in src/MoonBase/Module.cpp: // BEFORE:
Module::Module(String moduleName, PsychicHttpServer* server, ESP32SvelteKit* sveltekit) :
StatefulService<ModuleState>(),
_httpEndpoint(read, update, this, server, String("/rest/" + moduleName), ...),
_eventEndpoint(read, this, sveltekit->getSocket(), moduleName),
_webSocketServer(read, update, this, server, String("/ws/" + moduleName), ...),
_fsPersistence(read, update, this, &LITTLEFS, String("/config/" + moduleName + ".json")),
...
// AFTER:
Module::Module(String moduleName, PsychicHttpServer* server, ESP32SvelteKit* sveltekit) :
StatefulService<ModuleState>(),
_moduleName(moduleName), // Store module name first
// Use .c_str() to get const char* from String concatenation
_httpEndpoint(read, update, this, server, (String("/rest/") + moduleName).c_str(), ...),
_eventEndpoint(read, this, sveltekit->getSocket(), moduleName.c_str()),
_webSocketServer(read, update, this, server, (String("/ws/") + moduleName).c_str(), ...),
_fsPersistence(read, update, this, &LITTLEFS, (String("/config/") + moduleName + ".json").c_str()),
...
Better approach - Add path members to Module.h: // In Module.h private section:
class Module : public StatefulService<ModuleState> {
private:
String _moduleName;
String _httpPath; // ADDED: Store constructed path
String _wsPath; // ADDED: Store constructed path
String _fsPath; // ADDED: Store constructed path
HttpEndpoint<ModuleState> _httpEndpoint;
EventEndpoint<ModuleState> _eventEndpoint;
WebSocketServer<ModuleState> _webSocketServer;
FSPersistence<ModuleState> _fsPersistence;
...
};
// In Module.cpp constructor:
Module::Module(String moduleName, PsychicHttpServer* server, ESP32SvelteKit* sveltekit) :
StatefulService<ModuleState>(),
_moduleName(moduleName),
_httpPath("/rest/" + moduleName), // Construct and store
_wsPath("/ws/" + moduleName), // Construct and store
_fsPath("/config/" + moduleName + ".json"), // Construct and store
_httpEndpoint(read, update, this, server, _httpPath.c_str(), ...),
_eventEndpoint(read, this, sveltekit->getSocket(), _moduleName.c_str()),
_webSocketServer(read, update, this, server, _wsPath.c_str(), ...),
_fsPersistence(read, update, this, &LITTLEFS, _fsPath.c_str()),
...Expected ResultsHeap Savings:
Trade-offs:
Testing checklist:
Would you like me to proceed with Phase 2 (pointer-based lazy initialization) after you've tested Phase 1? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/MoonBase/Module.cpp (1)
333-378: Module::begin now calls Http/Event endpoint begin() twice – likely unintended and wastes heapThe new heap‑measurement block is helpful, but it also introduces duplicate
begin()calls:size_t heapBefore = ESP.getFreeHeap(); _httpEndpoint.begin(); Serial.printf(...); heapBefore = ESP.getFreeHeap(); _eventEndpoint.begin(); Serial.printf(...); heapBefore = ESP.getFreeHeap(); _webSocketServer.begin(); Serial.printf(...); EXT_LOGV(MB_TAG, ""); _httpEndpoint.begin(); // ← second call _eventEndpoint.begin(); // ← second call _fsPersistence.readFromFS();Calling
_httpEndpoint.begin()and_eventEndpoint.begin()twice can:
- Register the same routes/handlers twice, depending on PsychicHttpServer semantics.
- Allocate underlying resources twice, which is counter to the heap‑optimization goal.
You likely only want a single
begin()per endpoint. A minimal fix:void Module::begin() { - size_t heapBefore = ESP.getFreeHeap(); - - _httpEndpoint.begin(); - Serial.printf("%s HttpEndpoint used: %d bytes\n", - _moduleName.c_str(), - heapBefore - ESP.getFreeHeap()); - heapBefore = ESP.getFreeHeap(); - - _eventEndpoint.begin(); - Serial.printf("%s EventEndpoint used: %d bytes\n", - _moduleName.c_str(), - heapBefore - ESP.getFreeHeap()); - heapBefore = ESP.getFreeHeap(); - - _webSocketServer.begin(); // ← This probably uses the most - Serial.printf("%s WebSocketServer used: %d bytes\n", - _moduleName.c_str(), - heapBefore - ESP.getFreeHeap()); - - EXT_LOGV(MB_TAG, ""); - _httpEndpoint.begin(); - _eventEndpoint.begin(); + size_t heapBefore = ESP.getFreeHeap(); + + _httpEndpoint.begin(); + Serial.printf("%s HttpEndpoint used: %u bytes\n", + _moduleName.c_str(), + (unsigned)(heapBefore - ESP.getFreeHeap())); + + heapBefore = ESP.getFreeHeap(); + _eventEndpoint.begin(); + Serial.printf("%s EventEndpoint used: %u bytes\n", + _moduleName.c_str(), + (unsigned)(heapBefore - ESP.getFreeHeap())); + + heapBefore = ESP.getFreeHeap(); + _webSocketServer.begin(); // likely largest consumer + Serial.printf("%s WebSocketServer used: %u bytes\n", + _moduleName.c_str(), + (unsigned)(heapBefore - ESP.getFreeHeap())); + + EXT_LOGV(MB_TAG, ""); _fsPersistence.readFromFS(); updateOriginId = _moduleName + "server"; ...(Using
%u+ cast is optional but avoids signed/unsigned mismatch withsize_t.)
🧹 Nitpick comments (6)
src/MoonBase/Utilities.cpp (1)
166-170: Global runInAppTask queue is unbounded and may fragment heapThe
runInAppTaskvector is a globalstd::vector<std::function<void()>>that can grow without bound. Even thoughdriverTaskswaps it into a local vector and destroys it each loop, a burst of enqueued tasks will still allocate multiplestd::functionobjects and backing storage, which can fragment the limited ESP32 heap.Consider:
- Adding a simple upper bound (e.g., drop or log when
runInAppTask.size()exceeds a safe threshold).- Optionally
reserve()‑ing a small fixed capacity at startup to avoid repeated growth/shrink cycles.- If you want more control/tracking, wiring this through your
VectorRAMAllocatorwould also make the memory source more explicit.src/main.cpp (1)
86-89: Heap-optimization comments are useful but may drift over timeThese comments give good context on per-module memory usage and constraints. Just be aware they can get stale as modules evolve; consider keeping them in sync with automated
sizeof/heap instrumentation (e.g., tests you already have) rather than relying on manual updates.src/MoonBase/Utilities.h (1)
383-387: Header now exposes std::vector/std::mutex but doesn’t include their headersThese externs introduce
std::vector<std::function<void()>>andstd::mutexin the public header, but the header itself doesn’t include<vector>,<functional>, or<mutex>. This works only as long as every translation unit includingUtilities.hhappens to pull those in transitively.To make this header self‑contained and less fragile, I’d add the standard headers here:
#include <Arduino.h> #include <ESPFS.h> #include "ArduinoJson.h" + #include <vector> + #include <functional> + #include <mutex>(Alternatively, add them to a central common header if that’s your pattern.)
Also, for clarity, you may want to use the same guard macro here as at the call sites (e.g.,
#if FT_ENABLED(FT_MOONLIGHT)), though that’s optional as long as the compile‑time definitions line up.src/MoonBase/Module.cpp (3)
106-115: Async onReOrderSwap queuing looks correct but capture could be more explicitRouting
onReOrderSwapthroughrunInAppTaskunderFT_ENABLED(FT_MOONLIGHT)is a good way to move the heavier re‑mapping work off the HTTP server stack. The lambda:runInAppTask.push_back([&, stateIndex, newIndex]() { onReOrderSwap(stateIndex, newIndex); });implicitly captures
thisby reference via[&], which is safe here becauseModuleState/Moduleare long‑lived. However, it also risks accidentally capturing future locals by reference.For clarity and to make lifetime assumptions explicit, I’d suggest:
- runInAppTask.push_back([&, stateIndex, newIndex]() { onReOrderSwap(stateIndex, newIndex); }); + runInAppTask.push_back([this, stateIndex, newIndex]() { + onReOrderSwap(stateIndex, newIndex); + });This keeps the heap profile the same but reduces the chance of subtle captures if this block is edited later.
138-148: execOnUpdate async path copies UpdatedItem; comment/capture can be tightenedOffloading
onUpdateviarunInAppTaskis the right move to protect the HTTP task’s stack, but a couple of details are worth tightening:runInAppTask.push_back([&, updatedItem]() mutable { // mutable as updatedItem is called by reference (&) onUpdate(updatedItem); });
updatedItemis actually captured by value here (good, because the original goes out of scope afterexecOnUpdatereturns). The comment suggesting “called by reference” is misleading.- The lambda doesn’t modify
updatedItem, somutableis unnecessary.- Capturing everything else by reference via
[&]implicitly drags inthis; making that explicit is clearer.You can simplify to:
- runInAppTask.push_back([&, updatedItem]() mutable { - onUpdate(updatedItem); - }); + runInAppTask.push_back([this, updatedItem]() { + onUpdate(updatedItem); + });This preserves semantics (including the necessary copy of
UpdatedItem) while slightly reducing capture scope and avoiding confusion about value vs reference.
386-389: Duplicate heap-optimization comment block – consider consolidatingThis heap-optimization comment near
setupDefinitionlargely repeats the earlier block above theModuleconstructor. It’s harmless, but having the same note in multiple places increases the chance they’ll diverge over time.Consider keeping a single, well‑maintained comment (or moving this information into a short design note) to reduce duplication.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
firmware/esp32-d0.ini(2 hunks)firmware/esp32-p4.ini(0 hunks)firmware/esp32-s3.ini(0 hunks)lib/framework/EthernetSettingsService.h(1 hunks)src/MoonBase/Module.cpp(5 hunks)src/MoonBase/Utilities.cpp(1 hunks)src/MoonBase/Utilities.h(1 hunks)src/main.cpp(3 hunks)
💤 Files with no reviewable changes (2)
- firmware/esp32-p4.ini
- firmware/esp32-s3.ini
🧰 Additional context used
🧬 Code graph analysis (1)
src/MoonBase/Module.cpp (1)
src/MoonBase/NodeManager.h (2)
onReOrderSwap(287-298)onUpdate(134-285)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
lib/framework/EthernetSettingsService.h (1)
36-36: Clarify whether this is a breaking change or a new path definition.This path change has no impact on heap usage (both are compile-time constants) and appears unrelated to the PR's stated heap optimization objectives.
However, the critical discovery is that
FSPersistence.mkdirs()automatically creates missing parent directories, so the new/.config/directory will be created on first save. This prevents the "directory doesn't exist" failure.The remaining concern: The search found zero references to
/config/ethernetSettings.jsonanywhere in the codebase, yet the review assumes this was the previous path. Either:
- This is a new path definition (not a modification), or
- The old path references were already removed before this PR
Please clarify:
- Is this a modification of existing code or a new definition?
- If users have existing settings at the old path, should they be migrated?
- Why was this path changed as part of a heap-optimization PR?
firmware/esp32-d0.ini (1)
89-90: Verify 6 KB HTTPD stack size against worst‑case usageSetting
HTTPD_STACK_SIZE=6144for the MoonBase-only envs makes sense to give the HTTP server more headroom, but it also eats into limited internal RAM on PSRAM‑less boards.I’d recommend verifying on real hardware that:
- The HTTP server task’s
uxTaskGetStackHighWaterMarkstays comfortably above zero under worst‑case requests (large JSON, many modules).- You’re not over‑provisioning compared to what’s actually needed now that heavy work is offloaded via
runInAppTask.Also applies to: 107-108
src/main.cpp (1)
295-313: Rebalanced effect/driver task stack sizes – confirm high‑water marksYou’ve shifted 1 KB of stack from
AppEffectTasktoAppDriverTaskon non‑PSRAM boards:
AppEffectTask:(psramFound() ? 6 : 3) * 1024AppDriverTask:(psramFound() ? 6 : 4) * 1024Given that
driverTasknow also drainsrunInAppTaskand may execute arbitrary lambdas, the larger driver stack is reasonable, but the reduced effect stack (3 KB) is tight on ESP32.Please verify on real hardware that both tasks still have safe
uxTaskGetStackHighWaterMark()margins under worst‑case load (many modules, complex effects, heavy updates) and adjust if needed.
Back end ======= - Module and FileManager, add webSocketServer.begin - Module: UpdatedItem: parent, name, oldValue: String to Char<16> - control["value"] and updatedItem.value: use to<String>.c_str() - Node: updateControl, onUpdate: <Char<16> oldValue - Utiltities: add operator=(String&)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
45-84: Undefinednameidentifier and opportunity for safer, allocation‑free comparisonInside both
effectsanddriversloops,nameis no longer defined, but still used in the final log lines (Lines 62 and 81). This will fail to compile. At the same time, you can avoid temporaryStringallocations fromJsonVariantby extracting aconst char*once per node and reusing it.Consider something along these lines:
- _moduleEffects->read([&](ModuleState& effectsState) { - for (JsonObject nodeState : effectsState.data["nodes"].as<JsonArray>()) { - - if (updatedItem == nodeState["name"]) { + _moduleEffects->read([&](ModuleState& effectsState) { + for (JsonObject nodeState : effectsState.data["nodes"].as<JsonArray>()) { + const char* nodeName = nodeState["name"].as<const char*>(); + if (nodeName && updatedItem == nodeName) { EXT_LOGV(ML_TAG, "updateHandler equals current item -> livescript compile %s", updatedItem.c_str()); LiveScriptNode* liveScriptNode = (LiveScriptNode*)_moduleEffects->findLiveScriptNode(nodeState["name"]); if (liveScriptNode) { liveScriptNode->compileAndRun(); _moduleEffects->requestUIUpdate = true; // update the Effects UI } - - EXT_LOGV(ML_TAG, "update due to new node %s done", name.c_str()); + EXT_LOGV(ML_TAG, "update due to new node %s done", nodeName); } index++; } }); - _moduleDrivers->read([&](ModuleState& effectsState) { - for (JsonObject nodeState : effectsState.data["nodes"].as<JsonArray>()) { - - if (updatedItem == nodeState["name"]) { + _moduleDrivers->read([&](ModuleState& effectsState) { + for (JsonObject nodeState : effectsState.data["nodes"].as<JsonArray>()) { + const char* nodeName = nodeState["name"].as<const char*>(); + if (nodeName && updatedItem == nodeName) { EXT_LOGV(ML_TAG, "updateHandler equals current item -> livescript compile %s", updatedItem.c_str()); LiveScriptNode* liveScriptNode = (LiveScriptNode*)_moduleDrivers->findLiveScriptNode(nodeState["name"]); if (liveScriptNode) { liveScriptNode->compileAndRun(); _moduleDrivers->requestUIUpdate = true; // update the Effects UI } - - EXT_LOGV(ML_TAG, "update due to new node %s done", name.c_str()); + EXT_LOGV(ML_TAG, "update due to new node %s done", nodeName); } index++; } });This both fixes the missing identifier and avoids constructing a temporary
StringfromnodeState["name"]on each comparison. Note thenodeName &&guard to avoid callingString::operator==with a null C string.src/MoonLight/Nodes/Effects/E_MoonLight.h (1)
34-38: Initialize loop index in SolidEffect::loop to avoid undefined behaviorThe uninitialized variable
int indexin the loop at line 37 is confirmed undefined behavior:for (int index; index < layer->nrOfLights; index++)The variable has an indeterminate starting value, causing unpredictable loop behavior (may not run, iterate incorrectly, or access out of bounds).
Fix:
if (layer->layerP->lights.header.offsetWhite != UINT8_MAX && white > 0) - for (int index; index < layer->nrOfLights; index++) layer->setWhite(index, white * brightness / 255); + for (int index = 0; index < layer->nrOfLights; index++) { + layer->setWhite(index, white * brightness / 255); + }src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1)
33-36: Fix control name mismatch so onUpdate() handler responds to UI changes
setup()registers the checkbox as"DDP"(line 34):addControl(ddp, "DDP", "checkbox");but
onUpdate()checks for"ddp"(line 52):if (control["name"] == "ddp") {ArduinoJson string comparisons are case‑sensitive, so this branch will never execute. Align the control name in the
onUpdate()check to match the registered name:- if (control["name"] == "ddp") { + if (control["name"] == "DDP") {
♻️ Duplicate comments (1)
src/MoonBase/Module.cpp (1)
138-149: Same lifetime concern applies here.This suffers from the same issue as the
onReOrderSwaplambda: capturingthisby reference (&) can lead to dangling references if the Module is destroyed before task execution.Additionally, the comment "mutable as updatedItem is called by reference (&)" is slightly misleading—
mutableallows modification of the captured copy, butonUpdatereceives the updatedItem by reference, which is valid.
🧹 Nitpick comments (6)
src/MoonLight/Nodes/Drivers/D_Infrared.h (1)
126-195: onUpdate signature matches new Node APIUpdating the handler to
onUpdate(Char<16>& oldValue, JsonObject control) overridekeeps this driver consistent with the rest of the node system. The body doesn’t useoldValue, but that’s harmless and keeps the override compatible.You could mark
oldValueunused (e.g.Char<16>& /*oldValue*/) to silence potential warnings.src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
35-39: Template onUpdate fix is good; consider restoringoverride
- Changing to
Char<16>& oldValueand fixingif (control["value"] == 0)correctly aligns the example with the new Node API and fixes the prior assignment bug.- To keep compile‑time checking consistent with other nodes, it would be better to declare this as
void onUpdate(Char<16>& oldValue, JsonObject control) override.- Since
oldValueisn’t used, you can also mark it unused (e.g./*oldValue*/) if warnings are an issue.src/MoonBase/NodeManager.h (1)
23-24: Char<16> migration here is fine; just confirm max lengthsChanging
defaultNodeNameandUpdatedItem.oldValue/parent/name(used here) toChar<16>reduces heap pressure and works with the updatedNode::updateControl/onUpdateAPIs, but it does cap stored strings at 15 characters. That seems safe for current identifiers like"nodes","controls","name","on", and typical short node names, yet will truncate longer filenames or control values if you ever rely on the full text in logs or logic.Given this is central infrastructure, it’s worth double‑checking:
- Expected maximum node file name and control name lengths.
- Whether any downstream comparisons (e.g., against longer path strings) could silently fail due to truncation.
The updated logs using
as<const char*>()fornodeState["name"]are a nice win to avoid extra temporaryStringallocations.Also applies to: 98-100, 248-249, 255-256, 260-262, 271-272
src/MoonBase/Module.h (1)
27-33: UpdatedItem and updateOriginId Char<16> choices: check expected max lengthsSwitching
UpdatedItem::parent[2],name,oldValue, andModule::updateOriginIdtoChar<16>is a big win against heap churn, but it hard‑caps each at 15 visible characters. For current usage (keys like"nodes","controls","name","lightPreset","number of Particles", short origin IDs), this is probably fine, but it will silently truncate longer values.Before this spreads further, it’s worth validating:
- Typical and worst‑case lengths for:
- JSON parent path segments (
parent[0],parent[1])- Control names and module names stored in
name- Any logic that compares
oldValuetextually (beyond checking for""/"null")updateOriginIdcontents- Whether any external integrations rely on full filenames or IDs longer than 15 chars.
If you expect longer names or IDs, bumping some of these to
Char<32>(especiallyoldValueandupdateOriginId) would still keep heap usage controlled while avoiding unexpected truncation.Also applies to: 124-125
src/MoonBase/Nodes.h (1)
144-146: Consider simplifying the initialization pattern.The two-step initialization can be simplified to a single line for better readability:
Apply this diff:
- Char<16> oldValue; - oldValue = ""; + Char<16> oldValue = "";Note: Verify that 16 bytes is sufficient for all potential
oldValuecontent. If longer values are expected, consider increasing the buffer size to prevent truncation.src/MoonBase/Module.cpp (1)
310-316: Be aware of per-Module WebSocketServer memory footprint.As noted in the PR objectives, each Module now instantiates its own WebSocketServer (488 bytes each × 15 modules = ~7KB). The String concatenations in the initializer list create temporary String objects that allocate heap memory.
Consider the following for future optimization (Phase 2/3 as mentioned in PR):
- Use string literals or pre-constructed paths where possible
- Share a single WebSocketServer with multiplexing (Phase 3)
- Convert member objects to pointers with lazy allocation (Phase 2)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
docs/develop/modules.md(1 hunks)src/MoonBase/Module.cpp(9 hunks)src/MoonBase/Module.h(3 hunks)src/MoonBase/Modules/FileManager.cpp(1 hunks)src/MoonBase/Modules/ModuleIO.h(1 hunks)src/MoonBase/NodeManager.h(6 hunks)src/MoonBase/Nodes.cpp(2 hunks)src/MoonBase/Nodes.h(3 hunks)src/MoonBase/Utilities.h(2 hunks)src/MoonLight/Modules/ModuleLightsControl.h(0 hunks)src/MoonLight/Modules/ModuleLiveScripts.h(3 hunks)src/MoonLight/Nodes/Drivers/D_ArtnetIn.h(1 hunks)src/MoonLight/Nodes/Drivers/D_Infrared.h(1 hunks)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h(1 hunks)src/MoonLight/Nodes/Drivers/D__Sandbox.h(1 hunks)src/MoonLight/Nodes/Effects/E_MoonLight.h(2 hunks)src/MoonLight/Nodes/Effects/E_MoonModules.h(1 hunks)src/MoonLight/Nodes/Effects/E__Sandbox.h(1 hunks)
💤 Files with no reviewable changes (1)
- src/MoonLight/Modules/ModuleLightsControl.h
🧰 Additional context used
🧬 Code graph analysis (10)
src/MoonLight/Nodes/Effects/E_MoonModules.h (6)
src/MoonBase/Nodes.cpp (2)
onUpdate(394-534)onUpdate(394-394)src/MoonLight/Nodes/Drivers/D_Infrared.h (1)
onUpdate(126-196)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
onUpdate(925-929)onUpdate(1121-1125)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (5)
src/MoonBase/Nodes.cpp (2)
onUpdate(394-534)onUpdate(394-394)src/MoonLight/Nodes/Drivers/D_Infrared.h (1)
onUpdate(126-196)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
onUpdate(925-929)onUpdate(1121-1125)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (5)
src/MoonBase/Nodes.cpp (2)
onUpdate(394-534)onUpdate(394-394)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
onUpdate(925-929)onUpdate(1121-1125)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (5)
src/MoonBase/Nodes.cpp (2)
onUpdate(394-534)onUpdate(394-394)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
onUpdate(925-929)onUpdate(1121-1125)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonLight/Nodes/Effects/E_MoonLight.h (5)
src/MoonBase/NodeManager.h (1)
onUpdate(133-282)src/MoonBase/Nodes.cpp (2)
onUpdate(394-534)onUpdate(394-394)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonBase/Nodes.h (4)
src/MoonBase/NodeManager.h (2)
void(78-78)onUpdate(133-282)src/MoonBase/Nodes.cpp (4)
updateControl(17-58)updateControl(17-17)onUpdate(394-534)onUpdate(394-394)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)
src/MoonBase/Nodes.cpp (5)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
onUpdate(925-929)onUpdate(1121-1125)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonLight/Nodes/Drivers/D_Infrared.h (6)
src/MoonBase/Nodes.cpp (2)
onUpdate(394-534)onUpdate(394-394)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
onUpdate(925-929)onUpdate(1121-1125)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonLight/Nodes/Effects/E__Sandbox.h (8)
src/MoonBase/Modules/ModuleIO.h (1)
onUpdate(391-404)src/MoonBase/NodeManager.h (1)
onUpdate(133-282)src/MoonBase/Nodes.cpp (2)
onUpdate(394-534)onUpdate(394-394)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
onUpdate(925-929)onUpdate(1121-1125)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)src/MoonLight/Modules/ModuleChannels.h (1)
onUpdate(58-97)
src/MoonBase/Module.h (1)
lib/framework/FSPersistence.h (1)
readFromFS(44-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (17)
docs/develop/modules.md (1)
79-79: Logging sample is consistent and safeThe updated log line correctly uses C‑string accessors and keeps the temporary
value.as<String>()alive for the duration of the logging call. No functional or heap‑safety issues here.src/MoonBase/Modules/ModuleIO.h (1)
391-403: Switch toupdateOriginId.contains("server")preserves behaviorThe new checks against
updateOriginId.contains("server")keep the previous “skip self‑originated updates” logic while simplifying the code. As long asupdateOriginIdis an ArduinoString(or equivalent with acontainsAPI), this is a safe and clearer replacement.Please double‑check that
updateOriginId’s type inModuleindeed providescontains(const char*)on all supported targets.src/MoonBase/Modules/FileManager.cpp (1)
141-183: Starting_webSocketServerinbegin()is correctHooking
_webSocketServer.begin()into the FileManager startup sequence aligns it with the HTTP and event endpoints and ensures the/ws/FileManagerchannel is actually live. Assuming no other code callsbegin()on this instance, there’s no downside here.src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
31-35: onUpdate signature + comparison fix look correctSwitch to
Char<16>& oldValuematches the updated base API, and fixingcontrol["value"] == 0removes the previous assignment bug; no extra heap impact or behavioral regressions here.src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
69-73: Signature update is consistent with base classUsing
Char<16>& oldValuekeeps this override aligned withDriverNode::onUpdateand the rest of the Char-based update pipeline; no additional issues.src/MoonBase/Nodes.cpp (1)
394-398: DriverNode::onUpdate signature + logging change look goodUpdating
DriverNode::onUpdatetoChar<16>& oldValueand logging the control name viaas<const char*>()is consistent with the new API and reduces transientStringusage; the light‑preset logic itself is untouched.src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
121-125: GameOfLifeEffect onUpdate migration is correctSwitching
onUpdatetoChar<16>& oldValuekeeps this effect aligned with the updated baseNode::onUpdateAPI; the rule‑rebuild behavior is unchanged.src/MoonLight/Nodes/Effects/E_MoonLight.h (1)
924-929: RubiksCubeEffect / ParticlesEffect onUpdate changes are alignedBoth
RubiksCubeEffect::onUpdateandParticlesEffect::onUpdatenow takeChar<16>& oldValue, matching the newNode::onUpdatesignature. Their logic (togglingdoInit/ callingsettingUpParticles()when specific controls change) is unchanged and independent ofoldValue, so this refactor is safe from a behavior and heap perspective.Also applies to: 1121-1125
src/MoonBase/Nodes.h (1)
154-154: LGTM! Consistent migration to fixed-size buffers.The migration from
String&toChar<16>&for theoldValueparameter is consistent across all method signatures and aligns with the heap optimization goals. This change reduces dynamic heap allocations, which is beneficial for ESP32 platforms with limited heap.Also applies to: 156-156, 268-268
src/MoonBase/Utilities.h (4)
172-173: Appropriate handling of mixed JsonVariant types.The
.as<String>().c_str()approach correctly handles non-string JsonVariant values by converting them to strings first. While this ensures consistent behavior, be aware that this conversion adds overhead for numeric types.
179-182: Good addition for String compatibility.The new
operator=(const String&)improves API completeness and allows direct assignment from Arduino String objects without requiring explicit.c_str()calls.
189-189: Excellent const-correctness improvements.Adding
constqualifiers to these operators improves type safety, allows binding to temporary values, and prevents unnecessary copies. These are best practices for C++ operator overloading.Also applies to: 197-197, 203-203, 210-210
383-387: Externs properly defined—code is correct.Both
runInAppTaskandrunInAppTask_mutexare correctly defined insrc/MoonBase/Utilities.cpp(lines 167-168), matching their extern declarations. The declarations and definitions are consistent and properly organized.src/MoonBase/Module.cpp (4)
131-131: Cleaner string comparison using Char API.The use of
.contains()is more readable than explicitc_str()calls and leverages the Char template's API appropriately.
167-167: Proper JsonString to Char assignments.The assignments from JsonString to Char<16> fields leverage the appropriate operator overloads defined in the Char template. These conversions are handled correctly.
Also applies to: 208-209, 213-214, 256-259
336-340: Proper initialization sequence.The initialization order is appropriate: endpoints are started before reading from the filesystem, ensuring all communication channels are ready before loading persisted state.
107-116: Verify the architectural assumption that all Module instances are long-lived and never dynamically destroyed.The lambda in Module.cpp:111 captures
thisby reference. While current code shows all Module instances are global and should outlive task execution, this is an architectural assumption not enforced by the code itself.To confirm this is safe:
- Verify that Module instances (and derived classes like ModuleEffects, ModuleDrivers, etc.) are never dynamically allocated with
newormake_shared- Confirm no module pooling/recycling or hot-reloading exists
- Ensure no shutdown sequence destroys modules before
runInAppTaskdrainsIf Module instances could be dynamically allocated or destroyed, consider refactoring to capture a
shared_ptror implementing explicit task draining in the Module destructor.
| EXT_LOGW(ML_TAG, "liveScriptNode not found %s", scriptState["name"].as<const char*>()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against null scriptState["name"] when logging
scriptState["name"].as<const char*>() may legitimately return nullptr if the field is missing or not a string. Passing a null pointer to a %s format in EXT_LOGW is undefined and can crash.
Recommend extracting and guarding the name:
- } else
- EXT_LOGW(ML_TAG, "liveScriptNode not found %s", scriptState["name"].as<const char*>());
+ } else {
+ const char* scriptName = scriptState["name"].as<const char*>();
+ EXT_LOGW(ML_TAG, "liveScriptNode not found %s", scriptName ? scriptName : "<null>");
+ }This keeps heap usage low while avoiding undefined behavior when the name is absent or invalid.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/MoonLight/Modules/ModuleLiveScripts.h around lines 172-173, the log call
uses scriptState["name"].as<const char*>() which can be nullptr and cause
undefined behavior when passed to %s; extract the name into a local const char*
(or check scriptState.has("name") && scriptState["name"].is_string()), replace
nullptr with a safe sentinel (e.g. "<unknown>") before calling EXT_LOGW, or
format the message using a conditional string so the logger never receives a
null pointer.
Back End ======== - Add const in onUpdate - Module: bugfix one global gModulesDoc!, use this instead of [&], remove mutable - Nodes: updateControl safety checks on strings - Utilities: add const - E_MoonLight: Solid: white channel bugfix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/develop/modules.md (1)
67-80: Update doc example to use.c_str()consistently with Char<16> fieldsThe example now declares
void onUpdate(const UpdatedItem &updatedItem), and the finalEXT_LOGDcorrectly callsupdatedItem.parent[0].c_str()/updatedItem.name.c_str().However, the earlier log lines still use
updatedItem.namedirectly:EXT_LOGD(MB_TAG, "handle %s = %s -> %s", updatedItem.name, ...);Given
UpdatedItem.nameis now aChar<N>, this should also use.c_str()to match real code:EXT_LOGD(MB_TAG, "handle %s = %s -> %s", updatedItem.name.c_str(), updatedItem.oldValue.c_str(), updatedItem.value.as<String>().c_str());Otherwise, users copying this snippet won’t compile.
♻️ Duplicate comments (1)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
170-173: Null-safe logging forscriptState["name"]in not-found path
scriptState["name"].as<const char*>()can benullptrif the field is absent or not a string; passing that directly to a%sformat inEXT_LOGWis undefined behavior and can crash, even though this is a failure path.You can keep the heap win (no
Stringallocation) while making the log safe by adding a small guard:- } else - EXT_LOGW(ML_TAG, "liveScriptNode not found %s", scriptState["name"].as<const char*>()); + } else { + const char* scriptName = scriptState["name"].as<const char*>(); + EXT_LOGW(ML_TAG, "liveScriptNode not found %s", scriptName ? scriptName : "<null>"); + }
🧹 Nitpick comments (7)
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
924-929: Approve signature change; consider const-correctness forcontrolparameter.The change from
String&toconst Char<16>&aligns well with the PR's heap optimization goals. However, thecontrolparameter should beconst JsonObjectfor consistency with other onUpdate implementations in the codebase (e.g., D_VirtualDriver.h, E_MoonModules.h).Apply this diff to add const:
- void onUpdate(const Char<16>& oldValue, const JsonObject control) override { + void onUpdate(const Char<16>& oldValue, const JsonObject const control) override {Based on learnings, maintaining const-correctness improves code safety and aligns with the broader pattern seen in relevant code snippets.
1120-1125: Approve signature change; consider const-correctness forcontrolparameter.The change from
String&toconst Char<16>&aligns with the heap optimization effort. However, thecontrolparameter should beconst JsonObjectfor consistency with other onUpdate implementations throughout the codebase.Apply this diff to add const:
- void onUpdate(const Char<16>& oldValue, const JsonObject control) override { + void onUpdate(const Char<16>& oldValue, const JsonObject const control) override {Based on learnings, this maintains const-correctness and aligns with the pattern in D_VirtualDriver.h, E_MoonModules.h, and other effect/driver files.
src/MoonLight/Modules/ModuleLiveScripts.h (1)
49-81: Heap-friendly name comparison looks good; consider centralizing null-safe name handlingThe change to compare
updatedItemdirectly againstnodeState["name"]in both effects and drivers loops avoids constructing a temporaryStringper node and is a good heap optimization.Given how often this pattern appears across modules, you might want to (optionally) factor out a small helper that extracts a
const char*name with a safe fallback (e.g."<unnamed>") and reuse it for logging, rather than callingnodeState["name"].as<const char *>()inline in each log site.Example (effects loop sketch):
- _moduleEffects->read([&](ModuleState& effectsState) { - for (JsonObject nodeState : effectsState.data["nodes"].as<JsonArray>()) { - - if (updatedItem == nodeState["name"]) { + _moduleEffects->read([&](ModuleState& effectsState) { + for (JsonObject nodeState : effectsState.data["nodes"].as<JsonArray>()) { + const char* nodeName = nodeState["name"].as<const char*>(); + if (nodeName && updatedItem == nodeName) { EXT_LOGD(ML_TAG, "updateHandler equals current item -> livescript compile %s", updatedItem.c_str()); LiveScriptNode* liveScriptNode = (LiveScriptNode*)_moduleEffects->findLiveScriptNode(nodeState["name"]); if (liveScriptNode) { liveScriptNode->compileAndRun(); _moduleEffects->requestUIUpdate = true; // update the Effects UI } - EXT_LOGD(ML_TAG, "update due to new node %s done", nodeState["name"].as<const char *>()); + EXT_LOGD(ML_TAG, "update due to new node %s done", nodeName); }Same idea can be mirrored in the drivers loop.
src/MoonBase/Utilities.h (2)
167-182: Char assignment from JsonVariant still allocates; consider usingas<const char*>()to cut heap churn
Char& operator=(const JsonVariant rhs)currently does:if (!rhs.isNull()) strlcpy(s, rhs.as<String>().c_str(), sizeof(s));This builds a temporary
Stringon the heap each time. Given this PR’s heap focus, consider:Char& operator=(const JsonVariant rhs) { - if (!rhs.isNull()) strlcpy(s, rhs.as<String>().c_str(), sizeof(s)); + if (!rhs.isNull()) { + const char* v = rhs.as<const char*>(); + if (v) strlcpy(s, v, sizeof(s)); + else s[0] = '\0'; + } else { + s[0] = '\0'; + } return *this; }This avoids a transient
Stringallocation and makes the null case explicit. Similar reasoning applies anywhereCharis fed fromJsonVarianthot paths.Also applies to: 189-205, 208-213, 229-233, 242-242
383-387: Make Utilities.h self‑contained for new async queue types and document usage constraints
runInAppTask/runInAppTask_mutexintroducestd::vector,std::function, andstd::mutexin this header, but the file doesn’t include their headers. Right now this relies on transitive includes.I’d add at the top:
+#include <vector> +#include <functional> +#include <mutex>Also, since this queue is called from multiple tasks, ensure every producer/consumer side consistently locks
runInAppTask_mutexaround vector access, and keep captured lambdas trivial to avoid surprise heap use.src/MoonBase/NodeManager.h (1)
23-24: SwitchingdefaultNodeNametoChar<16>is safe here
defaultNodeNamenow usesChar<16>, and it’s only written via its implicit default ("") and read viadefaultNodeName.c_str()for JSON defaults. For typical node script filenames this should be sufficient and saves aStringinstance perNodeManager. If you expect longer default filenames later, consider bumping this toChar<32>to avoid truncation.Also applies to: 89-100
src/MoonBase/Nodes.h (1)
152-156: Node / DriverNode onUpdate/updateControl signatures are now consistent and const‑correct
virtual void updateControl(const Char<16>& oldValue, const JsonObject control);virtual void onUpdate(const Char<16>& oldValue, const JsonObject control) {}void onUpdate(const Char<16>& oldValue, const JsonObject control) override;inDriverNodeThese match the usages in
NodeManager::onUpdateand in individual drivers/effects. The constness onoldValueandcontrolmatches current implementations, which only read from them.One follow‑up you might consider later: making helpers like
Char::length()andChar::contains()const as well, so they’re usable onconst UpdatedItemfields without needing non‑const access.Also applies to: 267-268
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
docs/develop/modules.md(2 hunks)src/MoonBase/Module.cpp(11 hunks)src/MoonBase/Module.h(5 hunks)src/MoonBase/Modules/ModuleIO.h(1 hunks)src/MoonBase/NodeManager.h(7 hunks)src/MoonBase/Nodes.cpp(2 hunks)src/MoonBase/Nodes.h(3 hunks)src/MoonBase/Utilities.h(3 hunks)src/MoonLight/Modules/ModuleChannels.h(1 hunks)src/MoonLight/Modules/ModuleLightsControl.h(1 hunks)src/MoonLight/Modules/ModuleLiveScripts.h(5 hunks)src/MoonLight/Nodes/Drivers/D_ArtnetIn.h(1 hunks)src/MoonLight/Nodes/Drivers/D_Infrared.h(1 hunks)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h(1 hunks)src/MoonLight/Nodes/Drivers/D__Sandbox.h(1 hunks)src/MoonLight/Nodes/Effects/E_MoonLight.h(3 hunks)src/MoonLight/Nodes/Effects/E_MoonModules.h(1 hunks)src/MoonLight/Nodes/Effects/E__Sandbox.h(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/MoonLight/Nodes/Effects/E_MoonModules.h
- src/MoonLight/Modules/ModuleLightsControl.h
- src/MoonLight/Nodes/Drivers/D__Sandbox.h
- src/MoonLight/Nodes/Drivers/D_Infrared.h
- src/MoonLight/Nodes/Effects/E__Sandbox.h
🧰 Additional context used
🧬 Code graph analysis (6)
src/MoonLight/Modules/ModuleLiveScripts.h (3)
src/MoonBase/NodeManager.h (1)
onUpdate(133-282)src/MoonBase/Nodes.cpp (2)
onUpdate(402-542)onUpdate(402-402)src/MoonLight/Modules/ModuleChannels.h (1)
onUpdate(58-97)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (3)
src/MoonBase/Nodes.cpp (2)
onUpdate(402-542)onUpdate(402-402)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)
src/MoonLight/Modules/ModuleChannels.h (3)
src/MoonBase/Modules/ModuleIO.h (1)
onUpdate(391-404)src/MoonBase/NodeManager.h (1)
onUpdate(133-282)src/MoonLight/Modules/ModuleLiveScripts.h (1)
onUpdate(153-178)
src/MoonBase/Module.cpp (3)
src/MoonBase/Modules/ModuleIO.h (1)
onUpdate(391-404)src/MoonLight/Modules/ModuleChannels.h (1)
onUpdate(58-97)src/MoonLight/Modules/ModuleLiveScripts.h (1)
onUpdate(153-178)
src/MoonLight/Nodes/Effects/E_MoonLight.h (7)
src/MoonBase/Modules/ModuleIO.h (1)
onUpdate(391-404)src/MoonBase/Nodes.cpp (2)
onUpdate(402-542)onUpdate(402-402)src/MoonLight/Modules/ModuleLiveScripts.h (1)
onUpdate(153-178)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (4)
src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
onUpdate(925-929)onUpdate(1121-1125)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (15)
src/MoonLight/Nodes/Effects/E_MoonLight.h (1)
34-38: LGTM: Loop initialization is correct.The for-loop properly initializes the
indexvariable and correctly iterates over all lights to set white channel brightness.src/MoonLight/Modules/ModuleLiveScripts.h (1)
153-178: Const-correctonUpdateoverride aligns with other modulesSwitching
onUpdateto takeconst UpdatedItem&matches the pattern in other modules and avoids accidental mutation/copies of the update descriptor. No issues from a correctness or heap-usage standpoint.src/MoonLight/Modules/ModuleChannels.h (1)
58-97: Const‑correctonUpdatelooks consistent and safe
onUpdatenow takesconst UpdatedItem&, and the implementation only reads fromupdatedItemand_state, so this change is non‑breaking and aligns with the rest of the const‑ref refactor. No issues spotted with bounds or channel indexing beyond existing behavior.src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1)
50-56: Good: onUpdate signature aligned and assignment bug fixedSwitching to
void onUpdate(const Char<16>& oldValue, const JsonObject control)matches the newNode/DriverNodeAPI, and correctingcontrol["value"] = 0to== 0removes a silent logic bug. No additional issues in this handler.src/MoonBase/NodeManager.h (1)
132-282: Node controls invalidation/removal and new constUpdatedItem&flow look correct
onUpdate(const UpdatedItem& updatedItem)matches the new module API, and allupdatedItemusage is read‑only.The “invalidate controls” step:
if (nodeState["controls"].isNull()) { nodeState["controls"].to<JsonArray>(); } else { for (JsonObject control : nodeState["controls"].as<JsonArray>()) { control["valid"] = false; } }combined with the backwards removal loop removes only controls left with
valid == false, which is correct and avoids index-shift issues.The backwards loop is safe even when
controls.size() == 0becauseint i = size - 1;becomes-1and the loop body is skipped.Calls to
nodeClass->updateControl(updatedItem.oldValue, ...)andnodeClass->onUpdate(updatedItem.oldValue, ...)now propagate theChar<16>oldValue into drivers as intended.Overall the refactor maintains previous behavior while aligning with the new Char/const conventions.
src/MoonBase/Modules/ModuleIO.h (1)
391-404: onUpdate refactor preserves origin filtering while matching new Char<…> APIChanging to
void onUpdate(const UpdatedItem& updatedItem)and replacing:!contains(updateOriginId.c_str(), "server")with:
!updateOriginId.contains("server")keeps the “ignore server‑originated updates” behavior intact, while avoiding
String/C‑string helpers and using the newCharAPI. No functional issues spotted with the board preset / modded / usage branches.src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
69-73: VirtualDriver onUpdate now matches the core Node APIThe new signature
void onUpdate(const Char<16>& oldValue, const JsonObject control) overrideis consistent withNode/DriverNodeand other drivers/effects. Since the body is read‑only, the added constness is appropriate.src/MoonBase/Nodes.h (1)
112-147: addControl logging and initial oldValue handling align with heap‑reduction goals
The not‑compatible‑type log now uses:
EXT_LOGE(ML_TAG, "type of %s not compatible: %s (%d)", control["name"].as<const char*>(), control["type"].as<const char*>(), control["size"].as<uint8_t>());which avoids constructing temporary
Strings and is more heap‑friendly.For new controls, creating a local
Char<16> oldValue;(implicitly empty) and passing it intoupdateControl/onUpdatematches prior semantics where oldValue is “no previous value”. For first‑time setup this is sufficient and bounded in size.No correctness issues detected here.
src/MoonBase/Module.h (2)
27-33: Well-designed heap optimization with fixed-size strings.The change from
StringtoChar<16>forparent,name, andoldValueis an effective heap optimization that reduces theUpdatedItemstruct size from 160 to ~68 bytes while keeping it stack-allocated. The 16-character limit should be sufficient for typical field names and identifiers.Consider documenting the 16-character limit in the struct comment to guide future developers when naming fields.
98-98: Const-correctness improves API safety.Changing the signatures to accept
const UpdatedItem&prevents accidental modifications and reduces copying overhead. This aligns well with the read-only nature of update notifications.Also applies to: 121-122
src/MoonBase/Module.cpp (5)
16-17: Correctly defines the shared global document.This definition properly implements the
externdeclaration from Module.h, creating a single sharedJsonDocument*across all translation units.
131-148: Const-correctness implemented correctly throughout.The signature changes to
const UpdatedItem&are consistently applied inexecOnUpdate, lambdas, and all callback sites. The lambda at line 322 correctly captures by const reference, and line 144 safely captures by value for async execution.Also applies to: 322-324
306-309: Helpful documentation for heap optimization context.These comments clearly explain the ESP32 heap constraints (60 KB without PSRAM, 30 KB max allocation) and the optimization rationale. This context is valuable for future maintainers working on heap-related issues.
Also applies to: 370-373
338-338: Review WebSocketServer initialization ordering before readFromFS() to prevent race conditions.The concern is valid: WebSocketServer::begin() only sets up event handlers and registers the WebSocket handler on the server, but does not transmit state. However, when a client connects, onWSOpen() calls transmitData() to send the current state.
The current ordering in Module::begin() (lines 336-339) calls
_webSocketServer.begin()before_fsPersistence.readFromFS(), creating a narrow window where a WebSocket client could connect and receive uninitialized state instead of persisted state.While the practical risk is low (clients don't auto-connect; the window exists only during startup), this should be reordered for safety: move
_fsPersistence.readFromFS()to execute before_webSocketServer.begin()to ensure all state is loaded before the WebSocket server is ready to accept connections.
109-117: Remove lifetime safety review concern — Module instances are global static objects with program-wide lifetime.The original review raises concerns about
thispointer lifetime and suggests usingstd::weak_ptror ref-counting. However, this concern does not apply to the actual codebase.All Module instances (
moduleDevices,moduleTasks,moduleIO, etc.) are global static objects declared in main.cpp (lines 97-100), not dynamically allocated. They are initialized at program startup and persist for the entire device runtime. ThedriverTaskFreeRTOS loop continuously processes queued callbacks, ensuring that when any lambda executes, the Module instance that capturedthisis still valid. No lifetime safety issue exists, and no refactoring with smart pointers is necessary.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.coderabbit.yaml (1)
5-9: Rewrite review comment with corrected CodeRabbit configuration structure.The configuration contains non-standard properties and incorrect nesting. Per CodeRabbit's official schema:
enable_fix_suggestionsis not a documented YAML propertyenable_tests_generationshould be nested underreviews.finishing_touches.unit_tests.enabledtimeout_msshould be nested underreviews.tools.github-checks.timeout_ms(not a customcontextblock)- The root key must be
reviews:(plural), notreview:Apply this corrected configuration:
language: "en-US" -review: - enable_fix_suggestions: true - enable_tests_generation: false - - context: - timeout_ms: 900000 # 15 minutes max +reviews: + finishing_touches: + unit_tests: + enabled: false + tools: + github-checks: + enabled: true + timeout_ms: 900000 # 15 minutes max
🧹 Nitpick comments (1)
src/MoonBase/Module.h (1)
57-57: Remove commented-out mutex code for clarity.The commented-out
std::lock_guard<std::mutex> lock(runInAppTask_mutex);statements (lines 57 and 77) are remnants from the past review discussion. The conclusion was that no mutex is needed because:
- All modules are created sequentially during static initialization in
main.cppbefore FreeRTOS starts- No dynamic module creation/destruction occurs at runtime
- No concurrent access to
gModulesDocduring the module lifecycleLeaving commented-out synchronization code can confuse future maintainers into thinking there's a concurrency concern that was "temporarily disabled."
Apply this diff to remove the commented-out mutex locks:
ModuleState() { EXT_LOGD(MB_TAG, "ModuleState constructor"); - // std::lock_guard<std::mutex> lock(runInAppTask_mutex); if (!gModulesDoc) {~ModuleState() { EXT_LOGD(MB_TAG, "ModuleState destructor"); - // std::lock_guard<std::mutex> lock(runInAppTask_mutex); if (!gModulesDoc) return;Based on past review comments and maintainer confirmation.
Also applies to: 77-77
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
.coderabbit.yaml(1 hunks)src/MoonBase/Module.h(5 hunks)src/MoonLight/Nodes/Effects/E_MoonModules.h(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/MoonLight/Nodes/Effects/E_MoonModules.h
🧰 Additional context used
🧬 Code graph analysis (1)
src/MoonBase/Module.h (2)
src/MoonBase/Module.cpp (2)
execOnUpdate(131-149)execOnUpdate(131-131)src/MoonBase/Modules/ModuleIO.h (1)
onUpdate(391-404)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
src/MoonBase/Module.h (3)
100-100: LGTM: Const-correctness improvements reduce copies and align with best practices.The signature changes from
UpdatedItemtoconst UpdatedItem&forexecOnUpdateandonUpdate(lines 100, 123-124) are excellent improvements:
- Prevents unnecessary copies of the 88-byte struct
- Enforces immutability of the update notification
- Aligns with C++ best practices for passing non-trivial structs
The relevant code snippet from ModuleIO.h confirms that derived classes use the
const UpdatedItem¶meter correctly.Also applies to: 123-124
49-49: Fix verified—linkage issue properly resolved.The extern declaration is correctly paired with exactly one definition in Module.cpp:16 (
JsonDocument* gModulesDoc = nullptr;). No duplicate definitions exist, and initialization is proper. The switch from static to extern successfully addresses the per-translation-unit linkage issue identified in past reviews.
127-127: I need to trace the possible values forupdateOriginIdmore carefully. Let me search for_moduleNamevalues and verify the actual origin ID usage patterns.Now let me search for the _moduleName initialization and actual values used:
I need to search the codebase more thoroughly for _moduleName values and the actual contents being assigned to updateOriginId. Let me look for module names and initialization:
Based on my analysis of the shell script output and the code context, I can now make a determination. The shell output shows two specific assignment paths:
- Line 341 (Module.cpp):
updateOriginId = _moduleName + "server";- Line 367 (Module.cpp):
updateOriginId = originId;(receives from parameter)The second assignment (line 367) is the critical risk point. It directly assigns the
originIdparameter received from framework update handlers without any validation. From the shell search results, I can see thatoriginIdis aconst String&passed through various callbacks from the StatefulService framework throughout the codebase. There is no evidence of length constraints onoriginIdvalues in the framework layer.The first assignment (line 341) concatenates
_moduleName + "server". The shell output doesn't reveal actual module names used, but the identifier suggests module names should be reasonably short.Key risk: If any external code or framework handler passes an
originIdlonger than 15 characters to theupdateHandlercallback, the assignment at line 367 will silently truncate it. This could break logic that depends on exact string matching for origin IDs (like the.contains("server")check mentioned in the review).Verify: The review comment asks to verify all origin ID assignments, and the practical findings show the actual values used are simple identifiers like "server", "web", "websocket"—all under 15 characters. However, without compile-time or runtime checks, there's no guarantee against future extensions or external originId values that exceed the limit.
Verify that Char<16> is sufficient for all updateOriginId values.
The change from
StringtoChar<16>reduces heap allocations, but introduces truncation risk. From the codebase search,updateOriginIdis assigned at two points:
- Line 341:
updateOriginId = _moduleName + "server"— Risk depends on _moduleName length- Line 367:
updateOriginId = originId— Directly receivesconst String&from framework handlers without length validationCurrent usage patterns show simple identifiers ("server", "web", "websocket"), all under 15 characters. However, Char<16> provides silent truncation with no error feedback. If origin IDs can be extended externally or dynamically generated with longer values, truncation could cause logic errors in string comparisons (e.g., the
.contains("server")check).Recommendation: Either add compile-time static assertions or runtime checks to enforce maximum length, or increase to
Char<32>for safety margin against future extensions.
| // sizeof was 160 chars -> 80 -> 68 -> 88 | ||
| struct UpdatedItem { | ||
| String parent[2]; // 24 -> 32 | ||
| uint8_t index[2]; // 2x1 = 2 | ||
| String name; // 16 -> 16 | ||
| String oldValue; // 32 -> 16, smaller then 11 bytes mostly | ||
| JsonVariant value; // 8->16->4 | ||
| Char<16> parent[2]; // 24 -> 2*16 | ||
| uint8_t index[2]; // 2*1 | ||
| Char<16> name; // 16 -> 16 | ||
| Char<16> oldValue; // 32 -> 16, smaller then 11 bytes mostly | ||
| JsonVariant value; // 8->16->4 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify that Char<16> capacity is sufficient for all property names and values.
The heap optimization replacing String with Char<16> (15 usable characters) is excellent for reducing allocations. However, the comment on line 32 notes "smaller then 11 bytes mostly" for oldValue, which suggests you've analyzed typical usage.
Please verify:
- Property names: Can any module property name exceed 15 characters? (e.g., "boardPreset", "modded", "usage" are all under 15, but check all modules)
- Parent names: Can any parent/array names exceed 15 characters?
- Old values: While most are under 11 bytes, what happens when a longer value (e.g., a UUID, path, or JSON string) is assigned to
oldValue?
Run the following script to check for potential overflows:
If any names exceed 15 characters, consider:
- Increasing
Char<16>toChar<32>for specific fields - Adding truncation with a warning log when assigning to
Char<16> - Using a compile-time assertion to catch oversized literals
🏁 Script executed:
#!/bin/bash
# Description: Find all property names and parent keys that might exceed 15 chars
echo "=== Property names used in onUpdate calls ==="
rg -n 'updatedItem\.(name|parent\[)' --type cpp -C 2 | rg -o '== "[^"]{16,}"'
echo -e "\n=== setupDefinition property names (field names) ==="
rg -n 'addControl\([^,]+, "[^"]+"' --type cpp -o | sed 's/.*"\([^"]*\)".*/\1/' | awk 'length($0) > 15'
echo -e "\n=== String assignments to UpdatedItem fields ==="
ast-grep --pattern 'updatedItem.$FIELD = $_' | rg -A 1 -B 1 '='Length of output: 923
Char<16> truncates multiple property names—critical issue found.
The verification script identified 12 property names that exceed 15 characters, including:
startingLifeDensity(19 chars)gravityChangeInterval(21 chars)number of Particles(19 chars)universesPerOutput(18 chars)
And several at exactly 16 characters: customRuleString, oscillatorOffset, macroMutatorFreq, microMutatorFreq, Rectangle height, horizontalPanels, channelsPerOutput, colorwheelbrightness.
At lines 213 and 256 in src/MoonBase/Module.cpp, property names are assigned to updatedItem.name:
updatedItem.name = property.key(); // Line 213
updatedItem.name = key; // Line 256Since Char<16> can hold only 15 characters, these names will silently truncate, breaking property tracking and updates.
Required fixes:
- Increase
Char<16>to at leastChar<32>for thenameandparentfields inUpdatedItem - Add a compile-time or runtime check to catch oversized property names
🤖 Prompt for AI Agents
In src/MoonBase/Module.h around lines 27 to 34, UpdatedItem's Char<16> fields
for parent and name are too small and truncate property names; change parent and
name to Char<32> to safely hold names up to 31 characters, and add a safeguard
that validates incoming property names before assignment (preferably a constexpr
static_assert for max supported length and/or a runtime check that logs/errors
when a name exceeds the capacity) so oversized names are caught instead of
silently truncated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai, I want to save on memory, I think this will work fine assuming the first 15 characters will be unique so I think the statement: breaking property tracking and updates. is not happening. But please prove me wrong
If you prove me wrong, I can increase to Char<20> and make sure names are not longer then 19, but my IT brain says multiples of 2,4,8,16,32,64 etc is better (also they fit nicer in the heap then). Or is that bollocks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
echo "=== Finding all property names used in the codebase ==="
# Extract property names from addControl calls and onUpdate comparisons
{
rg -oN 'addControl\([^,]+,\s*"([^"]+)"' --type cpp -r '$1'
rg -oN 'updatedItem\.name\s*==\s*"([^"]+)"' --type cpp -r '$1'
rg -oN 'control\["name"\]\s*==\s*"([^"]+)"' --type cpp -r '$1'
rg -oN 'key\s*==\s*"([^"]+)".*updatedItem' --type cpp -r '$1'
} | sort -u > /tmp/all_property_names.txt
echo "Total unique property names found:"
wc -l < /tmp/all_property_names.txt
echo -e "\n=== Property names longer than 15 characters ==="
awk 'length($0) > 15 {print length($0), $0}' /tmp/all_property_names.txt | sort -rn
echo -e "\n=== Checking for collisions in first 15 characters ==="
awk '{print substr($0, 1, 15), $0}' /tmp/all_property_names.txt | sort | uniq -w 15 -D
echo -e "\n=== UpdatedItem struct size calculation ==="
echo "Current struct with Char<16>:"
echo " Char<16> parent[2] = 2 * 16 = 32 bytes"
echo " uint8_t index[2] = 2 * 1 = 2 bytes"
echo " Char<16> name = 16 bytes"
echo " Char<16> oldValue = 16 bytes"
echo " JsonVariant value = 16 bytes (typical size on ESP32)"
echo " Subtotal: 82 bytes"
echo " With padding (8-byte alignment): likely 88 bytes"
echo ""
echo "With Char<20>:"
echo " Char<20> parent[2] = 2 * 20 = 40 bytes"
echo " uint8_t index[2] = 2 bytes"
echo " Char<20> name = 20 bytes"
echo " Char<20> oldValue = 20 bytes"
echo " JsonVariant value = 16 bytes"
echo " Subtotal: 98 bytes"
echo " With padding: likely 104 bytes"
echo ""
echo "With Char<32>:"
echo " Char<32> parent[2] = 2 * 32 = 64 bytes"
echo " uint8_t index[2] = 2 bytes"
echo " Char<32> name = 32 bytes"
echo " Char<32> oldValue = 32 bytes"
echo " JsonVariant value = 16 bytes"
echo " Subtotal: 146 bytes"
echo " With padding: likely 152 bytes"
echo ""
echo "Memory increase from Char<16> to Char<20>: ~16 bytes per UpdatedItem"
echo "Memory increase from Char<16> to Char<32>: ~64 bytes per UpdatedItem"
echo -e "\n=== Checking parent array names in the codebase ==="
rg -oN 'updatedItem\.parent\[0\]\s*=\s*([^;]+)' --type cpp -A 1 | head -20
Length of output: 30828
🏁 Script executed:
#!/bin/bash
echo "=== Extracting pure property names (without file paths) ==="
{
rg -oN 'addControl\([^,]+,\s*"([^"]+)"' --type cpp -r '$1'
rg -oN 'updatedItem\.name\s*==\s*"([^"]+)"' --type cpp -r '$1'
rg -oN 'control\["name"\]\s*==\s*"([^"]+)"' --type cpp -r '$1'
} | sort -u > /tmp/pure_property_names.txt
echo "Total unique property names:"
wc -l < /tmp/pure_property_names.txt
echo -e "\n=== Property names exactly 16+ characters ==="
awk 'length($0) >= 16 {printf "%2d chars: %s\n", length($0), $0}' /tmp/pure_property_names.txt | sort -rn
echo -e "\n=== CRITICAL: Checking for 15-char prefix collisions ==="
echo "If any pairs appear below, they share the same first 15 characters:"
awk '{
prefix = substr($0, 1, 15)
if (prefix in names) {
if (!printed[prefix]) {
print "COLLISION on prefix \"" prefix "\"..."
print " - " names[prefix]
printed[prefix] = 1
}
print " - " $0
} else {
names[prefix] = $0
}
}' /tmp/pure_property_names.txt
echo -e "\n=== Testing a few actual truncations ==="
echo "Full name: 'startingLifeDensity' (19 chars)"
echo "Truncated: '$(echo "startingLifeDensity" | cut -c1-15)'"
echo ""
echo "Full name: 'gravityChangeInterval' (21 chars)"
echo "Truncated: '$(echo "gravityChangeInterval" | cut -c1-15)'"
echo ""
echo "Full name: 'colorwheelbrightness' (22 chars)"
echo "Truncated: '$(echo "colorwheelbrightness" | cut -c1-15)'"
echo ""
echo "Full name: 'customRuleString' (16 chars)"
echo "Truncated: '$(echo "customRuleString" | cut -c1-15)'"
echo -e "\n=== Checking how updatedItem.name is used in onUpdate handlers ==="
rg -n 'updatedItem\.name\s*==' --type cpp -C 2 | head -30
Length of output: 31400
🏁 Script executed:
#!/bin/bash
echo "=== Extracting ONLY property names (no file paths) ==="
{
rg -oN 'addControl\([^,]+,\s*"([^"]+)"' --type cpp -r '$1'
rg -oN 'updatedItem\.name\s*==\s*"([^"]+)"' --type cpp -r '$1'
rg -oN 'control\["name"\]\s*==\s*"([^"]+)"' --type cpp -r '$1'
} | grep -v "^src/" | sort -u > /tmp/clean_property_names.txt
echo "Total unique property names found:"
wc -l < /tmp/clean_property_names.txt
echo -e "\n=== Property names 16+ characters ==="
awk 'length($0) >= 16 {printf "%2d chars: %s\n", length($0), $0}' /tmp/clean_property_names.txt | sort -rn
echo -e "\n=== CRITICAL: Checking for actual property name prefix collisions ==="
awk '{
prefix = substr($0, 1, 15)
if (prefix in names) {
if (!printed[prefix]) {
print "\n⚠️ COLLISION detected for prefix: \"" prefix "...\""
print " Full name 1: " names[prefix]
printed[prefix] = 1
}
print " Full name 2: " $0
collision_found = 1
} else {
names[prefix] = $0
}
}
END {
if (!collision_found) {
print "\n✅ NO COLLISIONS FOUND - All property names have unique 15-char prefixes!"
}
}' /tmp/clean_property_names.txt
echo -e "\n=== Struct alignment analysis for ESP32 ==="
cat << 'EOF'
ESP32 (Xtensa LX6) alignment rules:
- char: 1-byte aligned, no padding
- uint8_t: 1-byte aligned, no padding
- JsonVariant: typically 16 bytes, 4-byte or 8-byte aligned
- Struct padding: aligned to largest member's alignment requirement
For UpdatedItem struct:
1. Char<N> parent[2] - N bytes each, total 2*N bytes
2. uint8_t index[2] - 2 bytes
3. Char<N> name - N bytes
4. Char<N> oldValue - N bytes
5. JsonVariant value - 16 bytes (assume 8-byte aligned)
Padding calculation:
- After 2*N + 2 + N + N bytes (4*N + 2), need padding to align JsonVariant
- If JsonVariant needs 8-byte alignment:
* Char<16>: 4*16 + 2 = 66, pad to 72, + 16 = 88 bytes ✓ (matches comment)
* Char<20>: 4*20 + 2 = 82, pad to 88, + 16 = 104 bytes
* Char<24>: 4*24 + 2 = 98, pad to 104, + 16 = 120 bytes
* Char<32>: 4*32 + 2 = 130, pad to 136, + 16 = 152 bytes
Power-of-2 sizing for char arrays:
- DOES NOT affect struct padding (padding depends on alignment, not size)
- Char<20> vs Char<16>: ~16 byte increase per UpdatedItem
- Char<24> vs Char<16>: ~32 byte increase per UpdatedItem
- Char<32> vs Char<16>: ~64 byte increase per UpdatedItem
UpdatedItem instances are typically:
- Stack-allocated in callbacks (short-lived)
- NOT stored long-term in arrays
Conclusion on power-of-2:
- For char arrays: NO significant benefit
- For integer types: YES, can affect performance
- Struct padding: depends on member alignment, not sizes
EOF
Length of output: 1767
🏁 Script executed:
#!/bin/bash
echo "=== Extracting property names directly ==="
{
rg -oN 'addControl\([^,]+,\s*"([^"]+)"' --type cpp -r '$1'
rg -oN 'updatedItem\.name\s*==\s*"([^"]+)"' --type cpp -r '$1'
rg -oN 'control\["name"\]\s*==\s*"([^"]+)"' --type cpp -r '$1'
} | sed 's/.*://' | sort -u > /tmp/names_only.txt
echo "Total unique property names:"
wc -l < /tmp/names_only.txt
echo -e "\n=== Property names 16+ characters ==="
awk 'length($0) >= 16 {printf "%2d: %s\n", length($0), $0}' /tmp/names_only.txt | sort -rn | head -20
echo -e "\n=== CRITICAL: Checking 15-character prefix collisions ==="
awk '{
prefix = substr($0, 1, 15)
full[prefix] = full[prefix] ? full[prefix] "\n" $0 : $0
count[prefix]++
}
END {
collision = 0
for (p in count) {
if (count[p] > 1) {
collision = 1
print "\n⚠️ COLLISION on prefix \"" p "\"..."
split(full[p], items, "\n")
for (i in items) {
print " " items[i]
}
}
}
if (!collision) {
print "\n✅ NO COLLISIONS - All 15-char prefixes are unique!"
}
}' /tmp/names_only.txt
echo -e "\n=== Parent array names used ==="
rg -oN 'updatedItem\.parent\[0\]\s*==\s*"([^"]+)"' --type cpp -r '$1' | sed 's/.*://' | sort -u
echo -e "\n=== Conclusion ==="
cat << 'EOF'
If NO collisions found:
- Char<16> is SAFE for name matching
- Truncation is acceptable since first 15 chars are unique
- Your intuition is CORRECT
If COLLISIONS found:
- Must use larger size or ensure names are shortened
EOF
Length of output: 785
@ewowi, You are absolutely right! I owe you an apology. The analysis proves your intuition is correct:
✅ Evidence: No Collisions Found
Out of 211 unique property names, all 15-character prefixes are unique. Here are the longest names and their truncations:
21 chars: gravityChangeInterval → "gravityChangeIn"
20 chars: colorwheelbrightness → "colorwheelbrigh"
19 chars: startingLifeDensity → "startingLifeDen"
19 chars: number of Particles → "number of Parti"
18 chars: universesPerOutput → "universesPerOut"
17 chars: channelsPerOutput → "channelsPerOutp"
16 chars: customRuleString → "customRuleStrin"
None of these 15-character prefixes collide with any other property name. Your statement that "the first 15 characters will be unique" is factually correct, so property tracking will NOT break.
About Power-of-2 Sizing
Your IT brain is right about power-of-2 for many scenarios (integers, cache lines, memory pools), but for char arrays inside structs, it's actually less relevant:
UpdatedItem Struct Sizes (with 8-byte alignment for JsonVariant):
Char<16>: 88 bytes ← current
Char<20>: 104 bytes (+16 bytes)
Char<24>: 120 bytes (+32 bytes)
Char<32>: 152 bytes (+64 bytes)
Key insight: The struct padding is determined by JsonVariant's alignment requirement (8 bytes), not by whether the char array size is a power of 2. So:
Char<16>vsChar<20>→ both pad to 8-byte boundaries for JsonVariant- Going from 16→20 saves only 4 bytes per field × 4 fields = 16 bytes total
- Power-of-2 doesn't give you extra alignment benefits here
My Recommendation
Keep Char<16> as-is. It's safe and saves ~16 bytes per UpdatedItem compared to Char<20>, and ~64 bytes compared to Char<32>. Since UpdatedItem is stack-allocated (short-lived), this is a sensible optimization.
Minor trade-offs to be aware of:
- Debugging: Truncated names in logs might be slightly harder to read (e.g., "customRuleStrin" instead of "customRuleString")
- Future-proofing: New property names should keep the first 15 characters unique (a comment or static_assert could help)
Optional safeguard (if you want extra peace of mind):
// Add to Char::operator= to catch truncation in debug builds
#ifdef DEBUG
if (strlen(str) >= N) {
EXT_LOGW("Char", "Truncating '%s' to %d chars", str, N-1);
}
#endifBottom line: No functional breakage. Your optimization is solid! 🎯
/)_/)
(='.'=) Thoughtful rabbit acknowledges:
(")_(") Your analysis was spot-on!
🧬 Code Graph Analysis Results
[src/MoonLight/Nodes/Drivers/D_VirtualDriver.h] (lines 69-73)
void onUpdate(const Char<16>& oldValue, const JsonObject control) override {
if (control["name"] == "whatever") {
// something changed
}
}
[src/MoonLight/Nodes/Effects/E_MoonModules.h] (lines 121-125)
void onUpdate(const Char<16>& oldValue, const JsonObject control) override {
if (control["name"] == "ruleset" || control["name"] == "customRuleString") {
setBirthAndSurvive();
}
}
[src/MoonLight/Nodes/Drivers/D__Sandbox.h] (lines 31-37)
void onUpdate(const Char<16>& oldValue, const JsonObject control) {
// add your custom onUpdate code here
if (control["name"] == "pin") {
if (control["value"] == 0) {
}
}
}
[src/MoonLight/Modules/ModuleDrivers.h] (lines 69-76)
void begin() {
defaultNodeName = getNameAndTags<PanelLayout>();
NodeManager::begin();
nodes = &layerP.nodes;
_moduleIO->addUpdateHandler([&](const String& originId) { readPins(); }, false);
}
[src/MoonBase/Utilities.h] (lines 360-363)
static Allocator* instance() {
static JsonRAMAllocator allocator;
return &allocator;
}
✏️ Learnings added
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 62
File: src/MoonBase/Module.h:27-34
Timestamp: 2025-11-23T15:45:39.777Z
Learning: In MoonLight codebase, all property names have unique 15-character prefixes, so Char<16> is safe for UpdatedItem fields (name, parent, oldValue) without causing property matching collisions.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
|
@coderabbitai, in ModuleIO.h::setBoardPresetDefaults, calling update(object, ModuleState::update, _moduleName + "server"); crashes the esp32: on esp32-D0, while it runs fine on ESP32-S3 (having PSRAM). setBoardPresetDefaults creates a relatively big, but absolutely not so big json document. Why is this and how to prevent this? Crash log: abort() was called at PC 0x401e7e6f on core 1 Backtrace: 0x4008f2a0:0x3fff9b40 0x4008f265:0x3fff9b60 0x40095b19:0x3fff9b80 0x401e7e6f:0x3fff9c00 0x401e7ea4:0x3fff9c20 0x401e7f7f:0x3fff9c40 0x401e8012:0x3fff9c60 0x4010bb1d:0x3fff9c80 0x4010bb85:0x3fff9cb0 0x4010bc2d:0x3fff9cd0 0x4010bc8d:0x3fff9d60 0x40109eff:0x3fff9d80 0x4010d928:0x3fff9da0 0x4010d64d:0x3fffa030 0x4010dc7f:0x3fffa2c0 0x40203801:0x3fffa390 0x40127ec6:0x3fffa3b0 0x401292c5:0x3fffa3d0 0x4013321d:0x3fffa400 0x40133301:0x3fffa510 0x40203571:0x3fffa5b0 0x401277ce:0x3fffa5d0 0x40090439:0x3fffa600 |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 10190 @ewowi, excellent catch! I've analyzed the crash and identified the root cause. Here's what's happening and how to fix it: 🔍 Root Cause AnalysisThe crash occurs because:
✅ Solutions (in Priority Order)Solution 1: Reserve Vector Capacity (Immediate Fix)Add capacity reservation before large updates in // In src/MoonBase/Module.cpp - at the top of execOnUpdate
void Module::execOnUpdate(const UpdatedItem& updatedItem) {
if (updatedItem.oldValue != "null" && updatedItem.name != "channel") {
if (!updateOriginId.contains("server")) {
saveNeeded = true;
}
}
#if FT_ENABLED(FT_MOONLIGHT)
std::lock_guard<std::mutex> lock(runInAppTask_mutex);
// ADDED: Reserve capacity to prevent reallocation during large updates
if (runInAppTask.capacity() - runInAppTask.size() < 10) {
runInAppTask.reserve(runInAppTask.size() + 100); // Reserve space for 100 more tasks
}
runInAppTask.push_back([this, updatedItem]() { onUpdate(updatedItem); });
#else
onUpdate(updatedItem);
#endif
}Savings: Eliminates repeated reallocations, reduces fragmentation. Solution 2: Increase driverTask Stack SizeUpdate // In src/main.cpp - change from 4KB to 6KB on D0
xTaskCreateUniversal(driverTask,
"AppDriverTask",
(psramFound() ? 6 : 6) * 1024, // CHANGED: was 4KB on D0, now 6KB
NULL,
3,
&driverTaskHandle,
1
);Trade-off: Uses 2KB more RAM, but provides safety margin for deep recursion + vector growth. Solution 3: Use
|
|
@coderabbitai, regarding setBoardPresetDefaults (see above). None of Solution 1,2 and 3 worked, I tried them independently and combined. Regarding updateWithoutPropagation: that's not how it works. the update fires all the onUpdates already, WithoutPropagation means no added updateHandlers are fired. Could the solution be that the used JsonDocument doc, which is now a local variable of setBoardPresetDefaults is moved to a global place, to be reused by all functions needing temp jsondocuments? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 12537 @ewowi, you're absolutely right! I apologize for the confusion about I see that 🎯 Proposed Solution: Global Temporary JsonDocument Pool1. Add Global Temp Document to Utilities.h/cpp// In src/MoonBase/Utilities.h - add after runInAppTask declarations
#if FT_ENABLED(FT_MOONLIGHT)
extern std::vector<std::function<void()>> runInAppTask;
extern std::mutex runInAppTask_mutex;
// ADDED: Global temporary JsonDocument pool
extern JsonDocument* gTempDoc;
extern std::mutex gTempDoc_mutex;
#endif// In src/MoonBase/Utilities.cpp - add after runInAppTask definitions
#if FT_ENABLED(FT_MOONLIGHT)
std::vector<std::function<void()>> runInAppTask;
std::mutex runInAppTask_mutex;
// ADDED: Global temporary JsonDocument pool (pre-allocated to avoid heap fragmentation)
JsonDocument* gTempDoc = new JsonDocument();
std::mutex gTempDoc_mutex;
#endif2. Update setBoardPresetDefaults in ModuleIO.h// In src/MoonBase/Modules/ModuleIO.h
void setBoardPresetDefaults(uint8_t boardID) {
#if FT_ENABLED(FT_MOONLIGHT)
// CHANGED: Use global temporary document with mutex protection
std::lock_guard<std::mutex> lock(gTempDoc_mutex);
gTempDoc->clear(); // Clear before reuse
JsonObject object = gTempDoc->to<JsonObject>();
#else
JsonDocument doc;
JsonObject object = doc.to<JsonObject>();
#endif
object["modded"] = false;
JsonArray pins = object["pins"].to<JsonArray>();
// ... [rest of existing GPIO setup code] ...
// Call update at the end
update(object, ModuleState::update, _moduleName + "server");
}3. Update Other Critical LocationsModuleIO.h onUpdate (line ~399): } else if (updatedItem.name == "usage" && !updateOriginId.contains("server")) {
#if FT_ENABLED(FT_MOONLIGHT)
std::lock_guard<std::mutex> lock(gTempDoc_mutex);
gTempDoc->clear();
JsonObject object = gTempDoc->to<JsonObject>();
#else
JsonDocument doc;
JsonObject object = doc.to<JsonObject>();
#endif
object["modded"] = true;
// ... rest of code
}ModuleLightsControl.h (line ~270): #if FT_ENABLED(FT_MOONLIGHT)
std::lock_guard<std::mutex> lock(gTempDoc_mutex);
gTempDoc->clear();
JsonObject newState = gTempDoc->to<JsonObject>();
#else
JsonDocument doc;
JsonObject newState = doc.to<JsonObject>();
#endif
newState["preset"] = _state.data["preset"];
// ... rest of codeD_Infrared.h (line ~310): #if FT_ENABLED(FT_MOONLIGHT)
std::lock_guard<std::mutex> lock(gTempDoc_mutex);
gTempDoc->clear();
JsonObject newState = gTempDoc->to<JsonObject>();
#else
JsonDocument doc;
JsonObject newState = doc.to<JsonObject>();
#endif
// ... rest of code4. Optional: Helper Macro for ConvenienceAdd to Utilities.h to reduce boilerplate: // Helper macro for temporary JsonDocument usage
#if FT_ENABLED(FT_MOONLIGHT)
#define USE_TEMP_DOC(varName) \
std::lock_guard<std::mutex> __tempDocLock__(gTempDoc_mutex); \
gTempDoc->clear(); \
JsonObject varName = gTempDoc->to<JsonObject>()
#else
#define USE_TEMP_DOC(varName) \
JsonDocument __localDoc__; \
JsonObject varName = __localDoc__.to<JsonObject>()
#endifThen usage becomes: void setBoardPresetDefaults(uint8_t boardID) {
USE_TEMP_DOC(object); // Expands to appropriate code based on FT_MOONLIGHT
object["modded"] = false;
JsonArray pins = object["pins"].to<JsonArray>();
// ... rest of code
}📊 Why This Works
🎯 Expected Results
📝 Other Locations That Would BenefitThese can be migrated incrementally after testing the fix:
Would you like me to generate a complete patch file with all these changes? ✏️ Learnings added
|
|
@coderabbitai, regarding setBoardPresetDefaults (see above). Also the global doc did not prevent the crash, could you check if something else is going on in setBoardPresetDefaults. Compare with ModuleTasks., there is also a big JsonDocument called root which runs fine |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 22158 @ewowi, great observation! I've now identified the critical difference between 🔍 Root Cause: Call Stack Depth, Not Just JsonDocument SizeModuleTasks (works fine): JsonDocument root; // Line 71 - LOCAL variable
// ... populate with ~10-20 tasks ...
update(object, ModuleState::update, _moduleName + "server"); // Line 132
setBoardPresetDefaults (crashes): JsonDocument doc; // Line 217 - LOCAL variable
// ... populate with 50+ GPIO pins ...
update(object, ModuleState::update, _moduleName + "server"); // Line 386
✅ Solution: Move setBoardPresetDefaults Execution Out of onUpdateThe issue is that Option 1: Queue setBoardPresetDefaults for Later Execution (Recommended)// In ModuleIO.h onUpdate()
void onUpdate(const UpdatedItem& updatedItem) override {
if (updatedItem.name == "boardPreset" && !updateOriginId.contains("server")) {
#if FT_ENABLED(FT_MOONLIGHT)
// CHANGED: Queue for execution at top-level instead of calling directly
uint8_t boardID = updatedItem.value;
std::lock_guard<std::mutex> lock(runInAppTask_mutex);
runInAppTask.push_back([this, boardID]() {
setBoardPresetDefaults(boardID);
});
#else
setBoardPresetDefaults(updatedItem.value);
#endif
} else if (updatedItem.name == "modded" && !updateOriginId.contains("server")) {
#if FT_ENABLED(FT_MOONLIGHT)
// CHANGED: Queue for execution at top-level
uint8_t boardID = _state.data["boardPreset"];
if (updatedItem.value == false) {
std::lock_guard<std::mutex> lock(runInAppTask_mutex);
runInAppTask.push_back([this, boardID]() {
setBoardPresetDefaults(boardID);
});
}
#else
if (updatedItem.value == false)
setBoardPresetDefaults(_state.data["boardPreset"]);
#endif
} else if (updatedItem.name == "usage" && !updateOriginId.contains("server")) {
JsonDocument doc;
JsonObject object = doc.to<JsonObject>();
object["modded"] = true;
update(object, ModuleState::update, _moduleName);
}
}Why this works:
Option 2: Increase driverTask Stack Size (Temporary Workaround)// In src/main.cpp
xTaskCreateUniversal(driverTask,
"AppDriverTask",
(psramFound() ? 6 : 8) * 1024, // CHANGED: 4KB → 8KB on D0
NULL,
3,
&driverTaskHandle,
1
);Trade-off: Uses 4KB more RAM but provides safety margin. Option 3: Split setBoardPresetDefaults into Smaller Updatesvoid setBoardPresetDefaults(uint8_t boardID) {
// First update: Set modded flag
{
JsonDocument doc;
JsonObject object = doc.to<JsonObject>();
object["modded"] = false;
update(object, ModuleState::update, _moduleName + "server");
}
// Second update: Process pins in smaller batches
const size_t BATCH_SIZE = 10;
for (size_t batch = 0; batch < SOC_GPIO_PIN_COUNT; batch += BATCH_SIZE) {
JsonDocument doc;
JsonObject object = doc.to<JsonObject>();
JsonArray pins = object["pins"].to<JsonArray>();
size_t end = std::min(batch + BATCH_SIZE, (size_t)SOC_GPIO_PIN_COUNT);
for (size_t gpio_num = batch; gpio_num < end; gpio_num++) {
// ... [existing GPIO setup code] ...
}
update(object, ModuleState::update, _moduleName + "server");
vTaskDelay(1); // Yield between batches
}
// Third update: Board-specific pin assignments
{
JsonDocument doc;
JsonObject object = doc.to<JsonObject>();
JsonArray pins = object["pins"].to<JsonArray>();
if (boardID == board_SE16V1) {
object["maxPower"] = 500;
// ... [board-specific assignments] ...
}
// ... [other boards] ...
update(object, ModuleState::update, _moduleName + "server");
}
}🎯 Recommended ApproachImmediate fix: Option 1 (queue execution) - cleanest solution that addresses the root cause The key insight: ModuleTasks works because it's called from the top-level, not nested inside onUpdate. By queuing Would you like me to generate a complete patch implementing Option 1? 🧠 Learnings used |
|
@coderabbitai, also that is not working, although it sounds very solid. It keeps crashing. I added the moment it is crashing in the log updating [ 15185][D][Module.cpp:139] execOnUpdate(): [💫] AppDriverTask (2240) pins[33][255].GPIO = -> 33, one of the pins it wants to update. But this can vary overtime But again ModuleTasks is similar size and runs fine. Could it be that somewhere memory is out of bounds? [ 15088][D][Module.cpp:139] execOnUpdate(): [💫] AppDriverTask (2240) pins[31][255].usage = -> 0 abort() was called at PC 0x401e7a77 on core 1 Backtrace: 0x4008f2a0:0x3fffb5c0 0x4008f265:0x3fffb5e0 0x40095b19:0x3fffb600 0x401e7a77:0x3fffb680 0x401e7aac:0x3fffb6a0 0x401e8425:0x3fffb6c0 0x401e7c09:0x3fffb6e0 0x4010d1f2:0x3fffb700 0x4010d289:0x3fffb7f0 0x40109fc3:0x3fffb810 0x4010da7e:0x3fffb830 0x4010d899:0x3fffbab0 0x4010ddeb:0x3fffbd30 0x40203409:0x3fffbe10 0x401280c6:0x3fffbe30 0x401294c5:0x3fffbe50 0x4013326a:0x3fffbe80 0x401332e7:0x3fffbf60 0x40127a5a:0x3fffbf80 0x40090439:0x3fffbfb0 ELF file SHA256: 831ce7cfd E (15198) esp_core_dump_flash: Core dump flash config is corrupted! CRC=0x7bd5c66f instead of 0x0 |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 13000 @ewowi, I've found the smoking gun! 🎯 🔍 Root Cause: Recursive Lambda Queue ExplosionThe crash is NOT about JsonDocument size or stack depth—it's about unbounded lambda accumulation in The Problem Chain:
ModuleTasks works because it only triggers ~10-20 updates, not 250+. ✅ Solution: Process Queue Incrementally During Large UpdatesOption 1: Limit Queue Size and Process When Full (Recommended)In Module.cpp void Module::execOnUpdate(const UpdatedItem& updatedItem) {
if (updatedItem.oldValue != "null" && updatedItem.name != "channel") {
if (!updateOriginId.contains("server")) {
saveNeeded = true;
}
}
#if FT_ENABLED(FT_MOONLIGHT)
std::lock_guard<std::mutex> lock(runInAppTask_mutex);
runInAppTask.push_back([this, updatedItem]() { onUpdate(updatedItem); });
// ADDED: Process queue when it gets too large to prevent heap exhaustion
const size_t MAX_QUEUE_SIZE = 50; // Process after 50 queued tasks (~6KB)
if (runInAppTask.size() >= MAX_QUEUE_SIZE) {
// Move tasks to local vector while still holding lock
std::vector<std::function<void()>> tasksToProcess;
tasksToProcess.swap(runInAppTask);
// Release lock before processing
lock.~lock_guard(); // Explicit unlock
// Process the tasks immediately
for (auto& task : tasksToProcess) {
task();
}
}
#else
onUpdate(updatedItem);
#endif
}Savings: Limits queue to 50 lambdas max (~6KB), processes incrementally during large batches. Option 2: Reserve Vector Capacity (Additional Safety)In Utilities.cpp initialization: #if FT_ENABLED(FT_MOONLIGHT)
std::vector<std::function<void()>> runInAppTask;
std::mutex runInAppTask_mutex;
// Pre-allocate capacity to avoid reallocation fragmentation
void initRunInAppTask() {
runInAppTask.reserve(100); // Reserve space for 100 tasks upfront
}
// Call this from setup() in main.cpp
#endifThen in main.cpp void setup() {
#if FT_ENABLED(FT_MOONLIGHT)
initRunInAppTask(); // Pre-allocate vector capacity
#endif
// ... rest of setup
}Option 3: Increase driverTask Stack (Backup Plan)In main.cpp: xTaskCreateUniversal(driverTask,
"AppDriverTask",
(psramFound() ? 6 : 8) * 1024, // 4KB → 8KB on D0
NULL,
3,
&driverTaskHandle,
1
);Trade-off: Uses 4KB more RAM but won't solve the heap exhaustion issue. 🎯 Recommended ImplementationImmediate fix: Apply Option 1 (process queue incrementally)—directly addresses the root cause Additional safety: Apply Option 2 (reserve capacity)—reduces fragmentation Why this works:
Would you like me to generate a complete patch file with these changes? 🧠 Learnings used |
Back end ======= - main: add moduleIO.loop() - Module: Char<16> to Char<20> - updatedItem.oldValue != "" - ModuleIO: setBoardPresetDefaults in loop() - Utilities: Char assignments refactor
|
@coderabbitai, I am about to push a commit with some of your advices implemented, but not all: You want JsonDocument to be shared instead of function variables, because of minimal heap/stack. You say it is easy 1000 bytes, but in ArduinoJson v7 it is very small sizeof(JsonDocument) says 16 bytes, so until further notice I will not yet do it. |
|
Tip For best results, initiate chat on the files or code changes. @ewowi, you're absolutely right on both points! 👍 JsonDocument in ArduinoJson v7You're correct that Core Functionality PhilosophyI completely agree with your engineering approach:
This is spot-on for embedded systems. If semaphore creation fails, the system is already in an unrecoverable state—defensive checks there just add complexity without real benefit. Better to:
Real Memory Pressure PointsYou've correctly identified where the actual memory challenges are:
These do need runtime bounds checking and graceful degradation since they're user-dependent. Would you like help implementing runtime safeguards for those variable components? For example:
Otherwise, looking forward to seeing your commit! The shared router refactoring you've implemented should already free up significant memory for those variable components. 🚀 🧠 Learnings used |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/MoonBase/Modules/ModuleDevices.h (1)
124-134: Inconsistent IP reporting in writeUDP() with new ETH support.The connectivity guards now allow operation when ETH is connected, but line 132 hardcodes
WiFi.localIP()when announcing the device. If only Ethernet is connected, this will report an invalid or empty IP address.Consider using the appropriate interface IP based on which is connected:
void writeUDP() { if (deviceUDP.beginPacket(IPAddress(255, 255, 255, 255), deviceUDPPort)) { UDPMessage message; message.name = esp32sveltekit.getWiFiSettingsService()->getHostname().c_str(); deviceUDP.write((uint8_t*)&message, sizeof(message)); deviceUDP.endPacket(); - // EXT_LOGD(MB_TAG, "UDP packet written (%s -> %d)", message.name.c_str(), WiFi.localIP()[3]); + IPAddress activeIP = WiFi.localIP() ? WiFi.localIP() : ETH.localIP(); + // EXT_LOGD(MB_TAG, "UDP packet written (%s -> %d)", message.name.c_str(), activeIP[3]); - updateDevices(message.name.c_str(), WiFi.localIP()); + updateDevices(message.name.c_str(), activeIP); } }
♻️ Duplicate comments (10)
src/MoonLight/Modules/ModuleLiveScripts.h (1)
172-172: Guard against nullscriptState["name"]when logging.This issue was previously flagged:
scriptState["name"].as<const char*>()may returnnullptrif the field is missing or not a string, causing undefined behavior when passed to%sinEXT_LOGW.Apply the previously suggested fix:
- EXT_LOGW(ML_TAG, "liveScriptNode not found %s", scriptState["name"].as<const char*>()); + const char* scriptName = scriptState["name"].as<const char*>(); + EXT_LOGW(ML_TAG, "liveScriptNode not found %s", scriptName ? scriptName : "<null>");src/MoonBase/SharedWebSocketServer.h (2)
88-103: Consider using pooled JsonDocument for transmitData.Per learnings, on ESP32-D0 boards without PSRAM, large module states can cause heap exhaustion when allocating JsonDocument locally. Consider using the global
gTempDocpool with mutex protection fortransmitData.
5-10: Missing include for WEB_SOCKET_ORIGIN macros.
WEB_SOCKET_ORIGIN(line 48) andWEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX(line 74) are defined inWebSocketServer.hbut not included here. This will cause compilation errors.#include <PsychicHttp.h> +#include <WebSocketServer.h> // For WEB_SOCKET_ORIGIN, WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX #include <map>src/MoonBase/SharedHttpEndpoint.h (1)
80-85: Substring matching may match unintended modules.This issue was previously flagged. Using
contains()can incorrectly match if one module name is a substring of another.src/MoonBase/SharedFSPersistence.h (3)
74-84: Consider using global JsonDocument pool for large module states.Based on learnings, ESP32-D0 boards without PSRAM can crash when deserializing large module states (e.g., ModuleIO with 50+ GPIO pins) due to heap exhaustion. Consider using the global
gTempDocpool protected by mutex, as recommended in previous reviews.
10-11: Global delayed writes queue lacks thread synchronization.The
sharedDelayedWritesvector is accessed from multiple contexts (update handlers,writeToFSDelayed) without mutex protection. On ESP32, concurrent access from HTTP/WebSocket handlers and the main loop can cause data races.+#include <mutex> + // ADDED: Global delayed writes queue (matches templated version) inline std::vector<std::function<void(char)>> sharedDelayedWrites; +inline std::mutex sharedDelayedWritesMutex;Then wrap accesses in
writeToFS(line 102) andwriteToFSDelayed(line 153) withstd::lock_guard<std::mutex>.
172-176:applyDefaultscreates an empty/null JsonObject, not a valid empty object.
doc.as<JsonObject>()on a freshly createdJsonDocumentreturns a null object (not an empty object). This passes a null object toupdateWithoutPropagation, which may not trigger proper default initialization.void applyDefaults(ModuleInfo& info) { JsonDocument doc; - JsonObject obj = doc.as<JsonObject>(); + JsonObject obj = doc.to<JsonObject>(); // Use to<> to create a valid empty object info.module->updateWithoutPropagation(obj, ModuleState::update); }src/MoonBase/Module.cpp (1)
38-38: StaticupdateOriginIdlacks thread synchronization.
ModuleState::updateOriginIdis a static variable written byModuleState::update()(line 265) and potentially read by other code paths. Without synchronization, concurrent updates from HTTP/WebSocket handlers can cause race conditions where the wrong originId is observed.Consider making this a per-instance field or adding mutex protection.
src/MoonBase/Module.h (2)
63-72: Shared gModulesDoc lifetime and cleanup now look correct for the single-constructor pattern.The switch to
extern JsonDocument* gModulesDocwith lazy allocation in the constructor and null-guarded cleanup in the destructor resolves the earlier linkage and null-dereference issues. Given that modules (and theirModuleStates) are constructed statically and never deleted at runtime, the one-timenew JsonDocument(...)plus per-instanceadd<JsonObject>()and destructor-sidearr.remove(i)is a reasonable trade-off between RAM usage and simplicity. No additional mutexing is needed here under the current initialization model.Also applies to: 79-94
57-61: Binary semaphore creation/usage lacks NULL guards and can hard-fault on low heap; also handshake assumes semaphores always exist.
xSemaphoreCreateBinary()can returnnullptrunder memory pressure; today you immediately callxSemaphoreGive(updateProcessedSem)in the constructor and laterxSemaphoreTake/xSemaphoreGiveinpostUpdate()/getUpdate()with no handle checks. On an ESP32-D0 already close to its heap limits, this will result in a crash rather than a graceful degradation when semaphore allocation fails.You can keep the nice bounded producer/consumer design but make it fail-safe like this:
ModuleState() { EXT_LOGD(MB_TAG, "ModuleState constructor"); - updateReadySem = xSemaphoreCreateBinary(); // assuming this will be successful - updateProcessedSem = xSemaphoreCreateBinary(); // assuming this will be successful - xSemaphoreGive(updateProcessedSem); // Ready for first update + updateReadySem = xSemaphoreCreateBinary(); + updateProcessedSem = xSemaphoreCreateBinary(); + if (!updateReadySem || !updateProcessedSem) { + EXT_LOGE(MB_TAG, "Failed to create update semaphores"); + if (updateReadySem) { + vSemaphoreDelete(updateReadySem); + updateReadySem = nullptr; + } + if (updateProcessedSem) { + vSemaphoreDelete(updateProcessedSem); + updateProcessedSem = nullptr; + } + // Fall back to direct processing in postUpdate() when semaphores are missing. + } else { + // Ready for first update + xSemaphoreGive(updateProcessedSem); + } } @@ ~ModuleState() { @@ - if (updateReadySem) vSemaphoreDelete(updateReadySem); - if (updateProcessedSem) vSemaphoreDelete(updateProcessedSem); + if (updateReadySem) vSemaphoreDelete(updateReadySem); + if (updateProcessedSem) vSemaphoreDelete(updateProcessedSem); } @@ void postUpdate(const UpdatedItem& updatedItem) { @@ - if (contains(taskName, "SvelteKit") || contains(taskName, "loopTask")) { // at boot, the loopTask starts, after that the loopTask is destroyed + if (contains(taskName, "SvelteKit") || contains(taskName, "loopTask")) { // at boot, the loopTask starts, after that the loopTask is destroyed if (processUpdatedItem) processUpdatedItem(updatedItem); } else { - if (xSemaphoreTake(updateProcessedSem, portMAX_DELAY) == pdTRUE) { + // If semaphores are unavailable (OOM at init), fall back to direct processing. + if (!updateReadySem || !updateProcessedSem) { + if (processUpdatedItem) processUpdatedItem(updatedItem); + return; + } + if (xSemaphoreTake(updateProcessedSem, portMAX_DELAY) == pdTRUE) { this->updatedItem = updatedItem; xSemaphoreGive(updateReadySem); } @@ bool getUpdate() { - if (xSemaphoreTake(updateReadySem, 0) == pdTRUE) { + if (!updateReadySem || !updateProcessedSem) return false; + if (xSemaphoreTake(updateReadySem, 0) == pdTRUE) { if (processUpdatedItem) processUpdatedItem(updatedItem); xSemaphoreGive(updateProcessedSem); return true; // Update retrievedThis keeps heap usage bounded (at most one
UpdatedItemin-flight per module) while avoiding NULL-handle crashes and providing a reasonable fallback when semaphore allocation is impossible. You may additionally want to ensure anyprocessUpdatedItempaths arenoexceptor otherwise guaranteed not to throw, to avoid leavingupdateProcessedSempermanently taken.Also applies to: 95-97, 116-137
🧹 Nitpick comments (13)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1)
50-56: onUpdate signature is heap‑friendly; consider addingoverrideand markingoldValueunusedSwitching to
const Char<20>& oldValueandJsonObjectis aligned with the heap‑reduction goals (no per‑updateStringallocations, JsonObject is a lightweight handle). The body currently ignoresoldValue, though, and the function isn’t markedoverridelike inD_VirtualDriver.h.To tighten this up without behavior change:
- void onUpdate(const Char<20>& oldValue, const JsonObject control) { + void onUpdate(const Char<20>& /*oldValue*/, const JsonObject control) override {or equivalently cast to void inside the body to silence “unused parameter” warnings.
lib/framework/SystemStatus.cpp (1)
173-175: ArduinoJson JsonVariant usage is acceptable but could be simplifiedUsing
root["heap_info_app"].to<JsonVariant>()/.to<JsonVariant>()and thenvariant.set(...)works, but is a bit roundabout for just storing a string. You could equivalently passroot["heap_info_app"]/root["heap_info_dma"]directly toheapHealthand callvariant.set(...)there without.to<>(), which keeps the code slightly simpler and avoids an extra template call. This is purely stylistic; current code is correct.interface/src/routes/system/status/SystemStatus.svelte (2)
164-173: Docs link: semantics OK, consider precomputing GitHub parts (optional)The reflowed anchor markup looks fine and keeps behavior unchanged. If you ever touch this again, consider computing
owner/repoonce in<script>(or exposing a helper) instead of callingpage.data.github.split('/')twice in the template for readability and to avoid repeated work on re-renders.
208-237: Battery visibility condition and sub‑blocks look correctThe guard on
page.data.features.batteryplus the>= 0checks forsoc,voltage, andcurrentmatches the intended “only show when valid” behavior, and the nested blocks render each metric independently. This is a nice, explicit structure; no functional issues spotted.If you want to trim template noise later, you could destructure
$telemetry.batteryinto local variables in<script>or a$derivedstore and reference those instead of repeating$telemetry.battery.*in the markup.interface/vite.config.ts (1)
21-26: Hardcoded development IP should not be committed.These proxy target IPs (192.168.1.152) are developer-specific and will break for other contributors with different network setups. Consider using environment variables or a local override file (e.g.,
.env.localor a gitignoredvite.config.local.ts).+// Add to .env.local (gitignored): +// VITE_DEV_TARGET=192.168.1.152 '/rest': { - target: 'http://192.168.1.152', + target: `http://${process.env.VITE_DEV_TARGET || 'localhost'}`, changeOrigin: true }, '/ws': { - target: 'ws://192.168.1.152', + target: `ws://${process.env.VITE_DEV_TARGET || 'localhost'}`, changeOrigin: true, ws: true }src/MoonBase/SharedWebSocketServer.h (1)
1-1: Inconsistent file path in comment.The comment states
// In lib/framework/SharedWebSocketServer.hbut the file is located atsrc/MoonBase/SharedWebSocketServer.h. Update to match the actual location.-// In lib/framework/SharedWebSocketServer.h +// src/MoonBase/SharedWebSocketServer.hsrc/MoonBase/SharedHttpEndpoint.h (2)
1-1: Inconsistent file path in comment.The comment states
// In a new file: lib/framework/SharedHttpEndpoint.hbut the file is located atsrc/MoonBase/SharedHttpEndpoint.h.-// In a new file: lib/framework/SharedHttpEndpoint.h +// src/MoonBase/SharedHttpEndpoint.h
8-8: Unused include.
<map>is included but nostd::mapis used in this file.-#include <map>src/MoonBase/NodeManager.h (1)
304-310: Consider removing or explaining the commented-out update code.This block is disabled with a comment "disable for the time being (locks _accessMutex)". If this code path is no longer needed, consider removing it entirely. If it's temporarily disabled, add a TODO with context about when/why it should be re-enabled.
if (requestUIUpdate) { requestUIUpdate = false; // reset the flag // EXT_LOGD(ML_TAG, "requestUIUpdate"); - - //disable for the time being (locks _accessMutex), need to check if this is needed at all - // update state to UI - // update( - // [&](ModuleState& state) { - // return StateUpdateResult::CHANGED; // notify StatefulService by returning CHANGED - // }, - // _moduleName); + // TODO: Re-evaluate if UI state update is needed here after mutex refactoring }src/MoonBase/Module.h (4)
23-29: UpdatedItem Char<20> vs earlier Char<16> is a small RAM regression; consider reverting for tighter heap use.Given the prior analysis that all property names have unique 15-character prefixes and typical
oldValuestrings are short,Char<16>remained safe forparent,name, andoldValue. Moving toChar<20>slightly increasesUpdatedItem(and thus eachModuleState) size for relatively little functional gain. If you still want maximum heap headroom on ESP32-D0, consider reverting the fields back toChar<16>(or selectively usingChar<16>forparent/nameand keeping more space only where really needed), accepting the already-verified prefix-uniqueness constraint.
Based on learnings, all property names have unique 15-character prefixes.
55-56: Static updateOriginId couples all modules; ensure the single-writer assumption holds or encapsulate origin per update.
updateOriginIdbeing a staticChar<20>shared across allModuleStateinstances and read inModule::processUpdatedItem()is a clever way to propagate origin through nested updates, but it does rely on only one update chain being active at a time. If, in the future, two tasks can concurrently callModuleState::update(...)with differentoriginIds, they will race on the same static buffer andsaveNeededdecisions may become non-deterministic.If your architecture truly guarantees a single producer (e.g., one HTTP/SvelteKit task) updating modules at a time, at minimum consider adding a comment here documenting that invariant. If you ever relax that constraint, a safer alternative would be to carry
originIdinUpdatedItemitself or as a per-ModuleStatefield instead of a global static.Also applies to: 156-167
146-154: Loop wiring to getUpdate() is a nice heap-bounded replacement for the old runInAppTask queue.Having
Module::loop()simply call_state.getUpdate()and letting the producer side usepostUpdate()gives you a fixed upper bound of one pendingUpdatedItemper module and avoids the unboundedstd::vectorof captured lambdas that was previously exhausting heap on ESP32-D0 during large updates. Just ensure all derived modules that overrideloop()actually callModule::loop()as the comment states; marking the override withoverridein derived classes will help enforce that at compile time.
181-193: Size-of comments are useful but likely stale after these changes; consider regenerating or removing.The inline
sizeof(...)comments documenting memory footprints (e.g.,sizeof(Module) // 1144) were accurate at the time but are almost certainly out of date now thatModuleStategained semaphores and additional fields. If you rely on these numbers for heap budgeting, it’s worth re-running thesizeofchecks and updating the comments, or alternatively removing them to avoid misleading future readers.Also applies to: 196-199
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (22)
interface/src/lib/types/models.ts(1 hunks)interface/src/routes/system/status/SystemStatus.svelte(4 hunks)interface/vite.config.ts(1 hunks)lib/framework/EventSocket.cpp(1 hunks)lib/framework/EventSocket.h(1 hunks)lib/framework/SystemStatus.cpp(1 hunks)lib/framework/SystemStatus.h(1 hunks)platformio.ini(1 hunks)src/MoonBase/Module.cpp(10 hunks)src/MoonBase/Module.h(5 hunks)src/MoonBase/Modules/ModuleDevices.h(3 hunks)src/MoonBase/Modules/ModuleTasks.h(3 hunks)src/MoonBase/NodeManager.h(11 hunks)src/MoonBase/SharedEventEndpoint.h(1 hunks)src/MoonBase/SharedFSPersistence.h(1 hunks)src/MoonBase/SharedHttpEndpoint.h(1 hunks)src/MoonBase/SharedWebSocketServer.h(1 hunks)src/MoonLight/Modules/ModuleChannels.h(2 hunks)src/MoonLight/Modules/ModuleLiveScripts.h(7 hunks)src/MoonLight/Nodes/Drivers/D_ArtnetIn.h(1 hunks)src/MoonLight/Nodes/Drivers/D_AudioSync.h(1 hunks)src/main.cpp(5 hunks)
✅ Files skipped from review due to trivial changes (1)
- interface/src/lib/types/models.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/MoonLight/Nodes/Drivers/D_AudioSync.h
- src/MoonLight/Modules/ModuleChannels.h
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-11-23T16:25:14.295Z
Learning: On ESP32-D0 boards without PSRAM, creating large local JsonDocument instances (e.g., with 50+ GPIO pins in setBoardPresetDefaults) can cause heap exhaustion and crashes due to insufficient contiguous heap memory. The solution is to use a global pre-allocated JsonDocument pool (gTempDoc) protected by a mutex, which eliminates repeated heap allocation/deallocation and fragmentation.
📚 Learning: 2025-11-23T16:25:14.295Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-11-23T16:25:14.295Z
Learning: On ESP32-D0 boards without PSRAM, creating large local JsonDocument instances (e.g., with 50+ GPIO pins in setBoardPresetDefaults) can cause heap exhaustion and crashes due to insufficient contiguous heap memory. The solution is to use a global pre-allocated JsonDocument pool (gTempDoc) protected by a mutex, which eliminates repeated heap allocation/deallocation and fragmentation.
Applied to files:
lib/framework/SystemStatus.cppsrc/MoonBase/SharedFSPersistence.hsrc/MoonBase/Module.hsrc/main.cppsrc/MoonBase/SharedWebSocketServer.hsrc/MoonBase/Module.cpp
📚 Learning: 2025-11-23T15:45:39.777Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 62
File: src/MoonBase/Module.h:27-34
Timestamp: 2025-11-23T15:45:39.777Z
Learning: In MoonLight codebase, all property names have unique 15-character prefixes, so Char<16> is safe for UpdatedItem fields (name, parent, oldValue) without causing property matching collisions.
Applied to files:
src/MoonBase/Module.hsrc/MoonBase/Module.cpp
🧬 Code graph analysis (10)
src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (2)
src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (2)
onUpdate(69-73)loop(75-84)src/MoonLight/Nodes/Drivers/D__Sandbox.h (2)
onUpdate(31-37)loop(43-47)
lib/framework/SystemStatus.h (1)
lib/framework/SystemStatus.cpp (2)
heapHealth(180-233)heapHealth(180-180)
src/MoonBase/SharedHttpEndpoint.h (5)
lib/framework/SecurityManager.h (4)
SecurityManager(82-117)AuthenticationPredicates(65-80)IS_ADMIN(76-79)IS_AUTHENTICATED(72-75)src/MoonBase/SharedFSPersistence.h (1)
begin(47-49)src/MoonBase/Module.h (2)
ModuleState(47-138)Module(140-202)lib/framework/StatefulService.h (1)
StateUpdateResult(26-106)src/MoonBase/Utilities.h (1)
contains(152-157)
src/MoonBase/SharedFSPersistence.h (3)
src/MoonBase/Module.h (2)
readFromFS(181-193)ModuleState(47-138)src/MoonBase/Module.cpp (4)
begin(313-337)begin(313-313)read(63-67)read(63-63)lib/framework/FSPersistence.h (1)
mkdirs(160-171)
src/MoonBase/Module.h (1)
src/MoonBase/Module.cpp (2)
update(262-293)update(262-262)
src/main.cpp (3)
lib/framework/WiFiSettingsService.h (1)
WiFiSettingsService(227-260)src/MoonBase/SharedEventEndpoint.h (1)
SharedEventEndpoint(10-50)src/MoonBase/Module.cpp (1)
Module(299-311)
src/MoonBase/SharedWebSocketServer.h (4)
lib/framework/SecurityManager.h (3)
SecurityManager(82-117)AuthenticationPredicates(65-80)IS_ADMIN(76-79)src/MoonBase/SharedHttpEndpoint.h (1)
registerModule(22-37)lib/framework/WebSocketServer.h (1)
transmitId(121-132)src/MoonBase/Utilities.h (1)
contains(152-157)
src/MoonBase/NodeManager.h (6)
src/MoonBase/Modules/ModuleIO.h (1)
onUpdate(393-413)src/MoonBase/Nodes.cpp (2)
onUpdate(401-541)onUpdate(401-401)src/MoonLight/Modules/ModuleLightsControl.h (1)
onUpdate(137-208)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)
lib/framework/EventSocket.h (1)
lib/framework/EventSocket.cpp (6)
emitEvent(111-118)emitEvent(111-111)emitEvent(121-152)emitEvent(121-121)emitEvent(155-216)emitEvent(155-155)
src/MoonBase/SharedEventEndpoint.h (3)
lib/framework/EventSocket.cpp (3)
EventSocket(5-11)begin(13-22)begin(13-13)lib/framework/EventSocket.h (1)
EventSocket(30-68)src/MoonBase/SharedHttpEndpoint.h (2)
registerModule(22-37)begin(39-40)
🪛 Clang (14.0.6)
src/MoonBase/SharedHttpEndpoint.h
[error] 5-5: 'PsychicHttp.h' file not found
(clang-diagnostic-error)
src/MoonBase/SharedFSPersistence.h
[error] 5-5: 'FS.h' file not found
(clang-diagnostic-error)
src/MoonBase/SharedWebSocketServer.h
[error] 5-5: 'PsychicHttp.h' file not found
(clang-diagnostic-error)
src/MoonBase/SharedEventEndpoint.h
[error] 5-5: 'EventSocket.h' file not found
(clang-diagnostic-error)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (30)
platformio.ini (1)
59-59: Date stamp updated.Routine build date update is appropriately maintained. No configuration changes needed for the heap-optimization scope covered in this PR.
src/MoonBase/Modules/ModuleDevices.h (1)
52-52: I'll verify the connectivity guard changes and ETH safety concerns. Let me search the codebase for ETH initialization patterns and examine the potential inconsistency at line 132.
<function_calls>
shell
#!/bin/bashSearch for ETH initialization, availability checks, and guards
echo "=== ETH Initialization & Availability Checks ==="
rg -n -C3 'ETH.(begin|available|isConnected|localIP)' --type=cpp --type=cecho -e "\n=== Conditional Compilation Guards for ETH ==="
rg -n '#if.*ETH|#ifdef.*ETH' --type=cpp --type=cecho -e "\n=== ETH Configuration/Feature Flags ==="
rg -n 'FT_.*ETH|ENABLE_ETH|HAS_ETH|CONFIG.*ETH|USE_ETH' --type=cpp --type=c
</function_calls><function_calls>
shell
#!/bin/bashExamine the specific file around line 132 to see writeUDP() implementation
echo "=== ModuleDevices.h lines around 132 (writeUDP function) ==="
sed -n '125,140p' src/MoonBase/Modules/ModuleDevices.hecho -e "\n=== Full writeUDP function ==="
sed -n '130,145p' src/MoonBase/Modules/ModuleDevices.hecho -e "\n=== Check for writeUDP implementation or if it's declared/commented out ==="
rg -n 'writeUDP' src/MoonBase/Modules/ModuleDevices.h -A 5 -B 1
</function_calls>src/MoonLight/Nodes/Drivers/D_ArtnetIn.h (1)
61-74: Connectivity guard looks correct; verify ETH availability on all targets
if (!WiFi.localIP() && !ETH.localIP()) return;is a good robustness improvement: Art‑Net/DDP handling only runs once either WiFi or Ethernet has a valid local IP, which helps avoid spurious UDP work and related transient allocations before connectivity is up.Please double‑check that:
ETHis defined and its header is included on all build variants where this driver is compiled, or- the call is wrapped in an appropriate
#if/#ifdefso non‑Ethernet boards don’t hit a missing symbol.lib/framework/SystemStatus.h (1)
38-38: Private heapHealth helper declaration looks goodSignature matches the implementation and keeps the heap diagnostics internal without changing the public API. No issues here.
lib/framework/SystemStatus.cpp (1)
179-233: I'll verify the concerns raised in this review comment about division by zero and numeric conversions in theheapHealthfunction. Let me start by examining the actual code and understanding the context.
<function_calls>
cat -n lib/framework/SystemStatus.cpp | sed -n '170,240p'
</function_calls>The code will be examined to confirm the exact implementation and line numbers.
Let me also search for how
heap_caps_get_infobehaves and whethertotal_free_bytescan be zero:
<function_calls>
rg -n "heap_caps_get_info|total_free_bytes" lib/framework/SystemStatus.cpp -A 2 -B 2
</function_calls>Now let me check if there are any other usages of this function or similar heap diagnostics in the codebase:
<function_calls>
rg -n "heapHealth|total_free_bytes.*0" --type cpp -g "lib/**" | head -20
</function_calls>Let me also verify the data types being used and check the ESP-IDF documentation context:
<function_calls>
rg -n "multi_heap_info_t|largest_free_block" --type cpp -g "lib/**" -B 3 -A 3 | head -30
</function_calls>Finally, let me check if there are any existing guards against zero values elsewhere in the codebase:
I need to gather evidence to verify the claims in the review comment. Let me run these shell scripts to examine the actual code.
<function_calls>
rg -n "total_free_bytes.*>|>.*total_free_bytes" --type cpp | head -10
</function_calls>interface/src/routes/system/status/SystemStatus.svelte (2)
203-203: Performance line change is safeUsing
{systemInformation.lps} loops/sis straightforward and consistent with the rest of the card. No issues from a correctness or rendering perspective.
360-361: Firmware Size block: cosmetic comment onlyRenaming/commenting around “Firmware Size” is purely cosmetic and does not affect behavior. No issues here.
src/MoonLight/Modules/ModuleLiveScripts.h (4)
36-36: LGTM: Method signature update.The
begin()signature update to match the base classModuleoverride is appropriate.
153-153: LGTM: Const-correct parameter.Making
updatedItemaconstreference prevents accidental modification and clarifies thatonUpdatedoes not mutate the item, improving const-correctness.
183-183: LGTM: Enhanced connectivity check.Adding the Ethernet IP check ensures the function returns early when neither WiFi nor Ethernet is connected, improving robustness for devices with Ethernet capabilities.
158-158: The oldValue condition change is correct and aligns with the codebase initialization pattern.The change from
!= "null"to!= ""is intentional and fixes the boot-detection logic:
- oldValue initialization: The
UpdatedItemconstructor does not explicitly initializeoldValue, so it defaults to an empty string (Char<20>()="").- Boot vs. user update: When loading initial state,
oldValue = ""(empty); when a user updates a value,oldValuecontains the previous state value (non-empty).- No "null" assignments: The literal string
"null"is never assigned tooldValueanywhere in the codebase, so the old check was ineffective.- Consistent pattern: Other modules (e.g.,
ModuleIO.hline 396) already use the sameoldValue == ""pattern to detect boot-time initialization.The new check correctly prevents script actions (kill, execute, delete) from running during boot while allowing them on user updates.
src/MoonBase/Modules/ModuleTasks.h (1)
51-57: Good defensive improvements.Adding ETH connectivity check and initializing
totalRunTime = 1to prevent division by zero are solid defensive coding practices.lib/framework/EventSocket.h (1)
43-46: Good const-correctness improvements.The updated signatures with
const String&andconst JsonObject&avoid unnecessary copies, and the newJsonDocumentoverload provides a cleaner API for pre-built documents. The implementations in EventSocket.cpp show proper buffer reuse.src/MoonBase/SharedEventEndpoint.h (2)
38-50: Good optimization: early return when no clients connected.Checking
getConnectedClients()before creating the JsonDocument avoids unnecessary allocations when there are no subscribers.
17-21: I'll help you verify the potential dangling pointer issue withc_str()in the SharedEventEndpoint.h file. Let me search for theEventSocket::registerEventimplementation to understand how it handles the event name parameter.
<function_calls>
shell
#!/bin/bashSearch for EventSocket class and registerEvent method
rg -n "class EventSocket" --type cpp -A 5
</function_calls>Let me also search more broadly for the registerEvent method:
<function_calls>
shell
#!/bin/bashSearch for registerEvent method definition
rg -n "registerEvent" --type cpp -B 2 -A 8
</function_calls>Let me search for EventSocket header files:
<function_calls>
shell
#!/bin/bashFind EventSocket related files
fd -e h -e hpp | xargs rg -l "EventSocket" | head -20
</function_calls><function_calls>
shell
#!/bin/bashLook at the SharedEventEndpoint.h file to understand the context
cat -n src/MoonBase/SharedEventEndpoint.h | head -50
</function_calls>src/MoonBase/SharedHttpEndpoint.h (1)
54-78: Clean POST handler implementation.The
updateWithoutPropagationpattern with proper error handling and returning updated state in the response is well-structured.src/MoonBase/SharedWebSocketServer.h (1)
68-69: Socket ID not removed from _initializedSockets on disconnect.When a client disconnects, its socket ID remains in
_initializedSockets. This causes unbounded growth and potential logic errors if socket IDs are reused._handler.onClose([this](PsychicWebSocketClient* client) { + _initializedSockets.erase(client->socket()); ESP_LOGI(SVK_TAG, "ws[%s][%u] disconnect", client->remoteIP().toString().c_str(), client->socket()); });Likely an incorrect or invalid review comment.
src/main.cpp (4)
122-163: LGTM! Semaphore-based producer/consumer model for effect/driver synchronization.The binary semaphore handshake between
effectTaskanddriverTaskis well-structured. The 100ms timeout with fallback ensures effects continue at ~10fps even without a driver, and the explicitvTaskDelay(1)prevents CPU starvation.
259-267: Null check for shared router allocation is properly implemented.The allocation failure check with
esp_restart()provides a clear fail-fast strategy for memory exhaustion scenarios. This addresses the previous review feedback.
269-291: Good memory pre-allocation strategy for module registration.The
modules.reserve(12)prevents reallocations during module registration. The centralized registration loop with shared routers reduces per-module memory overhead significantly compared to the previous per-module endpoint approach.
330-346: Now I have enough information to complete the verification. The review comment is valid, and I found additional issues:Key findings:
- Documentation mismatch:
docs/moonbase/tasks.mdshows AppEffectTask should use 6KB (PSRAM) / 4KB (non-PSRAM), but the actual code uses 4KB / 3KB- Incorrect comment: The comment says "updated from 4 to 6" but the code shows values reduced from 6 to 4, making the comment backwards
- Stack size concern is valid: 3-4KB is significantly smaller than comparable system tasks (Wi-Fi: 12KB, HTTPD: 6-8KB, OTA: 9KB)
- LiveScript enabled by default: FT_LIVESCRIPT=1 is set in platformio.ini, so 3KB must support LiveScript stack depth
- Stack monitoring in place: The codebase uses
uxTaskGetStackHighWaterMark()in all logging macros, but no indication of validation resultsFinal Output
Update stack size documentation and clarify the stack allocation strategy for non-PSRAM boards.
The reduced stack sizes (3KB for non-PSRAM, 4KB for PSRAM) warrant verification, especially with LiveScript enabled by default. Additionally, the inline comment is outdated—it claims "updated from 4 to 6" but code shows 4KB/3KB; documentation in
docs/moonbase/tasks.mdlists different values (6KB/4KB).Since stack monitoring via
uxTaskGetStackHighWaterMark()is integrated into all logging macros, run tests to confirm 3KB is sufficient under LiveScript load and update the comment to reflect actual changes and any validation results. If the reduction from the documented 6KB/4KB was deliberate, document the rationale and any testing methodology used.src/MoonBase/NodeManager.h (2)
23-23: Good heap optimization: Using fixed-sizeChar<20>instead ofString.Replacing
StringwithChar<20>fordefaultNodeNameeliminates per-instance heap allocation, aligning with the PR's heap optimization goals.
134-134: Correct const-correctness for UpdatedItem parameter.The signature change to
const UpdatedItem&ensures the updated item isn't accidentally modified and enables potential compiler optimizations.lib/framework/EventSocket.cpp (3)
120-152: Good heap optimization: Static buffers for JSON/MsgPack serialization.The introduction of static
outBuffer(bothStringandvector<uint8_t>) that are reused across calls significantly reduces heap allocation churn. Thereserve()calls withmeasureJson()/measureMsgPack()ensure capacity without repeated reallocations.
111-118: LGTM! Clean refactoring to support pre-constructed JsonDocument.The overload delegation pattern is clean - the
(event, jsonObject)overload builds a document and delegates to the new(JsonDocument)overload, avoiding code duplication.
124-128: I'll help you verify the concurrency concern regarding the static buffer inEventSocket.cpp. Let me start by investigating whereemitEventis called and whether it can be accessed from multiple tasks concurrently.
<function_calls>
Search for emitEvent calls and analyze potential concurrency
#!/bin/bashFind all calls to emitEvent in the codebase
echo "=== emitEvent call sites ==="
rg -n "emitEvent(" --type cpp --type h -C3 | head -100echo ""
echo "=== EventSocket.cpp context around emitEvent function ==="Get context around the emitEvent function definition
rg -n "emitEvent" lib/framework/EventSocket.cpp -B5 -A15 | head -100
</function_calls>
<function_calls>
Check for threading/task mechanisms in the codebase
#!/bin/bashLook for mutex, locks, or threading patterns
echo "=== Checking for synchronization mechanisms in EventSocket.cpp ==="
rg -n "mutex|lock|thread|task" lib/framework/EventSocket.cpp -i -C2echo ""
echo "=== Check EventSocket.h for class members and threading context ==="
rg -n "class EventSocket" lib/framework/EventSocket.h -A30
</function_calls>
<function_calls>
Examine EventSocket.cpp file structure and understand emitEvent
#!/bin/bashGet line count first
echo "=== EventSocket.cpp line count ==="
wc -l lib/framework/EventSocket.cppecho ""
echo "=== Lines 110-150 (context around lines 124-128) ==="
sed -n '110,150p' lib/framework/EventSocket.cpp
</function_calls>src/MoonBase/Module.cpp (4)
243-246: Good fix: Using fullkey.c_str()for stateData access.The code now correctly uses
key.c_str()instead ofupdatedItem.name.c_str()for stateData access, preventing key truncation issues for property names longer than 19 characters. This addresses the previous critical review feedback.
262-266: LGTM! Origin ID propagation through update flow.The
updatemethod now accepts and propagatesoriginIdthrough the state update flow, enabling proper tracking of update sources for save/sync decisions.
308-310: Good design: Lambda-based processUpdatedItem callback binding.Binding
processUpdatedItemas a callback in the constructor enables the new postUpdate flow while keeping the processing logic in the Module class. The const reference parameter is appropriate.
16-16: GlobalgModulesDocis properly initialized and used—no null pointer dereference risk.
gModulesDocis declared at line 16 and is initialized inModuleStateconstructor (Module.h lines 63–69) with proper null checks before all uses. The code checksif (!gModulesDoc)at line 63 and again at line 70 before dereferencing, with error handling for initialization failure. No action needed.
| <!-- 🌙 --> | ||
| <div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> | ||
| <div class="mask mask-hexagon bg-primary h-auto w-10 flex-none"> | ||
| <Heap class="text-primary-content h-auto w-full scale-75" /> | ||
| </div> | ||
| <div> | ||
| <div class="font-bold">App RAM</div> | ||
| <div class="text-sm opacity-75"> | ||
| {systemInformation.heap_info_app} | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- 🌙 --> | ||
| <div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> | ||
| <div class="mask mask-hexagon bg-primary h-auto w-10 flex-none"> | ||
| <Heap class="text-primary-content h-auto w-full scale-75" /> | ||
| </div> | ||
| <div> | ||
| <div class="font-bold">DMA RAM</div> | ||
| <div class="text-sm opacity-75"> | ||
| {systemInformation.heap_info_dma} | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard / normalize new App RAM & DMA RAM fields for older firmware
The new “App RAM” and “DMA RAM” tiles are useful additions and align with the memory‑optimization work. One thing to consider is compatibility with existing devices/firmware that may not yet send heap_info_app / heap_info_dma:
- If these fields are missing or
undefined, the template will currently render the literal stringundefined. - If they’re raw byte counts, the UI will show large integers without units, unlike the other memory tiles.
You can keep the UI behavior but make it more robust and consistent with a small tweak:
- <div class="font-bold">App RAM</div>
- <div class="text-sm opacity-75">
- {systemInformation.heap_info_app}
- </div>
+ <div class="font-bold">App RAM</div>
+ <div class="text-sm opacity-75">
+ {systemInformation.heap_info_app ?? 'n/a'}
+ </div>
...
- <div class="font-bold">DMA RAM</div>
- <div class="text-sm opacity-75">
- {systemInformation.heap_info_dma}
- </div>
+ <div class="font-bold">DMA RAM</div>
+ <div class="text-sm opacity-75">
+ {systemInformation.heap_info_dma ?? 'n/a'}
+ </div>If the backend instead returns fully formatted, human‑readable strings, this keeps that intact while providing a clean fallback.
📝 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.
| <!-- 🌙 --> | |
| <div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> | |
| <div class="mask mask-hexagon bg-primary h-auto w-10 flex-none"> | |
| <Heap class="text-primary-content h-auto w-full scale-75" /> | |
| </div> | |
| <div> | |
| <div class="font-bold">App RAM</div> | |
| <div class="text-sm opacity-75"> | |
| {systemInformation.heap_info_app} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 🌙 --> | |
| <div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> | |
| <div class="mask mask-hexagon bg-primary h-auto w-10 flex-none"> | |
| <Heap class="text-primary-content h-auto w-full scale-75" /> | |
| </div> | |
| <div> | |
| <div class="font-bold">DMA RAM</div> | |
| <div class="text-sm opacity-75"> | |
| {systemInformation.heap_info_dma} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 🌙 --> | |
| <div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> | |
| <div class="mask mask-hexagon bg-primary h-auto w-10 flex-none"> | |
| <Heap class="text-primary-content h-auto w-full scale-75" /> | |
| </div> | |
| <div> | |
| <div class="font-bold">App RAM</div> | |
| <div class="text-sm opacity-75"> | |
| {systemInformation.heap_info_app ?? 'n/a'} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 🌙 --> | |
| <div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> | |
| <div class="mask mask-hexagon bg-primary h-auto w-10 flex-none"> | |
| <Heap class="text-primary-content h-auto w-full scale-75" /> | |
| </div> | |
| <div> | |
| <div class="font-bold">DMA RAM</div> | |
| <div class="text-sm opacity-75"> | |
| {systemInformation.heap_info_dma ?? 'n/a'} | |
| </div> | |
| </div> | |
| </div> |
🤖 Prompt for AI Agents
In interface/src/routes/system/status/SystemStatus.svelte around lines 262 to
287, the new App RAM and DMA RAM tiles can render "undefined" or raw bytes for
older firmware; update the template to guard and normalize these fields: check
each of systemInformation.heap_info_app and heap_info_dma and if undefined/null
show a safe fallback like "N/A" or "—", if the value is a number convert it to a
human-readable unit string (KB/MB/GB) via the existing bytes-format helper or a
small helper function, and otherwise render the value as-is so preformatted
strings remain intact.
src/MoonBase/Modules/ModuleTasks.h
Outdated
|
|
||
| Char<32> summary; | ||
| summary.format("%s %5.0f%% @ P%d", state, totalRunTime ? (100.0f * ts->ulRunTimeCounter) / totalRunTime : 0.0f, ts->uxCurrentPriority); | ||
| summary.format("%s %d%% @ P%d", state, 100 * ts->ulRunTimeCounter / totalRunTime, ts->uxCurrentPriority); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential integer overflow in percentage calculation.
100 * ts->ulRunTimeCounter can overflow uint32_t when ulRunTimeCounter > ~42.9 million (about 43 seconds of CPU time). Reorder to divide first or cast to 64-bit.
-summary.format("%s %d%% @ P%d", state, 100 * ts->ulRunTimeCounter / totalRunTime, ts->uxCurrentPriority);
+summary.format("%s %d%% @ P%d", state, (uint32_t)(100ULL * ts->ulRunTimeCounter / totalRunTime), ts->uxCurrentPriority);📝 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.
| summary.format("%s %d%% @ P%d", state, 100 * ts->ulRunTimeCounter / totalRunTime, ts->uxCurrentPriority); | |
| summary.format("%s %d%% @ P%d", state, (uint32_t)(100ULL * ts->ulRunTimeCounter / totalRunTime), ts->uxCurrentPriority); |
🤖 Prompt for AI Agents
In src/MoonBase/Modules/ModuleTasks.h around line 99, the expression "100 *
ts->ulRunTimeCounter / totalRunTime" can overflow uint32_t before division;
change the calculation to perform it in 64-bit and guard against
division-by-zero — e.g., cast operands to uint64_t and compute
(uint64_t)ts->ulRunTimeCounter * 100 / (uint64_t)totalRunTime (or divide first
if preferred) so the multiplication cannot overflow, and ensure totalRunTime !=
0 before formatting the percentage.
| return request->reply(400); | ||
| } else if (outcome == StateUpdateResult::CHANGED) { | ||
| // Persist changes to FS | ||
| module->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for HTTP_ENDPOINT_ORIGIN_ID definition
rg -n "HTTP_ENDPOINT_ORIGIN_ID" --type cpp -C 3Repository: MoonModules/MoonLight
Length of output: 1599
🏁 Script executed:
#!/bin/bash
# Check the includes at the top of SharedHttpEndpoint.h
head -30 src/MoonBase/SharedHttpEndpoint.hRepository: MoonModules/MoonLight
Length of output: 1096
🏁 Script executed:
#!/bin/bash
# Check if HttpEndpoint.h is included by PsychicHttp.h or SecurityManager.h
echo "=== Checking PsychicHttp.h includes ==="
rg -n "#include.*HttpEndpoint" lib/ src/ --type cpp -A 2 -B 2
echo ""
echo "=== Checking SecurityManager.h includes ==="
rg -l "SecurityManager" --type cpp | head -5 | xargs -I {} sh -c 'echo "File: {}"; head -20 {} | grep "#include"'Repository: MoonModules/MoonLight
Length of output: 3229
🏁 Script executed:
#!/bin/bash
# Check SecurityManager.h includes
echo "=== SecurityManager.h includes ==="
rg -n "^#include" lib/framework/SecurityManager.h --type cpp | head -20Repository: MoonModules/MoonLight
Length of output: 202
Add #include <HttpEndpoint.h> to src/MoonBase/SharedHttpEndpoint.h.
The constant HTTP_ENDPOINT_ORIGIN_ID is defined in lib/framework/HttpEndpoint.h (line 11) but is not included in SharedHttpEndpoint.h. The file uses this constant at line 70 without having access to its definition. This will cause a compilation error. The pattern throughout the codebase shows files explicitly include HttpEndpoint.h when they need HTTP_ENDPOINT_ORIGIN_ID.
🤖 Prompt for AI Agents
In src/MoonBase/SharedHttpEndpoint.h around line 70, the file uses the constant
HTTP_ENDPOINT_ORIGIN_ID but does not include the header that defines it; add
#include <HttpEndpoint.h> at the top of SharedHttpEndpoint.h so the symbol is
visible and the file compiles, following the existing project pattern of
including HttpEndpoint.h wherever HTTP_ENDPOINT_ORIGIN_ID is used.
| Module* findModule(const String& path) { | ||
| for (Module* module : modules) { | ||
| if (contains(path.c_str(), module->_moduleName.c_str())) return module; | ||
| } | ||
| return nullptr; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Substring matching may match unintended modules.
Using contains() can incorrectly match if one module name is a substring of another (e.g., "IO" would match "/ws/ModuleIO"). Use exact suffix matching instead.
Module* findModule(const String& path) {
for (Module* module : modules) {
- if (contains(path.c_str(), module->_moduleName.c_str())) return module;
+ String expectedSuffix = "/" + module->_moduleName;
+ if (path.endsWith(expectedSuffix)) return module;
}
return nullptr;
}🤖 Prompt for AI Agents
In src/MoonBase/SharedWebSocketServer.h around lines 105 to 110, the findModule
function currently uses contains(path.c_str(), module->_moduleName.c_str())
which can match substrings unintentionally; change the logic to perform exact
suffix matching of the module name against the path (for example, check that the
path ends with the module name and that the character before the suffix is a
path separator such as '/'), using std::string methods (rfind/compare or substr)
to ensure exact match of the module name at the end of the path and return the
module only when that exact suffix condition holds.
| if (updatedItem == nodeState["name"]) { | ||
| EXT_LOGD(ML_TAG, "updateHandler equals current item -> livescript compile %s", updatedItem.c_str()); | ||
| LiveScriptNode* liveScriptNode = (LiveScriptNode*)_moduleEffects->findLiveScriptNode(nodeState["name"]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll help you verify the review comment concerns about null pointer safety and operator comparison semantics. Let me start by examining the codebase to understand the types and context.
<function_calls>
#!/bin/bash
Locate ModuleLiveScripts.h and get its line count
find . -name "ModuleLiveScripts.h" -type f
Also check for any related header/source files
find . -name "ModuleLiveScripts" -type f
Now let me read the file to see the context around lines 51-53 and line 62:
<function_calls>
#!/bin/bash
Get line count first
wc -l src/MoonLight/Modules/ModuleLiveScripts.h
Read the section around lines 45-70
sed -n '45,70p' src/MoonLight/Modules/ModuleLiveScripts.h
Let me also check for UpdatedItem type definition:
<function_calls>
#!/bin/bash
Search for UpdatedItem definition
rg -n "struct UpdatedItem|class UpdatedItem" --type h --type cpp -A10
Search for operator== overloads involving UpdatedItem
rg -n "operator==" --type h --type cpp -B5 -A3 | grep -A8 -B8 "UpdatedItem"
Let me search more broadly for UpdatedItem and nodeState usage:
<function_calls>
#!/bin/bash
Find where UpdatedItem is defined or used
rg -n "UpdatedItem" --type h --type cpp -B2 -A2
Check for JsonVariant usage and .as<const char*>()
rg -n ".as<const char" --type h --type cpp -B3 -A3
Let me look at the wider context of ModuleLiveScripts.h to understand the types:
<function_calls>
#!/bin/bash
Read the beginning of the file to see includes and type definitions
head -n 100 src/MoonLight/Modules/ModuleLiveScripts.h
Let me get more context around lines 40-75 to see the full structure:
<function_calls>
#!/bin/bash
Read a larger range around the problem lines
sed -n '40,75p' src/MoonLight/Modules/ModuleLiveScripts.h
Let me search for EXT_LOGD and the .as<const char*>() pattern:
<function_calls>
#!/bin/bash
Look for the exact line with "update due to new node"
rg -n "update due to new node" --type h --type cpp -B5 -A2
Now let me find any type information and includes to determine if this is ArduinoJson:
<function_calls>
#!/bin/bash
Search for ArduinoJson or JsonVariant in the codebase
rg -n "JsonVariant|JsonDocument|ArduinoJson" --type h --type cpp -m 5
Check includes in ModuleLiveScripts.h
head -n 30 src/MoonLight/Modules/ModuleLiveScripts.h | cat -n
Let me check the complete file structure more carefully:
<function_calls>
#!/bin/bash
Get full file to understand structure
cat -n src/MoonLight/Modules/ModuleLiveScripts.h
Let me check if this is indeed a header file or if there's actual implementation:
<function_calls>
#!/bin/bash
Check file size
stat src/MoonLight/Modules/ModuleLiveScripts.h
Look for .cpp companion file
find src -name "ModuleLiveScripts.cpp" -type f
Let me search for where the comparison happens and the logging calls:
<function_calls>
#!/bin/bash
Search across all files for the pattern with updatedItem and nodeState
rg -n "updatedItem.*nodeState.*name" --type h --type cpp -B3 -A3
Also search for the specific logging pattern mentioned in review
rg -n "EXT_LOGD.*nodeState.*name.*as<const char" --type h --type cpp -B5 -A2
Let me look at the types more systematically:
<function_calls>
#!/bin/bash
Look for UpdatedItem struct/class across the repository
rg -n "^struct UpdatedItem|^class UpdatedItem" --type h --type cpp -A20
Check for typedef patterns
rg -n "using UpdatedItem|typedef.*UpdatedItem" --type h --type cpp -A5
Let me search for where updatedItem is declared in function parameters:
<function_calls>
#!/bin/bash
Search for updatedItem declarations/parameters
rg -n "updatedItem" --type h --type cpp -B10 -A2 | head -n 80
Let me get the complete ModuleLiveScripts files to understand the context:
<function_calls>
#!/bin/bash
Find all ModuleLiveScripts files
find . -name "ModuleLiveScripts"
Count lines in header
wc -l src/MoonLight/Modules/ModuleLiveScripts.h
Let me directly read the specific range more carefully:
<function_calls>
#!/bin/bash
Get exact line range from review
cat -n src/MoonLight/Modules/ModuleLiveScripts.h | sed -n '45,70p'
Let me try a Python-based approach to carefully examine the file:
<function_calls>
#!/bin/bash
python3 << 'PYEOF'
import os
file_path = "src/MoonLight/Modules/ModuleLiveScripts.h"
if os.path.exists(file_path):
with open(file_path, 'r') as f:
lines = f.readlines()
print(f"Total lines: {len(lines)}\n")
# Show lines around 51-53 and 62
print("=== Lines 45-70 ===")
for i in range(44, min(70, len(lines))):
print(f"{i+1:3d}: {lines[i]}", end='')
print("\n\n=== Search for .as<const char ===")
for i, line in enumerate(lines):
if ".as<const char" in line:
print(f"Line {i+1}: {line.rstrip()}")
if i > 0:
print(f" Before: {lines[i-1].rstrip()}")
if i+1 < len(lines):
print(f" After: {lines[i+1].rstrip()}")
else:
print(f"File not found: {file_path}")
PYEOF
</function_calls>
Now let me search for type definitions of UpdatedItem and JsonVariant patterns:
<function_calls>
#!/bin/bash
Search for UpdatedItem in all source files with context
rg "UpdatedItem" -n --type h --type cpp | head -n 30
Search for JsonVariant.as patterns
rg ".as<" -n --type h --type cpp | head -n 20
Check if ArduinoJson is being used
rg "ArduinoJson|#include.*Json" -n --type h --type cpp | head -n 10
Let me check dependencies and imports:
<function_calls>
#!/bin/bash
Get includes at top of ModuleLiveScripts.h
head -n 50 src/MoonLight/Modules/ModuleLiveScripts.h | grep -n "#include"
Also search for any comments mentioning types
head -n 100 src/MoonLight/Modules/ModuleLiveScripts.h | grep -i "json|variant|updated"
Let me search for the comparison operator and understand the types:
<function_calls>
#!/bin/bash
Find context around the exact comparison mentioned in review
rg 'if.updatedItem.==.*nodeState' -n --type h --type cpp -B10 -A5
Let me read the entire file since it seems relatively small:
<function_calls>
#!/bin/bash
Read the entire ModuleLiveScripts.h file
cat src/MoonLight/Modules/ModuleLiveScripts.h
Perfect! Now let me search for where the types are defined:
<function_calls>
#!/bin/bash
Find UpdatedItem type definition
rg "^using UpdatedItem|^typedef.*UpdatedItem|^struct UpdatedItem" -n --type h --type cpp
Search for where updatedItem is used as a variable
rg "std::string.*updatedItem|String.*updatedItem" -n --type h --type cpp
Check for imports of types
rg "#include" src/MoonLight/Modules/ModuleLiveScripts.h
Let me search in other header files for UpdatedItem:
<function_calls>
#!/bin/bash
Find where UpdatedItem is defined across the codebase
find . -name ".h" -o -name ".hpp" | xargs grep -l "UpdatedItem" 2>/dev/null
Let me check what files include ModuleLiveScripts.h or where it's used:
<function_calls>
#!/bin/bash
Search for files that include ModuleLiveScripts.h
rg "ModuleLiveScripts.h" -l --type h --type cpp
Look for UpdatedItem in neighboring files
rg "UpdatedItem" -n --type h --type cpp -C5 | head -n 100
Let me check if UpdatedItem is a using alias or typedef:
<function_calls>
#!/bin/bash
Look in all header files in the project for UpdatedItem
find . -name "*.h" -exec grep -n "UpdatedItem" {} + | head -n 20
Also check for any String or string type that might be updatedItem
rg "updated.*Item|Updated.*Item" -ni --type h --type cpp | head -n 20
Let me examine the directory structure to find related files:
<function_calls>
#!/bin/bash
List files in the ModuleLiveScripts directory
ls -la src/MoonLight/Modules/ | head -n 30
Look for CMakeLists or build files that might reveal dependencies
find . -name "CMakeLists.txt" -o -name "*.cmake" | head -n 5
Let me search for function definitions that use updatedItem:
<function_calls>
#!/bin/bash
Search for method/function signatures with updatedItem parameter
rg "void.*(.updatedItem|bool.(.*updatedItem" -n --type h --type cpp -B2 -A5
Search backward from line 51 to find the function definition
head -n 51 src/MoonLight/Modules/ModuleLiveScripts.h | tail -n 20
Let me read around line 51 more systematically:
<function_calls>
#!/bin/bash
Show context before line 51 to find function signature
sed -n '1,60p' src/MoonLight/Modules/ModuleLiveScripts.h | tail -n 40
Let me check imports and understand what types are available:
<function_calls>
#!/bin/bash
Get the complete includes section
head -n 30 src/MoonLight/Modules/ModuleLiveScripts.h
Now let me search for JsonVariant and understand the .as<const char*>() behavior:
<function_calls>
#!/bin/bash
Check if ArduinoJson is used
find . -name "*.h" | xargs grep -l "class JsonVariant" 2>/dev/null | head -n 3
Also check for any documentation or comments about null safety
rg "as<const char" --type h --type cpp -B3 -A1 | head -n 40
Let me look for the specific line 62 mentioned in the review:
<function_calls>
#!/bin/bash
Line 62 specifically
sed -n '60,65p' src/MoonLight/Modules/ModuleLiveScripts.h | cat -n
Let me verify the complete file content more carefully:
<function_calls>
#!/bin/bash
Get exact line count and all content with line numbers
cat -n src/MoonLight/Modules/ModuleLiveScripts.h
Now I can see the file clearly. Let me search for where UpdatedItem parameter comes from:
<function_calls>
#!/bin/bash
Search for the function or callback that uses updatedItem
rg "updatedItem" --type h --type cpp -B5 -A2 | head -n 80
Check if it's a using statement for a type
rg "using.*=" src/MoonLight/Modules/ModuleLiveScripts.h
Let me search more broadly across the codebase for UpdatedItem definition:
<function_calls>
#!/bin/bash
Search all files for UpdatedItem type definition
find . -type f ( -name ".h" -o -name ".hpp" -o -name "*.cpp" ) -exec grep -l "UpdatedItem" {} ;
Let me check for a specific usage pattern:
<function_calls>
#!/bin/bash
Look for the complete context around line 51-53
python3 << 'PYEOF'
with open("src/MoonLight/Modules/ModuleLiveScripts.h") as f:
lines = f.readlines()
Show context around line 51 (0-indexed line 50)
print("Lines 1-80 with line numbers:")
for i in range(min(80, len(lines))):
print(f"{i+1:3}: {lines[i]}", end='')
PYEOF
Based on the file content I can see, let me now search for where String type (which updatedItem appears to be) has comparison operators:
<function_calls>
#!/bin/bash
Search for String class definition or imports
rg "#include.*String" --type h --type cpp | head -n 10
Check what string type is being used
rg "std::string|String|string\s" src/MoonLight/Modules/ModuleLiveScripts.h
Let me search for JsonVariant and its .as() method documentation:
<function_calls>
#!/bin/bash
Check if there's ArduinoJson library included
find . -path "/ArduinoJson" -name "*.h" | head -n 5
Search for documentation or patterns of .as<const char*>()
rg ".as<const char" --type h --type cpp -C3 | head -n 50
Let me search the broader codebase for patterns of nodeState and JsonVariant:
<function_calls>
#!/bin/bash
Search for nodeState declarations
rg "nodeState" --type h --type cpp -B3 -A1 | head -n 100
Now let me look at the comparison semantics. Let me check what happens when you compare a String with JsonVariant:
<function_calls>
#!/bin/bash
Search for operator== definitions involving these types
rg "operator.*==" --type h --type cpp -B5 | head -n 100
Look for includes that might define String class
head -n 20 src/MoonLight/Modules/ModuleLiveScripts.h
Let me check for WString or Arduino String:
<function_calls>
#!/bin/bash
Search for WString or Arduino String includes
rg "WString|#include <String" --type h --type cpp
Check if nodeState is JsonVariant
rg "JsonVariant" --type h --type cpp
Let me look for how .as<const char*>() behaves in the context:
<function_calls>
#!/bin/bash
Get all uses of .as<const char*>() in the codebase
rg ".as<const char*>()" --type h --type cpp -B2 -A2
Now let me search in external documentation context. Let me check project dependencies:
<function_calls>
#!/bin/bash
Look for package.json or platformio.ini to understand dependencies
find . -name "platformio.ini" -o -name "package.json" -o -name "conanfile.txt" | xargs cat 2>/dev/null | head -n 50
Let me check if there's a requirements or dependencies file:
<function_calls>
#!/bin/bash
Look for Arduino or build configuration
find . -name ".ino" -o -name ".cpp" -o -name "*.h" | xargs grep -h "ArduinoJson|#include <" | sort -u | grep -i json | head -n 10
Based on my analysis so far, let me look at the exact code location in context more clearly:
<function_calls>
#!/bin/bash
Extract specific lines around the concern
python3 << 'PYEOF'
with open("src/MoonLight/Modules/ModuleLiveScripts.h") as f:
lines = f.readlines()
print("Lines 45-65 (exact range from review):")
for i in range(44, min(65, len(lines))):
marker = ""
if i+1 in [51, 52, 53, 62]:
marker = " <-- FLAGGED LINE"
print(f"{i+1:3}: {lines[i].rstrip()}{marker}")
PYEOF
Perfect! Now I can see the code. Let me search for the types involved. The key question is: what type is nodeState? Let me check:
<function_calls>
#!/bin/bash
Find where nodeState is declared or passed as parameter
rg "nodeState" --type h --type cpp -B10 | head -n 150
Let me search more specifically in ModuleLiveScripts and related modules:
<function_calls>
#!/bin/bash
Check all files in Modules directory
ls -la src/MoonLight/Modules/
Look for related files that might show nodeState type
rg "nodeState" src/MoonLight/Modules/ -B5 | head -n 200
Now let me check the JsonVariant behavior from ArduinoJson documentation context:
<function_calls>
ArduinoJson JsonVariant as<const char*>() nullptr safety
</function_calls>
Let me also check Arduino String operator== behavior:
<function_calls>
Arduino String class operator== JsonVariant comparison
</function_calls>
Let me verify the specific behavior of .as<const char*>():
<function_calls>
"ArduinoJson" ".as<const char*>()" return value null
</function_calls>
Let me check the ESP32 Arduino core and String implementation:
<function_calls>
ESP32 Arduino String class operator== behavior implicit conversion
</function_calls>
Now let me verify if there are any null checks nearby in the codebase for similar patterns:
<function_calls>
#!/bin/bash
Search for patterns where .as<const char*>() result is checked
rg ".as<const char*>()" --type h --type cpp -A3 | grep -A3 -B1 "if|?.*:"
Also search for null/nullptr checks on pointers from .as()
rg ".as<" --type h --type cpp -A1 | grep -B1 -A1 "nullptr|!= NULL|== NULL"
Let me also search for any other EXT_LOGD calls with similar patterns to understand the usual approach:
<function_calls>
#!/bin/bash
Search for other EXT_LOGD uses
rg "EXT_LOGD" --type h --type cpp -A1 | head -n 80
Specifically look for similar patterns with as<const char*>()
rg "EXT_LOGD.*as<const char" --type h --type cpp -B2 -A1
Let me now verify if there's actual documentation or examples showing the null safety concern:
<function_calls>
ArduinoJson version 6 JsonVariant as const char pointer null return
</function_calls>
Let me check for test files or examples showing proper null handling:
<function_calls>
#!/bin/bash
Search for test files that might show proper patterns
find . -name "test" -o -name "example" | xargs rg ".as<const char" 2>/dev/null | head -n 30
Also look for uses of .as() with null handling
rg ".as<" --type h --type cpp -B1 -A2 | grep -B3 -A3 "nullptr|NULL|!" | head -n 50
Let me verify the exact comparison pattern at line 51:
<function_calls>
#!/bin/bash
Check if Arduino String's operator== can handle comparison with JsonVariant
Also check what updatedItem type actually is
Search for where updatedItem parameter is defined in function signature
head -n 51 src/MoonLight/Modules/ModuleLiveScripts.h | tail -n 20
Let me look at the broader function context to understand parameter types:
<function_calls>
#!/bin/bash
Get the complete file with context awareness
python3 << 'PYEOF'
with open("src/MoonLight/Modules/ModuleLiveScripts.h") as f:
content = f.read()
lines = content.split('\n')
Find the context around line 51 - look for function/method signatures
for i in range(max(0, 50-20), min(len(lines), 65)):
print(f"{i+1:3}: {lines[i]}")
PYEOF
Now let me check if there are any similar callback functions in the codebase that might show the type:
<function_calls>
#!/bin/bash
Look for similar callback patterns in related files
rg "updateHandler|onUpdate|update.*Handler" --type h --type cpp -B5 -A5 | head -n 100
Let me check nearby files for context on how updates are handled:
<function_calls>
#!/bin/bash
Search the modules directory for similar patterns
find src/MoonLight/Modules -name ".h" -o -name ".cpp" | xargs grep -l "update" | head -n 5
Check one of these files for patterns
rg "update.*Handler|Update.*Item" src/MoonLight/Modules/Module.h -B3 -A3 2>/dev/null | head -n 80
Let me check the Module.h file directly:
<function_calls>
#!/bin/bash
Check if Module.h exists and contains type information
find . -name "Module.h" | xargs head -n 100
Let me verify the exact behavior we need to check. Based on ArduinoJson documentation:
<function_calls>
"JsonVariant" "as<const char*>" EXT_LOGD format string %s
</function_calls>
Now let me check Arduino String comparison implementation:
<function_calls>
"Arduino String" "operator==" "const char*" implicit conversion JsonVariant
</function_calls>
Let me search for how comparison happens at line 51:
<function_calls>
#!/bin/bash
The pattern is: if (updatedItem == nodeState["name"])
updatedItem appears to be std::string or Arduino String (likely const std::string&)
nodeState["name"] is JsonVariant
Search for JsonVariant operator== implementations in the codebase
rg "JsonVariant.*operator==" --type h --type cpp -B2 -A5
Also search for String class comparison operators
rg "class String|class.*String" --type h | head -n 10
Let me check what type nodeState must be by looking at the broader context:
<function_calls>
#!/bin/bash
Search for how nodeState is created/populated
rg "nodeState\s*=" --type h --type cpp -B3 -A3 | head -n 150
Based on my investigation, let me create a comprehensive analysis. Let me check one more crucial thing - whether the code actually compiles and how the comparison works:
<function_calls>
#!/bin/bash
Search for JsonObject or DynamicJsonDocument to understand nodeState type better
rg "DynamicJsonDocument|JsonObject|StaticJsonDocument" --type h --type cpp -B2 -A2 | head -n 100
Let me now look for the specific pattern where the pointer might be null:
<function_calls>
#!/bin/bash
Search for logging patterns that handle null from .as()
rg 'EXT_LOGD.*%s' --type h --type cpp | head -n 20
Check if there are any safe logging patterns in the codebase
rg 'as<const char.*?(' --type h --type cpp -A3 | grep -B1 -A2 "EXT_LOGD|printf|log"
Let me verify the ArduinoJson behavior specifically for null safety in format strings:
<function_calls>
ArduinoJson 6.x JsonVariant["key"].as<const char*>() behavior missing key or wrong type
</function_calls>
Let me make a final comprehensive check of the actual safety issue:
<function_calls>
#!/bin/bash
Final verification - check if there are ANY guards around similar patterns
python3 << 'PYEOF'
import re
with open("src/MoonLight/Modules/ModuleLiveScripts.h") as f:
content = f.read()
Find all .as<const char*>() calls
pattern = r'.as<const char\s**>\s*()'
matches = list(re.finditer(pattern, content))
print(f"Found {len(matches)} uses of .as<const char*>()")
lines = content.split('\n')
for match in matches:
# Find line number
line_num = content[:match.start()].count('\n') + 1
line_content = lines[line_num - 1]
print(f"\nLine {line_num}: {line_content.strip()}")
# Check if result is immediately used in a format string
after = content[match.end():match.end()+100]
print(f" After: {after[:60]}")
PYEOF
Now let me verify the key comparison semantic issue:
<function_calls>
#!/bin/bash
Understand what happens when you compare std::string with JsonVariant
Check if there's an operator== that handles this
python3 << 'PYEOF'
Check ArduinoJson documentation and common patterns
The issue is:
Line 51: if (updatedItem == nodeState["name"])
updatedItem is likely const std::string& (from function parameter)
nodeState["name"] is JsonVariant
For this to work, JsonVariant must have operator==(const std::string&)
or operator==(const char*) or similar
Let's verify by checking includes and available comparison operators
with open("src/MoonLight/Modules/ModuleLiveScripts.h") as f:
lines = f.readlines()
print("Key code snippets:")
print("\nLine 51-53:")
for i in range(50, 53):
print(f" {i+1}: {lines[i].rstrip()}")
print("\nLine 62:")
print(f" 62: {lines[61].rstrip()}")
print("\n\nAnalysis:")
print("- Line 51: if (updatedItem == nodeState["name"])")
print(" Requires: JsonVariant::operator==(std::string) or similar")
print(" Or: std::string::operator==(JsonVariant) if JsonVariant converts to const char*")
print()
print("- Line 62: EXT_LOGD(ML_TAG, "update due to new node %s done", nodeState["name"].as<const char >())")
print(" Issue: If nodeState["name"] is not a string or doesn't exist,")
print(" .as<const char>() might return nullptr, which will crash in printf with %s")
PYEOF
Perfect! Now I have enough information. Let me verify one more time by searching for how ArduinoJson behaves:
<function_calls>
ArduinoJson6 JsonVariant::operator== const char*
</function_calls>
Let me check for the null return behavior explicitly:
<function_calls>
ArduinoJson JsonElement as method returns null pointer missing field
</function_calls>
Based on my thorough investigation, let me now write the rewritten review comment:
Guard against null pointer when logging nodeState["name"].as<const char*>().
At line 62, nodeState["name"].as<const char*>() may return nullptr if the "name" field is missing from the JSON document or is not a string type. Passing a null pointer to the %s format specifier in EXT_LOGD causes undefined behavior and crashes on ESP32.
Additionally, verify that the comparison at line 51 (if (updatedItem == nodeState["name"])) is safe. This requires JsonVariant to have an appropriate operator== overload that accepts the type of updatedItem, or a conversion operator that allows the comparison.
Apply this fix to the logging call:
- EXT_LOGD(ML_TAG, "update due to new node %s done", nodeState["name"].as<const char *>());
+ const char* nodeName = nodeState["name"].as<const char*>();
+ EXT_LOGD(ML_TAG, "update due to new node %s done", nodeName ? nodeName : "<null>");Also applies to: 62-62
🤖 Prompt for AI Agents
In src/MoonLight/Modules/ModuleLiveScripts.h (around lines 51–62), retrieve
nodeState["name"] into a const char* first, check it for nullptr before using it
in the if comparison and in EXT_LOGD (e.g., const char* name =
nodeState["name"].as<const char*>(); if (name && updatedItem == name) { ...
EXT_LOGD(..., name); } else { EXT_LOGD(..., name ? name : "<missing>"); }), and
ensure the comparison uses the C-string (or convert to std::string) rather than
relying on an implicit operator== between UpdatedItem and JsonVariant.
| _moduleDrivers->read([&](ModuleState& driversState) { | ||
| for (JsonObject nodeState : driversState.data["nodes"].as<JsonArray>()) { | ||
|
|
||
| if (updatedItem == name) { | ||
| EXT_LOGV(ML_TAG, "updateHandler equals current item -> livescript compile %s", updatedItem.c_str()); | ||
| if (updatedItem == nodeState["name"]) { | ||
| EXT_LOGD(ML_TAG, "updateHandler equals current item -> livescript compile %s", updatedItem.c_str()); | ||
| LiveScriptNode* liveScriptNode = (LiveScriptNode*)_moduleDrivers->findLiveScriptNode(nodeState["name"]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against null when logging nodeState["name"] in drivers section.
The same null-pointer concern from the effects section applies here. At line 81, nodeState["name"].as<const char*>() may return nullptr, causing undefined behavior when passed to %s in EXT_LOGD.
Apply this diff to guard the logging:
- EXT_LOGD(ML_TAG, "update due to new node %s done", nodeState["name"].as<const char *>());
+ const char* nodeName = nodeState["name"].as<const char*>();
+ EXT_LOGD(ML_TAG, "update due to new node %s done", nodeName ? nodeName : "<null>");Also applies to: 81-81
🤖 Prompt for AI Agents
In src/MoonLight/Modules/ModuleLiveScripts.h around lines 67-72 and line 81,
guard against nodeState["name"] being null before passing it to EXT_LOGD and
findLiveScriptNode: read the name into a const char* via
nodeState["name"].as<const char*>(), check if it's nullptr and substitute a safe
fallback like "<null>" for logging and for the findLiveScriptNode call, and also
ensure you check the returned LiveScriptNode* for nullptr before dereferencing
or using it.
|
@coderabbitai, here are some notes on your comments. Wait with replying until I add my next commit, that you can make one reply on both instead of 2: scriptState["name"] should never be null, and if so it is a dev error which should be fixed. Regarding #62 (comment) : Note: SharedFSPresistence is not used yet. I will use it in one of the subsequent commits |
|
Tip For best results, initiate chat on the files or code changes. @ewowi, thank you for the detailed feedback! I understand and will wait for your next commit before providing a consolidated reply. This will keep the PR review thread cleaner and more focused. I've noted all your points:
Looking forward to your next commit! 🚀 ✏️ Learnings added
🧠 Learnings used |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/MoonLight/Nodes/Effects/E_MoonLight.h (1)
115-135: UninitializedframeNrin LinesEffect::loop causes undefined behavior
frameNris declared but never initialized before being used in the color calculations, then incremented afterward:int frameNr; ... int colorNr = (frameNr / layer->size.y) % 3; ... (frameNr)++;This is undefined behavior and can produce random artifacts. Make
frameNrinitialized and persistent across frames, e.g.:- void loop() override { - int frameNr; + uint32_t frameNr = 0; // member variable + + void loop() override { ... - int colorNr = (frameNr / layer->size.y) % 3; + int colorNr = (frameNr / layer->size.y) % 3; ... (frameNr)++; }(or declare
static uint32_t frameNr = 0;insideloop()if you prefer a local static).lib/framework/FSPersistence.h (1)
21-128: GlobaldelayedWritesvector is not synchronized and can race across tasks
delayedWritesis mutated inwriteToFS()and iterated/cleared inwriteToFSDelayed()with no locking. If different FreeRTOS tasks trigger updates (HTTP, WS, UI) while delayed writes are pending, this can corrupt the vector or invoke freed lambdas.Consider protecting
delayedWriteswith a small mutex (or a critical section) aroundpush_back, iteration, andclear(), similar to howrunInAppTask_mutexis used elsewhere.
♻️ Duplicate comments (7)
src/MoonBase/Modules/ModuleTasks.h (1)
50-58: Task stats arithmetic changes look safe and fix prior overflow
- The added
if (!WiFi.localIP() && !ETH.localIP()) return;cheaply skips work when completely offline.- Initializing
totalRunTimeto 1 and computing(uint32_t)(100ULL * ts->ulRunTimeCounter / totalRunTime)removes the earlier 32‑bit overflow risk while still avoiding division by zero.- Reporting
runtimein seconds viaulRunTimeCounter / 1000000is straightforward, assuming the run‑time stats timer runs at 1 MHz.These look good; just ensure the FreeRTOS run‑time stats frequency matches the
1'000'000divisor.Also applies to: 98-108
src/MoonBase/SharedHttpEndpoint.h (2)
90-95:findModulesubstring search can match the wrong moduleUsing
contains(path.c_str(), module->_moduleName.c_str())risks false positives when one module name is a substring of another (e.g.,"io"vs"ModuleIO"). Prefer matching a path suffix like"/" + module->_moduleNameto ensure only the intended module is returned.Module* findModule(const String& path) { for (Module* module : modules) { - if (contains(path.c_str(), module->_moduleName.c_str())) return module; + String expectedSuffix = "/" + module->_moduleName; + if (path.endsWith(expectedSuffix)) return module; } return nullptr; }
15-20:HTTP_ENDPOINT_ORIGIN_IDrequires includingHttpEndpoint.h
handlePostusesHTTP_ENDPOINT_ORIGIN_IDbut this header doesn’t include the definition. Unless every translation unit that includesSharedHttpEndpoint.halso happens to includeHttpEndpoint.hearlier, this will not compile.Add
#include <HttpEndpoint.h>here so the macro/constant is always visible where it’s used.Also applies to: 79-82
src/MoonBase/SharedFSPersistence.h (2)
77-99: Consider reusing the global JsonDocument pool for FS (de)serialization
readFromFS,writeToFSNow, andapplyDefaultseach construct a freshJsonDocument doc;. For modules with large state, this repeatedly grows and shrinks dynamic JSON capacity and can fragment heap on ESP32‑D0.Since the project already uses a global
gTempDocguarded by a mutex for heavy JSON operations, consider switching these spots to that pool (lock, clear, use, serialize, unlock) to keep a single contiguous buffer for FS persistence.Also applies to: 136-157, 181-186
20-22: GlobalsharedDelayedWritesqueue is unsynchronized across tasks
sharedDelayedWritesis modified inwriteToFS()and iterated/cleared inwriteToFSDelayed()with no synchronization. If multiple tasks trigger module writes concurrently, this can corrupt the vector or execute stale callbacks.Add a small mutex (or critical section) around
sharedDelayedWrites.push_back, the for‑loop, andclear()so the delayed‑write queue is thread‑safe.Also applies to: 101-167
src/MoonBase/SharedWebSocketServer.h (2)
87-107: Optional: reuse JSON pool for WS payloads and tighten module lookupBoth
transmitIdandtransmitDataallocate a localJsonDocumentper call. For large module states on ESP32‑D0, you could again reuse the globalgTempDocwith its mutex to keep JSON buffer allocations bounded, similar to other pooled usages.Also,
findModulecurrently usescontains(path.c_str(), module->_moduleName.c_str()), which can match unintended modules when names overlap. Matching an exact path suffix (e.g.,"/" + module->_moduleName) is safer and consistent with the HTTP router.Also applies to: 115-120
15-20:WEB_SOCKET_ORIGIN*macros needWebSocketServer.hincluded
clientId()and the initialtransmitDatacall inonFrameuseWEB_SOCKET_ORIGIN_CLIENT_ID_PREFIXandWEB_SOCKET_ORIGIN, but this header doesn’t includeWebSocketServer.h, where those are defined. In translation units that only includeSharedWebSocketServer.h, these macros will be undefined.Add
#include <WebSocketServer.h>here so the symbols are always available.Also applies to: 84-84
🧹 Nitpick comments (9)
src/MoonBase/Modules/ModuleDevices.h (3)
50-62: Dual-interface IP guard is logically correct; consider centralizing active-IP logic.The new
if (!WiFi.localIP() && !ETH.localIP()) return;checks inloop1s()andloop10s()correctly ensure at least one interface is up before doing UDP I/O. That’s a good correctness + small CPU/heap win (no UDP work when fully offline).To reduce duplication and make the WiFi/ETH selection more self-documenting, you could optionally introduce a small helper:
+inline IPAddress getActiveIP() { + IPAddress ip = WiFi.localIP(); + if (!ip) ip = ETH.localIP(); + return ip; +} void loop1s() { if (!_socket->getConnectedClients()) return; - if (!WiFi.localIP() && !ETH.localIP()) return; + if (!getActiveIP()) return; ... } void loop10s() { if (!_socket->getConnectedClients()) return; - if (!WiFi.localIP() && !ETH.localIP()) return; + if (!getActiveIP()) return; ... }This avoids repeating the WiFi/ETH condition and makes the “active IP” concept explicit.
73-111: New IP guard inupdateDevices()is good; a couple of small heap trims are possible nearby.The added
if (!WiFi.localIP() && !ETH.localIP()) return;before building/sending the JSON snapshot is helpful: when the node has no IP, you now skip thestd::vectorbuild, sort, andJsonDocumentallocation, which is a nice small win on constrained heap.Right around here you can shave a bit more heap churn with minimal change:
- Avoid repeated
ip.toString()allocations in the loop and on insert:- for (JsonObject dev : _state.data["devices"].as<JsonArray>()) { - if (dev["ip"] == ip.toString()) { + String ipStr = ip.toString(); // one allocation + for (JsonObject dev : _state.data["devices"].as<JsonArray>()) { + if (dev["ip"] == ipStr) { device = dev; ... } } if (device.isNull()) { device = _state.data["devices"].as<JsonArray>().add<JsonObject>(); EXT_LOGD(ML_TAG, "added ...%d %s", ip[3], name); - device["ip"] = ip.toString(); + device["ip"] = ipStr; }This removes multiple transient
Stringallocations per call, which helps heap fragmentation on ESP32, especially ifupdateDevices()is hit frequently.
- Optional (only if device lists can grow large): if you expect many devices, consider whether
std::vector<JsonObject>+std::sortplus a freshJsonDocument devices;each time is acceptable on D0 boards. If not, a small fixed-capacity container or reusing a sharedJsonDocument/buffer (as you do elsewhere) could further reduce heap pressure. Otherwise, the current approach is fine. Based on learnings, focus on this only if device counts are realistically high.
124-134:activeIPselection is correct; minor clarity/refactor improvement possible.Picking
activeIP = WiFi.localIP() ? WiFi.localIP() : ETH.localIP();and feeding it intoupdateDevices()fixes the previous WiFi-only behavior for Ethernet-only setups and aligns with the new guards.For readability and reuse with the other IP checks, you could align this with a shared helper (as sketched earlier):
- IPAddress activeIP = WiFi.localIP() ? WiFi.localIP() : ETH.localIP(); + IPAddress activeIP = getActiveIP(); + if (!activeIP) return; // defensive, if you want extra safety // EXT_LOGD(MB_TAG, "UDP packet written (%s -> %d)", message.name.c_str(), activeIP[3]); updateDevices(message.name.c_str(), activeIP);This keeps the “pick WiFi, else ETH” rule in one place and avoids subtle differences if more call sites appear later.
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
36-38: SolidEffect white-channel loop fix is correct; optional micro‑optimizationInitialization of
indexin the loop header is correct and fixes the previous undefined behavior. If you want a tiny CPU win, you could precompute the brightness‑scaled white once before the loop:- if (layer->layerP->lights.header.offsetWhite != UINT8_MAX && white > 0) - for (int index = 0; index < layer->nrOfLights; index++) layer->setWhite(index, white * brightness / 255); + if (layer->layerP->lights.header.offsetWhite != UINT8_MAX && white > 0) { + const uint8_t whiteLevel = white * brightness / 255; + for (int index = 0; index < layer->nrOfLights; index++) { + layer->setWhite(index, whiteLevel); + } + }
264-271: Caching active IP is fine; consider avoiding doubleWiFi.localIP()and handling no‑IP case
IPAddress activeIP = WiFi.localIP() ? WiFi.localIP() : ETH.localIP();is heap‑neutral and works, but it callsWiFi.localIP()twice and assumes at least one interface has a non‑zero IP. You could improve clarity and robustness by caching once and falling back explicitly if both are unset:- IPAddress activeIP = WiFi.localIP() ? WiFi.localIP() : ETH.localIP(); + IPAddress wifiIP = WiFi.localIP(); + IPAddress ethIP = ETH.localIP(); + IPAddress activeIP = wifiIP ? wifiIP : ethIP; + // Optional: if both are 0.0.0.0, show a placeholder instead of ".0"Then continue to use
activeIP[3]as you do now.lib/framework/APSettingsService.h (1)
52-54: Origin-aware update signature is consistent; consider markingoriginIdunusedAdding
originIdtoAPSettings::updatekeeps it consistent with other settings services, even though it’s currently unused. To avoid compiler warnings, you might want to mark it as intentionally unused (e.g.,(void)originId;or an[[maybe_unused]]attribute).Also applies to: 115-144
src/MoonBase/SharedEventEndpoint.h (1)
27-60: Heap-friendly captures; consider reusing the global JsonDocument pool insyncStateGood:
registerModulelambdas capturemoduleby pointer, avoiding per‑handlerStringallocations, andsyncStateearly‑returns when there are no connected clients.If you still want to squeeze heap usage on ESP32‑D0, consider reusing the shared
gTempDoc(with its mutex) insidesyncStateinstead of a freshJsonDocument doc;each time, especially for large module states. That keeps the JSON serialization buffer contiguous and avoids repeated allocate/free cycles.src/MoonBase/Module.h (2)
51-56: New semaphore-basedpostUpdate/getUpdateflow is heap-friendly but relies on the loop() contractThe producer/consumer pattern using
updateReadySem/updateProcessedSemandpostUpdate/getUpdatenicely replaces the prior unbounded lambda queue with a single in‑flightUpdatedItemper module, which is much safer for heap usage.The trade‑off is that correct behavior now depends on every module that overrides
loop()callingModule::loop()sogetUpdate()runs; otherwise producers can block indefinitely. You might eventually want a small helper (e.g., a non‑virtualloop()that calls a virtualloopImpl()) to make that contract harder to violate, but the current design is workable if callers respect the comment.Also applies to: 116-137, 149-168
55-56: StaticupdateOriginIdis efficient but assumes serialized updatesUsing a single static
Char<20> updateOriginIdwritten byModuleState::updateand consumed inprocessUpdatedItemkeeps memory overhead low, but it implicitly assumes only one update is “active” at a time across tasks. If you ever introduce true parallel updates from different origins, you may want to move the origin ID into per‑state or per‑update context instead of a static.Given current architecture (single app loop + serialized update paths), this is acceptable; just something to keep in mind for future concurrency changes.
Also applies to: 156-167
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
factory_settings.ini(1 hunks)lib/framework/APSettingsService.h(2 hunks)lib/framework/FSPersistence.h(1 hunks)src/MoonBase/Module.h(5 hunks)src/MoonBase/Modules/ModuleDevices.h(4 hunks)src/MoonBase/Modules/ModuleTasks.h(3 hunks)src/MoonBase/SharedEventEndpoint.h(1 hunks)src/MoonBase/SharedFSPersistence.h(1 hunks)src/MoonBase/SharedHttpEndpoint.h(1 hunks)src/MoonBase/SharedWebSocketServer.h(1 hunks)src/MoonLight/Nodes/Drivers/D_ArtnetIn.h(1 hunks)src/MoonLight/Nodes/Drivers/D_ArtnetOut.h(1 hunks)src/MoonLight/Nodes/Effects/E_MoonLight.h(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/MoonLight/Nodes/Drivers/D_ArtnetIn.h
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-11-23T16:25:14.295Z
Learning: On ESP32-D0 boards without PSRAM, creating large local JsonDocument instances (e.g., with 50+ GPIO pins in setBoardPresetDefaults) can cause heap exhaustion and crashes due to insufficient contiguous heap memory. The solution is to use a global pre-allocated JsonDocument pool (gTempDoc) protected by a mutex, which eliminates repeated heap allocation/deallocation and fragmentation.
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-11-27T13:51:19.831Z
Learning: On MoonLight ESP32 firmware, core infrastructure components (semaphores, basic memory allocations for Module initialization, etc.) are expected to succeed and should be validated through testing rather than runtime NULL guards. Memory safeguards should focus on variable/user-configurable components like lights.channels arrays, mappingTable sizes, and Node counts that vary per user configuration.
📚 Learning: 2025-11-23T16:25:14.295Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-11-23T16:25:14.295Z
Learning: On ESP32-D0 boards without PSRAM, creating large local JsonDocument instances (e.g., with 50+ GPIO pins in setBoardPresetDefaults) can cause heap exhaustion and crashes due to insufficient contiguous heap memory. The solution is to use a global pre-allocated JsonDocument pool (gTempDoc) protected by a mutex, which eliminates repeated heap allocation/deallocation and fragmentation.
Applied to files:
src/MoonBase/SharedWebSocketServer.hsrc/MoonBase/SharedFSPersistence.hsrc/MoonBase/Module.h
📚 Learning: 2025-11-27T13:51:19.831Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-11-27T13:51:19.831Z
Learning: On MoonLight ESP32 firmware, core infrastructure components (semaphores, basic memory allocations for Module initialization, etc.) are expected to succeed and should be validated through testing rather than runtime NULL guards. Memory safeguards should focus on variable/user-configurable components like lights.channels arrays, mappingTable sizes, and Node counts that vary per user configuration.
Applied to files:
src/MoonBase/SharedWebSocketServer.hsrc/MoonBase/SharedFSPersistence.hsrc/MoonBase/Module.hsrc/MoonLight/Nodes/Effects/E_MoonLight.h
📚 Learning: 2025-11-23T15:45:39.777Z
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 62
File: src/MoonBase/Module.h:27-34
Timestamp: 2025-11-23T15:45:39.777Z
Learning: In MoonLight codebase, all property names have unique 15-character prefixes, so Char<16> is safe for UpdatedItem fields (name, parent, oldValue) without causing property matching collisions.
Applied to files:
src/MoonBase/Module.h
🧬 Code graph analysis (5)
src/MoonBase/SharedWebSocketServer.h (3)
lib/framework/SecurityManager.h (3)
SecurityManager(82-117)AuthenticationPredicates(65-80)IS_ADMIN(76-79)lib/framework/WebSocketServer.h (1)
transmitId(121-132)src/MoonBase/Utilities.h (1)
contains(152-157)
src/MoonBase/Module.h (1)
src/MoonBase/Module.cpp (2)
update(262-293)update(262-262)
lib/framework/APSettingsService.h (5)
lib/framework/EthernetSettingsService.h (1)
update(72-99)lib/framework/MqttSettingsService.h (1)
update(109-120)lib/framework/NTPSettingsService.h (1)
update(66-73)lib/framework/StatefulService.h (2)
update(123-134)update(144-155)src/MoonBase/Module.cpp (2)
update(262-293)update(262-262)
src/MoonLight/Nodes/Effects/E_MoonLight.h (4)
src/MoonBase/Nodes.cpp (2)
onUpdate(401-541)onUpdate(401-401)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)src/MoonLight/Nodes/Effects/E_MoonModules.h (1)
onUpdate(121-125)
src/MoonBase/SharedHttpEndpoint.h (4)
lib/framework/SecurityManager.h (1)
SecurityManager(82-117)src/MoonBase/Module.h (2)
ModuleState(47-138)Module(140-192)lib/framework/StatefulService.h (1)
StateUpdateResult(26-106)src/MoonBase/Utilities.h (1)
contains(152-157)
🪛 Clang (14.0.6)
src/MoonBase/SharedWebSocketServer.h
[error] 15-15: 'PsychicHttp.h' file not found
(clang-diagnostic-error)
src/MoonBase/SharedFSPersistence.h
[error] 15-15: 'FS.h' file not found
(clang-diagnostic-error)
src/MoonBase/SharedEventEndpoint.h
[error] 15-15: 'EventSocket.h' file not found
(clang-diagnostic-error)
src/MoonBase/SharedHttpEndpoint.h
[error] 15-15: 'PsychicHttp.h' file not found
(clang-diagnostic-error)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (5)
factory_settings.ini (1)
19-19: AP channel default change appears orthogonal to heap optimization PR objectives.The change from channel 1 to channel 6 is a reasonable WiFi best practice (channel 6 sits in the center of the spectrum and typically experiences less interference), and the comment clearly documents the rationale. However, this configuration update is not mentioned in the PR objectives, which focus on heap optimization, shared routers, and memory-bounded queuing.
Please confirm whether this configuration change is intended as part of this heap optimization PR or should be submitted separately.
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
925-930: RubiksCubeEffect::onUpdate signature change is consistent and avoidsStringheap usageSwitching to
void onUpdate(const Char<20>& oldValue, const JsonObject control) overridealigns this node with the new Char-based API (as in other nodes) and removes the ArduinoString&from the callback path, which helps keep heap usage down on ESP32. The body only inspectscontrol["name"]and remains behaviorally equivalent.
1110-1126: ParticlesEffect::onUpdate updated to new signature; behavior unchanged and heap‑friendlyThe new
onUpdate(const Char<20>& oldValue, const JsonObject control) overridematches the standardized callback signature and keeps heap allocations out of the parameter list. Re‑initializing particles only when"number of Particles"or"barriers"change is the same behavior as before and reuses the existing staticparticles[255]buffer, so no new heap pressure is introduced.lib/framework/FSPersistence.h (1)
176-181: Switch toto<JsonObject>()inapplyDefaultslooks correctUsing
jsonDocument.to<JsonObject>()here is consistent with other call sites and ArduinoJson v7 patterns; it guarantees the root is an object before passing it into_stateUpdater.src/MoonBase/Module.h (1)
23-37: UpdatedItem and ModuleState layout substantially reduce heap churnReplacing the
Stringfields inUpdatedItemwith fixedChar<20>buffers and centralizing state storage in the sharedgModulesDocdocument eliminates a lot of per‑update heap allocation. Combined with the staticupdateOriginIdand the comment about expected write patterns, this is a good fit for the constrained ESP32 heap and the unique‑prefix property naming scheme.Also applies to: 51-56
Back end ======== - NodeManager: onUpdate: remove controls when new node (revert comment) - Shared endpoints: findModule: path.endsWith - Physical layer: add nrOfAssignedPins, sizeof(ledsPerPin) - Nodes: memcpy and memset sizeof() - Drivers: nrOfPins = min(pins)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (4)
src/MoonBase/SharedHttpEndpoint.h (2)
15-20: Missing#include <HttpEndpoint.h>forHTTP_ENDPOINT_ORIGIN_ID.The file uses
HTTP_ENDPOINT_ORIGIN_IDat line 80 but does not include the header that defines it. This constant is defined inlib/framework/HttpEndpoint.h.#include <PsychicHttp.h> #include <SecurityManager.h> +#include <HttpEndpoint.h> #include <map>
90-95:endsWithmatching still vulnerable to suffix collisions.While
endsWithis better thancontains, it still has edge cases. For example, if you have modules "IO" and "ModuleIO", the path "/rest/ModuleIO" would match module "IO" because"/rest/ModuleIO".endsWith("IO")is true.Module* findModule(const String& path) { for (Module* module : modules) { - if (path.endsWith(module->_moduleName.c_str())) return module; + String expectedSuffix = "/" + module->_moduleName; + if (path.endsWith(expectedSuffix)) return module; } return nullptr; }src/MoonBase/SharedWebSocketServer.h (2)
15-20: Missing#include <WebSocketServer.h>forWEB_SOCKET_ORIGINmacros.The file uses
WEB_SOCKET_ORIGIN(line 58) andWEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX(line 87), which are defined inlib/framework/WebSocketServer.hbut not included.#include <PsychicHttp.h> +#include <WebSocketServer.h> #include <map> #include <set>
118-123:endsWithmatching still vulnerable to suffix collisions.Same issue as in
SharedHttpEndpoint.h. Use exact path segment matching to avoid false positives when module names are suffixes of other module names.Module* findModule(const String& path) { for (Module* module : modules) { - if (path.endsWith(module->_moduleName.c_str())) return module; + String expectedSuffix = "/" + module->_moduleName; + if (path.endsWith(expectedSuffix)) return module; } return nullptr; }
🧹 Nitpick comments (4)
src/MoonLight/Nodes/Drivers/D_AudioSync.h (1)
39-41: Usingsizeof(sharedData.bands)inmemcpyis safer; consider guarding against size mismatchSwitching from an element-count constant to
sizeof(sharedData.bands)for thememcpysize is an improvement: the copy size now automatically tracks the actual storage ofsharedData.bandsin bytes and won’t silently go stale if the array type/length changes.Two optional robustness tweaks you might consider:
- If
sync.fftResultis also a fixed array, add astatic_assert(sizeof(sharedData.bands) <= sizeof(sync.fftResult), "AudioSync: fftResult smaller than sharedData.bands");near the class to catch mismatches at compile time.- Alternatively, if you want to be ultra-defensive, copy
min(sizeof(sharedData.bands), sizeof(sync.fftResult)), though that’s usually overkill on constrained MCUs.As-is, the change is correct and a clear improvement over a raw channel-count constant.
src/MoonLight/Nodes/Effects/E_MovingHeads.h (1)
58-67: Consider making the color palette static to reduce per-instance heap usage.Since
colorwheelpaletteis never modified and contains the same constant values for all instances, declaring it asstatic const(orstatic constexprwith astd::array) would eliminate per-instance heap allocation—directly supporting this PR's ESP32 heap-optimization goals.Apply this diff to make the palette static:
- std::vector<CRGB> colorwheelpalette = { + static const std::vector<CRGB> colorwheelpalette; +}; + +inline const std::vector<CRGB> Troy1MoveEffect::colorwheelpalette = { CRGB(255, 255, 255), // White CRGB(255, 0, 0), // Red CRGB(0, 255, 0), // Green CRGB(0, 0, 255), // Blue CRGB(255, 255, 0), // Yellow CRGB(0, 255, 255), // Cyan CRGB(255, 165, 0), // Orange CRGB(128, 0, 128) // Purple - }; +};Or, for a more compact solution using
std::array:- std::vector<CRGB> colorwheelpalette = { + static constexpr std::array<CRGB, 8> colorwheelpalette = {{ CRGB(255, 255, 255), // White CRGB(255, 0, 0), // Red CRGB(0, 255, 0), // Green CRGB(0, 0, 255), // Blue CRGB(255, 255, 0), // Yellow CRGB(0, 255, 255), // Cyan CRGB(255, 165, 0), // Orange CRGB(128, 0, 128) // Purple - }; +}};Note: If using
std::array, update thefindClosestColorWheelIndexsignature to acceptconst std::array<CRGB, 8>&instead ofconst std::vector<CRGB>&.src/MoonBase/SharedWebSocketServer.h (2)
87-87: Consider makingclientIdprivate.This method is only used internally by
transmitId. Moving it to the private section would improve encapsulation.
101-116: Consider JsonDocument pooling for heap-constrained devices.On ESP32-D0 (no PSRAM), allocating a new
JsonDocumentper transmission could contribute to heap fragmentation with large module states. The PR discussion notes ArduinoJson v7'ssizeof(JsonDocument)is small, making pooling optional. If heap pressure is observed, consider using a shared/pooled document with mutex protection.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
src/MoonBase/Modules/ModuleDevices.h(4 hunks)src/MoonBase/NodeManager.h(10 hunks)src/MoonBase/SharedHttpEndpoint.h(1 hunks)src/MoonBase/SharedWebSocketServer.h(1 hunks)src/MoonBase/Utilities.h(4 hunks)src/MoonLight/Layers/PhysicalLayer.cpp(4 hunks)src/MoonLight/Layers/PhysicalLayer.h(1 hunks)src/MoonLight/Modules/ModuleDrivers.h(3 hunks)src/MoonLight/Nodes/Drivers/D_ArtnetOut.h(1 hunks)src/MoonLight/Nodes/Drivers/D_AudioSync.h(1 hunks)src/MoonLight/Nodes/Drivers/D_FastLED.h(1 hunks)src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h(3 hunks)src/MoonLight/Nodes/Effects/E_MoonLight.h(6 hunks)src/MoonLight/Nodes/Effects/E_MoonModules.h(3 hunks)src/MoonLight/Nodes/Effects/E_MovingHeads.h(1 hunks)src/MoonLight/Nodes/Effects/E_WLED.h(4 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/MoonLight/Nodes/Effects/E_WLED.h
🚧 Files skipped from review as they are similar to previous changes (2)
- src/MoonLight/Nodes/Drivers/D_ArtnetOut.h
- src/MoonBase/Modules/ModuleDevices.h
🧰 Additional context used
🧬 Code graph analysis (5)
src/MoonBase/NodeManager.h (3)
src/MoonLight/Modules/ModuleChannels.h (1)
onUpdate(58-96)src/MoonBase/Modules/ModuleIO.h (1)
onUpdate(393-413)src/MoonBase/Nodes.cpp (2)
onUpdate(401-541)onUpdate(401-401)
src/MoonLight/Nodes/Effects/E_MoonModules.h (4)
src/MoonLight/Nodes/Effects/E_MoonLight.h (2)
onUpdate(926-930)onUpdate(1122-1126)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonLight/Modules/ModuleDrivers.h (3)
src/MoonBase/NodeManager.h (2)
begin(37-75)loop(298-311)src/MoonBase/Module.cpp (2)
begin(313-337)begin(313-313)src/MoonBase/Utilities.cpp (2)
isInPSRAM(103-126)isInPSRAM(103-103)
src/MoonLight/Nodes/Effects/E_MoonLight.h (4)
src/MoonBase/Nodes.cpp (2)
onUpdate(401-541)onUpdate(401-401)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
onUpdate(35-41)
src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (1)
src/MoonLight/Nodes/Drivers/parlio.cpp (1)
show_parlio(244-245)
🪛 Clang (14.0.6)
src/MoonBase/SharedHttpEndpoint.h
[error] 15-15: 'PsychicHttp.h' file not found
(clang-diagnostic-error)
src/MoonBase/SharedWebSocketServer.h
[error] 15-15: 'PsychicHttp.h' file not found
(clang-diagnostic-error)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (39)
src/MoonLight/Nodes/Drivers/D_AudioSync.h (1)
26-37: Connectivity guard andinitlifecycle look good; confirmWLEDSync::begin()re-init behaviorThe new
if (!WiFi.isConnected() && !ETH.connected())guard plusinit = false; return;gives you a clean “no network, no work” path and ensuressync.begin()is only called once per connectivity period, which is what you want.The only thing to double‑check is the behavior of
WLEDSync::begin()across multiple calls over the device lifetime:
- With this pattern, every WiFi/ETH reconnect will re-run
sync.begin()(once).- If
begin()internally allocates sockets/buffers without fully reusing/freeing them across calls, you could slowly leak or fragment heap on ESP32, especially on D0 without PSRAM.If the library guarantees
begin()is idempotent and safe to call on reconnects, this is fine. Otherwise, consider pairinginit = falsewith an explicitsync.end()/reset (if available) when losing connectivity, or guarding re-init with an explicit “hasBegun” flag and only callingbegin()once per object lifetime.src/MoonLight/Nodes/Effects/E_MovingHeads.h (1)
349-349: LGTM! Critical fix for array initialization.The change correctly fixes a bug where only
NUM_GEQ_CHANNELSbytes were zeroed instead of the full array size. SincebandSpeedisuint16_t[NUM_GEQ_CHANNELS], the old code would have left half the array uninitialized (assuminguint16_tis 2 bytes on ESP32).src/MoonBase/Utilities.h (5)
118-118: LGTM! Clean simplification of fromJson.The one-liner constructor-based approach is idiomatic and equivalent to the previous multi-line implementation.
166-182: Good const-correctness improvements to Char operators.The changes properly handle null checks for JsonVariant/JsonString and add the missing
operator=(const String&)overload. The const reference parameters avoid unnecessary copies.
189-211: Const-correctness applied consistently across operators.The
operator+,operator+=, and comparison operators now correctly useconstqualifiers and references where appropriate.
242-242: Correct:c_str()is now const.This allows calling
c_str()on constChar<N>instances, which is the expected behavior.
389-391: Good optimization:fastDiv255is a well-known technique.This uses the multiplication-by-reciprocal trick
(x * 0x8081) >> 23which approximatesx / 255in 3-4 cycles instead of a costly division. This is commonly used in graphics/LED code for color scaling.src/MoonLight/Nodes/Effects/E_MoonModules.h (3)
81-82: Good: Usingsizeof()instead of hardcoded array size.This is safer and self-documenting. If the array sizes change, the memset will automatically adapt.
109-109: Control size now matches the array size.The
customRuleStringis declared aschar[20]on line 54, so the control limit of 20 is correct and consistent.
121-125: Signature update aligns with codebase-wide refactor.The
onUpdatesignature change toconst Char<20>&matches the pattern used in other nodes (D_VirtualDriver.h, D__Sandbox.h, E__Sandbox.h, E_MoonLight.h) as shown in the relevant code snippets.src/MoonLight/Nodes/Effects/E_MoonLight.h (6)
37-37: Critical fix: Loop index now properly initialized.This was previously flagged as a critical issue. Using an uninitialized
indexvariable caused undefined behavior when iterating through lights.
116-116: Good:frameNrnow initialized to 0.Prevents undefined behavior from using an uninitialized local variable.
264-270: Good: IP address cached to avoid repeated API calls.Caching
activeIPavoids callingWiFi.localIP()orETH.localIP()multiple times within the same loop iteration. This is both a micro-optimization and improves code clarity.
617-620: Good: Usingsizeof()for array clearing.This is safer than hardcoding the array sizes. The memset calls will automatically adapt if the array declarations change.
926-930: Signature update consistent with codebase refactor.The
onUpdatesignature change toconst Char<20>&aligns with the pattern established across other effect and driver nodes.
1122-1126: Signature update consistent with codebase refactor.Same pattern as RubiksCubeEffect - the
onUpdatesignature is correctly updated toconst Char<20>&.src/MoonBase/NodeManager.h (6)
23-23: Good:defaultNodeNamechanged to fixed-sizeChar<20>.This aligns with the PR's heap optimization goals by avoiding heap-allocated
Stringobjects. Node names are typically short identifiers that fit within 20 characters.
134-134: Good:onUpdatenow takesconst UpdatedItem&.The const reference is more efficient (avoids copy) and safer (prevents accidental modification). This aligns with the pattern in ModuleIO.h and ModuleChannels.h.
205-205: Consistent with the semantic change at line 151.Same pattern - checking for non-empty oldValue to determine if an old node existed.
249-249: Good: Usingas<const char*>()for logging.This avoids creating temporary
Stringobjects for logging purposes.
298-299: Good:loop()now explicitly overrides base class.The
overridespecifier ensures compile-time checking that this correctly overrides a virtual method. CallingModule::loop()ensures base class behavior is preserved.
151-155: The comparison change from"null"to""is not evident in the codebase—the code already consistently uses empty string semantics.After verifying the codebase, I found that
UpdatedItem.oldValue(defined asChar<20>in Module.h:28) is consistently initialized and compared using empty string""throughout the codebase, not the literal string"null".Evidence:
- In Nodes.h:145, new controls initialize
oldValue = ""- In Module.cpp:200 and Module.cpp:244,
oldValueis assigned from actual property values- In ModuleIO.h:396, the same comparison pattern is already in use:
updatedItem.oldValue == ""- Comment in Nodes.h:153 confirms: "called in addControl (oldValue = "")"
- No
"null"string comparisons exist in the repositoryThe comparison
updatedItem.oldValue != ""correctly checks whether a previous value existed before removing controls. This is the established pattern in the codebase and does not represent a semantic change.src/MoonLight/Modules/ModuleDrivers.h (3)
69-76: Good:begin()now hasoverridespecifier.The
overridekeyword provides compile-time verification that this correctly overrides the virtual method fromNodeManager. The order of operations is correct: assignnodespointer before callingNodeManager::begin().
158-159: Good: Added pre-construction logging for debugging.This log statement captures the node name, pointer address, and PSRAM status before calling the constructor chain. Useful for debugging memory allocation issues on ESP32.
179-189: Good:loop()now hasoverridespecifier.The
overridekeyword ensures compile-time verification. The method correctly delegates toNodeManager::loop()while adding module-specific initialization logic.src/MoonBase/SharedHttpEndpoint.h (2)
36-38: CORS OPTIONS handler uses different auth predicate.The CORS handler uses
IS_AUTHENTICATEDwhile the actual GET/POST handlers use_authenticationPredicate(defaulting toIS_ADMIN). This may be intentional for lighter preflight auth, but verify this is the desired behavior.
64-88: POST handler implementation looks correct.The
updateWithoutPropagation+callUpdateHandlerspattern properly separates state mutation from persistence/propagation. Returning the updated state in the response is good REST practice.src/MoonBase/SharedWebSocketServer.h (2)
33-37: LGTM!The update handler registration correctly captures the module pointer and constructs the path for transmission.
78-82: Socket cleanup properly implemented.The
onClosehandler now correctly erases the socket from_initializedSockets, preventing unbounded growth and stale entries.src/MoonLight/Nodes/Drivers/D_FastLED.h (2)
97-101: LGTM! Safe pin count calculation prevents unassigned pin access.The
nrOfPinscalculation correctly bounds the iteration to the minimum of configured and assigned pins, preventing potential out-of-bounds access. The early return when no pins are assigned is appropriate.
104-104: LGTM! Loop correctly enforces both logical and hardware pin limits.The condition properly caps iteration at the minimum of
nrOfPinsand 4 (FastLED RMT hardware limit), preventing both logical errors and hardware constraint violations.src/MoonLight/Nodes/Drivers/D_PhysicalDriver.h (3)
52-59: LGTM! Consistent pin count calculation prevents driver API misuse.The computed
nrOfPinscorrectly bounds the pin count passed toshow_parlio, ensuring the low-level driver doesn't attempt to access unassigned pins or exceed buffer boundaries.
76-83: LGTM! Safe guard prevents initialization with invalid pin configuration.The early return when
nrOfPins == 0prevents unnecessary driver initialization work and potential errors from operating with no assigned pins.
90-111: LGTM! Driver initialization and updates use accurate pin counts.The use of
nrOfPinsininitled()andupdateDriver()calls ensures the underlying driver infrastructure operates on the correct subset of assigned pins, preventing potential memory access issues or initialization errors.src/MoonLight/Layers/PhysicalLayer.h (1)
136-136: LGTM! New field enables safe pin-count tracking.The
nrOfAssignedPinsfield provides runtime tracking of actually assigned pins, enabling the derivednrOfPinscalculations in driver code to prevent out-of-bounds access. The zero initialization is a safe default.src/MoonLight/Layers/PhysicalLayer.cpp (4)
34-36: Informational comments document heap constraints and optimization goals.The comments provide useful context for understanding the memory allocation strategy on PSRAM-less boards, where heap is limited to ~60KB.
136-136: LGTM! Explicit comment clarifies memset intent.The explicit
0xFFvalue with the// UINT16_MAXcomment clarifies that eachuint16_telement will be initialized to 0xFFFF (UINT16_MAX), which is the intended sentinel value for unassigned pins.
179-189: LGTM! Critical fix: corrects loop bound to prevent array overrun.The original code used
i < sizeof(ledsPerPin), which for auint16_t[20]array would evaluate to 40 (bytes), causing iteration far beyond array bounds. The fix toi < MAXLEDPINScorrectly bounds the loop to the actual array length. The addition ofnrOfAssignedPinstracking (line 186) enables safe pin iteration in driver code.
78-78: LGTM! Cosmetic spacing adjustment.Minor formatting improvement for code readability.
Front end ======== - Monitor width*1 and 1*height fix (webgl) Back end ======== - *: add const & - Utilities: Char<> refactors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
interface/src/routes/moonbase/monitor/monitor.ts (1)
98-109: Missing null check forgl.createProgram()result.
gl.createProgram()can returnnull. Lines 100-102 useprogramwithout verifying it's not null, which could cause silent failures or runtime errors.const createProgram = (gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram => { const program = gl.createProgram(); + if (!program) { + throw new Error("Unable to create program"); + } gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program);src/MoonLight/Nodes/Effects/E_MoonLight.h (1)
115-135:LinesEffect:frameNrreinitialization prevents color cycling
frameNris declared and initialized to 0 at the start ofloop()and only incremented once at the end. Because it’s a local variable, its value is lost on each call, so:int frameNr = 0; ... int colorNr = (frameNr / layer->size.y) % 3; ... (frameNr)++;means
colorNris always 0 and the effect never rotates through the three colors.If the intent is to have a time‑evolving pattern, make
frameNrpersistent, e.g.:- void loop() override { - int frameNr = 0; + int frameNr = 0; // member of LinesEffect + + void loop() override { @@ - (frameNr)++; + frameNr++;(or declare
static int frameNrinsideloop()if you prefer a function‑local static). That will allow the color selection to advance across frames as originally designed.src/MoonBase/Module.cpp (1)
143-151: Based on my verification, I can now provide the rewritten review comment:Char<20> truncation risk is real but not currently manifesting.
Line 145 assigns
parent(aJsonString) toupdatedItem.parent[depth](aChar<20>). While parent array names in active use like"nodes"and"controls"are short, the code structure is vulnerable to truncation for any JSON key exceeding 19 characters assigned at depth 0 or 1 duringcompareRecursive(). For example,"mappingTableIndexes#"(20 chars) from ModuleMoonLightInfo would exceed the limit, though this particular module is read-only so comparisons never occur. The truncation would be silent. Consider either: (1) expandingChar<20>to a larger capacity, or (2) adding runtime validation to assert parent key lengths.
♻️ Duplicate comments (7)
src/MoonLight/Nodes/Effects/E_MoonLight.h (1)
34-38: SolidEffect white loop index fix confirmedInitializing
indexto 0 in the white channel loop removes the prior undefined behavior and is the correct fix; the loop is now well‑defined for allnrOfLights.src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
38-38: Bug fix already addressed.The assignment operator
=has been correctly changed to comparison operator==. This was previously flagged and is now resolved.src/MoonBase/SharedHttpEndpoint.h (2)
15-20: Missing include forHTTP_ENDPOINT_ORIGIN_ID.The constant
HTTP_ENDPOINT_ORIGIN_IDis used at line 79 but is defined inHttpEndpoint.h, which is not included. Add the missing include to prevent compilation errors.#include <PsychicHttp.h> #include <SecurityManager.h> +#include <HttpEndpoint.h> #include <map>
89-94: Undefinedmodulesvariable and improved but incomplete path matching.
The
modulesvariable on line 90 is not declared in this file. It appears to reference an external global—add anexterndeclaration or pass it as a constructor parameter.Using
endsWith()is better thancontains(), but can still incorrectly match if one module name is a suffix of another (e.g., path/rest/moduleiowould match module named"io"). Consider matching the full path segment.+extern std::vector<Module*> modules; // Add at file scope, or pass via constructor Module* findModule(const String& path) { + String expectedSuffix = "/rest/"; for (Module* module : modules) { - if (path.endsWith(module->_moduleName.c_str())) return module; + if (path == expectedSuffix + module->_moduleName) return module; } return nullptr; }src/MoonBase/Module.cpp (1)
262-266: StaticupdateOriginIdwithout synchronization.As flagged in a previous review,
ModuleState::updateOriginIdis static and written byupdate()then read inprocessUpdatedItem(). Without synchronization, concurrent updates from different tasks could observe the wrong origin. However, the new semaphore-based flow (updateReadySem/updateProcessedSem) appears to serialize update processing, which may mitigate this in practice.src/MoonBase/SharedWebSocketServer.h (2)
101-116: JsonDocument allocated without size limit in transmitData.As noted in a previous review, allocating
JsonDocumenton the stack without size constraints can cause heap exhaustion for large module states. Consider using a shared document pool with mutex protection or pre-sizing the document.
15-20: Missing include forWEB_SOCKET_ORIGIN_CLIENT_ID_PREFIXandWEB_SOCKET_ORIGIN.Line 87 uses
WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIXand line 70 usesWEB_SOCKET_ORIGIN, both defined inlib/framework/WebSocketServer.h. Add the include to avoid compilation errors.#include <PsychicHttp.h> +#include <WebSocketServer.h> #include <map> #include <set>
🧹 Nitpick comments (13)
src/MoonLight/Layers/VirtualLayer.h (1)
282-282: Consider making both parameters const references for consistency.The first parameter
awas changed toconst Coord3D&(good for avoiding copies), but the second parameterbremains pass-by-value. For consistency and to align with the heap-optimization goals of this PR, consider making both parameters const references.Apply this diff for consistency:
- void drawLine3D(const Coord3D& a, Coord3D b, CRGB color, bool soft = false, uint8_t depth = UINT8_MAX) { drawLine3D(a.x, a.y, a.z, b.x, b.y, b.z, color, soft, depth); } + void drawLine3D(const Coord3D& a, const Coord3D& b, CRGB color, bool soft = false, uint8_t depth = UINT8_MAX) { drawLine3D(a.x, a.y, a.z, b.x, b.y, b.z, color, soft, depth); }interface/src/routes/moonbase/monitor/monitor.ts (2)
119-123: Add input validation to prevent division by zero.If
setMatrixDimensions(0, 0)is called,getMVPMatrix()will divide by zero at line 161-162 when computingnormalizedWidth/Height. Consider clamping to minimum values.export function setMatrixDimensions(width: number, height: number) { - matrixWidth = width; - matrixHeight = height; + matrixWidth = Math.max(1, width); + matrixHeight = Math.max(1, height); }
177-177: Stale comment: value doesn't match description.The comment says "1.2 adds some padding" but the actual multiplier is
2.5. Update the comment to reflect the current value.- const cameraDistance = Math.max(distanceForHeight, distanceForWidth) * 2.5; // 1.2 adds some padding + const cameraDistance = Math.max(distanceForHeight, distanceForWidth) * 2.5; // 2.5 adds paddingsrc/MoonLight/Layers/PhysicalLayer.cpp (1)
136-136: Pin bookkeeping: consider also resettingnrOfAssignedPinsinonLayoutPreThe change to initialize
ledsPerPinwith0xFFand thewhile (i < MAXLEDPINS && ledsPerPin[i] != UINT16_MAX)loop is correct and safely bounded byMAXLEDPINS. UpdatingnrOfAssignedPins = i + 1when a new pin is assigned also looks right.To avoid stale state between layouts when you deallocate pins, it would be slightly safer to also reset
nrOfAssignedPinsalongsidememset(ledsPerPin, 0xFF, ...)inonLayoutPre()(pass 1). That guarantees any consumer relying onnrOfAssignedPinsdoesn’t see the previous layout’s pin count before the firstnextPin()call of the new layout.Also applies to: 179-188
src/MoonLight/Modules/ModuleLightsControl.h (3)
37-69:begin()wiring looks correct; clarify ModuleIO member usageCalling
Module::begin()first, logging the struct sizes, wiring the FileManager update handler, and then usingmoduleIO.addUpdateHandler(...)plus an initialreadPins()gives a clean startup path for brightness control.This class also stores a
ModuleIO* _moduleIOin the constructor but only uses the unqualifiedmoduleIOmember here and inreadPins(). If_moduleIOis truly unused, consider removing it (and possibly the constructor parameter) to save a few bytes and avoid confusion about which ModuleIO instance is authoritative.
84-134: Const-correct definition and preset guard look good; minor cleanup possibleSwitching
setupDefinitiontoconst JsonArray& rootandonUpdatetoconst UpdatedItem&aligns this module with the new const‑correct API surface. The updated preset guardif (updatedItem.oldValue != "" && !updatedItem.value["action"].isNull()) { ... }is a clearer way to skip preset application on the initial boot/default state while still reacting to user‑initiated “click”/“dblclick” actions.
Within
setupDefinition,JsonArray details = root;is no longer used and could be dropped to trim a little code and keep the definition focused.Also applies to: 137-207
238-284: Preset loop: good removal ofrunInAppTask; consider staticJsonDocumentHaving
loop()callModule::loop()first and then handlingpresetLoopdirectly (instead of pushing lambdas intorunInAppTask) is a solid improvement for both clarity and heap stability on D0 boards.The only remaining heap touchpoint here is the local
JsonDocument doc;used whenpresetLoopis active. Given this runs relatively infrequently and is gated bypresetLoop, it’s probably fine; but if you want to fully avoid dynamic JSON allocations in this path, you could switch to a smallStaticJsonDocument<N>or a shared memberJsonDocumentwith reserved capacity andclear()it before reuse.src/MoonLight/Nodes/Effects/E_MoonLight.h (1)
264-271: IP caching in ScrollingTextEffect is fine; offline case still yields.0Using
IPAddress activeIP = WiFi.isConnected() ? WiFi.localIP() : ETH.localIP();and thentext.format(".%d", activeIP[3]);is a reasonable simplification and avoids repeatedlocalIP()calls.Just be aware that when neither interface has a valid IP, this will show “.0”. If you ever want a more explicit “offline” indication in that preset, you could gate this on
sharedData.connectionStatusor checkactiveIP == INADDR_NONEand fall back to a different text.src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
35-41: Missingoverridespecifier ononUpdate.Other effect classes declare
onUpdatewithoverride(seeE_MoonLight.hlines 925, 1121). This sandbox template should also includeoverrideto ensure it correctly overrides the base class method and catches signature mismatches at compile time.- void onUpdate(const Char<20>& oldValue, const JsonObject control) { + void onUpdate(const Char<20>& oldValue, const JsonObject control) override {src/MoonLight/Modules/ModuleMoonLightInfo.h (1)
128-130: Unused loop variableindexP.The variable
indexPis declared but never used inside the loop body—onlynrOfMoreLightsis incremented. Consider using a simpler counting approach to avoid the unused variable warning.case m_moreLights: - for (uint16_t indexP : layer->mappingTableIndexes[map.indexes]) { - nrOfMoreLights++; - } + nrOfMoreLights += layer->mappingTableIndexes[map.indexes].size(); break;If
mappingTableIndexes[map.indexes]doesn't have a.size()method, you can suppress the warning with:- for (uint16_t indexP : layer->mappingTableIndexes[map.indexes]) { + for ([[maybe_unused]] uint16_t indexP : layer->mappingTableIndexes[map.indexes]) {src/MoonBase/SharedHttpEndpoint.h (1)
49-49: Emptybegin()method.The
begin()method is empty. If no initialization is needed, consider removing it or adding a comment explaining why it exists (e.g., for API consistency with other shared components).src/MoonBase/Modules/ModuleIO.h (1)
391-412: Good pattern: Deferring heavy work to main loop.The
newBoardIDqueue mechanism correctly addresses the stack depth issue that caused crashes duringsetBoardPresetDefaults. By deferring the preset application toloop(), the update callback completes quickly, preventing stack exhaustion during large state updates.However, consider using
std::atomic<uint8_t>fornewBoardIDsince it could theoretically be written from a WebSocket handler task while being read in the main loop, though in practice the semaphore-based flow likely serializes access.src/MoonBase/Module.h (1)
23-29: UpdatedItem Char<20> refactor looks good; comments are slightly misleading.Using
Char<20>here is a solid heap win versusStringand still comfortably covers all known property names and values (and then some, based on prior length analysis). The only nit is that the inline sizing comments (// 24 -> 2*20,// 32 -> 16 -> 20,// 8->16->4) no longer match the actual field sizes and history, which can confuse future readers.Consider either updating or removing those numeric comments; the code itself is fine.
Based on learnings, the current Char<20> capacity is more than sufficient for existing property names.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (26)
docs/develop/modules.md(3 hunks)interface/src/routes/moonbase/monitor/monitor.ts(7 hunks)src/MoonBase/Module.cpp(13 hunks)src/MoonBase/Module.h(3 hunks)src/MoonBase/Modules/FileManager.cpp(3 hunks)src/MoonBase/Modules/ModuleDevices.h(5 hunks)src/MoonBase/Modules/ModuleIO.h(4 hunks)src/MoonBase/Modules/ModuleTasks.h(3 hunks)src/MoonBase/NodeManager.h(11 hunks)src/MoonBase/Nodes.h(4 hunks)src/MoonBase/SharedHttpEndpoint.h(1 hunks)src/MoonBase/SharedWebSocketServer.h(1 hunks)src/MoonBase/Utilities.h(6 hunks)src/MoonLight/Layers/PhysicalLayer.cpp(4 hunks)src/MoonLight/Layers/VirtualLayer.h(3 hunks)src/MoonLight/Modules/ModuleChannels.h(3 hunks)src/MoonLight/Modules/ModuleDrivers.h(4 hunks)src/MoonLight/Modules/ModuleEffects.h(5 hunks)src/MoonLight/Modules/ModuleLightsControl.h(6 hunks)src/MoonLight/Modules/ModuleLiveScripts.h(8 hunks)src/MoonLight/Modules/ModuleMoonLightInfo.h(2 hunks)src/MoonLight/Nodes/Effects/E_MoonLight.h(7 hunks)src/MoonLight/Nodes/Effects/E_MoonModules.h(4 hunks)src/MoonLight/Nodes/Effects/E_WLED.h(6 hunks)src/MoonLight/Nodes/Effects/E__Sandbox.h(1 hunks)src/idf_component.yml.orig(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/idf_component.yml.orig
🚧 Files skipped from review as they are similar to previous changes (4)
- src/MoonBase/Modules/ModuleDevices.h
- src/MoonLight/Modules/ModuleLiveScripts.h
- src/MoonLight/Nodes/Effects/E_WLED.h
- src/MoonLight/Nodes/Effects/E_MoonModules.h
🧰 Additional context used
🧬 Code graph analysis (9)
src/MoonLight/Modules/ModuleLightsControl.h (3)
src/MoonBase/Module.cpp (4)
begin(313-337)begin(313-313)setupDefinition(343-353)setupDefinition(343-343)src/MoonLight/Modules/ModuleDrivers.h (2)
begin(69-76)readPins(34-66)src/MoonLight/Modules/ModuleChannels.h (1)
setupDefinition(24-56)
src/MoonBase/SharedWebSocketServer.h (3)
lib/framework/SecurityManager.h (3)
SecurityManager(82-117)AuthenticationPredicates(65-80)IS_ADMIN(76-79)lib/framework/WebSocketServer.h (1)
transmitId(121-132)src/MoonBase/Module.h (2)
ModuleState(47-137)Module(139-191)
src/MoonBase/Modules/FileManager.cpp (5)
src/MoonBase/Module.cpp (2)
update(262-293)update(262-262)lib/framework/APSettingsService.h (1)
update(115-144)lib/framework/MqttSettingsService.h (1)
update(109-120)lib/framework/NTPSettingsService.h (1)
update(66-73)lib/framework/StatefulService.h (2)
update(123-134)update(144-155)
src/MoonLight/Nodes/Effects/E__Sandbox.h (7)
src/MoonLight/Nodes/Effects/E_MoonLight.h (3)
onSizeChanged(1237-1243)onUpdate(926-930)onUpdate(1122-1126)src/MoonLight/Nodes/Effects/E_MoonModules.h (2)
onSizeChanged(182-198)onUpdate(121-125)src/MoonLight/Nodes/Effects/E_WLED.h (2)
onSizeChanged(33-40)onSizeChanged(340-346)src/MoonBase/NodeManager.h (1)
onUpdate(134-283)src/MoonBase/Nodes.cpp (2)
onUpdate(401-541)onUpdate(401-401)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D__Sandbox.h (1)
onUpdate(31-37)
src/MoonBase/Module.cpp (2)
lib/framework/StatefulService.h (4)
update(123-134)update(144-155)read(165-170)read(172-177)lib/framework/WiFiSettingsService.h (1)
read(90-224)
src/MoonLight/Modules/ModuleDrivers.h (1)
src/MoonBase/Utilities.cpp (2)
isInPSRAM(103-126)isInPSRAM(103-103)
src/MoonBase/Modules/ModuleTasks.h (4)
src/MoonBase/Module.cpp (2)
setupDefinition(343-353)setupDefinition(343-343)src/MoonBase/Modules/ModuleDevices.h (1)
setupDefinition(33-48)src/MoonBase/NodeManager.h (1)
setupDefinition(84-131)src/MoonLight/Modules/ModuleChannels.h (1)
setupDefinition(24-56)
src/MoonBase/Nodes.h (5)
src/MoonBase/Module.cpp (2)
addControl(355-364)addControl(355-355)src/MoonBase/NodeManager.h (2)
onUpdate(134-283)void(79-79)src/MoonBase/Nodes.cpp (4)
onUpdate(401-541)onUpdate(401-401)updateControl(17-66)updateControl(17-17)src/MoonLight/Nodes/Drivers/D_VirtualDriver.h (1)
onUpdate(69-73)src/MoonLight/Nodes/Drivers/D_Infrared.h (1)
onUpdate(126-196)
src/MoonLight/Layers/VirtualLayer.h (1)
src/MoonLight/Layers/VirtualLayer.cpp (2)
drawLine3D(445-550)drawLine3D(445-445)
🪛 Clang (14.0.6)
src/MoonBase/SharedHttpEndpoint.h
[error] 15-15: 'PsychicHttp.h' file not found
(clang-diagnostic-error)
src/MoonBase/SharedWebSocketServer.h
[error] 15-15: 'PsychicHttp.h' file not found
(clang-diagnostic-error)
🪛 LanguageTool
docs/develop/modules.md
[grammar] ~64-~64: Ensure spelling is correct
Context: ...UpdatedItems are created in the httpd / webserver task and via the loop() function sent t...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🪛 markdownlint-cli2 (0.18.1)
docs/develop/modules.md
64-64: Unordered list indentation
Expected: 2; Actual: 4
(MD007, ul-indent)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (62)
src/MoonLight/Layers/VirtualLayer.h (3)
30-33: Good documentation of heap constraints.The comments clearly document the memory constraints (60 KB heap, 30 KB max allocation) for ESP32 boards without PSRAM, providing valuable context for the optimization effort.
63-68: Clear explanation of mapping architecture.The comments effectively document the purpose and constraints of the virtual-to-physical light mapping structures, including the 12288 LED target for non-PSRAM boards.
72-73: I'll help you verify that the new size-tracking fields are properly maintained throughout the codebase. Let me start by exploring the repository structure and examining the relevant implementation files.
<function_calls>
First, find the VirtualLayer implementation file
fd -t f "VirtualLayer.(cpp|cc|h)"
</function_calls>Let me search for the implementation file more broadly:
<function_calls>
Find all VirtualLayer files
find . -name "VirtualLayer" -type f 2>/dev/null | head -20
</function_calls>Now let me search for assignments to the new tracking fields:
<function_calls>
#!/bin/bashSearch for assignments to mappingTableSizeUsed
echo "=== Assignments to mappingTableSizeUsed ==="
rg -n 'mappingTableSizeUsed\s*=' --type=cpp --type=h -C2echo ""
echo "=== Assignments to mappingTableIndexesSizeUsed ==="
rg -n 'mappingTableIndexesSizeUsed\s*=' --type=cpp --type=h -C2
</function_calls>Let me also search for modifications to the vectors themselves:
<function_calls>
#!/bin/bashSearch for modifications to mappingTable
echo "=== Operations on mappingTable ==="
rg -n 'mappingTable\s*.' --type=cpp --type=h -C1 | head -100echo ""
echo "=== Operations on mappingTableIndexes ==="
rg -n 'mappingTableIndexes\s*.' --type=cpp --type=h -C1 | head -100
</function_calls>Let me look at the VirtualLayer.h header file to understand the class structure better:
<function_calls>
Find and display VirtualLayer.h
rg -l "VirtualLayer" --type=h | grep -i virtual
Also get its line count to see if we can read it
fd -t f -e h | xargs grep -l "mappingTableSizeUsed" 2>/dev/null
</function_calls><function_calls>
Show the file with mappingTableSizeUsed
fd -t f -name ".h" -o -name ".cpp" | xargs grep -l "mappingTableSizeUsed" 2>/dev/null
</function_calls><function_calls>
Search more broadly for these fields
rg "mappingTableSizeUsed" -C3
</function_calls><function_calls>
Search for VirtualLayer files
git ls-files | grep -i virtual
</function_calls><function_calls>
Display the full file if it exists
git ls-files src/MoonLight/Layers/VirtualLayer.h && cat src/MoonLight/Layers/VirtualLayer.h
</function_calls><function_calls>
Search for the cpp file
git ls-files src/MoonLight/Layers/VirtualLayer.cpp && wc -l src/MoonLight/Layers/VirtualLayer.cpp
</function_calls><function_calls>
Get the full implementation file
git ls-files src/MoonLight/Layers/VirtualLayer.cpp && cat -n src/MoonLight/Layers/VirtualLayer.cpp
</function_calls><function_calls>
Check what's in the src/MoonLight/Layers directory
git ls-files src/MoonLight/Layers/ 2>/dev/null | head -20
</function_calls><function_calls>
Check if the file path exists at all
ls -la src/MoonLight/Layers/ 2>/dev/null || echo "Path does not exist"
Try broader search
find . -path "/Layers/" -type f 2>/dev/null | head -20
</function_calls><function_calls>
Let's see what's in the repo root
git ls-files | head -30
</function_calls>interface/src/routes/moonbase/monitor/monitor.ts (2)
158-185: LGTM!The normalized dimension scaling logic correctly handles varying matrix aspect ratios by normalizing to the larger dimension and computing appropriate camera distance for both vertical and horizontal fit.
82-96: LGTM!The shader creation function properly handles the null case from
gl.createShader()and throws descriptive errors for both creation and compilation failures.src/MoonLight/Layers/PhysicalLayer.cpp (1)
34-37: Heap notes and LED capacity comment are consistentThe new heap-optimization comments and the updated
lights.maxChannels = 4096 * 3;comment now accurately describe the current 4096‑LED constraint on non‑PSRAM ESP32 boards. No functional issues here; this is good documentation of the heap trade‑off.Also applies to: 44-44
src/MoonLight/Nodes/Effects/E_MoonLight.h (4)
617-621: FreqSawsEffect array initialization viasizeofis robustSwitching the band arrays’ initialization to:
memset(bandSpeed, 0, sizeof(bandSpeed)); memset(bandPhase, 0, sizeof(bandPhase)); memset(lastBpm, 0, sizeof(lastBpm)); memset(phaseOffset, 0, sizeof(phaseOffset));is correct and more future‑proof than hard‑coded element counts. No issues here.
925-930: RubiksCubeEffectonUpdateoverride matches new APIThe updated signature
void onUpdate(const Char<20>& oldValue, const JsonObject control) overridealigns this effect with the new base API and only usescontrol["name"], which is fine. LeavingoldValueunused here is acceptable for an override.
1122-1126: ParticlesEffectonUpdateoverride is consistent with new typesSimilarly, the new
onUpdate(const Char<20>& oldValue, const JsonObject control) overridesignature brings ParticlesEffect in line with the shared interface while only usingcontrol["name"]to triggersettingUpParticles(). This looks correct.
1237-1243: MoonManEffectonSizeChangednow uses const referenceChanging
onSizeChangedtovoid onSizeChanged(const Coord3D& prevSize) overrideavoids an unnecessary copy and matches the updated virtual signature in the base class. The body remains the same and continues to recreate the sprite on size changes.src/MoonBase/Modules/FileManager.cpp (3)
19-19: Good improvement: const reference for JsonArray parameter.Passing
JsonArrayby const reference instead of by value avoids unnecessary copy overhead, which aligns with the heap optimization goals of this PR.
58-58: Consistent with project-wide update signature changes.The
originIdparameter addition aligns with the updatedStatefulService::updatesignatures seen across the codebase (e.g.,MqttSettingsService,APSettingsService,NTPSettingsService). The parameter is unused in this function but maintains interface consistency.
153-153: WebSocket server initialization added correctly.The
_webSocketServer.begin()call is appropriately placed after the HTTP and event endpoint initialization, matching the initialization pattern for other endpoint types inFileManager.src/MoonLight/Nodes/Effects/E__Sandbox.h (1)
33-33: Consistent const-reference update.The
const Coord3D¶meter matches the pattern used across other effect classes (e.g.,E_WLED.h,E_MoonLight.h,E_MoonModules.h).src/MoonLight/Modules/ModuleMoonLightInfo.h (2)
22-22: Consistent signature update with const reference.The
const JsonArray¶meter andoverridespecifier align with the project-wide pattern adopted in other modules.
100-101: Proper override specifier and base class invocation.The
overridekeyword ensures compile-time verification of the virtual method signature, andModule::begin()is correctly called first.src/MoonLight/Modules/ModuleEffects.h (4)
27-32: Proper initialization order with override specifier.The
overridespecifier is correctly added, andNodeManager::begin()is appropriately called after initializing module-specific fields (defaultNodeName,nodes).
60-60: Consistent const-reference parameter.The
const JsonArray¶meter aligns with the project-wide convention for avoiding unnecessary copies.
90-90: Appropriate const-correctness on virtual methods.Both
addNodesandaddNodeare correctly markedconstsince they don't modify the object's state—they only populate the provided containers or allocate new nodes.Also applies to: 162-162
296-296: LGTM: Simple delegation with override.The
loop()override correctly delegates toNodeManager::loop().src/MoonBase/Utilities.h (10)
118-118: Clean simplification of fromJson.The one-liner aggregate initialization is cleaner and equivalent to the previous implementation.
164-170: Well-designed constructor additions for Char.The default constructor,
const char*constructor, and converting template constructor from different-sizedChar<M>provide flexible initialization while maintaining the fixed-buffer semantics. The use ofstrlcpyensures proper null-termination and truncation safety.
178-189: Improved null handling in JsonVariant/JsonString assignment.Properly handling null values by assigning an empty string prevents undefined behavior from null pointer dereference.
190-195: Good: Cross-size Char assignment via template.The template
operator=acceptingChar<M>of any size enables seamless assignment between different-sizedCharinstances while safely truncating when needed.
197-200: Explicit conversion operator is safer.Making
operator const char*()explicit prevents unintended implicit conversions that could lead to dangling pointer issues or unexpected behavior in overload resolution.
211-215: Correct value semantics for operator+.Returning a new
Charby value instead of modifying in-place follows standard operator+ semantics and avoids surprising side effects.
231-234: Good const-correctness additions.Marking comparison operators and accessor methods as
constis correct since they don't modify the object state.Also applies to: 248-256
290-298: Non-member operator+ enables symmetric concatenation.This allows
"prefix" + charInstanceexpressions, which wouldn't work with only the memberoperator+. The implementation correctly handles buffer sizing.
419-419: Based on my verification, I have conclusive findings. TherunInAppTaskandrunInAppTask_mutexdeclarations are completely absent from the codebase and were not relocated to an active location—only disabled/commented-out references remain. There is no bounded queue implementation either.
runInAppTaskandrunInAppTask_mutexhave been completely removed, not relocated.Verification found:
- No active declarations of
runInAppTaskorrunInAppTask_mutexexist in the codebase- Only 2 references remain, both in commented-out code (src/MoonBase/Nodes.cpp:230 and src/MoonLight/Modules/ModuleLightsControl.h:246)
- No bounded queue processing recommendation was implemented—the feature was removed entirely
- The prior review's concern about unbounded queue growth on ESP32-D0 was addressed by eliminating the queue mechanism rather than adding capacity bounds
421-423: Accuracy verified for the stated 0–65535 range.All tested boundary values (0, 127–256, 510–512, 65534–65535) produce correct results matching integer division by 255. The magic constant
0x8081with shift-by-23 is mathematically sound for LED applications. No usages offastDiv255were found in the codebase, suggesting this is either unused utility code or part of an external library interface.docs/develop/modules.md (1)
41-41: Documentation correctly reflects const-correct API changes.The signature updates to
const JsonArray&andconst UpdatedItem&accurately mirror the codebase changes acrossModule.cppand derived module classes.Also applies to: 67-67
src/MoonBase/Modules/ModuleTasks.h (3)
57-57: Good defensive initialization to prevent division-by-zero.Initializing
totalRunTimeto 1 ensures the percentage calculation at line 99 cannot divide by zero, even ifuxTaskGetSystemStatefails to populate it.
99-99: Integer overflow fix correctly applied.The 64-bit arithmetic with
100ULL * ts->ulRunTimeCounterprevents the overflow issue identified in previous reviews.
107-107: Based on my verification, I've found important information:Key Findings:
- No explicit FreeRTOS runtime stats configuration is present in the repository. ESP-IDF 5.1+ has runtime stats disabled by default.
- Timer source ambiguity: The code divides by 1,000,000 (assuming 1 MHz ESP Timer), but if configured to use CPU clock (80-240 MHz), this divisor would be completely incorrect.
- Documentation mismatch: The project's own documentation (
docs/moonbase/tasks.md) describes the runtime field as "cpu cycles", yet the code treats it as seconds by dividing by 1,000,000.- No configuration verification: Without seeing the actual build configuration (sdkconfig), there's no way to confirm which timer source is active or if runtime stats are even enabled.
The review comment's concern is valid and substantial. The assumption cannot be verified from the codebase alone.
Verify FreeRTOS runtime counter timer source and enabled status.
The division by 1,000,000 assumes
ulRunTimeCounteris in microseconds (1 MHz ESP Timer source). This requiresCONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=yandCONFIG_FREERTOS_RUN_TIME_STATS_CLK=ESP_TIMER. If the project usesCPU_CLOCKinstead, the divisor should be the CPU frequency (80–240 MHz), not 1,000,000. Additionally, the documentation describes this field as "cpu cycles", which contradicts the seconds conversion. Confirm the actual FreeRTOS configuration in your build (check sdkconfig or enable via menuconfig) and update the divisor or documentation accordingly.src/MoonLight/Modules/ModuleChannels.h (3)
24-24: Const-correctness improvements align with codebase-wide API changes.The parameter changes to
const JsonArray&andconst UpdatedItem&are consistent with the broader refactoring across module interfaces.Also applies to: 58-58
69-73: State update pattern correctly uses centralized flow.Creating a local
JsonDocumentand callingupdate()with the new state object properly integrates with the module's update propagation mechanism.
78-78: Verify semantic change in oldValue check.The condition changed from checking
!= "null"(JSON null literal as string) to!= ""(empty string). Confirm this correctly handles the expected states ofoldValuewhen the channel action should be processed.src/MoonLight/Modules/ModuleDrivers.h (3)
69-76: Proper initialization order inbegin().Setting
defaultNodeNameandnodesbefore callingNodeManager::begin()ensures the base class has access to initialized state. Theoverridespecifier improves compile-time safety.
78-78: Const-reference parameters reduce unnecessary copies.Using
const JsonArray&forvaluesandcontrolsavoids copying JsonArray objects, which aligns with the heap optimization goals of this PR.Also applies to: 104-104
158-159: Useful PSRAM diagnostic logging for heap analysis.The
isInPSRAM(node)check in the log statement aids in understanding memory allocation patterns, directly supporting the PR's heap optimization objectives.src/MoonBase/SharedHttpEndpoint.h (1)
32-47: Shared HTTP endpoint registration pattern is well-structured.The registration logic properly wires GET/POST handlers with authentication, includes optional CORS support, and uses lambda captures appropriately.
src/MoonBase/Nodes.h (5)
75-75: LGTM: const-correctness improvement for addControl parameter.Using
const ControlType&instead ofControlType&is appropriate since the variable is only read to set default values, not modified.
141-141: LGTM: Correct use of ArduinoJson const char accessors.*Using
.as<const char*>()for JSON values is the proper way to access string values without heap allocation.
154-156: LGTM: Consistent signature updates for heap optimization.Changing from
String&toconst Char<20>&foroldValuealigns with the heap optimization goals and matches theUpdatedItemstructure changes.
172-172: LGTM: Pass-by-const-reference for Coord3D.Passing
Coord3Dby const reference instead of by value avoids unnecessary copies.
268-268: LGTM: DriverNode::onUpdate signature updated consistently.The override matches the base class signature change.
src/MoonBase/Modules/ModuleIO.h (2)
109-109: LGTM: Const-correct setupDefinition signature.The
const JsonArray¶meter aligns with the broader const-correctness improvements across the codebase.
431-438: LGTM: Deferred board preset execution.The
loop()override correctly checks for pendingnewBoardIDand executessetBoardPresetDefaultsfrom the main task context, which has more stack space and avoids the recursive update issue.src/MoonBase/Module.cpp (5)
16-17: LGTM: Global JsonDocument for shared module state.The
gModulesDocglobal allows modules to share a single document, reducing heap fragmentation from multiple document allocations.
110-116: LGTM: Swap notifications via postUpdate.Creating a local
UpdatedItemfor swap events and routing throughpostUpdateintegrates well with the new semaphore-based producer/consumer flow.
243-246: Correctly useskey.c_str()for stateData access.This addresses the previous review concern about using
updatedItem.name.c_str()(which could be truncated) as the JSON key. Now using the fullkey.c_str()for both write and read operations ensures property names longer than 19 characters are handled correctly.
308-310: LGTM: processUpdatedItem callback binding.Binding
_state.processUpdatedItemto the module'sprocessUpdatedItemmethod establishes the update handler for the semaphore-based flow.
343-343: LGTM: Const-correct method signatures.Both
setupDefinitionandaddControlnow takeconst JsonArray¶meters, aligning with the broader const-correctness improvements.Also applies to: 355-355
src/MoonBase/SharedWebSocketServer.h (3)
22-28: LGTM: Clean class structure with proper member initialization.The class design with private handler, server pointer, and security manager follows the existing framework patterns.
79-82: Good fix: Socket cleanup on disconnect.Erasing the socket from
_initializedSocketsinonCloseprevents the set from growing unbounded and ensures reused socket FDs will receive initial data on reconnection. This addresses the previous review concern.
118-123: Good fix: UsesendsWithfor module matching.Using
path.endsWith(module->_moduleName.c_str())instead of substring matching prevents false positives when module names are substrings of each other. This addresses the previous review concern.src/MoonBase/NodeManager.h (6)
23-23: LGTM: defaultNodeName uses Char<20> instead of String.This change avoids heap allocation for the default node name, aligning with the heap optimization goals. As per learnings, node names should fit within 20 characters.
31-31: LGTM: Constructor takes const reference.Using
const String&formoduleNameavoids an unnecessary copy of the string.
79-84: LGTM: Const-correct virtual method signatures.The
addNodes,addNode, andsetupDefinitionmethods now useconst JsonArray¶meters, improving const-correctness.
134-134: LGTM: onUpdate takes const reference.The
const UpdatedItem¶meter matches the new update flow pattern used throughout the codebase.
151-155: Correct: Empty string check instead of "null".Checking
updatedItem.oldValue != ""instead of comparing to"null"is correct sinceUpdatedItem.oldValueis now initialized to an emptyChar<20>rather than a string literal "null".Also applies to: 205-205
298-299: LGTM: loop() override calls base class.Calling
Module::loop()ensures the base class update processing (semaphore-based flow) is executed before checkingrequestUIUpdate.
| let program: WebGLProgram; | ||
| let positionBuffer: WebGLBuffer; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null assignment to non-nullable types.
gl.createBuffer() returns WebGLBuffer | null per WebGL spec. On lines 63 and 70, the return value is assigned directly to non-nullable variables without null checks. If buffer creation fails (e.g., GPU resource exhaustion), this will cause runtime errors when the buffers are used in updateScene().
Consider adding null checks or restoring nullable types with guards:
-let positionBuffer: WebGLBuffer;
-let colorBuffer: WebGLBuffer;
+let positionBuffer: WebGLBuffer | null = null;
+let colorBuffer: WebGLBuffer | null = null;Then guard usage in updateScene:
if (!gl || !positionBuffer || !colorBuffer) return;Also applies to: 19-19, 63-63, 70-70
🤖 Prompt for AI Agents
In interface/src/routes/moonbase/monitor/monitor.ts around lines 9-11 (and
noting buffer assignments at ~19, 63, 70), the code assigns the results of
gl.createBuffer() to non-nullable WebGLBuffer variables which can be null per
the WebGL spec; change the variable declarations to allow null (WebGLBuffer |
null), check the return values after each gl.createBuffer() call and handle
failure (log and early return or throw), and add null guards at the start of
updateScene (e.g., if (!gl || !positionBuffer || !colorBuffer) return;) so no
nullable buffer is used without verification.
| if (newControl) { | ||
| String oldValue = ""; | ||
| updateControl(oldValue, control); | ||
| Char<20> oldValue; | ||
| oldValue = ""; | ||
| // updateControl(oldValue, control); | ||
| onUpdate(oldValue, control); // custom onUpdate for the node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find updateControl and onUpdate implementations
rg -nA 15 'void Node::updateControl' src/Repository: MoonModules/MoonLight
Length of output: 1640
🏁 Script executed:
# Search for onUpdate definition and usage
rg -nA 10 'onUpdate.*oldValue.*control' src/Repository: MoonModules/MoonLight
Length of output: 10530
🏁 Script executed:
# Check the broader context around the commented-out code
sed -n '140,155p' src/MoonBase/Nodes.hRepository: MoonModules/MoonLight
Length of output: 660
The commented-out updateControl call should be restored — it performs essential pointer synchronization that onUpdate does not provide.
The evidence confirms your concern:
-
updateControldoes critical work: It dereferences pointers from the JSON control and writes values directly to variable memory locations (lines 27-28, 31-32 in Nodes.cpp). This is irreplaceable. -
onUpdateis not a replacement: It's a virtual function with an empty default implementation in the base Node class (Nodes.h:156). While subclasses can override it for custom logic, it performs no pointer synchronization by default. -
The design intent confirms the bug: The comment at line 153 states
updateControlis "called in addControl (oldValue = "")"—explicitly indicating it should handle new control initialization. The current code violates this. -
New controls remain uninitialized: When
newControl == true, the code now skipsupdateControlentirely and calls only the emptyonUpdate, leaving variables at uninitialized memory addresses until the next update.
This line should be uncommented:
updateControl(oldValue, control);🤖 Prompt for AI Agents
In src/MoonBase/Nodes.h around lines 143-147, the commented-out call to
updateControl(oldValue, control) must be restored because updateControl performs
necessary pointer synchronization for new controls; uncomment that call so
updateControl(oldValue, control) runs before onUpdate(oldValue, control)
(keeping the existing oldValue initialization) to ensure variables are
initialized immediately when newControl is true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure about
The commented-out updateControl call should be restored — it performs essential pointer synchronization that onUpdate does not provide.
Because addControl just adds the variable at class create time and the variable already contains the default value so updateControl would do the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
@ewowi, you raise a very good point. Let me reconsider the logic here.
🏁 Script executed:
#!/bin/bash
# Let's examine the full updateControl implementation to check for any side effects beyond value assignment
rg -nA 50 'void Node::updateControl' src/MoonBase/Nodes.cpp
Length of output: 3096
🏁 Script executed:
#!/bin/bash
# Also check if there are any other places where updateControl is called to understand its usage pattern
rg -n 'updateControl\(' src/ --type cpp
Length of output: 452
@ewowi, you're absolutely right, and I apologize for the confusion. My initial analysis was incorrect.
Looking at the evidence more carefully:
-
In the
newControlpath withinaddControl:- The variable already contains its default value when passed to
addControl control["value"] = variablecopies that existing default value into the JSON- The variable and
control["value"]are identical at this point
- The variable already contains its default value when passed to
-
What
updateControldoes:- It simply reads
control["value"]and writes it to the memory address atcontrol["p"] - In the
newControlcase, this would write the same value the variable already has (a no-op)
- It simply reads
-
The smoking gun - a comment in
updateControlitself (Nodes.cpp:19):// if (oldValue == "") return; // newControl, value already setThis explicitly confirms that when
oldValueis empty (thenewControlcase), the value is already set andupdateControlwould be redundant.
Conclusion: The change to comment out updateControl and call only onUpdate is correct. The onUpdate call allows nodes to perform custom initialization logic when controls are created, while avoiding the redundant write-back that updateControl would perform.
Thank you for catching my error! 🐰
🧬 Code Graph Analysis Results
-
src/MoonBase/Nodes.h | snippet around updateControl usage in addControl
-
Context: Inside Node::addControl, when a new control is created or an existing one is updated, the code previously called updateControl(...) and now uses onUpdate(...) for per-node handling. This snippet shows the portion that sets up the control, and how an oldValue and control object are passed to the handler.
-
Full excerpt:
// if control already exists only update it's pointer
JsonObject control;
for (JsonObject control1 : controls) {
if (control1["name"] == name) {
// EXT_LOGD(ML_TAG, "%s t:%s p:%p ps:%d", name, type, pointer, sizeof(ControlType));
control1["p"] = pointer;
control = control1; // set control to the found one
break;
}
}if (control.isNull()) { // if control not found, create a new one
control = controls.add();
control["name"] = name;
control["type"] = type;
control["default"] = variable;control["p"] = pointer;
if (ro) control["ro"] = true; // else if (!control["ro"].isNull()) control.remove("ro");
if (min != 0) control["min"] = min; // else if (!control["min"].isNull()) control.remove("min");
if (max != UINT8_MAX) control["max"] = max; // else if (!control["max"].isNull()) control.remove("max");
if (desc) control["desc"] = desc;newControl = true; // set flag to true, as control is new
}control["valid"] = true; // invalid controls will be deleted
// EXT_LOGD(ML_TAG, "%s t:%s p:%p ps:%d", name, type, pointer, sizeof(ControlType));
if (newControl) control["value"] = variable; // set default
// setValue
if (control["type"] == "slider" || control["type"] == "select" || control["type"] == "pin" || control["type"] == "number") {
... (omitted for brevity)
}if (newControl) {
Char<20> oldValue;
oldValue = "";
// updateControl(oldValue, control);
onUpdate(oldValue, control); // custom onUpdate for the node
}return control;
}
// called in addControl (oldValue = "") and in NodeManager onUpdate nodes[i].control[j]
virtual void updateControl(const Char<20>& oldValue, const JsonObject control); // see Nodes.cpp for implementation -
-
src/MoonBase/NodeManager.h | how updateControl is invoked when a control value changes
-
Context: When a node's control value changes (UI/edit), NodeManager locates the corresponding Node and calls the Node's updateControl with the previous value and the JsonObject describing the control. It then calls the node's onUpdate for additional handling.
-
Summary description:
- In the onUpdate handler for nodes[i].controls[j].value, NodeManager finds the NodeClass for updatedItem.index[0].
- It then calls nodeClass->updateControl(updatedItem.oldValue, nodeState["controls"][updatedItem.index[1]]);
- It follows with nodeClass->onUpdate(updatedItem.oldValue, nodeState["controls"][updatedItem.index[1]]);
- Finally, it calls nodeClass->requestMappings() to propagate any needed UI/physical changes.
-
Relevant excerpt (summary):
else if (updatedItem.parent[1] == "controls" && updatedItem.name == "value" && updatedItem.index[1] < nodeState["controls"].size()) { // nodes[i].controls[j].value
...
Node* nodeClass = (*nodes)[updatedItem.index[0]];
if (nodeClass != nullptr) {
nodeClass->updateControl(updatedItem.oldValue, nodeState["controls"][updatedItem.index[1]]);
nodeClass->onUpdate(updatedItem.oldValue, nodeState["controls"][updatedItem.index[1]]); // custom onUpdate for the nodenodeClass->requestMappings();} else { ... }
}
-
If there are no additional relevant snippets to include, I can summarize more succinctly or extract more exact lines.
| bool operator!=(const Coord3D& rhs) { | ||
| return x != rhs.x || y != rhs.y || z != rhs.z; | ||
| // return !(*this==rhs); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent const-ness: operator!= should be const.
operator== on line 65 is correctly marked const, but operator!= is not. This inconsistency can cause issues when comparing const Coord3D objects.
- bool operator!=(const Coord3D& rhs) {
+ bool operator!=(const Coord3D& rhs) const {📝 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.
| bool operator!=(const Coord3D& rhs) { | |
| return x != rhs.x || y != rhs.y || z != rhs.z; | |
| // return !(*this==rhs); | |
| } | |
| bool operator!=(const Coord3D& rhs) const { | |
| return x != rhs.x || y != rhs.y || z != rhs.z; | |
| // return !(*this==rhs); | |
| } |
🤖 Prompt for AI Agents
In src/MoonBase/Utilities.h around lines 61 to 64, operator!= is missing a const
qualifier which makes it unusable on const Coord3D instances; change its
signature to be const and implement it consistently (preferably as the logical
negation of operator==) so the method reads as a const member and returns
!(*this == rhs).
|
@coderabbitai, we created 4 new classes: SharedEventEndpoint, SharedFSPersistence, SharedHttpEndpoint and SharedWebSocketServer which are shared versions of EventEndpoint, FSPersistence, HttpEndpoint and WebSocketServer . Instead of duplicating the code from the original classes, could we redesign the Shared classes to use the original classes, or by inherit from them, or have them as Shared class variables (there was a name for this, which I forgot - it's even a design pattern if I am right, maybe you know what I mean) |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 37066 @ewowi, you're thinking of the Composition design pattern (has-a relationship)! This is an excellent idea - we can eliminate the code duplication by having the Shared classes contain instances of the original template classes. Here's the elegant solution: Composition-Based RefactoringSharedHttpEndpoint.h (Reusing Original)#ifndef SharedHttpEndpoint_h
#define SharedHttpEndpoint_h
#include <HttpEndpoint.h>
#include "Module.h"
#include <map>
#include <memory>
class SharedHttpEndpoint {
private:
// Composition: contains map of HttpEndpoint instances
std::map<String, std::unique_ptr<HttpEndpoint<ModuleState>>> _endpoints;
PsychicHttpServer* _server;
SecurityManager* _securityManager;
AuthenticationPredicate _authenticationPredicate;
public:
SharedHttpEndpoint(PsychicHttpServer* server,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN)
: _server(server),
_securityManager(securityManager),
_authenticationPredicate(authenticationPredicate) {}
void registerModule(Module* module) {
String path = "/rest/" + module->_moduleName;
// Create and store an HttpEndpoint instance for this module
auto endpoint = std::make_unique<HttpEndpoint<ModuleState>>(
ModuleState::read,
ModuleState::update,
module, // Module IS a StatefulService<ModuleState>
_server,
path.c_str(),
_securityManager,
_authenticationPredicate
);
endpoint->begin(); // Register HTTP GET/POST handlers
_endpoints[module->_moduleName] = std::move(endpoint);
ESP_LOGV(SVK_TAG, "Registered HTTP endpoint: %s", path.c_str());
}
void begin() {
// All endpoints are registered in registerModule
}
};
#endifSharedWebSocketServer.h (Reusing Original)#ifndef SharedWebSocketServer_h
#define SharedWebSocketServer_h
#include <WebSocketServer.h>
#include "Module.h"
#include <map>
#include <memory>
class SharedWebSocketServer {
private:
std::map<String, std::unique_ptr<WebSocketServer<ModuleState>>> _webSockets;
PsychicHttpServer* _server;
SecurityManager* _securityManager;
AuthenticationPredicate _authenticationPredicate;
public:
SharedWebSocketServer(PsychicHttpServer* server,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN)
: _server(server),
_securityManager(securityManager),
_authenticationPredicate(authenticationPredicate) {}
void registerModule(Module* module) {
String path = "/ws/" + module->_moduleName;
// Create and store a WebSocketServer instance for this module
auto ws = std::make_unique<WebSocketServer<ModuleState>>(
ModuleState::read,
ModuleState::update,
module, // Module IS a StatefulService<ModuleState>
_server,
path.c_str(),
_securityManager,
_authenticationPredicate
);
ws->begin(); // Register WebSocket handler
_webSockets[module->_moduleName] = std::move(ws);
ESP_LOGV(SVK_TAG, "Registered WebSocket: %s", path.c_str());
}
void begin() {
// All WebSockets are registered in registerModule
}
};
#endifSharedEventEndpoint.h (Reusing Original)#ifndef SharedEventEndpoint_h
#define SharedEventEndpoint_h
#include <EventEndpoint.h>
#include "Module.h"
#include <map>
#include <memory>
class SharedEventEndpoint {
private:
std::map<String, std::unique_ptr<EventEndpoint<ModuleState>>> _eventEndpoints;
EventSocket* _socket;
public:
SharedEventEndpoint(EventSocket* socket) : _socket(socket) {}
void registerModule(Module* module) {
// Create and store an EventEndpoint instance for this module
auto endpoint = std::make_unique<EventEndpoint<ModuleState>>(
ModuleState::read,
ModuleState::update,
module, // Module IS a StatefulService<ModuleState>
_socket,
module->_moduleName.c_str()
);
endpoint->begin(); // Register event handlers
_eventEndpoints[module->_moduleName] = std::move(endpoint);
ESP_LOGV(SVK_TAG, "Registered event: %s", module->_moduleName.c_str());
}
void begin() {
// All events are registered in registerModule
}
};
#endifSharedFSPersistence.h (Reusing Original)#ifndef SharedFSPersistence_h
#define SharedFSPersistence_h
#include <FSPersistence.h>
#include "Module.h"
#include <map>
#include <memory>
class SharedFSPersistence {
private:
std::map<String, std::unique_ptr<FSPersistence<ModuleState>>> _persistences;
FS* _fs;
public:
SharedFSPersistence(FS* fs) : _fs(fs) {}
void registerModule(Module* module, bool delayedWriting = false) {
String filePath = "/.config/" + module->_moduleName + ".json";
// Create and store an FSPersistence instance for this module
auto persistence = std::make_unique<FSPersistence<ModuleState>>(
ModuleState::read,
ModuleState::update,
module, // Module IS a StatefulService<ModuleState>
_fs,
filePath.c_str(),
delayedWriting
);
persistence->readFromFS(); // Read initial state
_persistences[module->_moduleName] = std::move(persistence);
ESP_LOGV(SVK_TAG, "Registered FS persistence: %s", filePath.c_str());
}
void begin() {
// All reading happens in registerModule
}
// Expose methods for external control
void disableUpdateHandler(const String& moduleName) {
auto it = _persistences.find(moduleName);
if (it != _persistences.end()) {
it->second->disableUpdateHandler();
}
}
void enableUpdateHandler(const String& moduleName) {
auto it = _persistences.find(moduleName);
if (it != _persistences.end()) {
it->second->enableUpdateHandler();
}
}
// Static method for delayed writes (from FSPersistence)
static void writeToFSDelayed(char writeOrCancel) {
FSPersistence<ModuleState>::writeToFSDelayed(writeOrCancel);
}
};
#endifBenefits of This Approach✅ Zero code duplication - reuses original template implementations Design Pattern ExplanationThis is the Composition pattern (also called "has-a" or "contains-a"):
Similar patterns you might have been thinking of:
Memory ComparisonCurrent Shared Classes (duplicated code):
Composition Approach:
Result: Nearly identical memory usage, but with zero code duplication! Would you like me to generate a complete patch applying this refactoring to all four Shared classes? |
|
Regarding
No no no! This is not what we should want because this brings us back at Square 1 , with even a worse solution! (Storing them in the Module class was more elegant). So we need one instance of each original class, not more. The shared class adds logic to find out for which module we handle the router functionality . The only thing I try to avoid is to rewrite the code for each action again |
|
Tip For best results, initiate chat on the files or code changes. @ewowi, you're absolutely right! 🐰 I lost focus - we need ONE instance of each class, not 15! Let me fix this. The problem: The original template classes are designed with a single ✅ Solution: Extract Common Logic into Utility FunctionsWe can extract the request handling logic into shared utility functions that both the original template classes AND the Shared classes can use: New Utility File: lib/framework/EndpointHelpers.h#ifndef EndpointHelpers_h
#define EndpointHelpers_h
#include <PsychicHttp.h>
#include <StatefulService.h>
namespace EndpointHelpers {
// Shared HTTP GET handler logic
template <class T>
esp_err_t handleHttpGet(PsychicRequest* request,
StatefulService<T>* service,
JsonStateReader<T> reader,
size_t bufferSize = DEFAULT_BUFFER_SIZE) {
PsychicJsonResponse response = PsychicJsonResponse(request, false, bufferSize);
JsonObject jsonObject = response.getRoot();
service->read(jsonObject, reader);
return response.send();
}
// Shared HTTP POST handler logic
template <class T>
esp_err_t handleHttpPost(PsychicRequest* request,
JsonVariant& json,
StatefulService<T>* service,
JsonStateUpdater<T> updater,
const String& originId,
size_t bufferSize = DEFAULT_BUFFER_SIZE) {
if (!json.is<JsonObject>()) {
return request->reply(400);
}
JsonObject jsonObject = json.as<JsonObject>();
StateUpdateResult outcome = service->updateWithoutPropagation(jsonObject, updater);
if (outcome == StateUpdateResult::ERROR) {
return request->reply(400);
}
if (outcome == StateUpdateResult::CHANGED) {
service->callUpdateHandlers(originId);
}
// Return updated state
PsychicJsonResponse response = PsychicJsonResponse(request, false, bufferSize);
JsonObject responseObj = response.getRoot();
service->read(responseObj, updater);
return response.send();
}
// Shared WebSocket transmit logic
template <class T>
void transmitWebSocketData(PsychicWebSocketClient* client,
PsychicWebSocketHandler* handler,
StatefulService<T>* service,
JsonStateReader<T> reader,
const String& originId) {
JsonDocument doc;
JsonObject root = doc.to<JsonObject>();
service->read(root, reader);
String buffer;
serializeJson(doc, buffer);
if (client) {
client->sendMessage(buffer.c_str());
} else {
handler->sendAll(buffer.c_str());
}
}
} // namespace EndpointHelpers
#endifUpdated HttpEndpoint.h (Uses Utilities)template <class T>
class HttpEndpoint {
protected:
// ... existing members ...
esp_err_t fetchConfig(PsychicRequest *request) {
// Reuse utility function
return EndpointHelpers::handleHttpGet(request, _statefulService, _stateReader, _bufferSize);
}
esp_err_t updateConfig(PsychicRequest *request, JsonVariant &json) {
// Reuse utility function
return EndpointHelpers::handleHttpPost(request, json, _statefulService, _stateUpdater,
HTTP_ENDPOINT_ORIGIN_ID, _bufferSize);
}
};Updated SharedHttpEndpoint.h (Uses Same Utilities!)#ifndef SharedHttpEndpoint_h
#define SharedHttpEndpoint_h
#include <EndpointHelpers.h>
#include "Module.h"
#include <map>
class SharedHttpEndpoint {
private:
std::map<String, Module*> _modules;
PsychicHttpServer* _server;
SecurityManager* _securityManager;
AuthenticationPredicate _authenticationPredicate;
public:
SharedHttpEndpoint(PsychicHttpServer* server,
SecurityManager* securityManager,
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN)
: _server(server), _securityManager(securityManager), _authenticationPredicate(authenticationPredicate) {}
void registerModule(Module* module) {
String path = "/rest/" + module->_moduleName;
_modules[path] = module;
}
void begin() {
// Single wildcard handler for ALL modules
_server->on("/rest/*", HTTP_GET,
_securityManager->wrapRequest([this](PsychicRequest* request) {
Module* module = findModule(request->path());
if (!module) return request->reply(404);
// Reuse the SAME utility function as HttpEndpoint!
return EndpointHelpers::handleHttpGet(request, module, ModuleState::read);
}, _authenticationPredicate));
_server->on("/rest/*", HTTP_POST,
_securityManager->wrapCallback([this](PsychicRequest* request, JsonVariant& json) {
Module* module = findModule(request->path());
if (!module) return request->reply(404);
// Reuse the SAME utility function as HttpEndpoint!
return EndpointHelpers::handleHttpPost(request, json, module, ModuleState::update,
"http");
}, _authenticationPredicate));
}
private:
Module* findModule(const String& path) {
auto it = _modules.find(path);
return (it != _modules.end()) ? it->second : nullptr;
}
};
#endifUpdated SharedWebSocketServer.h (Uses Same Utilities!)class SharedWebSocketServer {
private:
// ONE PsychicWebSocketHandler for all modules
PsychicWebSocketHandler _handler;
std::map<String, Module*> _modules;
// ...
void transmitData(const String& path, PsychicWebSocketClient* client, const String& originId) {
Module* module = findModule(path);
if (!module) return;
// Reuse the SAME utility function as WebSocketServer!
EndpointHelpers::transmitWebSocketData(client, &_handler, module, ModuleState::read, originId);
}
};Benefits✅ ONE instance of each Shared class (memory goal achieved) Does this approach match what you had in mind? We maintain the single-instance architecture while eliminating code duplication through shared utilities! 🐰 |
@CodeRabbit-ai can you analyze memory usage and suggest ways to reduce heap usage on ESP32?
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.