Skip to content

FluidNC Module — Files Screen (`fluidnc_module_files`)

FluidNC Module — Files Screen (fluidnc_module_files)

Section titled “FluidNC Module — Files Screen (fluidnc_module_files)”

The Files Screen is the SD-card file browser for the FluidNC CNC firmware target. It allows the operator to navigate the SD card directory tree, launch GCode jobs, tag files as quick-access macros, rename files, and delete files — all without leaving the pendant UI. The screen is built on top of the shared ListMenuScreen infrastructure and drives FluidNC through the GCode Host Service via native FluidNC $SD/… commands.

Source file: main/display/cnc/fluidnc/screens/files_screen.cpp
Namespace: filesScreen


FluidNC Firmware (CNC controller)

Macro System

CNC Integration Layer

UI Layer (LVGL / Core 1)

inherits / owns

modal dialogs

rename input

sendGcode()

serial / BT / socket

firmware_file_entry callback

firmware_status callback

setInitialState() + redirect

toggleMacro() / isMacro()

read/write INI

filesScreen

(files_screen.cpp)

ListMenuScreen

(base class)

ListMenuComponent

(scrollable list)

VirtualButtonsComponent

(Set / Refresh / Back)

messageBoxScreen

(confirm / info dialogs)

inputScreen

(rename text input)

ESP3DGCodeHandlerService

(esp3dGcodeHandler)

ESP3DValues

(observable value bus)

statusScreen

(post-job-launch target)

macroManager

(macro_manager.cpp)

GlobalFileSystem

(macro INI file on flash)

SD Card

$SD/List

$SD/Run

$SD/Delete

$SD/Rename


Every board-level BSP and factory application is documented in Board Support Packages. The files screen lives exclusively in the FluidNC UI target, as a sibling to the analogous screens in the grbl and grblHAL modules.

main/display/cnc/fluidnc/
├── components/
│ └── connection_status.h ← ConnectionStatusComponent (shared widget)
└── screens/
├── files_screen.cpp ← THIS MODULE
├── change_tool_screen.cpp ← see fluidnc_module_change_tool.md
└── probe_screen.cpp ← see fluidnc_module_probe.md

Represents a single item in the SD card listing. Three constructors cover the different item categories:

struct FileEntry {
std::string name; // Display name (filename or directory name)
std::string size; // Raw size string in bytes (empty for non-files)
enum class Type : uint8_t {
File, // Regular GCode / text file
Dir, // Subdirectory
Parent, // ".." navigation entry
Info, // Placeholder message (e.g. "Loading…")
Error // Error message row (non-selectable)
} type;
};

Info and Error rows are tagged as non-selectable via ui_manager.tagListNodeNonSelectable() so that encoder and touch navigation skip them automatically.


FileListState — Asynchronous Listing State Machine

Section titled “FileListState — Asynchronous Listing State Machine”

The SD listing is driven by FluidNC’s $SD/List command, whose output arrives line-by-line through the value-bus callback onFileEntryUpdate. A state machine tracks progress:

start_sd_listing() at root (/)

start_sd_listing() in subdirectory

Target DIR line matched at correct depth

[MSG:ERR: Failed to mount device] received

Watchdog fires after 10 s of no data

End marker [/sd/…] received

Entry at shallower depth detected (implicit end)

Low-memory guard triggered (≥30 KB threshold)

[MSG:ERR: Failed to mount device] received

Watchdog fires after 10 s of no data

Next call to start_sd_listing()

Next call to start_sd_listing()

Next call to start_sd_listing()

Idle

Collecting

Searching

Error

Timeout

Done

StateMeaning
IdleNo listing in progress
SearchingScanning output to find the target subdirectory header
CollectingAccumulating child entries at target_depth
DoneCollection complete; list handed to LVGL
ErrorSD mount failure or firmware error
TimeoutWatchdog expired — no new entries for 10 s

FileActionMode — Multi-Mode File Interaction

