Skip to content

grbl_module_files

The grbl_module_files module implements the Files screen for the GRBL CNC firmware target. It provides a touch- and encoder-driven interface for browsing the local pendant SD card, launching G-code jobs, managing macros, renaming files, and deleting files.

GRBL distinction: Unlike FluidNC (see fluidnc_module_files.md), GRBL exposes no firmware-side SD card or file-listing protocol. All file operations therefore target the pendant’s own SD card, and jobs are streamed line-by-line by the G-code host rather than triggered remotely.

Source file: main/display/cnc/grbl/screens/files_screen.cpp
Namespace: filesScreen
Parent module: grbl_module.md → UI_Framework_&_Screens


  1. Purpose and Scope
  2. Architecture Overview
  3. Key Data Structures
  4. Component Interaction Diagram
  5. Async File Scan Subsystem
  6. File Action Modes
  7. Screen Lifecycle
  8. Navigation and Transition Flow
  9. Data Flow
  10. State Machine
  11. Memory Safety Patterns
  12. Dependencies
  13. Related Modules

grbl_module_files is responsible for:

ResponsibilityDetails
SD browsingLists files and directories from the pendant’s local SD via globalFs
Job launchingStreams G-code files line-by-line to GRBL via gcodeHostService.addStream()
Extension filteringAccepts only files matching the configured extension list (esp3d_files_extensions NVS setting)
Directory navigationNavigates into subdirectories and back to parent with live path tracking
File operationsRename and delete files (directories are navigation-only)
Macro managementTags/untags files as macros via macroManager; saves on mode exit or screen leave
Action mode switchingSwitches between four operation modes via hardware switch or touch footer zone
Async scanningRuns the SD readdir loop in a separate FreeRTOS task so the LVGL spinner stays animated

Services

UI Layer

Shared State (volatile flags)

Scan Task (any core)

LVGL Task (Core 1)

filesScreen::create()

scan_check_timer_cb

(250 ms poll)

display_file_list()

Button / Switch callbacks

File action handlers

file_scan_task()

globalFs

(opendir / readdir / stat)

scan_completed

scan_result_valid

scan_cancel_requested

scan_error

scan_entries_count

staging_entries_

(written by scan task)

file_entries_

(owned by LVGL task)

ListMenuScreen

Loading spinner

+ status label

Footer

(action-mode indicator)

gcodeHostService

.addStream()

macroManager

esp3dXsettings

esp3dTranslationService

The design enforces a strict two-thread separation:

  • The scan task writes only to staging_entries_ and volatile flag variables.
  • The LVGL task reads staging_entries_ only after scan_completed is set (sequenced handoff), then atomically swaps it into file_entries_.
  • LVGL widget calls are never made from the scan task.

Represents a single item in the file listing.

struct FileEntry {
std::string name; // Display name (filename or "..")
std::string size; // File size as decimal string; empty for non-files
enum class Type : uint8_t {
File, // Regular file matching the extension filter
Dir, // Subdirectory — navigation only
Parent, // ".." entry — go up one level
Info, // Non-selectable informational row ("Loading…", "No files")
Error // Non-selectable error row ("No SD card", "SD card error")
} type;
};

Info and Error entries are tagged non-selectable via ui_manager.tagListNodeNonSelectable() so the user cannot accidentally act on them.

Tracks the async scan lifecycle, visible only to the LVGL task:

StateMeaning
IdleNo scan running, or list explicitly cleared
DoneScan completed successfully; file_entries_ is populated
ErrorScan failed (no SD card, or directory open error)

Controls what happens when a file entry is selected or the OK button is pressed:

ValueSwitch pos.SymbolAction
Process_file0LV_SYMBOL_DRIVELaunch job via gcode host
Set_as_macro1ESP3D_SYMBOL_ROBOTShow file info / toggle macro flag
Rename_file2LV_SYMBOL_EDITOpen rename input screen
Delete_file3LV_SYMBOL_TRASHConfirm and delete the file

The active mode is always reflected in the footer text (symbol + translated label).

