Skip to content

ui_core — UI Framework Core

ui_core is the foundational layer of the ESP3D TFT display system. It provides centralized management for every piece of UI-level state: LVGL task lifecycle, screen and component registries, theme system (color tokens + LVGL styles), display orientation, language selection, UI lock, and flash-partition resource loading.

All screens, reusable components, and CNC-firmware UI modules in ui_components, common_screens, and cnc_shared build on top of ui_core. It runs exclusively on Core 1 — the single thread that owns LVGL — and is the only valid source of lv_style_t objects used in the rest of the UI.

Source files:

FileRole
main/display/esp3d_x_ui.h/.cppLVGL FreeRTOS task runner (ESP3DXUi)
main/display/esp3d_ui.h/.cppCentralized state manager + style registry (UIManager)
main/display/esp3d_resources.cppPartition mmap, image/font/blob loader
main/display/esp3d_snapshot.hScreen-capture state (snapshot_state_t)
main/display/esp3d_translations_id_map.hNumeric file-ID ↔ ESP3DLabel enum lookup
main/display/esp3d_screen_transition.hESP3D_TRANSITION_START / timer body macros (with exception guard)
main/display/esp3d_unit_sections.hJog step / feedrate list split per work unit (mm / in sections)

The Small UI tier (main/display/small_ui/) does not use ui_core: it has its own LVGL task and no UIManager, theme or ui_resources. The 3D printer UI groundwork (main/display/3dprinter/) builds on ui_core like the CNC UI. See UI_Framework_&_Screens.


Consumers

BSP / Hardware Drivers

Core Platform

ui_core (main/display/)

pinned Core 1

create_application()

manages lifecycle

drives each tick

read/write

orientation notify

restart on lang change

apply* style API

registerScreen/Component

getTokens / getStyles

spi_flash_mmap

esp3d_resources_init

ESP3DXUi

esp3d_x_ui.h/cpp

LVGL task runner

UIManager ui_manager

esp3d_ui.h/cpp

State + style registry

Resource System

esp3d_resources.cpp

Partition mmap + font loader

snapshot_state_t

esp3d_snapshot.h

Translation ID Map

esp3d_translations_id_map.h

ESP3DSettings (NVS)

ESP3DValues

esp3d_log

board_init.c

lvgl_flush_cb / tick

RenderingClient

ui_resources partition

(flash subtype 0x42)

ui_components

common_screens

cnc_shared / firmware modules

See Core_Platform_&_Infrastructure.md for ESP3DSettings, ESP3DValues, and esp3d_log details. See Hardware_Peripheral_Drivers.md for BSP and RenderingClient details.


Files: esp3d_x_ui.h, esp3d_x_ui.cpp
Global instance: esp3dXui

ESP3DXUi owns the FreeRTOS task (tft_ui_task) that drives LVGL on Core 1. Nothing in the UI runs outside this task context.

LVGLtft_ui_task (Core 1)ESP3DXUimain.cpp (app_main)LVGLtft_ui_task (Core 1)ESP3DXUimain.cpp (app_main)loop[Every LVGL tick (min_delay..100 ms)]esp3dXui.begin()renderingClient.begin()xSemaphoreCreateBinary() × 2esp3d_task_create_pinned(Core 1)_lock_acquire → create_application() → _lock_release_lock_acquireesp3dXValues.handle()time_till_next = lv_timer_handler()_lock_releasenotifyFirstFrameRendered()waitFirstFrameRendered(timeout_ms)

Task configuration constants:

ConstantPurpose
LVGL_TASK_COREPinned CPU core (always Core 1)
LVGL_TASK_PRIORITYFreeRTOS priority
LVGL_TASK_STACK_SIZEStack size in bytes
LVGL_TICK_PERIOD_MSMinimum delay between timer handler calls
MethodSignaled whenUsed by
waitFirstFrameRendered(ms)After the first lv_timer_handler() pass following create_application()Boot sequence — safe to perform post-splash actions once the display is live
waitUpdateCompletionRendered(ms)After updateScreen::isCompletionHandled() returns trueBoot sequence — waits for the update result screen to be visible before rebooting

⚠️ LVGL lock rule: Every lv_obj_* / lv_style_* call must occur inside _lock_acquire(lvgl_lock) / _lock_release(lvgl_lock), or from within an LVGL event/timer callback (which already executes inside the task). Violating this causes race conditions on the single-threaded LVGL renderer.