Section titled “FileActionMode — Multi-Mode File Interaction”

A four-position virtual switch (hardware rotary switch or touch-emulated footer toggle) selects the action triggered when the operator clicks a file:

switch +

switch +

switch +

switch -

switch -

switch -

0 · Process

LV_SYMBOL_DRIVE

1 · Macro

ESP3D_SYMBOL_ROBOT

2 · Rename

LV_SYMBOL_EDIT

3 · Delete

LV_SYMBOL_TRASH

ModeSymbolFileDirectoryParent
Process_fileLV_SYMBOL_DRIVEConfirm → $SD/Run=<path>Navigate intoNavigate up
Set_as_macroESP3D_SYMBOL_ROBOTShow info + macro toggleError beepError beep
Rename_fileLV_SYMBOL_EDITOpen inputScreen text inputError beepError beep
Delete_fileLV_SYMBOL_TRASHConfirm → $SD/Delete=<path>Error beepError beep

Note: When macros are disabled in settings (esp3d_macros_enabled = 0), mode 1 degrades to a plain file-information dialog without the toggle action.


ESP3DValues busFluidNC firmwareGCodeHandlerServicefilesScreen (LVGL / Core 1)ESP3DValues busFluidNC firmwareGCodeHandlerServicefilesScreen (LVGL / Core 1)alt[DIR or FILE at targetdepth][End marker /shallow-depth return][Error marker]loop[For each output line]alt[Watchdog fires (10 s stall)]create() calledsubscribe(firmware_file_entry, onFileEntryUpdate)subscribe(firmware_status, on_firmware_status_update)start_sd_listing()create_loading_spinner()start_watchdog_timer() [1 s tick]sendGcode("$SD/List")$SD/List (via transport)publish(firmware_file_entry, line)onFileEntryUpdate(line)process_listing_line(line)file_entries_.emplace_back(...)list_state = Donedestroy_loading_spinner()display_file_list()list_state = Errordisplay_message(error, Error)list_state = Timeoutdisplay_message(sd_card_error, Error)
statusScreenGCodeHandlerServicemessageBoxScreenfilesScreenOperatorstatusScreenGCodeHandlerServicemessageBoxScreenfilesScreenOperatoralt[Confirmed][Cancelled]Click file (mode = Process_file)handleFileUse(entry)pending_job_filename = entry.nameshow_confirmation("Launch <file>?")Display dialogPress OKonConfirmJobLaunch()sendGcode("$SD/Run=<path>/<file>")setInitialState(JobStatus, Control)setRedirectTarget(ESP3DScreenType::status)Redirect (via create() deferred timer)Press CancelonCancelAction()pending_job_filename.clear()Return to file list (no re-scan)
GCodeHandlerServiceinputScreenfilesScreenOperatorGCodeHandlerServiceinputScreenfilesScreenOperatorClick file (mode = Rename_file)handleFileRename(entry)show_text_input(title, current_name, onFileRenamed)Show keyboard with pre-filled nameEdit name and confirmonFileRenamed(new_name)sendGcode("$SD/Rename=<old_path>><new_path>")Update file_entries_ in-place (re-sort + restore selection)display_file_list()

Protects against FluidNC not responding (SD removed, firmware hang):

ParameterValue
Interval1 000 ms
Stall threshold10 consecutive silent ticks → Timeout
On timeoutSpinner destroyed, error message displayed, buttons re-shown

The stall counter resets whenever total_entries_processed increases, so any data flow — even slow — keeps the watchdog alive.


A temporary lv_spinner widget with a status label ("Processing: N") is overlaid on the list container while the listing is in progress. It is created by create_loading_spinner() and destroyed by destroy_loading_spinner() as part of the Done / Error / Timeout transitions. Both widgets use theme tokens (ESP3D_SPINNER_*, ESP3D_ACCENT_ACTIVE_COLOR) for visual consistency. See UI Style Guide.