VariableWritten byRead byRole
staging_entries_scan taskLVGL task (after handoff)Intermediate scan buffer
file_entries_LVGL task onlyLVGL task onlyLive, displayed list
scan_completed (volatile)scan taskLVGL taskCompletion signal
scan_result_valid (volatile)scan taskLVGL taskWhether the scan succeeded
scan_cancel_requested (volatile)LVGL taskscan taskCooperative stop signal
scan_error (volatile)scan taskLVGL taskError type on failure
scan_entries_count (volatile)scan taskLVGL taskReal-time counter for spinner label
pending_rescanLVGL taskLVGL task (timer)Re-scan needed after cooperative cancel

Platform Services

CNC Shared Screens

Base UI Layer

grbl_module_files

owns

owns

shows modal

shows modal

transitions to

transitions to

load / save / toggle

opendir / readdir / stat

rename / remove

addStream()

readByte / readString

translate()

register / unregister

getComponent

filesScreen

ListMenuScreen

(list_menu_screen)

VirtualButtonsComponent

messageBoxScreen

inputScreen

statusScreen

(transition target)

mainScreen

(back target)

macroManager

globalFs

(local SD access)

gcodeHostService

(job streaming)

esp3dXsettings

(extensions, macros flag)

esp3dTranslationService

UIManager


The scan subsystem mirrors the pattern established by languages_screen but adds cooperative cancellation to handle fast directory navigation without force-killing a task that might hold a filesystem lock.

globalFs (SD)file_scan_task (FreeRTOS)scan_check_timer (250 ms)LVGL TaskglobalFs (SD)file_scan_task (FreeRTOS)scan_check_timer (250 ms)LVGL Taskloop[readdir loop]alt[result valid][error]alt[scan still running][scan finished]loop[every 250 ms]start_sd_listing()hide buttons, show spinnercreate scan_check_timerlaunch_scan_task()accessFS / opendir / readdir / statcheck scan_cancel_requestedpush entry to staging_entries_update scan_entries_count (volatile)closedir / releaseFSset scan_result_valid\nset scan_completed = truevTaskDelete(nullptr)check scan_completed flagupdate spinner label with scan_entries_countlv_timer_delete(scan_check_timer)destroy_loading_spinner()show buttonsswap staging_entries_ → file_entries_display_file_list()display_message(error text)

When the user navigates into a subdirectory while a scan is still running:

Yes

Yes

No

No

User navigates directory

scan_task_handle != nullptr?

scan_cancel_requested = true

pending_rescan = true

Scan task exits readdir loop

on next iteration check

scan_completed = true

(task self-deletes)

scan_check_timer fires: scan_completed

pending_rescan?

pending_rescan = false

launch_scan_task() for new path

Resume 250 ms polling

Normal display path

launch_scan_task() immediately

Why cooperative, not forced? vTaskDelete on a task mid-accessFS could leave the filesystem mutex held, deadlocking future SD access. The task checks scan_cancel_requested itself, closes the directory cleanly, then terminates.


Mode selection is driven by hardware switch events (LV_EVENT_SWITCH_PRESSED) or, when ESP3D_HARDWARE_SWITCH_FEATURE is not defined, by a touch-enabled footer zone that cycles through the four states.

initial (switch position 0)

switch → 1

switch → 2

switch → 3

switch wraps → 0

switch → 0\n(saves dirty macros first)

switch → 0

switch → 0

Process_file

Set_as_macro

Rename_file

Delete_file

macroManager::saveToFile()

called on any exit if isDirty()

Parent

Dir

File

Process_file

Set_as_macro

enabled

disabled

Rename_file

Delete_file

success

failure

success

failure

success

File entry clicked

or OK button pressed

Entry type?

get_parent_path()

start_sd_listing()

append_to_path()

start_sd_listing()

current_file_action_mode?

messageBoxScreen::show_confirmation()

→ onConfirmJobLaunch()

macros_enabled setting?

InformationWithAction dialog

(toggle macro button + OK)

Simple information dialog

(path + size, OK only)

inputScreen::show_text_input()

→ onFileRenamed()

messageBoxScreen::show_confirmation()

→ onConfirmFileDelete()

statusScreen::setInitialState(JobStatus, Control)

→ transition to status_screen

messageBoxScreen::show_error()

Remove from file_entries_

Rebuild display in-place

Restore focus index

messageBoxScreen::show_error()

Update entry name

Re-sort file_entries_

Find new position

Restore focus