2. UIManager — Centralized State Manager

Section titled “2. UIManager — Centralized State Manager”

Files: esp3d_ui.h, esp3d_ui.cpp
Global instance: ui_manager

UIManager is the singleton that all screens and components interact with for UI state. It owns the theme configuration (tokens + styles), the active screen/component registries, and all persistent UI settings.

UIManager

-user_lock_: bool

-system_lock_: bool

-hide_lock_: bool

-current_orientation_: Orientation

-current_theme_: Theme

-current_language_: string

-theme_: ThemeConfig

-active_screens_: unordered_map

-active_components_: unordered_map

-screens_mutex_: mutex

-components_mutex_: mutex

+initialize() : bool

+cleanup()

+getLockState() : bool

+userSetLockState(bool, save) : bool

+systemSetLockState(bool) : bool

+setHideLock(bool, save) : bool

+setOrientation(Orientation, save) : bool

+rotateCW() : bool

+rotateCCW() : bool

+setTheme(Theme, save) : bool

+nextTheme() : bool

+previousTheme() : bool

+setLanguage(string, save) : bool

+registerScreen(type, obj) : bool

+unregisterScreen(type) : bool

+getScreen(type) : lv_obj_t

+getActiveScreen() : ESP3DScreenType

+registerComponent(screen, component, ptr) : bool

+unregisterComponent(screen, component) : bool

+getComponent(screen, component) : void

+initializeStyles() : bool

+resetStyles()

+applyButtonDefaultStyle(obj, radius)

+applyListNodeStyle(obj, selected)

ThemeConfig

+tokens: ThemeTokens

+styles: ThemeStyles

ThemeTokens

+bg_main: uint32_t

+bg_panel: uint32_t

+bg_overlay: uint32_t

+bg_disabled: uint32_t

+text_primary: uint32_t

+text_secondary: uint32_t

+text_disabled: uint32_t

+icon_idle: uint32_t

+border_idle: uint32_t

+border_focus: uint32_t

+accent_select: uint32_t

+accent_active: uint32_t

+accent_alert: uint32_t

+accent_action: uint32_t

+glow_select: uint32_t

+glow_active: uint32_t

+glow_alert: uint32_t

+glow_action: uint32_t

+text_on_select: uint32_t

+text_on_active: uint32_t

+text_on_alert: uint32_t

+text_on_action: uint32_t

+indicator_success: uint32_t

+indicator_info: uint32_t

+indicator_error: uint32_t

+indicator_warning: uint32_t

ThemeStyles

+screen_background: lv_style_t

+screen_container: lv_style_t

+button_default: lv_style_t

+button_selected: lv_style_t

+button_disabled: lv_style_t

+label_small: lv_style_t

+label_medium: lv_style_t

+label_big: lv_style_t

+container_transparent: lv_style_t

+container_framed: lv_style_t

+list_node: lv_style_t

+list_node_selected: lv_style_t

+circular_menu_styles: lv_style_t

+panel_item_styles: lv_style_t

+axis_button_styles: lv_style_t

+progress_bar_styles: lv_style_t

+textarea_input: lv_style_t

+initialized: bool

No

Yes

No

Yes

initialize

esp3d_system_message_hook_log_errors

esp3d_resources_init

esp3d_sound.reloadDefinitions

(buzzer builds)

loadThemeNames from partition blobs

esp3dXValues

initialized?

esp3dXValues.initialize

Translation service

started?

esp3dTranslationService.begin

criticalAlert::begin

(CNC targets)

loadStateFromSettings

Read hide_lock → user_lock

Read orientation

Read theme → applyTheme

Read language

Sync orientation to ESP3DValues

initializeStyles

is_initialized_ = true

The UI is considered locked when either user_lock_ or system_lock_ is true. Each source is independent:

SourceSetterPersistedSubject to hide_lock_
user_lock_userSetLockState()Yes (NVS)Yes — forced to false when hide_lock_ is set
system_lock_systemSetLockState()No (runtime only)No
hide_lock_setHideLock()Yes (NVS)— forces user_lock_ = false when activated

ESP3D_NO_CONNECTION_LOCK_FEATURE builds: systemSetLockState() is a no-op. The system lock never engages — useful for UI testing while the CNC is disconnected.

Only one screen may be active at a time. All component types are globally unique (one instance per type across all screens).

