grblHAL Files Screen Module
Section titled “grblHAL Files Screen Module”The grblHAL Files Screen (grblhal_module_files) implements the SD-card file browser for pendant firmware targets that communicate with a grblHAL controller. It is the grblHAL-specific counterpart to the equivalent screens in the FluidNC and Grbl modules, but differs fundamentally in how the file tree is obtained and navigated:
- grblHAL lists the whole SD card flat and recursively in one
$Frequest — no[DIR:]entries are emitted. The response is a sequence of[FILE:/absolute/path.ext|SIZE:n]lines terminated by a synthesized[FILE_LIST_END]sentinel. - All directory navigation happens locally on the pendant after the single initial scan. No per-directory re-queries are issued to the controller.
- Rename is not supported by the grblHAL SD protocol; the 4-position switch’s fourth position therefore repeats Run rather than exposing a rename action.
Table of Contents
Section titled “Table of Contents”- Module Position in the System
- File Structure
- Architecture Overview
- Core Data Structures
- State Machines
- grblHAL SD Protocol
- File Listing Flow
- Local Directory Navigation
- File Action Modes
- Component Interactions
- Key Functions Reference
- Memory Safety Strategy
- Dependencies
1. Module Position in the System
Section titled “1. Module Position in the System”The files screen belongs to the grblhal_module subtree inside UI_Framework_&_Screens. It is created by the screen router (grblhal_module_screen_router.md) and shares common CNC infrastructure (status screen, macro manager, message box) via cnc_shared.md.
2. File Structure
Section titled “2. File Structure”main/display/cnc/grblhal/screens/└── files_screen.cpp ← entire module (namespace filesScreen) ├── FileEntry data structure for display rows ├── RawFile flat-listing record from $F ├── FileListState enum – async listing state machine ├── FileActionMode enum – 4-position switch actions ├── build_current_view() local tree navigation ├── process_listing_line() line parser for $F output └── compareFileEntries() sort comparatorEverything lives inside the filesScreen namespace, with static internal linkage. There is no public header — the screen is reached exclusively through createScreen(ESP3DScreenType::files) dispatched by the screen router.
3. Architecture Overview
Section titled “3. Architecture Overview”Key architectural decisions
Section titled “Key architectural decisions”| Decision | Rationale |
|---|---|
Single $F scan, local navigation | grblHAL emits the entire tree at once — no per-directory command exists |
RawFile master list kept in memory | Enables instant subdirectory traversal without controller round-trips |
[FILE_LIST_END] sentinel | grblHAL’s "ok" line is intercepted by the gcode handler which synthesises this marker; the screen relies on the sentinel, not on parsing "ok" |
| No rename action on position 3 | $F protocol has no rename command; position 3 of the switch repeats Run |
| Watchdog timer (10 s) | Guards against the controller failing to emit the end-of-listing sentinel |
4. Core Data Structures
Section titled “4. Core Data Structures”4.1 RawFile
Section titled “4.1 RawFile”struct RawFile { std::string path; // full absolute path, e.g. "/subdir/CUBE.gcode" std::string size; // size in bytes as string (may be empty)};static std::vector<RawFile> all_files_;The master flat listing — one entry per file reported by $F. Populated once per scan. Directory navigation never touches the controller again; build_current_view() derives the visible tree from this vector.
4.2 FileEntry
Section titled “4.2 FileEntry”struct FileEntry { std::string name; // display name (basename or directory component) std::string size; // formatted size (empty for directories) enum class Type : uint8_t { File, Dir, Parent, Info, Error } type;};static std::vector<FileEntry> file_entries_; // current directory viewThe display list for the current directory. Rebuilt by build_current_view() every time the user navigates into or out of a directory. Info and Error rows are tagged non-selectable by the UI manager.
4.3 FileActionMode
Section titled “4.3 FileActionMode”enum class FileActionMode : uint8_t { Process_file = 0, // LV_SYMBOL_DRIVE – $F=<path> Set_as_macro = 1, // ESP3D_SYMBOL_ROBOT – pendant-side macro tagging Delete_file = 2, // LV_SYMBOL_TRASH – $FD=<path> Process_file_2 = 3 // LV_SYMBOL_DRIVE – repeats Run (no rename on grblHAL)};Maps directly to the 4-position hardware switch position. On touch-only hardware, positions 0–2 are emulated (position 3 is suppressed).
5. State Machines
Section titled “5. State Machines”5.1 File Listing State Machine (FileListState)
Section titled “5.1 File Listing State Machine (FileListState)”| State | Meaning |
|---|---|
Idle | Initial state, no scan started yet |
Collecting | Receiving [FILE:...] lines from $F; listing_active = true |
Done | Full listing received; all_files_ is the authoritative source |
Error | SD card mount error or firmware error status received |
Timeout | Watchdog fired; no progress for 10 consecutive seconds |
5.2 Watchdog Timer Logic
Section titled “5.2 Watchdog Timer Logic”The watchdog fires every 1 second (WATCHDOG_INTERVAL_MS = 1000). It compares total_entries_processed against last_entry_count. If unchanged for MAX_STALL_COUNT = 10 consecutive ticks (10 seconds), the listing is aborted with a Timeout state.
6. grblHAL SD Protocol
Section titled “6. grblHAL SD Protocol”6.1 Commands
Section titled “6.1 Commands”| Command | Purpose |
|---|---|
$F | List all files recursively, flat format, absolute paths |
$F=<absolute-path> | Run (stream) a file |
$FD=<absolute-path> | Delete a file |
Note: Rename is not supported. The 4-position switch’s third position (index 3) therefore repeats the Run action.
6.2 Listing Response Format
Section titled “6.2 Listing Response Format”[FILE:/path/to/file.gcode|SIZE:315841][FILE:/another/file.nc|SIZE:1024][FILE:/deep/subdir/file.tap|SIZE:4096]...[FILE_LIST_END]- Each line begins with
[FILE:and contains the full absolute path (no separate[DIR:]entries). - The
|SIZE:nsegment is optional but typically present. - The
[FILE_LIST_END]sentinel is synthesized by the gcode handler (esp3d_gcode_handler_servicefor grblHAL target) when it sees the"ok"that terminates the$Fcommand. The screen itself never parses raw"ok"lines.
6.3 Error Detection
Section titled “6.3 Error Detection”[MSG:ERR: Failed to mount device]Detected by is_error_message(). Causes immediate transition to the Error state, clearing both all_files_ and file_entries_.
6.4 Parsing Pipeline
Section titled “6.4 Parsing Pipeline”7. File Listing Flow
Section titled “7. File Listing Flow”The complete sequence from screen creation to a populated file list:
Master List Persistence
Section titled “Master List Persistence”all_files_ is not cleared on screen destruction or messageBox transitions. This means:
- Returning from a confirmation dialog immediately re-displays the existing list without a re-scan.
all_files_is only cleared explicitly bystart_sd_listing()(new scan) or on SD error.
8. Local Directory Navigation
Section titled “8. Local Directory Navigation”8.1 Model
Section titled “8.1 Model”There are no controller requests during navigation. Every directory change rebuilds file_entries_ from all_files_ using build_current_view().
8.2 build_current_view() Logic
Section titled “8.2 build_current_view() Logic”8.3 Path Helpers
Section titled “8.3 Path Helpers”| Function | Input | Output |
|---|---|---|
get_parent_path(path) | /subdir/deep | /subdir |
get_parent_path(path) | /subdir | / |
get_parent_path(path) | / | / (no-op) |
append_to_path(name) | / + "subdir" | /subdir |
append_to_path(name) | /subdir + "deep" | /subdir/deep |
8.4 Sort Order (compareFileEntries)
Section titled “8.4 Sort Order (compareFileEntries)”1. Parent (..) entry — always first2. Error / Info placeholder rows — second3. Files (alphabetical, case-insensitive)4. Directories (alphabetical, case-insensitive)9. File Action Modes
Section titled “9. File Action Modes”The 4-position hardware switch (or 3-position touch emulation) controls what happens when the user selects a file:
Action Details
Section titled “Action Details”| Switch Position | Icon | Action on File Select |
|---|---|---|
0 — Process_file | LV_SYMBOL_DRIVE | Show confirmation → $F=<path> → transition to statusScreen |
1 — Set_as_macro | ESP3D_SYMBOL_ROBOT (or LV_SYMBOL_FILE if macros disabled) | Show file info / toggle macro tag via macroManager |
2 — Delete_file | LV_SYMBOL_TRASH | Show confirmation → $FD=<path> → update local all_files_ |
3 — Process_file_2 | LV_SYMBOL_DRIVE | Same as position 0 (grblHAL has no rename) |
Directory selection (any mode)
Section titled “Directory selection (any mode)”- Dir entry: descend into directory (local, no network request).
- Parent
..entry: ascend to parent (local). - Error / Info row: ignored (non-selectable).
- Directory in Delete mode: rejected with
ESP3D_ERROR_BEEP— only files can be deleted.
Footer display
Section titled “Footer display”The footer bar always shows the current action mode icon and a translated label. On touch hardware the footer’s left zone doubles as a mode-cycle button (3 states: Run → Macro → Delete).
10. Component Interactions
Section titled “10. Component Interactions”10.1 Interaction Diagram
Section titled “10.1 Interaction Diagram”10.2 Screen Lifecycle Events
Section titled “10.2 Screen Lifecycle Events”| Event | Handler | Effect |
|---|---|---|
| Screen created | create() → onScreenCreated() | Subscribe to values; decide scan vs. reuse list |
LVGL LV_EVENT_DELETE | onScreenDestroy() | Unregister from UIManager; delete C++ instance |
prepareForDestruction() | Called before any transition | Stop watchdog; unsubscribe; hide spinner; save macros if dirty |
redirect_target_ set | create() start | Deferred redirect via lv_timer; skips full screen build |
10.3 Transition Targets
Section titled “10.3 Transition Targets”| User action | Destination screen |
|---|---|
| Confirm job launch | statusScreen (JobStatus / Control mode) |
| Back button | mainScreen |
| Confirm / cancel dialog closes | Returns to filesScreen (re-created, reuses all_files_) |
11. Key Functions Reference
Section titled “11. Key Functions Reference”11.1 process_listing_line(const char *line) → bool
Section titled “11.1 process_listing_line(const char *line) → bool”Parses a single line emitted by the grblHAL gcode handler. Returns true to continue, false when the listing is complete or aborted.
Input variants [FILE:/path/name.ext|SIZE:n] → add RawFile to all_files_ [FILE_LIST_END] → finalize_listing() [MSG:ERR: ...] → error state, clear lists anything else → ignoredMemory guard: if free internal heap drops below MIN_FREE_HEAP_BYTES (30 KB), finalize_listing() is called immediately to stop collecting and display what has been gathered so far.
11.2 build_current_view()
Section titled “11.2 build_current_view()”Rebuilds file_entries_ from all_files_ for the current directory (current_path). Immediate file children become File entries; deeper paths contribute at most one Dir entry for their immediate subdirectory component.
Called by: display_file_list(), which is in turn called by finalize_listing() and by every local navigation action.
11.3 display_file_list()
Section titled “11.3 display_file_list()”Single authoritative entry point for populating the list widget. Sequence:
- Call
build_current_view(). - Update the list title to
current_path. - If
file_entries_is empty → displayno_filesinfo message. - Sort with
compareFileEntries(). - Prepend a
Parententry ifcurrent_path != "/". - Call
ListMenuScreen::updateItemList(). - Restore focus to
pending_job_selection_indexif set (returns from cancel).
11.4 compareFileEntries(a, b) → bool
Section titled “11.4 compareFileEntries(a, b) → bool”Strict weak ordering for std::sort. Priority: Parent → Error/Info → File (alpha) → Dir (alpha). Case-insensitive via strcasecmp.
11.5 RawFile (struct)
Section titled “11.5 RawFile (struct)”Internal storage for each file returned by $F. Fields: path (full absolute path), size (byte count as string). Stored in all_files_ (static, persists across screen visits).
11.6 finalize_listing()
Section titled “11.6 finalize_listing()”Shared termination for both the normal end-of-list path and the low-memory / stall cutoff path:
stop_watchdog_timer()list_state = Done,listing_active = falsedestroy_loading_spinner()display_file_list()- Show action buttons
11.7 setRedirectTarget(ESP3DScreenType target)
Section titled “11.7 setRedirectTarget(ESP3DScreenType target)”Sets a one-shot redirect. When create() is next called (e.g., after a messageBox closes), it immediately navigates to target via a zero-delay LVGL timer instead of building the files screen again. Used after a successful job launch to jump to statusScreen.
12. Memory Safety Strategy
Section titled “12. Memory Safety Strategy”The ESP32 heap is severely constrained (see docs/guides/esp32_memory_constraints.md). The module applies several defensive patterns:
12.1 Minimum Free Heap Guard
Section titled “12.1 Minimum Free Heap Guard”static const uint32_t MIN_FREE_HEAP_BYTES = 30000; // 30 KBChecked in process_listing_line() before every push_back. If the threshold is breached, finalize_listing() is called immediately — the user gets whatever files were already collected.
12.2 std::bad_alloc Containment
Section titled “12.2 std::bad_alloc Containment”Every std::vector mutation that occurs within an LVGL callback context is wrapped in try/catch(const std::exception&). An uncaught std::bad_alloc in LVGL context calls std::terminate and reboots the board. On catch, the function logs the error and returns with the partially built structure, never propagating the exception.
12.3 file_entries_ Preservation Across Dialogs
Section titled “12.3 file_entries_ Preservation Across Dialogs”prepareForDestruction() deliberately does not clear file_entries_ or all_files_. This preserves the user’s directory view across confirmation-dialog round-trips (confirm launch, confirm delete, cancel). The lists are only cleared by:
start_sd_listing()— explicit refreshon_firmware_status_update()— SD card errorprocess_listing_line()— mount error line
12.4 new (std::nothrow) for Screen Instance
Section titled “12.4 new (std::nothrow) for Screen Instance”files_screen_obj_instance = new (std::nothrow) ListMenuScreen(...);Prevents a thrown std::bad_alloc from escaping. Null is checked immediately; on failure the function returns without showing the screen.
13. Dependencies
Section titled “13. Dependencies”13.1 Compile-Time Dependencies
Section titled “13.1 Compile-Time Dependencies”| Dependency | Module | Role |
|---|---|---|
ListMenuScreen | common_screens.md | Base widget for the scrollable file list |
messageBoxScreen | common_screens.md | Confirmation and error dialogs |
macroManager | cnc_shared.md | Pendant-side macro tagging and persistence |
statusScreen | cnc_shared.md | Transition target after job launch |
mainScreen | cnc_shared.md | Back-navigation target |
ESP3DGCodeHandlerService | cnc_grblhal.md | Sends $F, $F=, $FD= commands |
ESP3DValues | values.md | Delivers firmware_file_entry and firmware_status callbacks |
ESP3DSettings | esp3d_core.md | Reads esp3d_macros_enabled setting |
ESP3DTranslationService | translations.md | Localized UI strings |
UIManager | ui_core.md | Screen registration and component lookup |
VirtualButtonsComponent | ui_components.md | Action buttons and switch position read |
esp3d_resources | ui_core.md | Image descriptors (ok_b, refresh_b, back_b, etc.) |
13.2 Runtime Value Subscriptions
Section titled “13.2 Runtime Value Subscriptions”ESP3DValuesIndex | Direction | Purpose |
|---|---|---|
firmware_file_entry | Subscribe | Receives each parsed [FILE:...] line and the [FILE_LIST_END] sentinel |
firmware_status | Subscribe | Detects "error:" prefixed status to abort listing on SD failure |
Both subscriptions are established in create() and torn down in prepareForDestruction(). start_sd_listing() unsubscribes and resubscribes firmware_file_entry defensively to prevent double-subscription across refresh cycles.
13.3 Sibling grblHAL Screens
Section titled “13.3 Sibling grblHAL Screens”| Screen | Module | Relationship |
|---|---|---|
| Screen router | grblhal_module_screen_router.md | Dispatches createScreen(ESP3DScreenType::files) |
| Connection status | grblhal_module_connection_status.md | Independent; shown on the main and status screens |
| Change tool | grblhal_module_change_tool.md | Launched when controller reports tool-change request |
| Probe | grblhal_module_probe.md | Independent probing workflow |
For the equivalent files screen implementations in other firmware targets, see fluidnc_module_files.md (event-driven, per-directory scan) and grbl_module_files.md (background scan task with dirent).
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. - Error during a listing. A firmware error status while listing (prefix tested case-insensitively: handlers publish
ERROR:<code>orError:…) shows the translatedno_sd_cardmessage: an error there means the card is missing or unreadable (for example FluidNC 4.xsdmmc_card_init failed, 3.xFailed to mount device).last_error_statusis not read at that point, since the handler publishesfirmware_statusbefore it and it still holds the previous error; the raw firmware text stays in the logs and the message history. - Listing lines arrive through
firmware_file_entry, a stream value that is never merged or skipped, set withset_value_wait()by the handler, so a burst of lines at 115200 baud is no longer truncated (see values.md).