OperationFileDirectoryParent (..)
Process (run job)✅ Confirmation → stream❌ Error beep❌
Set as macro✅ Info dialog + toggle❌ Error beep❌
Rename✅ Text input screen❌ Error beep❌
Delete✅ Confirmation → delete❌ Error beep❌
Click (any mode)Per mode above✅ Navigate into✅ Navigate out

Yes

No

No

Yes

Yes

No

createScreen(ESP3DScreenType::files)

[grbl_module_screen_router]

redirect_target_ set?

createScreen(redirect_target_)

deferred via lv_timer 0 ms

reset redirect_target_

Reset transition state

Read esp3d_files_extensions

Read switch position

Build initial_entries: Info('Loading…')

new (nothrow) ListMenuScreen(…)

Hardware switch

available?

setFooterActionZoneEnabled(true, 4)

(touch cycles 4 modes)

forceFooterDisplay(true)

(hardware switch mode)

registerScreen(ESP3DScreenType::files)

Register onScreenDestroy (LV_EVENT_DELETE)

Register switch_event_cb (LV_EVENT_SWITCH_PRESSED)

onScreenCreated()

update footer for initial mode

file_entries_ empty?

lv_timer (500 ms delay)

→ start_sd_listing()

display_file_list()

(reuse preserved list)

Async scan → scan_check_timer_cb → display_file_list()

Back / job launched / external

prepareForDestruction()

scan_cancel_requested = true

Delete scan_check_timer

Destroy spinner

Remove event callbacks

ListMenuScreen::prepareForDestruction()

LVGL LV_EVENT_DELETE → onScreenDestroy()

unregisterScreen()

delete files_screen_obj_instance

file_entries_ is never cleared in prepareForDestruction(). The list persists across modal transitions (confirm dialog → cancel → back to files screen) so the user’s scroll position and file content are preserved without a costly re-scan. The list is only cleared by:

TriggerFunction
User clicks Refresh buttononRefreshButtonRelease()
New scan starts (including after dir navigation)start_sd_listing()

Files menu item

Back button

Job launched OK

Rename file

OK / Cancel

Launch / Delete confirm

File info / Error

OK / Cancel / Action

redirect_target_ set

main_screen

files_screen

(grbl_module_files)

status_screen

(JobStatus / Control mode)

input_screen

(text, 255-char max)

message_box_screen

any target screen

(deferred 0 ms timer)

The redirect_target_ mechanism allows other screens to request a post-files redirect: setRedirectTarget(target) stores the target; create() detects it, resets the flag, and schedules createScreen(target) via a zero-delay lv_timer. This prevents nested creation calls.


globalFs readdir + stat

swap after scan_completed

sort + optional Parent entry

updateItemList()

displayFileItem callback

hasValidExtension()

show macro icon?

isMacro()

toggleMacro()

saveToFile()

file click / OK button

addStream(path, admin)

rename() / remove()

Local SD Card

/sd/...

staging_entries_

(scan task)

file_entries_

(LVGL task)

display_entries (local static)

ListMenuScreen → LVGL list widget

Rendered rows

(icon + label)

esp3dXsettings

esp3d_files_extensions

esp3dXsettings

esp3d_macros_enabled

macroManager

(SD-backed JSON)

Action handler

gcodeHostService

→ GRBL over serial / BT / USB

esp3d_files_extensions contains a semicolon-delimited list (e.g., gcode;gco;nc;tap;txt). The scan task calls hasValidExtension() for each DT_REG entry using exact token matching with sentinel semicolons, preventing partial matches (e.g., g would not match gco). Comparison is case-insensitive. The special value * bypasses filtering and accepts all files.

After scan completion file_entries_ is sorted by compareFileEntries():

  1. Parent always first
  2. Error / Info second (preserve insertion order)
  3. File before Dir (files are the primary content)
  4. Within the same type: case-insensitive alphabetical (strcasecmp)

create()

start_sd_listing()

scan_check_timer (success)

scan_check_timer (SD failure)

fast dir navigation during scan

pending_rescan → relaunch for new path

file action (confirm / rename / info)

modal dismissed (file list preserved)

Refresh button

Refresh button or dir navigation

Back button or job launched

Back button

Creating

Scanning

Displaying

ShowingError

CancelPending

Modal