registerScreen

registerComponent

getActiveScreen

getComponent

unregisterScreen on destroy

unregisterComponent on destroy

Screen creates lv_obj_t

active_screens_

ESP3DScreenType → lv_obj_t*

Component creates struct

active_components_

screenType_componentType → void*

Returns ESP3DScreenType::none

if empty

Returns typed pointer to caller

Entry erased

Entry erased

Both maps use std::mutex (separate screens_mutex_ and components_mutex_) for thread-safe access from any task. Component keys are the string "screenType_componentType".

registerScreen(type, screen, destructor) also stores the screen’s own cleanup function (the static prepareForDestruction() it passes when it opens another screen; optional, nullptr = unknown). getActiveScreenDestructor() returns it, so that code outside the screen flow — the critical alert — can open a screen on top of whatever is displayed, exactly like the active screen would (Pattern 3 of screen_transitions_flow.md).

Global transition signal — beginScreenTransition() is called when a transition is scheduled (ESP3D_TRANSITION_START, Pattern 3 create(), firmware status component); registerScreen() of the next screen clears it. isScreenTransitionPending() is the only way for code outside a screen to know that the active screen is being replaced (the per-screen flags are file-local). The signal is considered stale after 2 s (ESP3D_SCREEN_TRANSITION_STALE_MS, failed transition), so it can never block forever. LVGL thread only.

Exception guard — the transition timer body (esp3d_screen_transition.h) wraps create_screen_func() in try / catch: screen and component constructors can throw (std::runtime_error, std::bad_alloc), and an uncaught exception in an LVGL timer callback calls std::terminate (board reset). On failure it logs and falls back to the main screen.

Deg0 (0°) → rotateCW → Deg90 (90°) → rotateCW → Deg180 (180°) → rotateCW → Deg270 (270°) → rotateCW → Deg0

Angles are persisted to NVS and synchronized to ESP3DValues (index orientation) so screens can react to rotation events via the observable system. See values.md for how ESP3DValues notifies observers.

setLanguage(code) stores the locale string in NVS and calls esp3dTranslationService.end() followed by esp3dTranslationService.begin() to reload the .lng binary pack. See translations.md for the full translation pipeline and binary format.


3.1 ThemeTokens — Semantic Color Vocabulary

Section titled “3.1 ThemeTokens — Semantic Color Vocabulary”

26 RGBA tokens stored in DRAM (104 bytes total, format 0xRRGGBBAA). The low byte is the LVGL opacity:

GroupTokensTypical use
Backgroundsbg_main, bg_panel, bg_overlay, bg_disabledScreen fills, panels, semi-transparent overlays, disabled controls
Texttext_primary, text_secondary, text_disabledValues, labels, hints on dark backgrounds
Iconsicon_idleCircular/virtual menu icon recolor in resting state
Bordersborder_idle, border_focusIdle borders; encoder/touch focus ring
Accent fillsaccent_select, accent_active, accent_alert, accent_actionSelected, running, error, back/undo states
Glowsglow_select, glow_active, glow_alert, glow_actionLVGL outline simulating neon halos (darker shade of matching accent)
Text on accenttext_on_select, text_on_active, text_on_alert, text_on_actionText/icons drawn on each accent background
State indicatorsindicator_success, indicator_info, indicator_error, indicator_warningConnection dots, status labels on dark background

Token access helpers (inline, zero overhead):

lv_color_t color = esp3d_token_lv_color(tokens.accent_select); // RGBA → lv_color_t (RGB)
lv_opa_t opa = esp3d_token_opa(tokens.accent_select); // RGBA → alpha byte
uint32_t rgb = esp3d_token_rgb(tokens.accent_select); // RGBA → 0xRRGGBB

For the authoritative per-theme token values, token naming conventions, and design rationale see docs/ui_resources/theme_palette.md.

DRAM - 104 bytes

Flash

esp3d_resources_get_blob(THEME_PALETTE_IDS[idx])

partition absent or no blob for this theme

ui_resources partition

blobs: theme 0..3

104 bytes tokens + 24 byte name

esp3d_theme_palettes.h

(compiled-in defaults)

400 bytes .rodata

ThemeTokens

current_theme_

applyTheme() attempts to load tokens from the partition blob first. If unavailable (old partition, absent partition, corrupted entry), it silently falls back to the compiled-in palette — the system remains fully functional. Contrast checks using BT.601 luminance are logged after loading to flag insufficient foreground/background separation.

