Skip to content

FilesystemModule

Source: FilesystemModule.h

FilesystemModule

class FilesystemModule
src/core/FilesystemModule.h:81

Inherits: MoonModule

Control-list-driven JSON persistence — writes control values to flash so settings survive a reboot.

Always loaded, runs first in the scheduler so its load hook fires before any other module's [setup()]

Storage: one flat JSON file per top-level MoonModule under /.config/<TypeName>.json (the filename is MoonModule::typeName()). Children are encoded positionally with an <index>. key prefix — no nested objects, no arrays, loaded with the cheap first-match key helpers (parseString/Int/Bool). A control whose value is structured (a List control's array of objects) is read back with the recursive reader in core/JsonUtil.h via the control's own restore hook: the file's top level stays flat, the structure lives inside one control's value string. ReadOnly and Progress controls are never persisted — they are derived values, not state.

Structural reconciliation. The type field per child drives reconciliation at load time: when the JSON describes a child type at position N that differs from the live tree's child at N, the loader factory-creates the JSON type, runs its defineControls(), and swaps it into place. Children present in the live tree but missing from the JSON are torn down; children in the JSON beyond the live tree's end are appended. Phases 3+4 cascade into the reconciled tree, so newly-created children are fully initialised like any other.

Boot flow (Scheduler::setup, four phases):

  • phase 1 defineControls() — every module binds its full control set (incl. hidden)

  • phase 2 loadAllHook — this module reads each file and overlays bound variables

  • phase 2b rebuildControls() — re-runs defineControls so conditional hidden flags see the persisted values

  • phase 3 [setup()] — modules' own init runs with persisted values in members

  • phase 4 prepare() — buffers sized to final values

The Scheduler exposes setLoadAllHook() as a function pointer so it stays independent of this module's type (no circular include); the hook is wired from setScheduler().

Save flow.HttpServerModule calls markDirty() + noteDirty() on every successful mutation — control changes AND tree-shape changes (add / delete / move a module marks the parent dirty so its file is rewritten with the new child set). noteDirty() stamps lastDirtyMs_; tick1s() waits DEBOUNCE_MS (2 s) after the last dirty mark, then walks the tree and serialises any subtree with a dirty descendant to a flat JSON blob, written atomically (write to .tmp, then rename). A subtree's dirty flag clears only after its write succeeds; a failed write leaves it set so tick1s() retries. flushPending() forces all dirty subtrees through synchronously (the reboot handler calls it so an add-then-reboot doesn't lose the change); losing power before the debounce expires loses the in-flight change — the cost of debouncing for fewer flash writes.

Conditional visibility. Modules with conditional controls bind their full control set unconditionally and toggle a hidden flag per descriptor, so the persistence layer can find and overlay a value regardless of the live conditional state while the UI honours the flag. A Select change at runtime triggers rebuildControls() to re-evaluate the flags.

Platform layer. Filesystem access goes through platform::fs* (mount, mkdir, read, atomic write-then-rename, used/total). ESP32 uses LittleFS on a dedicated partition; desktop uses std::filesystem rooted at build/ (overridable via fsSetRoot for test isolation). Save and load are both cap-free: the save serializes into a growable JsonSink (heap, no ceiling) and the load reads the whole file into a heap buffer sized to it — a config of any size (many light presets, a wide fixture) round-trips in full.

First boot: no files exist → load is a no-op, modules run with default member-initialised values; after the first UI change the debounce creates the file. A missing key keeps the default, an unknown key is silently ignored (no schema migration).

FilesystemModule card

Public Methods

FilesystemModule() = default : Singleton is registered in setScheduler() (called by main.cpp on the real FilesystemModule), NOT in the constructor.

virtual inline bool respectsEnabled() const override : Persistence must keep flushing dirty subtrees regardless of the enabled toggle — otherwise the user could lose changes by accidentally disabling this module via the UI before the 2s debounce expires.

virtual inline bool appearsInUi() const override : Non-UI: a pure persistence engine with no controls — not shown as a card (its "last saved" status is displayed by FileManagerModule).

void setScheduler(Scheduler * s)

virtual void setup() override : Default lifecycle propagates to children.

virtual void tick1s() override

inline char * lastSavedStr() : The engine's live "last saved" buffer — "never" before the first save, else "5m ago".

void flush() : Synchronous save of every dirty subtree, bypassing the debounce.

Public Static Attributes

constexprconstchar * CONFIG_DIR = "/.config"

constexprsize_t MAX_PATH = 64

constexprsize_t MAX_KEY = 48

constexpruint32_t DEBOUNCE_MS = 2000

Public Static Methods

static inline FilesystemModule * instance() : The live singleton (the main-wired instance registered in setScheduler), or null before boot wiring.

static void flushPending() : Static convenience for callers (such as the reboot handler) that need to force any debounced saves through before a release — mirrors noteDirty's call style.

static void noteDirty() : Called by HttpServerModule on every successful control mutation so the 2s debounce starts.