Skip to content

System Lifecycle Screens

The system lifecycle screens module comprises the two screens that bracket the firmware’s operational life: the splash screen shown immediately at first boot, and the update screen displayed whenever a firmware, resources, or configuration update is being applied from the SD card. Both screens exist outside the normal CNC-workflow navigation tree — they are entered unconditionally by the boot sequence and exited only once their one-time mission is complete (transition to main, or reboot).

Source files:

  • main/display/screens/splash_screen.cpp
  • main/display/screens/update_screen.cpp

The two screens occupy distinct, non-overlapping phases of the device lifecycle:

main_screen / status_screenupdateScreensplashScreentft_ui_task (Core 1)ESP3DUpdateServiceESP3DX::begin()main_screen / status_screenupdateScreensplashScreentft_ui_task (Core 1)ESP3DUpdateServiceESP3DX::begin()alt[Success][Failure]alt[Pending update found][No update]detect() - scan SD for fw/config/resources filescreateScreen(ESP3DScreenType::update)updateScreen::create()executeXxxUpdate(updateCallbackWithDisplay)updateCallbackWithDisplay(percent, status)esp3dXValues.set_value(update_progress / update_status)onUpdateValueChange() - refresh progress barshowSuccess() - notifyUpdateCompletionRendered()waitUpdateCompletionRendered() - esp_restart()showError() - reveal OK buttonuser taps OK - esp_restart()createScreen(ESP3DScreenType::splash)splashScreen::create()load macros, start 1500 ms timercreateScreen(ESP3DScreenType::main)

Storage and Update (see storage_and_configuration.md)

Core Platform (see core_platform.md)

Common Screens (see common_screens.md)

UI Core (see ui_core.md)

system_lifecycle_screens (this module)

set_current_screen

loadFromFile

readByte macros_enabled

lv_timer_create createScreen

new GenericScreen

getVirtualButtons

subscribe / unsubscribe

set_value update_progress

getPendingUpdate / getStatus

updateCallbackWithDisplay

translate

registerScreen / unregisterScreen

waitUpdateCompletionRendered

applyLabelStyle

splashScreen

splash_screen.cpp

updateScreen

update_screen.cpp

ESP3DXUi

esp3dXui

UIManager

ui_manager

GenericScreen

VirtualButtonsComponent

ESP3DValues

esp3dXValues

ESP3DSettings

esp3dXsettings

ESP3DTranslationService

esp3dTranslationService

ESP3DUpdateService

esp3dUpdateService

macroManager


The splash screen is the very first screen the user sees after power-on. Its goals are:

  1. Give a branded “loading” moment while background initialisation completes.
  2. Load the macro list from the SD/Flash file (macroManager::loadFromFile) while the logo is visible.
  3. Transition automatically to the main operational screen.
SymbolEffect
ESP3D_SPLASH_DELAYIf 0, the splash screen is skipped entirely — create() immediately calls createScreen(NEXT_SCREEN) and returns. Otherwise the logo is shown for approximately 1 500 ms.
ESP3D_STARTING_SOUNDMacro emitted on successful screen creation; triggers the buzzer startup melody.
ESP3DSettingIndex::esp3d_macros_enabledNVS byte. If set, macroManager::loadFromFile() is called during the splash delay.
┌─────────────────────────────────────┐
│ │
│ │
│ ┌───────────┐ │
│ │ [LOGO] │ (centered) │
│ └───────────┘ │
│ │
│ Black background, no scrollbar │
└─────────────────────────────────────┘

The logo image (logo) is sourced from the ui_resources partition at runtime. Its rotation is aligned to the current display orientation via ui_manager.getOrientationAngle() using LVGL’s transform_angle / transform_pivot styles applied directly to the image widget — this avoids rotating the entire screen object.

Yes

No

Yes

No

Yes

No

splashScreen::create

ESP3D_SPLASH_DELAY == 0?

createScreen NEXT_SCREEN

immediate jump to main

lv_obj_create screen

lv_screen_load

previous screen valid?

lv_obj_del previous screen

vTaskDelay 50 ms

Set black bg

Scrollbar off

lv_image_create img

set_src logo

align CENTER

Read orientation angle

Set transform_pivot_x/y

