ModuleManager¶
Owns, wires, and drives the lifecycle of all application modules. Reads
state/modulemanager.jsonto instantiate modules via the TypeRegistry, configure them with props, wire data-flow inputs, restore persistent state, and register them with the Scheduler. Introduced in phase 7.
What it does¶
ModuleManager bridges the JSON config file and the runtime. On setup() it runs three passes:
| Pass | Action |
|---|---|
| 1 — create | Instantiate each module by type name via the TypeRegistry; call setProps() with the "props" object from state/modulemanager.json |
| 2 — wire | Resolve "inputs" entries by module id; call setInput() on each target module |
| 3 — restore + register | Load state/<id>.json if it exists and call loadState(); then register the module with the Scheduler |
After pass 3, saveAllState() is called immediately to persist the initial state (important on platforms like Arduino where teardown() is never reached).
On teardown() it calls saveAllState() and keeps modules alive — the Scheduler still holds raw pointers and will call teardown() on each module in order. owned_ is cleared at the start of the next setup() call.
Config file format¶
state/modulemanager.json — written automatically on first structural change (add/remove module) or on shutdown. On a fresh device/build the file does not pre-exist and the module list starts empty; populate via POST /api/modules or the frontend UI.
{"modules": [
{ "id": "producer1", "type": "EffectsLayer", "core": 1, "props": { "width": 16, "height": 16 } },
{ "id": "sine1", "type": "SineEffectModule", "core": 1, "parent_id": "producer1",
"props": { "frequency": 1.0, "amplitude": 1.0 }, "inputs": { "layer": "producer1" } },
{ "id": "consumer1", "type": "DriverLayer", "core": 1, "props": { "width": 16, "height": 16 },
"inputs": { "source": "producer1" } },
{ "id": "preview1", "type": "PreviewModule", "core": 1, "parent_id": "consumer1",
"inputs": { "source": "consumer1" } }
]}
- Array order = Scheduler execution order.
"id"— unique string; used as the per-module state file name (state/<id>.json) and as the target of"inputs"wiring."type"— must match a type name registered withREGISTER_MODULE(TypeName)."props"— construction-time parameters (optional). Passed tosetProps()."inputs"— data-flow wiring (optional). Each key/value pair callssetInput(key, resolvedModule)."parent_id"— display hierarchy (optional). Does not affect scheduler execution order."core"— scheduler core assignment (optional, default 1). Stored but not yet dispatched.
State persistence¶
State files live at state/<id>.json relative to the working directory (PC/rPi) or in the LittleFS root (ESP32). Only modules that override saveState() produce a state file.
| Event | Action |
|---|---|
setup() pass 3 |
Load state/<id>.json → loadState() if the file exists |
setup() end |
saveAllState() — persists initial props on first boot |
teardown() |
saveAllState() — persists last-known state on clean shutdown |
Log convention: state/sine1.json -> {...} means loaded; state/sine1.json <- {...} means written.
TypeRegistry and REGISTER_MODULE¶
ModuleManager creates modules by type name. Each module type must register itself:
// src/modules/ModuleRegistrations.cpp
REGISTER_MODULE(EffectsLayer)
REGISTER_MODULE(SineEffectModule)
// etc.
REGISTER_MODULE(T) expands to a static bool initializer that calls TypeRegistry::instance().registerType("T", []{ return new T; }). This file must be compiled directly into the executable (not into a static library) to prevent the linker dead-stripping the registrations.
API¶
ModuleManager(Scheduler& scheduler);
void setup(); // three-pass: create, wire, restore+register; then saveAllState()
void saveAllState(); // write state/<id>.json for all modules with non-empty saveState()
void teardown(); // calls saveAllState(); keeps owned_ alive for Scheduler teardown
bool addModule(const char* type, const char* id, ...); // add at runtime; persists immediately
RemoveResult removeModule(const char* id); // Ok / NotFound / Permanent / HasChildren
void getModulesJson(JsonArray out) const; // schema for GET /api/modules
void getTypesJson(JsonArray out) const; // type list for GET /api/types
Platform notes¶
- PC / rPi: paths are relative to the working directory. Run the binary from the repo root (e.g.
deploy/build/pc/projectMM) so thatstate/modulemanager.jsonandstate/<id>.jsonaccumulate in the repo'sstate/directory. Thestate/directory is created automatically on first write. - ESP32 / LittleFS: paths are normalised to have a leading
/byFileSystem.h.LittleFS.begin()must be called beforemm.setup(). State files are written at the end ofsetup()on first boot; they survive firmware updates (LittleFS is a separate flash partition).
Source¶
- src/core/ModuleManager.h
- src/core/ModuleManager.cpp
- src/core/TypeRegistry.h
- src/core/TypeRegistry.cpp
- src/core/FileSystem.h
- src/modules/ModuleRegistrations.cpp
- tests/test_modules.json
- tests/test_module_manager.cpp
Test coverage¶
→ Module Manager — TypeRegistry lookup, add/remove/duplicate/unknown-type/permanent rejection, parent-child wiring, Scheduler isolation, auto-create pipeline, state persistence.
→ HTTP Server — REST surface: getModulesJson, setModuleControl, flushIfDirty debounce, slider validation.
→ REST and WebSocket Integration — runtime addModule/removeModule reflected in WS state.