Skip to content

ModuleManager

Owns, wires, and drives the lifecycle of all application modules. Reads state/modulemanager.json to 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 with REGISTER_MODULE(TypeName).
  • "props" — construction-time parameters (optional). Passed to setProps().
  • "inputs" — data-flow wiring (optional). Each key/value pair calls setInput(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>.jsonloadState() 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 that state/modulemanager.json and state/<id>.json accumulate in the repo's state/ directory. The state/ directory is created automatically on first write.
  • ESP32 / LittleFS: paths are normalised to have a leading / by FileSystem.h. LittleFS.begin() must be called before mm.setup(). State files are written at the end of setup() on first boot; they survive firmware updates (LittleFS is a separate flash partition).

Source

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.