Each partition palette blob may append an optional 24-byte display name after the 104-byte color block. loadThemeNames() reads these once at initialize(). If absent, getThemeName() returns "Theme 1" … "Theme 4".

ui_resources partition/esp3dtheme.iniesp3d_theme_colors_update_from_sd()Update Serviceui_resources partition/esp3dtheme.iniesp3d_theme_colors_update_from_sd()Update Service'keys: bg_main=0xRRGGBB, accent_select=...\nname=My Theme (optional)'loop[each section seen]call on ESP3DUpdateType::theme_colorsread current palette blobs → base valuesparse [theme1]..[theme4] sections via ESP3DConfigFileesp3d_resources_write_blob(THEME_PALETTE_IDS[i], tokens+name)return count written (0 = failure)reboot (mmap view is stale after write)

INI values accept either 0xRRGGBB (6 hex digits, alpha appended as 0xFF) or 0xRRGGBBAA (8 digits, explicit alpha). Partial sections and missing keys are legal — missing keys keep the current partition values. See docs/ui_resources/theme_palette.md and the esp3dtheme.ini.example preset files for the full ini schema.

initializeStyles() allocates ~40 lv_style_t structs in DRAM from the active ThemeTokens. All screens and components must use UIManager’s apply*Style() methods rather than lv_style_init() directly. This guarantees a single setTheme() call atomically refreshes all styles:

Yes

No

setTheme

persist theme index to NVS

applyTheme

Load ThemeTokens from partition blob or fallback

styles.initialized?

resetStyles - lv_style_reset() all ~40 styles

initializeStyles - lv_style_init() all from new tokens

lv_obj_invalidate active_screen

Style categories and their apply* API:

CategoryKey methods
ScreenapplyScreenBackgroundStyle, applyScreenContainerStyle, applyRotaryContainerStyle, applyFramedContainerStyle
ButtonsapplyButtonDefaultStyle(obj, radius), applyButtonSelectedStyle(obj, state), applyButtonDisabledStyle(obj)
LabelsapplyLabelStyle(obj, LabelFontSize::small/medium/big)
ContainersapplyTransparentContainerStyle, applyFramedContainerStyle
Status indicatorsapplyStatusIndicatorStyle, applyStatusClickableAreaStyle, applyStatusPressCircleStyle
Circular menuapplyCircularMenuContainerStyle, applyCircularMenuOuterCircleStyle, applyCircularMenuInnerCircleStyle, applyCircularMenuCenterRingStyle, applyCircularMenuCenterLabelStyle, applyCircularMenuOuterArcStyle, applyCircularMenuInnerArcStyle, applyCircularMenuClickZoneStyle, applyCircularMenuHandleStyle
Panel itemsapplyPanelItemStyle(obj, PanelItemStyleState::Default/Focused/Active)
List menuapplyListContainerStyle, applyListHeaderStyle, applyListNodeStyle(obj, selected), tagListNodeStatusColor, tagListNodeNonSelectable
Jog / ProbeapplyAxisButtonStyle(obj, active), applyPinLabelStyle(obj, inverted), applyProgressBarStyle, applyPotentiometerBarStyle
Status screenapplySpindleButtonStyle(obj, active), applyPinIndicatorStyle(obj, active), applyJobProgressBarStyle
Probe screenapplyParamButtonStyle(obj, focused)
Range editorapplyRangeArcStyle(obj) (arc track, value indicator, knob)
Generic / modalapplyTitleSectionStyle, applyMessageListStyle, applyContentBoxStyle, applyTextareaInputStyle, applyTextareaCursorStyle, applySpinnerStyle, applyMessageBoxStyle

Style sharing rule: Styles are shared objects — do not call lv_obj_remove_style_all() on a styled widget. Use lv_obj_remove_style(obj, &style, part) to remove a specific style while leaving others intact.

For the full per-screen style inventory, visual review checklist, and design decisions see docs/ui_resources/ui_style_guide.md.

3.6 applyListNodeStyle — Special Propagation Logic

Section titled “3.6 applyListNodeStyle — Special Propagation Logic”

applyListNodeStyle() handles a LVGL inheritance edge case: class styles block token inheritance through child labels. The method directly sets text_on_active (selected node) or text_primary (unselected) on child label objects. Two LVGL user flags gate per-node behavior:

