MoonModule
Source:
MoonModule.h
MoonModule¶
src/core/MoonModule.h:92The base class for everything in the system — effects, modifiers, layouts, drivers, and system services all inherit from MoonModule.
It is the one deliberate class hierarchy: a single virtual-dispatch boundary and shallow subclasses, so the UI can render any module generically with zero per-module UI code. The design goal is the smallest possible base: zero bytes of instance overhead beyond the vtable pointer and control variables (the type name lives in flash, not per instance), because on an ESP32 without PSRAM dozens of modules load at once and every byte counts. Field order is grouped 8/4/2/1-byte to minimise padding.
Lifecycle.[setup()] / release() bracket the module's life; tick() / tick20ms() / tick1s() are the three tick rates the Scheduler paces. Two build hooks sit apart from [setup()]: [defineControls()] holds every addX() call and is idempotent + re-runnable (so a Select changing the visible control set rebuilds cleanly), and [prepare()] is the single derived-state hook (buffers, LUTs, the module's heap-byte report), reached at setup and via Scheduler::prepareTree() whenever a control that changes physical dimensions fires [affectsPrepare()]. This build sweep is what makes every config change apply live, with no reboot. Controls bind by reference, so persisted values overlay the bound variables before any [setup()] runs.
Parent/child. Modules form a tree — parent/child only, no arbitrary DAG. A dynamic children array plus addChild() / removeChild() / replaceChildAt() / moveChildTo() live once in this base (containers do not override them); the array starts empty (zero allocation for leaf modules) and grows on demand. Children are distinguished by [role()], and a container filters by role at the call site (a Layer ticks only Effects, not Modifiers). Two virtuals keep UI tree-mutation policy on the device: [acceptsChildRoles()] (what a parent offers in "+ add child") and [userEditable()] (whether the user may delete/replace this module). Parents own their children's lifecycle and propagate every hook down — only top-level modules register with the Scheduler.
Runtime add/remove lifecycle contract. When a child is added or removed after the parent's own setup has run, the caller drives the child's lifecycle: adding at runtime → call [setup()] → [defineControls()] → [prepare()] on the new child; removing at runtime → call release() on the child before removing it. A child added before the parent's setup needs none of this — the parent's [setup()] propagates down to it.
Enabled. Every module has an enabled flag (default true), toggled from the UI card header and via POST /api/control. The Scheduler always calls the three tick hooks regardless of enabled; each module decides what "disabled" means — a rendering module early-returns (its buffer freezes) while a system module ignores the flag ([respectsEnabled()] false) so the user can't lock themselves out. onEnabled(bool) fires once per transition for one-shot start/stop work, instead of polling enabled() in the hot path.
Self-reporting. Each module reports its own footprint and cost so the UI shows per-module visibility at any depth: [classSize()] (set once at registration via register_type<T>(), no per-class boilerplate), dynamicBytes() (heap set by [prepare()]), and tickTimeUs() (average microseconds per tick over a 1-second window; publishTiming() recurses the tree every second — parents time their children, the Scheduler times top-level modules). tickTimeUs is the primary performance metric; FPS is derived as 1000000 / tickTimeUs. setStatus(msg,
severity) carries a short user-facing message (Status / Warning / Error → ℹ️ / ⚠️ / ❌); the slot stores a pointer with no copy, so callers pass a flash literal or a module-owned buffer. markDirty() marks state touched so FilesystemModule can persist the subtree after a debounce.
Prior art: MoonLight's Node (https://github.com/ewowi/MoonLight/blob/main/src/MoonBase/Nodes.h) — a ~29-byte base + vtable with no std::string members (fixed-size strings), addControl() binding to a class variable by reference and storing a uintptr_t, and [classSize()] reporting the actual instance size.
Friends¶
friend class ScratchBufferBase
Public Methods¶
inline void * operator new(size_t size)
inline void operator delete(void * ptr) noexcept
MoonModule() = default
: Defaulted constructor.
MoonModule(constMoonModule &) = delete
: Deleted constructor.
MoonModule(MoonModule &&) = delete
: Deleted constructor.
virtual inline void setup()
: Default lifecycle propagates to children.
virtual inline void tick()
virtual inline void tick20ms()
virtual inline void tick1s()
virtual inline void release()
inline void applyState()
: The single orchestration point for the resource lifecycle — the enabled decision lives HERE, in core, so a catalog module's [prepare()] is pure "build my state" with no enabled() check.
virtual inline void onEnabled(bool)
: Called once when the enabled flag flips (the Scheduler runs a full prepareTree() right after, which routes through [applyState()] to re-derive state on the same toggle).
virtual inline void onControlChanged(constchar *)
: Cheap per-control reaction, tier 1 of the three-tier control-change split (mirrors MoonLight's onUpdate / requestMappings / onSizeChanged; see architecture.md § Rebuild propagation).
virtual inline bool affectsPrepare(constchar *) const
: Whether a value change to one of this module's controls triggers the pipeline-wide [prepare()] sweep.
virtual inline void defineControls()
: defineControls MUST be idempotent and pure: only controls_.clear() + controls_.addX().
inline void rebuildControls()
: Non-virtual helper: clear-and-rebuild for this module AND its descendants.
inline uint32_t schemaSignature() const
: FNV-1a hash of the schema-relevant control fields (name, type, bounds, UI flags, option strings) across this module's control list AND its whole subtree — the metadata the WS resync would push.
inline void mixSchema(uint32_t & h) const
inline void clearControlsRecursive()
virtual inline void prepare()
: Tier-3 of the control-change split (see onControlChanged above): the module (re)allocates / recomputes whatever derived state it owns — an effect's heap, a Layer's mapping LUT, the Drivers output buffer.
virtual inline bool firstOutputRgb(uint8_t) const
: Read this module's first output light as RGB into out[3], returning true if it has one.
inline constchar * name() const
inline void setName(constchar * n)
inline constchar * typeName() const
: typeName is the stable factory key (such as "NoiseEffect"), set once by ModuleFactory.
inline void setTypeName(constchar * tn)
inline bool enabled() const
inline void setEnabled(bool e)
virtual inline bool respectsEnabled() const
: Whether the Scheduler should honor enabled() for this module's loop callbacks.
inline bool effectivelyEnabled() const
: True unless this module — or an ancestor that respects the enabled flag — is disabled.
virtual inline bool appearsInUi() const
: Whether this module appears in the UI (/api/state → nav card).
inline bool dirty() const
: Dirty flag — set by HttpServerModule when a control changes.
inline void markDirty()
inline void clearDirty()
inline MoonModule * parent() const
inline void setParent(MoonModule * p)
inline void markWiredByCode()
: Marks this module as wired-by-code rather than wired-by-persistence.
inline bool isWiredByCode() const
inline ControlList & controls()
inline constControlList & controls() const
inline bool readBool(constchar * name, bool dflt) const
: Read one of this module's controls by name generically (no per-consumer control scan).
inline uint8_t readUint8(constchar * name, uint8_t dflt) const
virtual inline ModuleRole role() const
: Role for type identification (no RTTI needed).
virtual inline constchar * tags() const
: Curated emoji tags for the module picker's chip filter — extras beyond the role chip (which the UI derives from [role()] on its own).
virtual inline constchar * acceptsChildRoles() const
: Comma-separated role names this module accepts as user-added children ("effect,modifier").
virtual inline bool userEditable() const
: Whether the user may delete or replace this module from the UI.
virtual inline void quiesce()
: Stop any async worker this module owns that could be reading its child array or its children's state, and return only once that worker is idle.
inline bool addChild(MoonModule * child)
: Generic children — grows on demand, only allocates during setup.
inline bool removeChild(MoonModule * child)
inline MoonModule * replaceChildAt(uint8_t i, MoonModule * fresh)
: Replace child at position i with fresh.
inline bool moveChildTo(MoonModule * child, uint8_t newIndex)
: Move child to absolute position newIndex (0..childCount-1).
inline uint8_t childCount() const
inline MoonModule * child(uint8_t i) const
inline size_t classSize() const
: Per-module memory reporting: [classSize()] is the instance size (set once at registration), dynamicBytes() the heap this module allocated (set by [prepare()]).
inline void setClassSize(size_t s)
inline size_t dynamicBytes() const
inline void setDynamicBytes(size_t b)
inline constchar * status() const
inline Severity severity() const
inline void setStatus(constchar * msg, Severity sev = Severity::Status)
inline void clearStatus()
inline uint32_t tickTimeUs() const
: Per-module timing: parents time children, Scheduler times top-level modules.
inline void addAccumUs(uint32_t us)
inline void publishTiming(uint32_t frameCount)
: Called by Scheduler every ~1 second.
Public Static Methods¶
static inline void setSchemaChangedHook(SchemaChangedFn fn)
static inline void notifySchemaChanged()
: Fire the schema-changed hook directly — for a change that [rebuildControls()] doesn't cover but that the client must still full-resync for.
Public Types¶
enum Severity
: Per-module status slot.
| Value | Description |
|---|---|
Status |
ℹ️ neutral info, current state ("connected") |
Warning |
⚠️ silent degradation ("buffer reduced") |
Error |
❌ something failed ("WiFi auth failed") |
using SchemaChangedFn = void(*)()
: Install the schema-changed hook (HttpServerModule points it at requestFullResync).