Skip to content

Macro System

The macro system lets the pendant operator register frequently-used GCode files as one-tap shortcuts called macros. Once a file is bookmarked as a macro it appears in the dedicated Macros screen, where it can be run, renamed, reordered, or removed without navigating the full file tree every time.

The module is split into two cooperating layers:

LayerLocationResponsibility
macroManager namespacemain/display/cnc/screens/macro_manager.h/.cppIn-memory list management + INI persistence
macrosScreen namespacemain/display/cnc/screens/macros_screen.cppLVGL user interface built on ListMenuScreen

Both layers are shared across the three supported CNC firmware targets (FluidNC, grbl, grblHAL).


CNC Execution

Storage (LittleFS)

Data Layer

UI Layer (LVGL / Core 1)

toggleMacro(path)

CRUD queries

owns

element type

loadFromFile / saveToFile

onConfirmMacroRun

filesScreen

(all 3 firmware targets)

macrosScreen

(macros_screen.cpp)

macroManager namespace

(macro_manager.cpp)

MacroEntry

{ full_path, description }

static vector<MacroEntry>

/fs/macros.ini

esp3dGcodeHandler

.runMacroFile()


Defined in macro_manager.h.

struct MacroEntry {
std::string full_path; // e.g. "/jobs/start.gcode"
std::string description; // optional human-readable label
};

full_path is the canonical identifier; description is what the UI shows (falls back to the filename when empty).


Stateless from the caller’s perspective: all state lives in module-static variables inside macro_manager.cpp.

Query functions — read-only, safe to call at any time:

FunctionReturnsDescription
isMacro(path)boolTrue if the path is in the list
getMacroCount()size_tNumber of registered macros
getMacroAt(index)const MacroEntry*Entry at index, or nullptr
getMacroList()const vector<MacroEntry>&Full list reference

Modification functions — set the dirty flag whenever they change the list:

FunctionReturnsDescription
addMacro(path, desc)boolAppend; false if already present or list full
removeMacro(path)boolRemove by path; false if not found
updateDescription(path, desc)boolEdit the description in-place
moveMacro(from, to)boolReorder by indices
toggleMacro(path)boolAdd if absent, remove if present; returns new macro state
clearAllMacros()voidEmpties the list

Persistence functions:

FunctionReturnsDescription
loadFromFile()boolParse /fs/macros.ini at boot; returns true when file absent
saveToFile()boolWrite INI; no-op when not dirty
isDirty()boolTrue when in-memory list differs from the last save
markClean()voidForce-clears dirty flag (used after external file delete)

The list is capped at ESP3D_MAX_MACROS entries. Attempts to add beyond this limit return false and log an error. The same cap is enforced during parsing in loadFromFile().


Implements the Macros screen using ListMenuScreen as its base — title bar + scrollable item list + virtual button row.

See screen_base_infrastructure.md for the general screen class hierarchy and UI_Framework_&_Screens.md for the full UI module overview.

┌─────────────────────────────┐
│ MACROS │ ← title bar
├─────────────────────────────┤
│ 🤖 start.gcode │
│ 🤖 Home all axes │ ← scrollable macro list
│ 🤖 Tool change │
│ ... │
├─────────────────────────────┤
│ ▶ Run (mode badge) │ ← footer / mode indicator
├──────┬──────┬───────────────┤
│ OK │ CLR │ BACK │ ← virtual buttons
└──────┴──────┴───────────────┘

The switch control (hardware rotary switch or footer tap zone on touch-only boards) cycles the screen through four action modes. The current mode is shown in the footer.

StateIconModeBehaviour on item press
0LV_SYMBOL_DRIVERunConfirms, then calls esp3dGcodeHandler.runMacroFile()
1LV_SYMBOL_EDITEdit descriptionOpens inputScreen text editor
2LV_SYMBOL_LOOPReorderFirst press selects item; encoder moves it up/down; second press confirms
3LV_SYMBOL_TRASHRemoveConfirms, then calls macroManager::removeMacro()

Note: The CLR button (Button 1) is only visible in Remove mode. It triggers a “clear all macros” confirmation dialog.

ButtonLabelAction
0OK / SelectActivates the highlighted item in the current mode
1CLRClear all macros (visible only in Remove mode)
2BACKSave dirty list, then navigate back to Main screen

The screen wraps each macro in a MacroDisplayEntry that carries rendering metadata:

struct MacroDisplayEntry {
std::string display_name; // description if set, else filename
std::string full_path; // forwarded to macroManager / gcode handler
size_t macro_index; // index inside macroManager list
enum class Type : uint8_t { Macro, Info, Error } type;
};

Info and Error rows (e.g. the “No macros” placeholder) are tagged non-selectable via ui_manager.tagListNodeNonSelectable().


1 — Registering a Macro from the File Browser

Section titled “1 — Registering a Macro from the File Browser”
macroManagerfilesScreenUsermacroManagerfilesScreenUseris_dirty_ set to trueContext menu on a GCode fileBuild full_path from current_path + filenametoggleMacro(full_path)is_now_macro (bool)Refresh visible items