FlagMeaning
LV_OBJ_FLAG_USER_1Node uses status color (e.g. indicator) — skip text recolor
LV_OBJ_FLAG_USER_2Node is non-selectable — skip selected styling

3.7 UI Sounds and Sound State During Operations

Section titled “3.7 UI Sounds and Sound State During Operations”

esp3d_ui.h maps the UI sound macros to semantic events of the sound module (main/modules/buzzer/, see sound_system.md); the tones themselves are defined by the sound module (compiled defaults, overridable from ui_resources / /esp3dsound.ini), not in the UI:

MacroEvent
ESP3D_SELECTION_BEEPESP3DSoundEvent::selection
ESP3D_MOVEMENT_BACKWARD_BEEP / _FORWARD_BEEPbackward / forward
ESP3D_ACTIVE_ITEM_BEEPactive_item
ESP3D_RESPONSE_NO_BEEPresponse_no
ESP3D_ERROR_BEEPerror
ESP3D_BACK_BEEPback
ESP3D_LOCK_BEEP / ESP3D_UNLOCK_BEEPlock / unlock
ESP3D_WAKEUP_SOUND / ESP3D_SUCCESS_SOUND / ESP3D_STARTING_SOUNDwakeup / success / startup

Without ESP3D_BUZZER_FEATURE the macros are empty. ESP3D_TICK_BEEP no longer exists.

ESP3D_SAVE_SOUND_STATE_AND_DISABLE / ESP3D_RESTORE_SOUND_STATE call esp3d_sound.muteAndSave() / esp3d_sound.restoreMute() to suppress sounds during silent operations. A counter (mute_count) allows nested calls; the saved enabled state is restored when it returns to 0:

ESP3D_SAVE_SOUND_STATE_AND_DISABLE // → esp3d_sound.muteAndSave()
// ... silent operation ...
ESP3D_RESTORE_SOUND_STATE // → esp3d_sound.restoreMute()

4. Resource System — esp3d_resources.cpp

Section titled “4. Resource System — esp3d_resources.cpp”

The resource system memory-maps the entire ui_resources flash partition and resolves images, fonts, and blobs into LVGL-ready descriptors at boot. See docs/ui_resources/development.md for the full binary format specification, partition table configuration, and build pipeline integration.

Offset 0
┌───────────────────────────────────────────┐
│ PartitionHeader (24 bytes) │
│ magic="ESP3", variant_key[12], │
│ num_images, num_fonts, data_section_off │
├───────────────────────────────────────────┤
│ ImageDirEntry[num_images] (20 B each) │
│ id, cf, w, h, stride, │
│ data_offset, data_size │
├───────────────────────────────────────────┤
│ FontDirEntry[num_fonts] (16 B each) │
│ id, data_offset, data_size, slot_size │
│ (theme palette blobs share this space) │
├───────────────────────────────────────────┤ ← data_section_offset
│ DATA SECTION │
│ ├─ Image pixel data (XIP read) │
│ ├─ Font blobs (header+bitmaps, XIP) │
│ └─ Theme palette blobs (104 B + name) │
└───────────────────────────────────────────┘

variant_key (12 bytes, null-padded ASCII) must match the RESOURCES_VARIANT compile-time constant. A mismatch is logged and all resources remain at their compiled-in fallbacks — the system stays functional.

id lookup

match

no match

ImageDirEntry

id, cf, w, h, stride

data_offset, data_size

Static ID→lv_image_dsc_t* map

built from ESP3D_IMAGES_LIST macro

lv_image_dsc_t

header.cf/w/h/stride

data → flash XIP pointer

g_fallback_img_dsc

compiled-in L8 grayscale

24×24 or 32×32

All image descriptors default to g_fallback_img_dsc (a resolution-matched L8 grayscale placeholder selected by RESOURCES_ICON_SIZE at compile time). Images found in the partition directory overwrite the default descriptor in-place.

Font glyph bitmaps stay in flash (read via XIP cache — zero DRAM copy). Only the small pointer-resolution structures live in DRAM:

DRAM (~150-250 bytes/font)

Flash (XIP)

load_font()

offset fixup, no pixel memcpy

dsc pointer

Font blob

FontBlobHeader (48 B)

glyph_bitmap[]

glyph_dsc[]

Esp3dFontShadow

cmaps[ESP3D_FONT_MAX_CMAPS]