Set transform_angle

set_current_screen splash

ESP3D_STARTING_SOUND

macros_enabled?

macroManager::loadFromFile

lv_timer_create 1500 ms

Splash visible - awaiting timer

Timer fires: lv_timer_delete

createScreen NEXT_SCREEN

CallbackTriggerAction
onDestroyLV_EVENT_DELETE on the screen objectLogs heap free size; calls ui_manager.debugPrintAllInstances() in debug builds. Does not perform any resource cleanup — no subscriptions or heap allocations are made by this screen.

The module guards against re-entrant timer creation:

if (transition_timer) {
lv_timer_delete(transition_timer);
transition_timer = nullptr;
}

The timer callback is a lambda that deletes itself (lv_timer_delete(transition_timer)) before calling createScreen, avoiding a use-after-free if create() is invoked again unexpectedly.


The update screen handles the visual feedback for all SD-card-triggered update types:

Update TypeESP3DUpdateTypeTitle Translation Key
Firmware binaryfirmwareESP3DLabel::firmware_update
UI resources partitionresourcesESP3DLabel::resources_update
Resource patchesresource_patchesESP3DLabel::resources_update
Theme colourstheme_colorsESP3DLabel::resources_update
UI sounds (/esp3dsound.ini, a14)soundsESP3DLabel::resources_update
Configuration INI(all other)ESP3DLabel::settings_update

Protected by the compile-time feature flag ESP3D_UPDATE_FEATURE.

┌──────────────────────────────────────┐
│ [Title: "Firmware Update" / ...] │ ← title_label (montserrat_14)
│ │
│ ┌────────────────────────────────┐ │
│ │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ ← progress_bar (animated)
│ └────────────────────────────────┘ │
│ 42% │ ← percent_label
│ │
│ Please wait… │ ← status_label
│ (or error / success msg) │
│ │
│ [ ] [ OK ] [ ] │ ← VirtualButtonsComponent
│ hidden (error only) hidden │
└──────────────────────────────────────┘

All font references in this screen are forced to lv_font_montserrat_14 — a font compiled into the firmware binary itself — rather than the custom XIP fonts loaded from the ui_resources partition. This is intentional: a resources update erases and rewrites that partition while the screen is live, making XIP font pointers unsafe. The OK button image also uses a statically compiled ok_b_static symbol for the same reason.

"updateScreen.create()"

"updateCallbackWithDisplay(pct, status)"

"progress callback\nonUpdateValueChange()\nrefresh bar + percent label"

"ESP3DUpdateStatus.success\ncompletion_handled = true"

"ESP3DUpdateStatus.failed\ncompletion_handled = true"

"showSuccess()\nESP3D_SUCCESS_SOUND\nservice calls esp_restart()"

"showError()\nstatus label red\nESP3D_ERROR_BEEP\nOK button revealed"

"user taps OK\nonOkButtonRelease()\nesp_restart()"

Created

InProgress

Success

Failed

Rebooting

WaitingUser

External callers (ESP3DUpdateService, running on Core 0 / the boot task) report progress via updateCallbackWithDisplay. The update screen never touches LVGL objects directly from Core 0 — all mutations are routed through the ESP3DValues queue and executed on Core 1 by the LVGL task.

progress_bar / percent_label / status_labelLVGL task (Core 1)ESP3DValues (thread-safe queue)updateCallbackWithDisplay (Core 0)ESP3DUpdateService (Core 0)progress_bar / percent_label / status_labelLVGL task (Core 1)ESP3DValues (thread-safe queue)updateCallbackWithDisplay (Core 0)ESP3DUpdateService (Core 0)Updates queued safelycompletion_handled guards\nagainst duplicate callsupdateCallbackWithDisplay(42, "writing...")set_value(update_progress, "42")set_value(update_status, "writing...")esp3dXValues.handle() inside UI looponUpdateValueChange(update_progress, "42")lv_bar_set_value(42, LV_ANIM_ON)lv_label_set_text("42%")checkCompletionState()

ESP3DXUi provides two semaphore-backed methods used to coordinate the boot task with the LVGL task:

