grbl_module_files
Section titled “grbl_module_files”Overview
Section titled “Overview”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
Table of Contents
Section titled “Table of Contents”- Purpose and Scope
- Architecture Overview
- Key Data Structures
- Component Interaction Diagram
- Async File Scan Subsystem
- File Action Modes
- Screen Lifecycle
- Navigation and Transition Flow
- Data Flow
- State Machine
- Memory Safety Patterns
- Dependencies
- Related Modules
Purpose and Scope
Section titled “Purpose and Scope”grbl_module_files is responsible for:
| Responsibility | Details |
|---|---|
| SD browsing | Lists files and directories from the pendant’s local SD via globalFs |
| Job launching | Streams G-code files line-by-line to GRBL via gcodeHostService.addStream() |
| Extension filtering | Accepts only files matching the configured extension list (esp3d_files_extensions NVS setting) |
| Directory navigation | Navigates into subdirectories and back to parent with live path tracking |
| File operations | Rename and delete files (directories are navigation-only) |
| Macro management | Tags/untags files as macros via macroManager; saves on mode exit or screen leave |
| Action mode switching | Switches between four operation modes via hardware switch or touch footer zone |
| Async scanning | Runs the SD readdir loop in a separate FreeRTOS task so the LVGL spinner stays animated |
Architecture Overview
Section titled “Architecture Overview”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 afterscan_completedis set (sequenced handoff), then atomically swaps it intofile_entries_. - LVGL widget calls are never made from the scan task.
Key Data Structures
Section titled “Key Data Structures”FileEntry
Section titled “FileEntry”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.
FileListState
Section titled “FileListState”Tracks the async scan lifecycle, visible only to the LVGL task:
| State | Meaning |
|---|---|
Idle | No scan running, or list explicitly cleared |
Done | Scan completed successfully; file_entries_ is populated |
Error | Scan failed (no SD card, or directory open error) |
FileActionMode
Section titled “FileActionMode”Controls what happens when a file entry is selected or the OK button is pressed:
| Value | Switch pos. | Symbol | Action |
|---|---|---|---|
Process_file | 0 | LV_SYMBOL_DRIVE | Launch job via gcode host |
Set_as_macro | 1 | ESP3D_SYMBOL_ROBOT | Show file info / toggle macro flag |
Rename_file | 2 | LV_SYMBOL_EDIT | Open rename input screen |
Delete_file | 3 | LV_SYMBOL_TRASH | Confirm and delete the file |
The active mode is always reflected in the footer text (symbol + translated label).
Async Scan State Variables
Section titled “Async Scan State Variables”| Variable | Written by | Read by | Role |
|---|---|---|---|
staging_entries_ | scan task | LVGL task (after handoff) | Intermediate scan buffer |
file_entries_ | LVGL task only | LVGL task only | Live, displayed list |
scan_completed (volatile) | scan task | LVGL task | Completion signal |
scan_result_valid (volatile) | scan task | LVGL task | Whether the scan succeeded |
scan_cancel_requested (volatile) | LVGL task | scan task | Cooperative stop signal |
scan_error (volatile) | scan task | LVGL task | Error type on failure |
scan_entries_count (volatile) | scan task | LVGL task | Real-time counter for spinner label |
pending_rescan | LVGL task | LVGL task (timer) | Re-scan needed after cooperative cancel |
Component Interaction Diagram
Section titled “Component Interaction Diagram”Async File Scan Subsystem
Section titled “Async File Scan Subsystem”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.
Cooperative Cancellation Flow
Section titled “Cooperative Cancellation Flow”When the user navigates into a subdirectory while a scan is still running:
Why cooperative, not forced?
vTaskDeleteon a task mid-accessFScould leave the filesystem mutex held, deadlocking future SD access. The task checksscan_cancel_requesteditself, closes the directory cleanly, then terminates.
File Action Modes
Section titled “File Action Modes”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.
Per-Mode Behavior on File Click
Section titled “Per-Mode Behavior on File Click”Directory vs. File Operation Matrix
Section titled “Directory vs. File Operation Matrix”| Operation | File | Directory | Parent (..) |
|---|---|---|---|
| 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 |
Screen Lifecycle
Section titled “Screen Lifecycle”Key Lifecycle Invariant
Section titled “Key Lifecycle Invariant”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:
| Trigger | Function |
|---|---|
| User clicks Refresh button | onRefreshButtonRelease() |
| New scan starts (including after dir navigation) | start_sd_listing() |
Navigation and Transition Flow
Section titled “Navigation and Transition Flow”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.
Data Flow
Section titled “Data Flow”Extension Filtering
Section titled “Extension Filtering”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.
Entry Sort Order
Section titled “Entry Sort Order”After scan completion file_entries_ is sorted by compareFileEntries():
Parentalways firstError/Infosecond (preserve insertion order)FilebeforeDir(files are the primary content)- Within the same type: case-insensitive alphabetical (
strcasecmp)
State Machine
Section titled “State Machine”Full Screen State
Section titled “Full Screen State”In-Place Deletion Update
Section titled “In-Place Deletion Update”Rather than rescanning after a successful deletion, the module performs an optimistic local update to avoid the latency of a full SD re-scan:
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.
Memory Safety Patterns
Section titled “Memory Safety Patterns”The module targets an ESP32 with severely constrained DRAM. Several safeguards are applied:
Heap Guard in Scan Task
Section titled “Heap Guard in Scan Task”MIN_FREE_HEAP_BYTES = 30 000 bytesIf 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.
std::bad_alloc Catch Sites
Section titled “std::bad_alloc Catch Sites”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}Static Buffers for Recurring Allocations
Section titled “Static Buffers for Recurring Allocations”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.
Dependencies
Section titled “Dependencies”Runtime Dependencies
Section titled “Runtime Dependencies”| Component | Usage |
|---|---|
ListMenuScreen | Base screen class hosting the scrollable list, virtual buttons, and footer |
VirtualButtonsComponent | Three buttons: OK/Set, Refresh, Back; getSwitchPosition() for initial mode |
messageBoxScreen | show_confirmation, show_information, show_error, InformationWithAction dialogs |
inputScreen | show_text_input() for file rename (max 255 chars) |
statusScreen | Transition target after job launch; setInitialState(JobStatus, Control) called before transition |
mainScreen | Back-navigation target |
macroManager | isMacro(), toggleMacro(), isDirty(), saveToFile() |
gcodeHostService | addStream(path, ESP3DAuthenticationLevel::admin, false) — streams G-code file to GRBL |
globalFs | accessFS, opendir, readdir, closedir, releaseFS, stat, rename, remove |
esp3dXsettings | readString(esp3d_files_extensions), readByte(esp3d_macros_enabled) |
esp3dTranslationService | All user-visible strings |
UIManager | registerScreen, unregisterScreen, getComponent, tagListNodeNonSelectable, getOrientationAngle |
esp3d_task_create_unpinned | Creates the scan FreeRTOS task (stack 4096 bytes, idle+1 priority) |
| LVGL timers | lv_timer_create for scan poll (250 ms), transition, cleanup, delayed start (500 ms) |
Build-time Feature Guards
Section titled “Build-time Feature Guards”| Macro | Effect when not defined | Effect when defined |
|---|---|---|
ESP3D_HARDWARE_SWITCH_FEATURE | Footer action zone enabled; touch cycles through 4 modes | Hardware switch drives mode changes only; footer is display-only |
a14 Changes
Section titled “a14 Changes”- Sort comparator fixed.
std::sortrequires a strict weak ordering (compare(x, x)must be false). The previous earlyreturn truefor 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.
Related Modules
Section titled “Related Modules”| Module | Relationship |
|---|---|
| grbl_module.md | Parent module grouping all GRBL-specific screens and components |
| grbl_module_screen_router.md | createScreen(ESP3DScreenType::files) routes to filesScreen::create() |
| grbl_module_connection_status.md | Sibling component; shares the GRBL transport connection context |
| grbl_module_change_tool.md | Sibling screen in the GRBL module navigated from the same menu |
| fluidnc_module_files.md | FluidNC counterpart: uses firmware-side file events (onFileEntryUpdate) instead of a local SD scan task |
| common_screens.md | Provides ListMenuScreen, inputScreen, messageBoxScreen — all used by this module |
| ui_components.md | VirtualButtonsComponent, ListMenuComponent, PanelComponent |
| cnc_shared.md | Shared CNC screens: statusScreen, macrosScreen, mainScreen, macroManager |