kern union (pairs or classes)

dsc: lv_font_fmt_txt_dsc_t

lv_font_t

get_glyph_dsc → flash

get_glyph_bitmap → flash

ESP3D_FONT_MAX_CMAPS = 5 is tuned to the medium font’s 5 cmap ranges. load_font() validates every table offset against font_size before constructing any pointer — a corrupt blob with a valid magic is rejected rather than producing an out-of-bounds flash read at render time.

Two static_assert guards validate layout assumptions:

  • LV_FONT_FMT_TXT_LARGE == 0 — required for cmap/kern binary layout
  • sizeof(lv_font_fmt_txt_glyph_dsc_t) == 8 — required for glyph descriptor array stride

Theme palette blobs are stored in FontDirEntry slots, sharing the same directory and ID namespace as fonts. esp3d_resources_get_blob(id, &size) returns a const pointer directly into the mmapped partition — valid as long as the partition is mapped and no esp3d_resources_write_blob() call has invalidated it.

ResourceFallback when partition is unavailable
Image not in directoryg_fallback_img_dsc — build-resolution L8 grayscale placeholder
Font not in directorylv_font_montserrat_14 (built into LVGL)
Theme palette blob absentTHEME_PALETTES[] compiled-in FLASH defaults (400 bytes .rodata)
Partition absent or wrong variantAll images/fonts stay at fallback; system remains functional

Individual resource overrides can be applied without a full firmware reflash via files placed in /esp3dres/ on the SD card:

ui_resources partition/esp3dres/esp3d_resources_patch_sd()Update Serviceui_resources partition/esp3dres/esp3d_resources_patch_sd()Update Serviceloop[each *.bin (image) or *.fnt (font)]call on ESP3DUpdateType::resource_patchesopendir /esp3dresfread PatchImageHeader / PatchFontHeaderre-read partition directoryvalidate id / dimensions / slot sizesector R-M-E-W via patch_write_to_partition()rename to *.ok (success) or *.bad (failure)return count applied
  • *.bin image patch: must match cf, w, h, stride, and data_size of the existing directory entry.
  • *.fnt font patch: blob must fit within slot_size (padded slots allow smaller replacements). The data_size field in the directory is updated atomically via a separate sector write after the data is written.
  • Power loss between the data write and the rename leaves the file un-renamed; the patch is safely re-applied on the next boot.

See docs/guides/ui_resources_guide.md for the end-to-end SD-card update workflow.


When ESP3D_SNAPSHOT_FEATURE is enabled, snapshot_state_t tracks a live screen capture across multiple BSP flush callbacks:

FieldTypePurpose
fileFILE*Open output file receiving raw pixel rows
errorvolatile boolError flag set from the flush callback on write failure
expected_pixelsuint32_tTotal frame pixels for completion detection
captured_pixelsvolatile uint32_tCounter incremented per flush call
mutexSemaphoreHandle_tSerializes concurrent access to the state struct
initializedvolatile boolGuards the flush hook against uninitialized state access
ongoingvolatile boolActive-capture flag; guards the BSP flush hook path

The global instance g_snapshot is declared in esp3d_snapshot.h and consumed by both the BSP flush callback and esp3d_snapshot_deinit() in main/core/esp3d_lvgl.cpp.


6. Translation ID Map — esp3d_translations_id_map.h

Section titled “6. Translation ID Map — esp3d_translations_id_map.h”

Provides bidirectional lookup between the numeric file IDs used in .lng binary packs and the strongly-typed ESP3DLabel enum used throughout the UI source.

// Flash-stored lookup table (X-macro generated from .inc definition files)
static const ESP3DTranslationIdEntry ESP3DTranslationIdMap[];
// Bidirectional API (inline, linear scan):
ESP3DLabel label = getLabelFromFileId(file_id); // .lng numeric ID → enum
uint16_t id = getFileIdFromLabel(ESP3DLabel::btn_ok); // enum → .lng numeric ID

The table is stored in .rodata (flash) to avoid DRAM pressure. An unknown file ID returns ESP3DLabel::unknown_index; an unknown label returns UINT16_MAX.

See translations.md for the full .lng binary format and ESP3DTranslationService pipeline.


main.cpp

app_main

esp3dXui.begin()

tft_ui_task

(Core 1 pinned)

create_application()

ui_manager.initialize()

esp3d_resources_init()

spi_flash_mmap

