Skip to content

grbl_module_screen_router

The grbl_module_screen_router is the centralized screen dispatch layer for the grbl CNC firmware target. It owns a single public function — createScreen(ESP3DScreenType) — that translates a screen-type token into a concrete LVGL screen creation call. Every screen transition in the grbl target is funnelled through this one entry point, giving the rest of the system a stable, firmware-agnostic API for navigation.

The module lives at:

main/display/cnc/grbl/screens/esp3d_screen_type.cpp (implementation)
main/display/cnc/grbl/screens/esp3d_screen_type.h (enum + forward declaration)

A structurally identical counterpart (grblhal_module_screen_router) exists under main/display/cnc/grblhal/screens/. Both share the same enum definition and the same routing logic; they differ only in which firmware-specific screen implementations they include.


calls createScreen(type)

delegates to namespace::create()

delegates to namespace::create()

delegates to namespace::create()

instantiates with cfg

UIManager

(esp3d_ui.h)

createScreen()

grbl_module_screen_router

Common Screens

(common_screens)

CNC Shared Screens

(cnc_shared)

grbl-specific Screens

(grbl_module_files / change_tool / probe)

ConnectionStatusComponent

(grbl_module_connection_status)

UIManager (see ui_core.md) manages the active screen registry and owns the lifecycle of LVGL objects. When a transition is needed it calls createScreen() with a typed token; the router resolves that token to the correct namespace::create() function without the caller needing to know which implementation files are involved.


Defined in esp3d_screen_type.h. Every navigable destination has an enumerator; several are conditionally compiled based on hardware/feature flags:

EnumeratorConditionDescription
nonealwaysSentinel / no screen
splashalwaysBoot splash
mainalwaysMain CNC navigation hub
settingsalwaysSettings circular menu
settings_listalwaysFlat settings list
informationalwaysFirmware / device information
languagesalwaysLanguage selection
baudratealwaysBaud rate picker
pollingalwaysPolling interval picker
statusalwaysReal-time CNC status (feed, spindle, overrides)
gc_state (a14)alwaysGC state sub-screen of the status screen: parser state, WCS G54–G59, work unit G20/G21 (gcStateScreen::create())
range_editor (a14)alwaysGeneric bounded-value editor; in the enum but opened directly by its caller (rangeEditorScreen::create(data)), not through the router
jogalwaysJogging / axis movement
filesalwaysFile manager (grbl-native scan)
macrosalwaysMacro manager
change_toolalwaysTool change workflow
probealwaysProbing workflow
firmware_statusalwaysFirmware message log / status
connection_statusalwaysTransport + server connection status
message_boxalwaysModal message / confirmation dialog
inputalwaysGeneric text / numeric / PIN input
output_selectionBT or WiFi or UART extTransport output selector
screen_timeoutESP3D_BRIGHTNESS_CONTROL_FEATUREDisplay timeout picker
scan_btESP3D_BT_SERIAL_FEATURE or ESP3D_BT_BLE_FEATUREBluetooth device scan
wifi_scanESP3D_WIFI_FEATUREWiFi AP scan
server_scanESP3D_IP_CNC_CLIENT_FEATURECNC-over-IP server scan
updateESP3D_UPDATE_FEATUREOTA / SD update
emptyalwaysSentinel (end of enum)

Note: message_box, input, and firmware_status appear in the enum but are not dispatched through createScreen(). These are modal overlays launched directly by the screens that need them (e.g. via messageBoxScreen::show_confirmation() or inputScreen::show_numeric_input()).

Also defined in esp3d_screen_type.h. Used by UIManager as part of the component registry key (screenType_componentType):

EnumeratorLVGL Component
generic_screenGenericScreen base
circular_menu_screenCircularMenuScreen
virtual_buttonsVirtualButtonsComponent
circular_menuCircularMenuComponent
list_menuListMenuComponent
list_menu_screenListMenuScreen
connection_statusConnectionStatusComponent
firmware_statusFirmwareStatusComponent
message_boxMessage box widget
panelPanelComponent

When ESP3D_LOG is enabled the header also defines two string arrays and two helper macros:

#define SCREEN_STR(index) screen_type_names[static_cast<uint8_t>(index)]
#define COMPONENT_STR(index) component_type_names[static_cast<uint8_t>(index)]

These are used by createScreen() to emit a human-readable log line on every transition:

esp3d_log("Creating screen of type: %s", SCREEN_STR(screen_type));

// main/display/cnc/grbl/screens/esp3d_screen_type.h
void createScreen(ESP3DScreenType screen_type);

Thread / LVGL safety: Must be called exclusively from the LVGL task (Core 1). All screen transitions use timer callbacks (lv_timer_*) to defer destruction and creation out of event handlers; createScreen itself is the final step of that deferred sequence.

Error handling: An unknown screen_type value logs an error with esp3d_log_e and falls back to mainScreen::create(), preventing a blank display.


#if BT_SERIAL or BT_BLE

#if WIFI_FEATURE

#if IP_CNC_CLIENT

#if BT or WIFI or UART_EXT

#if BRIGHTNESS_CONTROL

#if UPDATE_FEATURE

default / unknown

createScreen(ESP3DScreenType)

splash

→ splashScreen::create()

main

→ mainScreen::create()

status

→ statusScreen::create()

gc_state

→ gcStateScreen::create()

jog

→ jogScreen::create()

files

→ filesScreen::create()

macros

→ macrosScreen::create()

probe

→ probeScreen::create()

change_tool

→ changeToolScreen::create()

settings

→ settingsScreen::create()

settings_list

→ settingsListScreen::create()

information

→ informationScreen::create()

languages

→ languagesScreen::create()

baudrate

→ baudrateScreen::create()

