ControlModule
Source:
ControlModule.h
Preset¶
src/core/ControlModule.h:835Public Attributes¶
uint32_t id = 0
char name = {}
char captures = {}
uint8_t slot = 0
bool hasSlot = false
ControlModule¶
src/core/ControlModule.h:63Inherits:
MoonModule,ListSource
Puts the device into a named state, and is where anything that wants to do that will live.
Its first capability is presets: a preset is a JSON file, saving one writes a file, selecting one reads it back. That is the whole model, taken from MoonLight's ModuleLightsControl and made generic — MoonLight's presets carry effects and modifiers only, ours carry whichever top-level subtrees the user chose to capture.
Top-level by necessity rather than convention: a preset reaches ACROSS Layouts, Effects, Drivers and Services, so this module cannot be a child of any of them.
Not to be confused with LightPresetsModule, which despite the name is a different thing: a library of named channel-role wirings (which channel of a light carries red, which carries pan) that drivers reference per fixture. That is a fixture profile; this is a device state.
The deep dives are under More info, below the attribute/method lists: what a preset captures, and portability, why files rather than slots.
Public Methods¶
inline void addSurface(ControlSurface * s)
: Attach a surface.
inline void resendTo(ControlSurface * s)
: Push EVERY value to one surface, whatever the mirror last sent.
inline void removeSurface(ControlSurface * s)
: Detach.
inline void applyEncoderDelta(uint8_t index, int8_t delta)
: A TURN, not a position.
inline void setTouched(SurfaceControl kind, uint8_t index, bool held)
: A hand is on this control.
inline void mirrorToSurfaces()
: Push changed values to every attached surface.
virtual inline void tick1s() override
virtual inline void defineControls() override
: defineControls MUST be idempotent and pure: only controls_.clear() + controls_.addX().
virtual inline void setup() override
: Default lifecycle propagates to children.
virtual inline void onControlChanged(const char * controlName) override
: save writes the current state; a fader drives whatever it targets; everything else is a value edit the base handles.
inline uint8_t listRowCount() const override
inline void writeListRow(JsonSink & sink, uint8_t row) const override
inline void writeListRowDetail(JsonSink & sink, uint8_t row) const override
: The expanded row: what the preset carries, its name, and the action that applies it.
inline uint8_t roleOf(uint8_t row) const
: The one role this preset carries, or kCaptureCount if the file names none or several.
inline bool isLookOnly(uint8_t row) const
: Is this preset a pure look? With one role per preset this is simply "its role is Effects".
inline const char * presetName(uint8_t row) const
: The preset's name, or null for an out-of-range row.
inline uint8_t presetCount() const
inline uint32_t presetsRevision() const
: Monotonic revision of the preset SET, bumped by every save, delete, rename and rescan.
inline bool applyLookByName(const char * name)
: Apply a LOOK by name.
inline const char * currentLook() const
: The look applied most recently, or "" when none is.
inline bool isEditableList() const override
inline bool persistsList() const override
: The preset FOLDER is the state; rescan() rebuilds these rows at setup.
inline bool listAsPads() const override
: Presets are triggered far more than they are edited, so the rows render as a grid of pads: one click applies, and the active one is highlighted.
inline uint8_t listGridCols() const override
: The surface's shape.
inline uint8_t listGridRows() const override
inline bool deleteListRow(uint32_t id) override
: Deleting a row deletes the file.
inline const char * surfaceTarget(uint8_t index) const
: A fader drives its target through Scheduler::setControl, the same domain-neutral primitive IR and the network bridges use: this module composes against that rather than reaching into another module's members.
inline const char * switchTarget(uint8_t index) const
: What a switch drives, as "Module.control", or null when it drives nothing yet.
inline const char * encoderTarget(uint8_t index) const
: What an encoder drives, as "Module.control", or null when it drives nothing yet.
inline void driveSwitch(uint8_t index)
: Drives whatever switchTarget declares.
inline void followTargets()
: Read every bound control back, so a surface FOLLOWS what it drives.
inline bool pullTarget(SurfaceControl kind, uint8_t index, uint8_t & value)
: One control's read-back.
inline void driveSurface(SurfaceControl kind, uint8_t index)
: Write a surface control's value onto whatever it targets.
inline void driveFader(uint8_t index)
inline void driveEncoder(uint8_t index)
inline void writeStrip(const char * fmt, ...)
: Put text on the display strip, and start its five-second life.
inline void settleStrip()
: Settle the strip once nothing has happened for a while: the change holds for five seconds, then "projectMM" for five, then the device's NAME, which is where it stays.
inline void showOnStrip(const char * module, const char * control, uint8_t value)
inline bool moveListRow(uint32_t id, uint8_t to) override
: Reorder: the grid can be arranged to match a physical control surface, so pad 3 here is fader 3 there.
inline bool setListRowField(uint32_t id, const char * field, const char * valueJson) override
: The row's editable fields carry the two actions a preset row needs.
Public Static Attributes¶
constexpr const char * kPresetDir = "/.config/presets"
constexpr uint8_t kGridCols = 8
: The surface is a fixed grid, so a pad has a POSITION rather than a place in a list: slot 14 is slot 14 whether or not anything is in it, and deleting slot 3 does not shuffle slot 4 into its place.
constexpr uint8_t kGridRows = 8
constexpr uint8_t kMaxPresets = * kGridRows
constexpr uint8_t kMaxNameLen = 32
constexpr const char * kCapturable = {"Layouts", "Effects", "Drivers", "Services"}
: The top-level subtrees a preset can carry.
constexpr const char * kCaptureRole = {"layout", "effects", "driver", "service"}
: What each capturable subtree covers, named after the CONTAINER rather than after a module inside it: a preset that captures Effects reports "effects".
constexpr uint8_t kCaptureCount = sizeof() / sizeof([0])
constexpr uint8_t kEffectsRole = 1
: Index of "Effects" within kCapturable — the role a pure look occupies.
constexpr uint8_t kFaderCount = 8
: How many faders the bank shows.
constexpr uint8_t kEncoderCount = 8
: A row of rotary encoders above the pads, mirroring where both the X-Touch and the QCon put theirs.
constexpr uint8_t kSwitchCount = 8
: The switch row.
constexpr uint32_t kStripHoldMs = 5000
: How long the strip holds what it was told, before falling back to the device's name.
Public Static Methods¶
static inline ControlModule * active()
: The boot ControlModule (exactly one exists).
static inline const ControlDescriptor * findControl(const char * moduleName, const char * controlName)
: A target control's descriptor, for its type and bounds.
static inline const char * deviceName()
: The device's name, read through the control system rather than by reaching into SystemModule: a module asks another module for a control, which is the one way anything here does that.
static inline bool paletteNameAt(uintptr_t optionsFn, uint8_t index, char * out, size_t outLen)
: Write what just happened onto the display strip.
More info¶
What a preset captures¶
The capture controls choose which top-level subtrees a save includes, and the file records the choice, so applying one is never a surprise about what it will touch.
That choice is what decides portability. A preset capturing Effects alone is a look: effects, modifiers, their settings, and nothing about the hardware — it applies on any board and drives whatever that board has. Adding Drivers makes it a device snapshot that carries pin maps and lane counts, which is what you want for cloning a board and NOT what you want for sharing a look.
MoonLight avoided the question by only ever capturing effects. Making it selectable is the cost of being generic, and the file header is what keeps it honest.
Why files¶
/.config/presets/<name>.json, one file per preset, free-form names. Deleting a preset is deleting a file; a preset copied onto the device through the File Manager simply appears in the list; backing up presets is copying a folder. Numbered slots would have bought a fixed grid in the UI at the cost of every one of those.
The bytes inside are exactly what FilesystemModule writes for that subtree, so save and restore reuse the engine that already reconciles a live tree against JSON rather than a second serializer that could drift from it.