StatefulModule¶
Base class for all application modules. Extends
Modulewith four virtual methods that the ModuleManager calls to inject configuration, wire data-flow inputs, and persist state. Introduced in phase 7.
What it does¶
StatefulModule adds no instance data to Module — sizeof(StatefulModule) == sizeof(Module). It defines the contract the ModuleManager uses to drive the full module lifecycle without any module knowing about the ModuleManager.
All four methods have default no-op implementations, so a module only overrides the ones it needs.
Interface¶
| Method | Called by | When | Purpose |
|---|---|---|---|
setProps(JsonObjectConst) |
ModuleManager | After construction, before setup() |
Inject construction-time parameters from the "props" field in state/modulemanager.json |
setInput(const char* key, Module*) |
ModuleManager | After setProps(), before setup() |
Wire a named data-flow input to another module |
loadState(JsonObjectConst) |
ModuleManager | After setInput(), before setup() |
Restore persisted controls from state/<id>.json |
saveState(JsonObject) const |
ModuleManager | On teardown() and end of setup() |
Serialise current controls to state/<id>.json |
The full lifecycle order is: construction → setProps() → setInput() → loadState() → setup() → loop() × N → teardown() / saveState().
Writing a module¶
A module that has one prop, one data-flow input, and one persisted control looks like:
class MyModule : public StatefulModule {
public:
MyModule() = default; // required: default-constructible for ModuleManager
void setProps(JsonObjectConst props) override {
if (props["width"].is<uint16_t>()) width_ = props["width"];
}
void setInput(const char* key, Module* src) override {
if (strcmp(key, "layer") == 0)
layer_ = static_cast<EffectsLayer*>(src);
}
void loadState(JsonObjectConst state) override {
if (state["speed"].is<float>()) speed_ = state["speed"];
}
void saveState(JsonObject state) const override {
state["speed"] = speed_;
}
// ... setup() / loop() / teardown() as usual
private:
uint16_t width_ = 0;
EffectsLayer* layer_ = nullptr;
float speed_ = 1.0f;
};
Then register it so the ModuleManager can instantiate it by name:
// src/modules/ModuleRegistrations.cpp
REGISTER_MODULE(MyModule)
Add it at runtime via the REST API (persists automatically to state/modulemanager.json):
curl -X POST http://localhost:80/api/modules \
-H 'Content-Type: application/json' \
-d '{"type":"MyModule","id":"my1","props":{"width":16},"inputs":{"layer":"producer1"}}'
Or use the frontend UI's + add child button on any parent card.
Rules¶
- Default-constructible is mandatory. The ModuleManager calls the zero-argument constructor; configuration comes through
setProps()andsetInput(), not the constructor. setPropsandsetInputare called once persetup()cycle, beforesetup(). Do not allocate resources here — that belongs insetup().loadStatemay override values set bysetProps. Both can set the same field;loadStatewins (persisted user choice beats config default).saveStatemust beconst. It serialises the current in-memory state; it does not write any file directly.
Source¶
Test coverage¶
→ Stateful Module — progress/button/string control types, sensitive fields, disableStatePersistence, integer clamping, meta() store.
→ Controls and KV Store — addControl, setControl (all types), onUpdate, getSchema, sensitive value exclusion.