MethodSignalled whenConsumer
waitFirstFrameRendered(timeout_ms)First LVGL render pass after screen creationBoot task waits before calling executeXxxUpdate() to guarantee the screen is visible
waitUpdateCompletionRendered(timeout_ms)After isCompletionHandled() is true and the final state is paintedBoot task waits before calling esp_restart() to guarantee the user sees the result

isCompletionHandled() is the public predicate the LVGL task polls between render cycles to decide when to call notifyUpdateCompletionRendered().

CallbackTriggerAction
onScreenDestroyLV_EVENT_DELETE on screen objectUnsubscribes both update_progress and update_status; calls ui_manager.unregisterScreen; deletes the GenericScreen instance; nullifies all widget pointers.
onOkButtonPressVirtual button press (index 1, centre)Plays ESP3D_SELECTION_BEEP.
onOkButtonReleaseVirtual button release (index 1, centre)esp_hal::wait(100) then esp_restart().
  • The GenericScreen instance is heap-allocated with new (std::nothrow) to avoid an abort on allocation failure; the code checks the pointer and returns gracefully instead of throwing.
  • completion_handled is reset to false at the start of every create() call, ensuring the flag is correct even if the screen is recreated.
  • All four widget pointers (progress_bar, status_label, title_label, percent_label) are explicitly nullified in onScreenDestroy before the GenericScreen destructor runs, preventing stale-pointer use in any lingering value callback.

DependencyModulePurpose
GenericScreencommon_screensScreen container + virtual button strip
VirtualButtonsComponentcommon_screensThree-button bottom bar; centre button shown/hidden dynamically
UIManager / ui_managerui_coreScreen registration, style application, orientation angle
ESP3DXUi / esp3dXuiui_coreCurrent-screen tracking; boot-sequence semaphores
ESP3DValues / esp3dXValuescore_platformThread-safe value bus bridging Core 0 progress updates to Core 1 LVGL mutations
ESP3DSettings / esp3dXsettingscore_platformNVS byte read for esp3d_macros_enabled
ESP3DTranslationServicecore_platformAll user-visible strings translated at runtime
ESP3DUpdateService / esp3dUpdateServicestorage_and_configurationUpdate type/status query; receives updateCallbackWithDisplay as progress hook
macroManagercnc_sharedMacro list loaded from file during splash delay
esp3d_log / esp3d_log_ecore_platformHeap and state logging throughout both screens

Why the splash screen does not use GenericScreen

Section titled “Why the splash screen does not use GenericScreen”

The splash screen has no button strip, no container, and no subscription to any value system. Using GenericScreen would add unnecessary memory overhead during the brief startup window when heap pressure is at its highest. A raw lv_obj_create(NULL) screen is sufficient and incurs no additional allocations.

Why the update screen uses firmware-embedded fonts and images

Section titled “Why the update screen uses firmware-embedded fonts and images”

During a resources partition update the ui_resources flash partition is erased and rewritten while the update screen is live. Any pointer into that partition (XIP font, image descriptor from esp3d_resources.cpp) references memory that is actively being modified, which would cause data corruption or a flash-cache coherency fault. The update screen is the only screen in the project that:

  • Overrides ui_manager.applyLabelStyle() with a hard lv_font_montserrat_14 reference (compiled into the firmware ELF).
  • Uses a statically compiled ok_b_static image symbol (generated into res_320_240/ok_b_static.c or res_480_320/ok_b_static.c at build time).

See docs/ui_resources/development.md for the ui_resources partition binary format and update mechanisms.

Why progress updates go through ESP3DValues

Section titled “Why progress updates go through ESP3DValues”

The update executes on Core 0 (the boot task), while all LVGL object mutations must happen on Core 1 (the LVGL task) to comply with LVGL’s single-thread constraint. Posting values through ESP3DValues provides a thread-safe FIFO that is drained by esp3dXValues.handle() inside the LVGL task loop, eliminating any explicit mutex or xTaskNotify in the screen’s own code. See docs/architecture/screens_architecture.md for the broader LVGL threading model.

checkCompletionState() is called from every onUpdateValueChange invocation. Without the completion_handled guard, a rapid burst of progress callbacks near 100 % (followed by a trailing status update) could invoke showSuccess() or showError() multiple times, corrupting widget state or triggering a double esp_restart().