Rather than rescanning after a successful deletion, the module performs an optimistic local update to avoid the latency of a full SD re-scan:

Delete succeeds on SD

Erase entry from file_entries_

Rebuild display_entries

(with Parent if not at root)

Clamp pending_job_selection_index

to new list size

updateItemList() → LVGL list

move_to_index(new_selection_index)

The same approach is used for rename: the entry is updated in file_entries_, the list is re-sorted, the renamed file’s new position is located, and pending_job_selection_index is updated accordingly.


The module targets an ESP32 with severely constrained DRAM. Several safeguards are applied:

MIN_FREE_HEAP_BYTES = 30 000 bytes

If free internal heap drops below this threshold during the readdir loop, the scan stops early with the entries collected so far. The spinner label reflects the partial count; the UI behaves identically to a normal scan completion.

Every std::vector::emplace_back() or insert() executed on the LVGL task is wrapped in try/catch(const std::exception &e). On allocation failure the existing partial list is retained, or the empty-list path displays a translated “No files” row, rather than propagating to std::terminate (which resets the board).

new (std::nothrow) with Null-Check for Screen Object

Section titled “new (std::nothrow) with Null-Check for Screen Object”
files_screen_obj_instance =
new (std::nothrow) ListMenuScreen(…);
if (!files_screen_obj_instance || !files_screen_obj_instance->isValid()) {
delete files_screen_obj_instance;
files_screen_obj_instance = nullptr;
return; // Previous screen stays active
}

display_entries, initial_entries, and message_entries are declared static inside their functions to avoid repeated heap allocations on every list refresh, following the ESP32 embedded memory discipline defined in the project’s coding rules.


ComponentUsage
ListMenuScreenBase screen class hosting the scrollable list, virtual buttons, and footer
VirtualButtonsComponentThree buttons: OK/Set, Refresh, Back; getSwitchPosition() for initial mode
messageBoxScreenshow_confirmation, show_information, show_error, InformationWithAction dialogs
inputScreenshow_text_input() for file rename (max 255 chars)
statusScreenTransition target after job launch; setInitialState(JobStatus, Control) called before transition
mainScreenBack-navigation target
macroManagerisMacro(), toggleMacro(), isDirty(), saveToFile()
gcodeHostServiceaddStream(path, ESP3DAuthenticationLevel::admin, false) — streams G-code file to GRBL
globalFsaccessFS, opendir, readdir, closedir, releaseFS, stat, rename, remove
esp3dXsettingsreadString(esp3d_files_extensions), readByte(esp3d_macros_enabled)
esp3dTranslationServiceAll user-visible strings
UIManagerregisterScreen, unregisterScreen, getComponent, tagListNodeNonSelectable, getOrientationAngle
esp3d_task_create_unpinnedCreates the scan FreeRTOS task (stack 4096 bytes, idle+1 priority)
LVGL timerslv_timer_create for scan poll (250 ms), transition, cleanup, delayed start (500 ms)
MacroEffect when not definedEffect when defined
ESP3D_HARDWARE_SWITCH_FEATUREFooter action zone enabled; touch cycles through 4 modesHardware switch drives mode changes only; footer is display-only

  • Sort comparator fixed. std::sort requires a strict weak ordering (compare(x, x) must be false). The previous early return true for Parent / Error / Info entries broke it when two such entries were compared (undefined behaviour, possible crash). Entries are now sorted by rank first: Parent, then Error / Info, then files, then directories; Parent / Error / Info are equivalent among themselves.

ModuleRelationship
grbl_module.mdParent module grouping all GRBL-specific screens and components
grbl_module_screen_router.mdcreateScreen(ESP3DScreenType::files) routes to filesScreen::create()
grbl_module_connection_status.mdSibling component; shares the GRBL transport connection context
grbl_module_change_tool.mdSibling screen in the GRBL module navigated from the same menu
fluidnc_module_files.mdFluidNC counterpart: uses firmware-side file events (onFileEntryUpdate) instead of a local SD scan task
common_screens.mdProvides ListMenuScreen, inputScreen, messageBoxScreen — all used by this module
ui_components.mdVirtualButtonsComponent, ListMenuComponent, PanelComponent
cnc_shared.mdShared CNC screens: statusScreen, macrosScreen, mainScreen, macroManager