ui_resources partition

Resolve images

→ lv_image_dsc_t[]

Load fonts

→ Esp3dFontShadow + lv_font_t

Retain font dir

for get_blob()

loadThemeNames()

esp3dXValues.initialize()

esp3dTranslationService.begin()

loadStateFromSettings()

NVS → orientation/theme/language/locks

applyTheme()

partition blob or fallback → ThemeTokens

initializeStyles()

ThemeTokens → ~40 lv_style_t

Splash screen

lv_timer_handler() first frame

notifyFirstFrameRendered()

LVGLui_resources partitionNVSUIManagerSettings ScreenLVGLui_resources partitionNVSUIManagerSettings Screenalt[Blob present][Partition absent or no blob]setTheme(Theme::Theme_2)writeByte(esp3d_theme_color, 2)applyTheme()esp3d_resources_get_blob(THEME_PALETTE_IDS[2])ThemeTokens (104 B) + optional nameuse THEME_PALETTES[2] compiled-in fallbacklog contrast sanity checks (BT.601 luminance)resetStyles() - lv_style_reset ~40 stylesinitializeStyles() - lv_style_init from new tokenslv_obj_invalidate(active_screen)

OK

Fail

/esp3dres/icon_home.bin

PatchImageHeader + pixels

esp3d_resources_patch_sd()

Validate

id / dims / slot_size

sector R-M-E-W

into partition

rename → icon_home.ok

rename → icon_home.bad


ModuleRelationship
Core_Platform_&_Infrastructure.mdUIManager reads/writes NVS via esp3dXsettings; notifies ESP3DValues on orientation change; uses esp3d_log macros throughout
Hardware_Peripheral_Drivers.mdESP3DXUi starts the RenderingClient; BSP lvgl_flush_cb / increase_lvgl_tick are provided by each board’s board_init.c
values.mdesp3dXValues.handle() is called inside the LVGL tick loop to propagate observable value changes to UI widgets without polling
translations.mdUIManager::setLanguage() restarts ESP3DTranslationService; ESP3DTranslationIdMap maps numeric IDs from .lng packs to ESP3DLabel enum values used in all UI source

RuleRationale
All lv_obj_* / lv_style_* calls must be inside _lock_acquire / _lock_release or within LVGL event/timer callbacksLVGL is strictly single-threaded on Core 1
Never destroy LVGL objects inside an event callbackUse lv_timer_create() cleanup timers — see docs/architecture/screen_transitions_flow.md
initializeStyles() / resetStyles() only from the LVGL task or before it startslv_style_* APIs are not thread-safe
Never call lv_obj_remove_style_all() on widgets styled via apply*Style()Styles are shared objects; removing all strips them from the shared pool, affecting all other widgets using the same style
esp3d_resources_init() is idempotent — safe to call multiple timesGuarded by g_img_base != nullptr check
After esp3d_resources_write_blob(), the mmap view is staleThe update service must reboot immediately after a successful write
All calloc/malloc in the resource and theme update paths must be checkedSystem operates on constrained heap (as low as 10 KB free in BT mode); any allocation failure must be logged with total free and largest block
LV_FONT_FMT_TXT_LARGE == 0 must holdRequired for the font blob binary layout assumed by load_font(); validated at build time via static_assert

DocumentContent
docs/architecture/screens_architecture.mdScreen base types, GenericScreen / CircularMenuScreen / ListMenuScreen inheritance and registration patterns
docs/architecture/screen_transitions_flow.mdTimer-based safe screen transition patterns; cleanup timer idiom for safe object destruction
docs/ui_resources/development.mdui_resources partition binary format (byte-map), esp3d_resources.h full API, generate_resources.py, SD-card update mechanisms
docs/ui_resources/theme_palette.mdAuthoritative 26-token reference; per-theme RGBA values; design conventions
docs/ui_resources/ui_style_guide.mdThemeStyles architecture; full apply* catalog; per-screen style inventory and visual review checklist
docs/guides/ui_resources_guide.mdHow to add/modify icons and fonts; regenerate ui_resources.bin; SD-card update and no-PC workflow
docs/guides/esp3d_log_guide.mdesp3d_log / esp3d_log_d macros and hook registration used by esp3d_system_message_hook_log_errors()
docs/guides/esp32_memory_constraints.mdHeap fragmentation guidance; why largest_free_block matters more than total free heap