polling

→ pollingScreen::create()

connection_status

→ connectionStatusScreen::create(cfg)

cfg.return_screen = settings

scan_bt

→ scanBTScreen::create()

wifi_scan

→ wifiScanScreen::create()

server_scan

→ serverScanScreen::create()

output_selection

→ outputSelectionScreen::create()

screen_timeout

→ screenTimeoutScreen::create()

update

→ updateScreen::create()

⚠ esp3d_log_e + mainScreen::create()


Infrastructure

Common Screens

grbl-Specific Screens

CNC-Shared Screens

esp3d_screen_type.cpp

grbl_module_screen_router

mainScreen

statusScreen

jogScreen

settingsScreen

settingsListScreen

firmwareStatusScreen

macrosScreen

informationScreen

filesScreen

grbl_module_files

changeToolScreen

grbl_module_change_tool

probeScreen

grbl_module_probe

splashScreen

languagesScreen

baudrateScreen

pollingScreen

connectionStatusScreen

wifiScanScreen

scanBTScreen

serverScanScreen

outputSelectionScreen

screenTimeoutScreen

updateScreen

esp3d_log

UIManager

GenericScreen


Several screen types are fully excluded from the binary when their backing feature is disabled. The same #if guards appear in both the enum (in the header) and in the switch (in the implementation), ensuring the compiler never sees an unresolvable enumerator or a missing namespace::create symbol.

enables

enables

enables

enables

enables

any one enables

any one enables

any one enables

ESP3D_BT_SERIAL_FEATURE

or ESP3D_BT_BLE_FEATURE

ESP3D_WIFI_FEATURE

ESP3D_IP_CNC_CLIENT_FEATURE

ESP3D_BRIGHTNESS_CONTROL_FEATURE

ESP3D_UPDATE_FEATURE

ESP3D_UART_EXT_FEATURE

scan_bt

wifi_scan

server_scan

screen_timeout

update

output_selection

Feature flags are declared as OPTION() in CMakeLists.txt and converted to preprocessor defines by cmake/features.cmake. Mutual-exclusion constraints (e.g. Bluetooth and WiFi cannot both be active) are validated by cmake/sanity_check.cmake. See docs/features/feature_resource_matrix.md for the full compatibility matrix.


All navigation follows the timer-based deferred pattern required by the LVGL single-thread model (Core 1). createScreen is always the destination step — it never destroys the previous screen directly.

UIManagernamespace::create()createScreen()cleanup_timer_cb(lv_timer, later tick)transition_timer_cb(lv_timer, next tick)Event Callback(LVGL task, Core 1)User / System EventUIManagernamespace::create()createScreen()cleanup_timer_cb(lv_timer, later tick)transition_timer_cb(lv_timer, next tick)Event Callback(LVGL task, Core 1)User / System EventReturns immediately - no LVGL objects modifiedtouch / encoder / value updatelv_timer_create(transition_timer_cb)createScreen(next_type)namespace::create()registerScreen(type, lv_obj)registerComponent(type, comp_type, ptr)ok(new screen live)(returns)schedule cleanup_timer_cbunregisterScreen / unregisterComponentprevious screen objects destroyed

LVGL Constraint (Critical): LVGL objects must never be created or destroyed inside the event callback that triggered the transition. The two-timer pattern (transition then cleanup) ensures both operations happen on separate LVGL ticks, safely outside the event stack. Violating this causes use-after-free crashes. See docs/architecture/screen_transitions_flow.md for the authoritative reference.


The connection_status case is the only one that passes a configuration struct to create(). The router hard-codes cfg.return_screen = ESP3DScreenType::settings, meaning the Back button on the connection status screen always returns to the Settings screen. Callers that need a different return destination must invoke connectionStatusScreen::create(cfg) directly rather than going through the router.

case ESP3DScreenType::connection_status:
{
connectionStatusScreen::ConnectionStatusScreenConfig cfg;
cfg.return_screen = ESP3DScreenType::settings;
connectionStatusScreen::create(cfg);
break;
}

The ConnectionStatusScreenConfig struct (defined in main/display/screens/connection_status_screen.h) contains the return_screen field (default ESP3DScreenType::main) and, since a14, open_server_scan_when_connected (default false, set by the WiFi scan screen, see connection_status_screen.md).


Both firmware targets have a structurally identical router file. The only meaningful difference is the filesScreen implementation that gets linked:

AspectgrblgrblHAL
File pathmain/display/cnc/grbl/screens/esp3d_screen_type.cppmain/display/cnc/grblhal/screens/esp3d_screen_type.cpp
Enum / headermain/display/cnc/grbl/screens/esp3d_screen_type.hmain/display/cnc/grblhal/screens/esp3d_screen_type.h
filesScreenNative SD scan via file_scan_task + scan_check_timer_cbPush-based updates via onFileEntryUpdate + watchdog_timer_cb
All other screensIdentical routing and implementationIdentical routing and implementation

The two switch bodies are identical. Target selection happens at build time through CMakeLists.txt; only one firmware target is compiled per build.


TopicReference
Screen architecture overviewdocs/architecture/screens_architecture.md
Safe screen transition patternsdocs/architecture/screen_transitions_flow.md
grbl files screengrbl_module_files.md
grbl probe screengrbl_module_probe.md
grbl tool change screengrbl_module_change_tool.md
grbl connection status componentgrbl_module_connection_status.md
CNC shared screens (status, jog, macros, …)cnc_shared.md
Common screens (splash, scan, settings pickers)common_screens.md
UIManager, theme, and style systemui_core.md
Feature flag compatibility matrixdocs/features/feature_resource_matrix.md
UX screen-by-screen flowsdocs/ux_flows/