toggleMacro() is called identically from the files screens of all three CNC firmware targets — see fluidnc_module_files.md, grbl_module_files.md, and grblhal_module_files.md. The list is written to disk the next time saveToFile() is triggered — when the Macros screen is exited or after a structural change inside that screen.


GCodeHandlermessageBoxScreenmacrosScreenUserGCodeHandlermessageBoxScreenmacrosScreenUserNext create() call redirects to statusScreenalt[Success][Failure]Press OK (mode = Run)show_confirmation(path)YesonConfirmMacroRun()runMacroFile(full_path)truesetRedirectTarget(status)falseshow_error(...)

The redirect mechanism (setRedirectTarget) is used because screen transitions must always happen from a timer callback, never from inside an event handler. When messageBoxScreen calls macrosScreen::create() after the user dismisses it, that function detects the redirect and schedules a one-shot timer to create statusScreen instead. See modal_dialog_screens.md for the full modal pattern.


filesScreenmacrosScreenLittleFSESP3DConfigFilemacroManagerBootfilesScreenmacrosScreenLittleFSESP3DConfigFilemacroManagerBootis_dirty_ = falseis_dirty_ = trueis_dirty_ = falseloadFromFile()check exists /fs/macros.iniexistsprocessFile(processMacroEntry cb)section / key / value triplestoggleMacro(path)saveToFile()fopen /fs/macros.ini "w" + fwrite sections

Screen created

Switch → Reorder mode

Press OK on target item\n(reorder_mode_active = true)

Rotate encoder\nmoveMacro(cur, cur±1)

Press OK again\n(confirm final position)

Switch changes mode\n(auto-exit + save if dirty)

Switch changes mode

BACK pressed (save if dirty)

Idle

Selecting

Moving

In reorder mode, the encoder intercept callback (encoderInterceptCallback) takes priority over list navigation. The selected item’s icon changes from the robot symbol to LV_SYMBOL_LOOP to give the operator visual feedback that an item is being moved.


The persistence file is stored at /fs/macros.ini on the LittleFS partition (mounted at /fs).

[macro_0]
path=/jobs/init.gcode
desc=
[macro_1]
path=/macros/home_all.nc
desc=Home all axes
[macro_2]
path=/jobs/tool_change.nc
desc=Tool change sequence

Sections are named macro_<index>. The desc key may be empty. Parsing is handled by ESP3DConfigFile (see config_file.md) through the processMacroEntry static callback, which accumulates path and desc fields per section and appends a MacroEntry to the in-memory vector each time a new section boundary is encountered.


Running on the ESP32’s constrained heap requires explicit defensive coding throughout this module.

RiskMitigation
std::vector::emplace_back throws std::bad_allocAll emplace_back calls are wrapped in try/catch; partial lists are kept rather than crashing
new ListMenuScreen allocation failureAllocated with std::nothrow; a validity check guards all subsequent use
List grows unboundedHard cap at ESP3D_MAX_MACROS; enforced in both addMacro() and loadFromFile()
LVGL object deletion inside event callbackScreen follows the timer-based destruction pattern — objects are deleted only via cleanup_timer_cb / transition_timer_cb, never directly inside event handlers
std::string in saveToFile()Section headers use snprintf into a fixed char[32] buffer; path/desc lines are std::string but the write loop breaks immediately on fwrite failure

See docs/guides/esp32_memory_constraints.md for general heap management guidance applicable to this module.


a14: macros_screen registers with ui_manager.registerScreen(macros, screen, prepareForDestruction), so a global overlay (critical alert) can tear it down; see ui_core.md.

ModuleHow it connects
filesScreen (FluidNC / grbl / grblHAL)Calls macroManager::toggleMacro() from the file context menu to add/remove macro bookmarks; see fluidnc_module_files.md, grbl_module_files.md, grblhal_module_files.md
ListMenuScreen / screen infrastructuremacrosScreen is composed on top of ListMenuScreen; see screen_base_infrastructure.md
inputScreenLaunched by macrosScreen when editing a description in Edit mode; see modal_dialog_screens.md
messageBoxScreenUsed for run / remove / clear-all confirmation dialogs and error reporting; see modal_dialog_screens.md
statusScreenmacrosScreen redirects here after a successful macro launch; see status_screen.md
ESP3DConfigFileDrives INI parsing in loadFromFile() via the processMacroEntry callback; see config_file.md
globalFsRaw fopen / fwrite / close for saveToFile(); exists() / remove() for the clear-all path; see filesystem.md
esp3dGcodeHandlerTarget-specific handler that routes the macro file to the CNC firmware; see gcode_host.md
ESP3DTranslationServiceAll user-visible strings (mode labels, confirmations, errors) fetched via esp3dTranslationService.translate(); see translations.md

The macro system runs entirely on Core 1 inside the LVGL task. All calls to macroManager functions originate from UI event handlers and are therefore single-threaded; no mutex is needed for the macro_list_ vector.

loadFromFile() is called once during system startup, before the LVGL task takes over, so it is safe at that point.

saveToFile() is always triggered from within LVGL callbacks (button release, mode switch) — always on Core 1.

See UI_Framework_&_Screens.md for the broader LVGL threading model and the Core 0 / Core 1 separation.