Only files whose extension (case-insensitive) appears in the extensions semicolon-delimited string are added to file_entries_. The value is read from NVS setting esp3d_files_extensions at screen creation time (default: "gcode;gco;nc;tap;txt"). Setting it to "*" disables filtering entirely.

extensions = "gcode;gco;nc;tap;txt"
→ ;gcode;gco;nc;tap;txt; (wrapped with semicolons)
→ matches ";gco;" → accept "part.gco"
→ no match for ";stl;" → reject "model.stl"

The macro system is managed by macroManager (shared across CNC firmware targets). The files screen integrates with it as follows:

isMacro(full_path)

toggleMacro(full_path)

isDirty()

saveToFile() on mode-change or Back

loadFromFile() on first use

saveToFile() writes INI

files_screen.cpp

macroManager

(macro_manager.cpp / .h)

GlobalFileSystem

/macros.ini on flash

  • Display: Files tagged as macros show ESP3D_SYMBOL_ROBOT instead of LV_SYMBOL_FILE in the list.
  • Persistence: The macro list is lazily saved — saveToFile() is called only when leaving macro mode (switch changes away from mode 1) or pressing Back with unsaved changes (isDirty() == true).

The Macro INI file format written by macroManager::saveToFile():

[macro_0]
path=/jobs/start.gcode
desc=
[macro_1]
path=/jobs/finish.gcode
desc=

After a successful job launch, the screen cannot destroy itself from within an LVGL callback. Instead it stores the target screen type and defers the transition:

void setRedirectTarget(ESP3DScreenType target); // called from onConfirmJobLaunch()

When create() is called next (by the screen rotation machinery), it detects a non-none redirect target, fires a one-shot LVGL timer with zero delay that calls createScreen(deferred_target), and returns immediately — preventing the files screen from being re-created on top of the target.


prepareForDestruction() intentionally does not clear file_entries_. The list persists in memory so that returning from a confirmation dialog (Confirm / Cancel) calls display_file_list() and rebuilds the view instantly without an SD re-scan. The list is discarded only when:

  1. start_sd_listing() begins a new scan (Refresh button or directory navigation).
  2. An SD error is detected in on_firmware_status_update() or process_listing_line().

The files screen runs on a heavily resource-constrained ESP32. Several guards are applied throughout:

GuardLocationAction on failure
30 KB free-heap minimum (MIN_FREE_HEAP_BYTES)process_listing_line() (Collecting state)Stop collection, display partial list
std::bad_alloc catch blocksAll emplace_back / insert callsLog error, stop collection or leave list unchanged
std::nothrow newListMenuScreen construction in create()Return without creating screen, previous screen preserved
file_entries_modification_in_progress flagConcurrent modification detectionDeclared volatile, prevents interleaved async updates

See ESP32 Memory Constraints Guide for the broader allocation policy.


LVGL runtimeListMenuScreen (LVGL object)filesScreen::create()UIManager / createScreen()LVGL runtimeListMenuScreen (LVGL object)filesScreen::create()UIManager / createScreen()Screen is livecreate()ESP3D_TRANSITION_RESET (clear timers/flags)Read esp3d_files_extensions from NVSsubscribe(firmware_file_entry)subscribe(firmware_status)new ListMenuScreen(...)instancelv_obj_add_event_cb(LV_EVENT_DELETE, onScreenDestroy)lv_obj_add_event_cb(LV_EVENT_SWITCH_PRESSED, switch_event_cb)onScreenCreated() - decide scan vs. reusestart_sd_listing() [deferred 500 ms]Back button → ESP3D_TRANSITION_STARTprepareForDestruction()stop_watchdog_timer()unsubscribe(firmware_file_entry)unsubscribe(firmware_status)LV_EVENT_DELETE → onScreenDestroy()ui_manager.unregisterScreen(files)delete files_screen_obj_instance

Like all CNC screens, the files screen uses the standard two-timer pattern to avoid modifying LVGL objects from within an event callback:

TimerPurpose
cleanup_timerCalls prepareForDestruction() then schedules transition_timer
transition_timerCalls createScreen(next_screen_target) after ESP3D_TRANSITION_SCREEN_DELAY_MS

For the full pattern specification see Screen Transitions Flow.


IndexIconPressRelease (short)Release (long)
0ok_b—Confirm selection (same as item click)—
1refresh_bSelection beepClear list + start_sd_listing()—
2back_bBack beepSave macros if dirty → transition to main screen—

All three buttons are hidden during an active SD listing and shown again once the listing completes (Done, Error, or Timeout).


OperationCommand sentResponse parsed
List SD card$SD/List[DIR:name], [FILE:name|SIZE:n], [/sd/…], [MSG:ERR:…]
Launch job$SD/Run=<full_path>Firmware status updates via firmware_status value
Delete file$SD/Delete=<full_path>Implicitly confirmed by absence of error
Rename file$SD/Rename=<old_path>><new_path>Implicitly confirmed by absence of error

FluidNC’s $SD/List output uses the number of spaces after : to encode hierarchy depth. The parsing rules are:

  • DIR entries: depth = number of spaces after :
  • FILE entries: depth = (number of spaces after :) − 1
[DIR:subdir] ← depth 0 (no leading space)
[DIR: child] ← depth 1 (one leading space)
[FILE: file.gco|SIZE:1024] ← depth 1 (two leading spaces → depth 2-1=1)
[/sd/sd] ← end marker

The parser sets target_depth to the depth of the target directory’s children. When a line arrives at a depth shallower than target_depth, collection is considered complete.


External Modules

files_screen.cpp internals

show/hide

start/stop

on Done → updateItemList()

Process → confirm

Rename → text input

Macro → toggle

Delete → confirm

onFileEntryUpdate

on_firmware_status_update

job launched

create() redirect

filters entries into

displayed in

FileEntry

(struct)

FileListState

(state machine)

FileActionMode

(switch position)

Watchdog

(lv_timer 1 s)

Loading Spinner

(lv_spinner + label)

Extension Filter

(semicolon list)

Redirect Target

(deferred navigation)

ListMenuScreen

messageBoxScreen

inputScreen

macroManager

GCodeHandlerService

ESP3DValues bus

statusScreen


  • 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.
  • Error during a listing. A firmware error status while listing (prefix tested case-insensitively: handlers publish ERROR:<code> or Error:…) shows the translated no_sd_card message: an error there means the card is missing or unreadable (for example FluidNC 4.x sdmmc_card_init failed, 3.x Failed to mount device). last_error_status is not read at that point, since the handler publishes firmware_status before it and it still holds the previous error. The FluidNC “no SD card” messages (sdmmc_card_init failed, Failed to mount device, SD Card is inaccessible) are ignored by the handler (no firmware error, no history entry, see cnc_fluidnc.md); the listing still recognizes [MSG:ERR: Failed to mount device] itself (is_error_message()).
  • Listing lines arrive through firmware_file_entry, a stream value that is never merged or skipped, set with set_value_wait() by the handler, so a burst of lines at 115200 baud is no longer truncated (see values.md).
  • The FluidNC end-of-listing marker is accepted as [/sd/ …] (older versions) or [/sd …] (recent versions).

TopicDocumentation
UI base infrastructure & screen lifecycleUI_Framework_Screens.md
macroManager (shared CNC screens)cnc_shared.md
Change Tool screen (FluidNC)fluidnc_module_change_tool.md
Probe screen (FluidNC)fluidnc_module_probe.md
Connection status componentfluidnc_module_connection_status.md
GCode Host & streaming architectureCNC_Firmware_Integration.md
Screen transition timer patternscreen_transitions_flow.md
Theme tokens & LVGL style guideui_style_guide.md
ESP32 memory constraintsesp32_memory_constraints.md