Skip to content

tools_build_scripts_ui_resources

The UI Resources Build Scripts module is the complete toolchain that converts raw source assets (PNG icons, lv_font_conv .c files, theme palette INI files, language pack .lng files, the UI sound definitions INI) into the binary ui_resources flash partition consumed by the firmware at runtime. It also produces every derived artifact the firmware and end users depend on: stable-ID C headers, JSON manifests for downstream tools, SD-card update binaries, and self-contained distribution kits — all with no manual offset management.


  1. Overview
  2. Architecture
  3. Component Reference
  4. Partition Binary Format
  5. Resource Group System
  6. Resource ID System
  7. Board Override System
  8. Data Flow: Build Pipeline
  9. Data Flow: SD Update Mechanisms
  10. Partition Layout Algorithm
  11. Generated C Headers
  12. Usage Reference
  13. Dependencies and Relationships

The firmware renders its entire UI from assets stored in a dedicated ui_resources flash partition — icons, fonts, theme color palettes, language packs, and the UI sound definitions. This module contains all the Python tooling that:

  • Defines which assets exist and which firmware/transport combinations include them (resources_config.py).
  • Builds the binary partition at firmware compile time (generate_resources.py).
  • Rebuilds the partition from user-supplied PNG/font overrides without touching the repository (generate_user_resources.py, build_ui_resources_from_manifest.py).
  • Patches a single icon or font slot without reflashing the whole partition (generate_single_resource.py).
  • Packages a self-contained end-user kit from the build outputs (package_user_resources_kit.py).

The firmware resolves every resource by a stable 16-bit djb2 ID looked up in the partition’s own binary directory — no compiled-in offsets — so partition layouts can change between builds without requiring a firmware recompile.

See also:


Shared helper modules (same directory)

IMAGES / FONTS / active_groups()

IMAGES / FONTS / active_groups()

ui_resources_manifest_*.json

ui_resources_manifest_*.json

ui_resources_manifest_*.json

esp3d_ui_offsets.h

esp3d_theme_palettes.h

esp3d_lang_packs.h

esp3d_sound_defaults.h

ui_resources_*.bin

ui_resources.bin

*.bin / *.fnt

ui_resources.bin

defaults/ + scripts

resources_config.py

(asset registry)

generate_resources.py

(build-time generator)

generate_user_resources.py

(SD full-update builder)

generate_single_resource.py

(single slot patcher)

build_ui_resources_from_manifest.py

(standalone manifest builder)

package_user_resources_kit.py

(kit packager)

Firmware

(main/display/)

Flash partition

(ui_resources)

SD card root

(full update)

SD card /esp3dres/

(slot patch)

Distributable kit folder

lv_font_c_parser.py

font_blob.py

theme_palettes.py

lang_packs.py

sound_defs.py

LVGLImage.py

(LVGL component)


3.1 resources_config.py — Asset Registry

Section titled “3.1 resources_config.py — Asset Registry”

File: tools/build_scripts/resources_config.py

Purpose: Single source of truth for every image and font asset in the ui_resources partition. Defines which assets exist, where their sources live, what LVGL format they use, and which firmware/transport group controls their inclusion.

ImageConfig

@dataclass
class ImageConfig:
resource_subdir: str # subfolder under resources/<resolution>/
source_file: str # PNG filename
format_type: str # LVGL color format ('I4', 'RGB565', ...)
group: str # inclusion group (see §5)
output_name: Optional[str] = None # override output symbol name

FontConfig

@dataclass
class FontConfig:
source_file: str # .c filename (lv_font_conv output)
group: str # inclusion group
output_name: Optional[str] = None # override output symbol name

The .c file is parsed (not compiled) by lv_font_c_parser.py and re-serialized into a position-independent blob format by font_blob.py — no compiler toolchain dependency at build time.

ListCountGroups Covered
FONTS3shared
IMAGES~80shared, cnc, fluidnc, grblhal, grbl, wifi, bt, sound, light, emulation

Defined fonts:

FONTS = [
FontConfig("orbitron_10.c", "shared", output_name="small_font"),
FontConfig("orbitron_14.c", "shared", output_name="medium_font"),
FontConfig("orbitron_22.c", "shared", output_name="large_font"),
]

Image groups (sample):

GroupExample Images
grblhallogo_grblhal/logo.png, read_only_s.png
fluidnclogo_fluidnc/logo.png
grbllogo_grbl/logo.png
cncM3_b, M4_b, Alarm_s, Home_s, Jog_s, Hold_feed_b, Soft_reset_b, Status_query_b, …
sharedSettings_m, Files_m, back_b, Ok_b, Lock_b, Edit_b, Play_b, Stop_b, …
wifiWifi_s, No_wifi_s, Connect_telnet_b, scan_wifi_b, scan_server_b
btbluetooth_m, bluetooth_ble_m, Connect_bt_b, Disconnect_bt_b, scan_m (moved from shared in a14: only the BT cases of the settings screen use it)
soundsound_m, no_sound_m
lightlight_m
emulationArrow_up_b, Arrow_down_b, Snowflake_mini_b, Code_mini_b, …
GROUPS_ALWAYS = {"shared", "sound", "light", "emulation"}
GROUPS_BY_FW = {
"fluidnc": {"fluidnc", "cnc"},
"grblhal": {"grblhal", "cnc"},
"grbl": {"grbl", "cnc"},
}
GROUPS_BY_TRANSPORT = {
"wifi": {"wifi"},
"bt": {"bt"},
"bt_serial": {"bt"},
"bt_ble": {"bt"},
"serial": set(),
}
def active_groups(fw: str, transport: str) -> set:
groups = set(GROUPS_ALWAYS)
groups |= GROUPS_BY_FW.get(fw, set())
groups |= GROUPS_BY_TRANSPORT.get(transport, set())
return groups

3.2 generate_resources.py — Build-Time Partition Generator

Section titled “3.2 generate_resources.py — Build-Time Partition Generator”

File: tools/build_scripts/generate_resources.py

Purpose: Converts source assets into the complete ui_resources binary partition and all derived C headers at firmware compile time. Called by each board’s build_scripts/common.py during idf.py build.

FunctionDescription
main()CLI entry point; orchestrates all phases
parse_variant(variant)Parses "8mb_wifi_grblhal" → {mem, transport, fw}
convert_png_to_bin(png, fmt, out_dir)Invokes LVGLImage.py --ofmt BIN, returns raw bytes
parse_lvgl_bin_header(data)Reads the 12-byte LVGL 9.x image binary header
resolve_board_source(board_root, rel_path, default)Path resolution with board override priority
read_ui_resources_partition_size(csv_path)Reads partition budget from partitions_*.csv
djb2_id(name)Stable uint16_t ID from lowercase djb2 hash
make_variant_key(transport, fw)Collision-free 12-byte ASCII partition header key
check_id_collisions(names, kind)Aborts build if two names share a 16-bit djb2 ID
align4(offset)Rounds up to 4-byte boundary
symbol_name(name)"ok_b" → "OK_B" for C symbol generation

Parse CLI: --variant, --resolution

Load resources_config.py

Read partition size from CSV

Filter IMAGES and FONTS

by active_groups(fw, transport)

Phase 1: Convert PNGs

LVGLImage.py to .bin per image

Strip 12-byte LVGL header to pixel_data

Phase 2a: Serialize Fonts

lv_font_c_parser.py parses .c

font_blob.py produces FNT1 blob

Phase 2b: Build Theme Palettes

load_default_palettes(themes_default.ini)

Apply board overlay if present

build_palette_blob() x 4 themes

Phase 2c: Load Language Packs

load_lang_packs(resources/lang/)

Reserve 3 fixed slots always

Phase 2b': Build Sounds Blob

load_default_sounds(sounds_default.ini)

Apply board sounds/esp3dsound.ini if present

build_sound_blob()

Phase 3: Calculate Layout

Images: tight pack

Fonts: equal padded slots

Lang, palette, sounds: fixed reserved slots

96-byte build trailer at the end

Phase 4: Assemble Binary

ESP3 header + directory + data section

pad to partition size + build trailer

Write outputs:

ui_resources_*.bin

esp3d_ui_offsets.h to main/display/

esp3d_theme_palettes.h to main/display/

esp3d_lang_packs.h to main/display/

esp3d_sound_defaults.h to main/modules/buzzer/

ui_resources_manifest_*.json

Output FileDestinationConsumer
ui_resources_{mem}MB_{transport}_{fw}.binbuild/<variant>/Flash programmer
esp3d_ui_offsets.hbuild/<variant>/ + main/display/Firmware C++ (image/font IDs, X-macros)
esp3d_theme_palettes.hbuild/<variant>/ + main/display/Firmware C++ (fallback palette values)
esp3d_lang_packs.hbuild/<variant>/ + main/display/Firmware C++ (lang slot IDs)
esp3d_sound_defaults.hbuild/<variant>/ + main/modules/buzzer/Firmware C++ (sounds blob ID, compiled-in fallback sounds)
ui_resources_manifest_*.jsonbuild/<variant>/generate_single_resource.py, package_user_resources_kit.py
ModeConditionBehavior
Normal build--partition-csv provided (--build-name then required)Enforces partition size limit; fonts get equal padded slots absorbing all remaining space; the binary is padded to the full partition size and ends with the build trailer
Size estimation--partition-csv omittedNo size limit enforced; fonts packed tight; the trailer size is counted; prints tight-fit size and recommended size (+10%/8 KB, rounded to 4 KB)

The last 96 bytes of the partition binary identify the exact build it belongs to: 'ESPB' magic (4 bytes), build name (60 bytes, null-padded), firmware version (32 bytes, null-padded). The build name is the installer variant name passed with --build-name (each board’s build_scripts/common.py passes build_config_name(), for example ESP32_PIBOT_CNC_PENDANT_V1_8MB_serial_grbl). The version is ESP3D_X_VERSION read from main/core/includes/esp3d_version.h (read_fw_version()). The space is taken from the font slot slack, so nothing else in the layout moves. esp3d_ui_offsets.h gets #define RESOURCES_BUILD_NAME "<build name>".

The variant key in the header does not tell boards, resolutions or releases apart; the trailer does. On an SD-card update the firmware requires the file to be exactly the partition size, with a trailer whose name and version match RESOURCES_BUILD_NAME and ESP3D_X_VERSION; otherwise it refuses the update and renames the file ui_resources.bad (executeResourcesUpdate() in main/modules/update/esp3d_update_service.cpp). The layout is duplicated in build_ui_resources_from_manifest.py (standalone kit script): keep both in sync.

Terminal window
# Normal build with partition size enforcement
python tools/build_scripts/generate_resources.py \
--variant 8mb_wifi_grblhal \
--resolution res_480_320 \
--partition-csv boards/dlc32_max_lcd/partitions_8mb.csv \
--build-name DLC32_MAX_LCD_8MB_wifi_grblhal
# With board-level asset overrides
python tools/build_scripts/generate_resources.py \
--variant 4mb_bt_fluidnc \
--resolution res_320_240 \
--partition-csv boards/pibot_pendant_v1_0/partitions_4mb.csv \
--build-name <installer variant name> \
--board-resources boards/pibot_pendant_v1_0/resources \
--out build/4mb_bt_fluidnc/
# Estimate partition size for a new board (no CSV required)
python tools/build_scripts/generate_resources.py \
--variant 8mb_serial_fluidnc \
--resolution res_800_480
# List known variants
python tools/build_scripts/generate_resources.py --list-variants
# Dry run: show which images would be included without converting
python tools/build_scripts/generate_resources.py \
--variant 8mb_wifi_grblhal --resolution res_480_320 --dry-run

3.3 generate_user_resources.py — SD-Card Full Update Builder

Section titled “3.3 generate_user_resources.py — SD-Card Full Update Builder”

File: tools/build_scripts/generate_user_resources.py

Purpose: Generates a ui_resources.bin for SD-card delivery, with user-supplied PNG/font/theme overrides applied on top of the repo defaults. The output filename is exactly what the firmware scans for at the SD card root.

For each asset, the script looks for a match by filename in --overrides first; if not found, falls back to the repo’s resources/ tree. Font overrides accept either a replacement .c (lv_font_conv output) or a pre-serialized .fnt blob — the stem match is tried first (orbitron_14.fnt before orbitron_14.c).

filename match

no match

--overrides/

(user files)

resources/

(repo defaults)

Use override

Use repo default

Asset TypeOverride FormatLookup Key
Image.pngsource_file from ImageConfig
Font.fnt (pre-serialized blob)<stem>.fnt — tried first
Font.c (lv_font_conv)source_file from FontConfig
Themeesp3dtheme.iniFixed filename; partial overlay merged onto defaults
Language*.lngMatched by target slot number
Soundsesp3dsound.iniFixed filename; partial overlay merged onto resources/sounds/sounds_default.ini

Key Differences from generate_resources.py

Section titled “Key Differences from generate_resources.py”
Aspectgenerate_resources.pygenerate_user_resources.py
Source resolutionresources/ tree only--overrides/ first, then resources/
Partition sizeFrom --partition-csv (required)From --partition-csv (required)
Build name--build-name (required with --partition-csv)--build-name (required): the variant flashed on the device
Output nameui_resources_{mem}MB_{transport}_{fw}.binAlways ui_resources.bin
C headersWritten + copied to main/display/Not written
JSON manifestWrittenNot written
Board overrides--board-resources flagN/A (user’s --overrides serves this role)
def symbol_name(name: str) -> str:
return name.upper()

Converts an asset name like "ok_b" to "OK_B". The same helper exists in generate_resources.py; the copy here is used for verbose output labels during user builds.

Terminal window
# Rebuild stock binary for a variant (no overrides)
python tools/build_scripts/generate_user_resources.py \
--variant 8mb_wifi_grblhal \
--resolution res_480_320 \
--partition-csv boards/dlc32_max_lcd/partitions_8mb.csv \
--build-name DLC32_MAX_LCD_8MB_wifi_grblhal \
--out ui_resources.bin
# With custom icons and a custom font
python tools/build_scripts/generate_user_resources.py \
--variant 8mb_wifi_grblhal \
--resolution res_480_320 \
--partition-csv boards/dlc32_max_lcd/partitions_8mb.csv \
--overrides ~/my_icons/ \
--build-name DLC32_MAX_LCD_8MB_wifi_grblhal \
--out ui_resources.bin
# Dry run: show which files are overridden before converting
python tools/build_scripts/generate_user_resources.py \
--variant 8mb_wifi_grblhal --resolution res_480_320 \
--partition-csv boards/dlc32_max_lcd/partitions_8mb.csv \
--build-name DLC32_MAX_LCD_8MB_wifi_grblhal \
--overrides ~/my_icons/ --dry-run

3.4 generate_single_resource.py — Individual Slot Patcher

Section titled “3.4 generate_single_resource.py — Individual Slot Patcher”

File: tools/build_scripts/generate_single_resource.py

Purpose: Generates a single-resource patch file for the /esp3dres/ SD-card mechanism. No full partition reflash required — the firmware validates the patch against the partition directory and writes only the changed slot in-place.

CommandOutputSD Card PathFirmware Action
listConsole listing——
image<name>.bin with ESPI header/esp3dres/<name>.binValidate ID + size, write pixel data, rename .ok/.bad
font<name>.fnt with ESPF header/esp3dres/<name>.fntValidate ID + blob size vs slot_size, write blob, rename .ok/.bad

Image patch header (ESPI, 20 bytes, little-endian):

FieldTypeDescription
magic4sb'ESPI'
idu16djb2 hash of image name
cfu8LVGL color format byte
reservedu80
wu16Width in pixels
hu16Height in pixels
strideu16Row stride in bytes
reserved2u160
data_sizeu32Pixel data byte count

Font patch header (ESPF, 12 bytes, little-endian):

FieldTypeDescription
magic4sb'ESPF'
idu16djb2 hash of font name
reservedu160
data_sizeu32Font blob byte count

The manifest’s slot_size is the maximum blob the firmware will accept for an in-place write:

  • Manifest with slot_size: len(blob) <= slot_size — allows custom fonts of any size up to the reserved headroom.
  • Old manifest without slot_size: len(blob) == data_size exactly — no headroom was reserved in that partition build.
Terminal window
# List available resources in a manifest
python tools/build_scripts/generate_single_resource.py list \
--manifest build/8mb_wifi_grblhal/ui_resources_manifest_8MB_wifi_grblhal.json
# Generate an image patch
python tools/build_scripts/generate_single_resource.py image \
--name wifi_client \
--png ~/my_wifi.png \
--manifest build/8mb_wifi_grblhal/ui_resources_manifest_8MB_wifi_grblhal.json \
--out wifi_client.bin
# Generate a font patch from a lv_font_conv .c file
python tools/build_scripts/generate_single_resource.py font \
--name medium_font \
--font ~/orbitron_14_custom.c \
--manifest build/8mb_wifi_grblhal/ui_resources_manifest_8MB_wifi_grblhal.json
# Generate a font patch from a pre-serialized .fnt blob
python tools/build_scripts/generate_single_resource.py font \
--name medium_font \
--font ~/orbitron_14.fnt \
--manifest build/8mb_wifi_grblhal/ui_resources_manifest_8MB_wifi_grblhal.json

3.5 build_ui_resources_from_manifest.py — Standalone Manifest Builder

Section titled “3.5 build_ui_resources_from_manifest.py — Standalone Manifest Builder”

File: tools/build_scripts/build_ui_resources_from_manifest.py

Purpose: Repo-independent partition builder driven entirely by a JSON manifest produced at firmware build time. Does not import resources_config.py. Designed to be distributed as part of the ui_resources_kit/ — users need no repository clone.

  • No group logic: the manifest already encodes the fully resolved asset list for one specific build.
  • Sanity checks before write: PNG dimensions are verified against manifest expectations (requires Pillow); font bpp is validated before serialization. A mismatch aborts with a clear error.
  • Layout recomputed from actual blobs: same algorithm as generate_resources.py (tight image pack, equal padded font slots), so a no-overrides run produces a byte-for-byte identical partition.
  • Partial override: supply only the files you want to change; everything else falls through from --overrides to --defaults.

font: try .fnt stem first

not found

not found

image: direct filename

Manifest entry

(source_file)

--overrides/stem.fnt

(pre-serialized font blob)

--overrides/source_file

(replacement .c or .png)

--defaults/source_file

(kit defaults)

Terminal window
# Full rebuild with custom overrides
python build_ui_resources_from_manifest.py \
--manifest ui_resources_manifest_8MB_wifi_grblhal.json \
--defaults defaults/ \
--overrides my_custom_files/ \
--out ui_resources.bin
# Validate sources and print plan without writing
python build_ui_resources_from_manifest.py \
--manifest ui_resources_manifest_8MB_wifi_grblhal.json \
--defaults defaults/ \
--dry-run
# Point at an external LVGLImage.py when running outside the repo
python build_ui_resources_from_manifest.py \
--manifest ui_resources_manifest_8MB_wifi_grblhal.json \
--defaults defaults/ \
--lvgl-image-script /path/to/LVGLImage.py \
--out ui_resources.bin

3.6 package_user_resources_kit.py — Kit Packager

Section titled “3.6 package_user_resources_kit.py — Kit Packager”

File: tools/build_scripts/package_user_resources_kit.py

Purpose: Assembles a self-contained distribution folder from a completed build. Called automatically by build_mgr.py after generate_resources.py succeeds.

ui_resources_kit/
├── ui_resources_manifest_*.json <- from generate_resources.py
├── build_ui_resources_from_manifest.py <- standalone builder
├── generate_single_resource.py <- slot patcher
├── lv_font_c_parser.py <- font .c parser
├── font_blob.py <- font blob serializer
├── theme_palettes.py <- palette helpers
├── lang_packs.py <- language pack helpers
├── sound_defs.py <- sound definitions helpers
├── LVGLImage.py <- LVGL image converter
├── ttf_to_fnt.py <- TTF to .fnt converter
├── font_c_to_fnt.py <- .c to .fnt converter
├── image_c_to_bin.py <- .c to .bin converter
├── requirements_font_tools.txt <- deps for font converters
├── README.md <- built from docs/user documentation/ui_resources_customization.md
├── LICENSES/ <- GPL-3.0.txt, MIT-Feather.txt, OFL-1.1-Orbitron.txt, MIT-LVGL.txt
└── defaults/
├── ok_b.png, back_b.png, ... <- all default PNGs for this variant
├── orbitron_10.c, ... <- all default font .c files
├── themes_default.ini <- default (or board-baked) theme palettes
├── sounds_default.ini <- default (or board-baked) sounds
├── en.lng, ... <- default language packs (if any)
└── Orbitron-*.ttf, FontAwesome*.ttf <- source TTFs (discovered from .c Opts: comments)

The kit is written to installer/<variant>/ui_resources_kit/. Since a14:

  • defaults/ is emptied first, so a file left by a previous packaging (an image removed from the variant, a moved license) cannot ship.
  • The license texts of everything the kit redistributes are in LICENSES/ (the Orbitron OFL text moved there from defaults/).
  • The manifest must carry build_name and fw_version (written by generate_resources.py --build-name); otherwise packaging stops. build_ui_resources_from_manifest.py refuses such an old manifest too, since its output would be refused by the firmware.
  • The packager reads the build’s CMake OPTIONs from the installer/<variant>/features/<OPTION>.tag files written by cmake/postbuild.cmake. Without TFT_UI_SERVICE no kit is packaged (no screen resources).

build_readme() makes the README specific to the kit’s variant, from the user doc ui_resources_customization.md:

  • A header with the firmware variant and version, target CNC firmware, CNC connection, flash size, screen resolution, language packs and, when the feature tags exist, SD card slot / touch screen / buzzer (yes / no).
  • A warning that the kit only works for this variant and this firmware version (otherwise the file is renamed ui_resources.bad), and, without SD_CARD_SERVICE, that the pendant cannot apply the file by itself.
  • Markers in the user doc: <!-- kit:remove --> blocks are dropped; the <!-- kit:images --> block is replaced by this variant’s image and font tables (image descriptions are taken from the doc’s own table; board overrides are flagged); <!-- kit:requires OPTION --> blocks are kept only when that CMake OPTION is ON (all are kept when the features are unknown).
  • A credits and licenses section (Font Awesome, Feather, Orbitron, LVGL, ESP3D-X under GPL-3.0).

When the build used --board-resources, the board’s theme overlay is merged into defaults/themes_default.ini rather than shipping as a separate overlay layer, so users see the board’s shipped appearance as their baseline. The board’s sounds/esp3dsound.ini is baked into defaults/sounds_default.ini the same way; the kit’s own esp3dsound.ini slot (in overrides/) is left for the end user.

Yes

No

generate_resources.py ran

with --board-resources

Board themes/esp3dtheme.ini

present?

load_default_palettes + apply_overrides

format_theme_ini() writes baked

defaults/themes_default.ini

shutil.copy2 repo themes_default.ini

to defaults/themes_default.ini

End user's esp3dtheme.ini

= their customizations only

The packager inspects each default font .c file’s Opts: comment (via font_opts.py) to discover which source TTF/WOFF files were used, then copies those files from resources/fonts/ttf/ or LVGL’s built-in font directory into defaults/. This allows kit users to regenerate any font at a different size or merge in an icon font without resupplying files the kit already contains.

Terminal window
python tools/build_scripts/package_user_resources_kit.py \
--variant 8mb_wifi_grblhal \
--manifest build/8mb_wifi_grblhal/ui_resources_manifest_8MB_wifi_grblhal.json \
--out installer/8mb_wifi_grblhal/ui_resources_kit

The ui_resources partition uses a custom binary format read by esp3d_resources_init() at firmware boot. All integers are little-endian.

Offset 0:
[0..3] 'ESP3' magic (4 bytes)
[4..15] variant_key (12 bytes, ASCII null-padded)
Offset 16 — Directory Header (8 bytes):
[16..17] num_images (u16)
[18..19] num_fonts (u16) — includes fonts + lang slots + palettes + sounds
[20..23] data_section_offset (u32)
Offset 24 — Image Directory (N x 20 bytes each)
After images — Font Directory (M x 16 bytes each)
(covers font blobs, language slots, palette slots, and the sounds slot)
Padding to 4-byte boundary.
Offset data_section_offset — Data Section:
· Image pixel blobs (tightly packed, each 4-byte aligned)
· Font FNT1 blobs (equal padded slots)
· Language pack blobs (fixed LANG_SLOT_SIZE slots x 3)
· Theme palette blobs (fixed slots x 4 themes)
· Sounds blob (one fixed SOUNDS_SLOT_SIZE slot)
· zero padding up to partition_size - 96
Last 96 bytes — Build trailer (normal build):
'ESPB' magic (4) + build_name (60) + fw_version (32), null-padded
OffsetFieldTypeDescription
0idu16djb2 hash of the image name
2cfu8LVGL color format byte
3reservedu8—
4wu16Width in pixels
6hu16Height in pixels
8strideu16Row stride in bytes
10reserved2u16—
12data_offsetu32Offset from data_section_offset
16data_sizeu32Pixel data size in bytes

Struct format: <HBBHHHHII

Font/Palette/Language Directory Entry (16 bytes)

Section titled “Font/Palette/Language Directory Entry (16 bytes)”
OffsetFieldTypeDescription
0idu16djb2 hash of the slot name
2reservedu16—
4data_offsetu32Offset from data_section_offset
8data_sizeu32Actual blob size (must be <= slot_size)
12slot_sizeu32Total bytes reserved for this slot

Struct format: <HHIII

NameContentSize Policy
small_font, medium_font, large_fontFNT1 font blobEqual padded slots absorbing all space remaining after images, lang, and palettes
lang1, lang2, lang3Language pack blob or emptyFixed LANG_SLOT_SIZE each — always reserved even when no .lng source exists
theme1, theme2, theme3, theme4Theme palette blobFixed align4(PALETTE_BLOB_SIZE + THEME_NAME_SIZE) each
soundsSound definitions of every UI sound event (sound_defs.build_sound_blob())One fixed SOUNDS_SLOT_SIZE slot (1640 bytes), sized for SLOT_EVENTS_MAX = 24 events of up to MAX_NOTES = 16 notes, so events added by a later firmware fit without a repack

Groups determine which assets are bundled into a given firmware build variant. The variant string (<mem>mb_<transport>_<fw>) fully determines the active group set.

fluidnc

grblhal

grbl

wifi

bt / bt_serial / bt_ble

serial

Always included:

shared, sound, light, emulation

Firmware target

+ fluidnc + cnc

+ grblhal + cnc

+ grbl + cnc

Transport

+ wifi

+ bt

(no transport group added)

active_groups(fw, transport)

Final group set

→ filtered IMAGES + FONTS

GroupWhen IncludedNotes
sharedAlwaysGeneric UI elements (buttons, icons, navigation)
cncAny CNC firmwareCNC-specific status icons, jog, alarm, probe
fluidncfw == "fluidnc"FluidNC logo; FluidNC-specific icons
grblhalfw == "grblhal"grblHAL logo; read-only status icon
grblfw == "grbl"GRBL legacy logo
wifiTransport is wifiWiFi status, scan, and telnet connect icons
btTransport is any BT typeBluetooth classic and BLE icons
soundAlwaysBuilt unconditionally; firmware gates via #ifdef BUZZER_SERVICE
lightAlwaysBuilt unconditionally; firmware gates via #ifdef TFT_BRIGHTNESS_CONTROL
emulationAlwaysBuilt unconditionally; firmware gates via #ifdef EMULATE_INPUT_HARDWARE

Every image and font is identified at runtime by a stable 16-bit ID derived from its name using the djb2 hash. The firmware resolves resources by ID — partition positions and sizes are read from the directory at boot, not compiled in.

def djb2_id(name: str) -> int:
"""Stable uint16_t ID from lowercase djb2 hash of the asset name."""
h = 5381
for c in name.lower():
h = ((h << 5) + h) + ord(c)
return h & 0xFFFF

check_id_collisions() runs in generate_resources.py for images and for fonts/palettes/lang separately. Any collision aborts the build with an explicit error naming both colliding strings. Collision is avoided by renaming one asset.

The 12-byte variant key is embedded in the partition binary header and emitted as RESOURCES_VARIANT in esp3d_ui_offsets.h. The firmware checks this at boot to detect a partition built for a different firmware variant.

ConditionKey FormatExample
transport_fw fits in 12 bytesUsed verbatimwifi_grblhal
transport_fw longer than 12 bytesFirst 8 chars + 4 hex digits of djb2(full)bt_seria3f2a

The hash suffix prevents two full strings sharing the same 8-character prefix from receiving identical keys.


Individual boards can supply their own asset files (custom logo, board-specific icons, different font) without modifying the shared resources/ tree. The override path is implicitly resolution-scoped, so a logo.png override in the FluidNC subfolder does not affect the grblHAL subfolder.

resolution/subdir/file.png

(same relative path as repo)

fallback if not in board_root

fonts/resolution/font.c

fallback

themes/esp3dtheme.ini

(partial overlay)

boards/board/resources/

resources/resolution/

resources/fonts/resolution/

resolve_board_source():

board path wins if it exists

Font path resolution

apply_overrides():

merge partial ini onto base defaults

resources/themes/themes_default.ini

(base palette)

The rel_path passed to resolve_board_source() carries the resolution and subdir components:

<resolution>/<resource_subdir>/<source_file>
e.g. res_320_240/logo_fluidnc/logo.png

So boards/pibot_pendant_v1_0/resources/res_320_240/logo_fluidnc/logo.png overrides the FluidNC logo only at the 320×240 resolution and only for the FluidNC subdir. No other variant or resolution is affected.

Terminal window
--board-resources boards/<board>/resources

If the directory does not exist, the flag is silently ignored and the build continues with repo defaults (a warning is printed). The board’s build_scripts/common.py only passes the flag when the directory is confirmed to exist.


main/display/package_user_resources_kit.pylang_packs.pytheme_palettes.pylv_font_c_parser + font_blobLVGLImage.pyresources_config.pygenerate_resources.pybuild_mgr.pymain/display/package_user_resources_kit.pylang_packs.pytheme_palettes.pylv_font_c_parser + font_blobLVGLImage.pyresources_config.pygenerate_resources.pybuild_mgr.pyloop[Each PNG]loop[Each font .c]--variant 8mb_wifi_grblhal --resolution res_480_320 ...active_groups("grblhal", "wifi"){shared, cnc, grblhal, wifi, sound, light, emulation}Filter IMAGES + FONTS by groupsconvert_png_to_bin(png, "I4", tmp/)raw BIN bytes with 12-byte headerstrip header to get pixel_dataparse_font_c_file(text)parsed font objectbuild_font_blob(parsed)FNT1 bytesload_default_palettes(themes_default.ini)4 palette token dictsload_lang_packs(resources/lang/)3 lang slot blobs or emptycheck_id_collisions (images, then fonts+lang+palettes)Phase 3: calculate layoutPhase 4: assemble binaryui_resources_*.bin writtenesp3d_ui_offsets.h (copy)esp3d_theme_palettes.h (copy)esp3d_lang_packs.h (copy)ui_resources_manifest_*.json written--manifest ... --out installer/.../ui_resources_kitcopy default assets to kit/defaults/copy builder scripts to kit/ui_resources_kit/ assembled

Three independent update paths cover different scope and depth trade-offs. All are applied automatically by the firmware at boot — no user interaction beyond placing the file on the SD card.

Theme-only update

esp3dtheme.ini

(partial token overrides)

SD card root /

Firmware: merge tokens

into palette slots in partition

Single slot patch

generate_single_resource.py

image or font subcommand

*.bin or *.fnt patch file

SD card /esp3dres/

Firmware: validate ID and size

write slot in-place

rename .ok or .bad

Full partition update via kit (no repo needed)

build_ui_resources_from_manifest.py

--defaults kit/defaults/ --overrides ...

ui_resources.bin

SD card root /

Same firmware detection path

Full partition update via generate_user_resources.py

generate_user_resources.py

with --overrides folder

ui_resources.bin

SD card root /

Firmware: ESP3DUpdateService

check build trailer

flash entire partition then reboot

PathScopeRequires RepoBinary FormatValidation
generate_user_resources.pyEntire partitionYesFull partition binaryPartition size check; firmware checks the build trailer
build_ui_resources_from_manifest.pyEntire partitionNo (kit only)Full partition binaryDimension + bpp + size checks; firmware checks the build trailer
generate_single_resource.pySingle slotManifest onlyESPI/ESPF patch fileID + slot_size check
esp3dtheme.ini on SDPalette slots onlyNoINI text fileToken key validation
esp3dsound.ini on SDSounds slot onlyNoINI text fileEvent name and limit checks

The sounds update (/esp3dsound.ini) is described in sound_system.md and buzzer.md.


The layout algorithm is identical across generate_resources.py, generate_user_resources.py, and build_ui_resources_from_manifest.py. This guarantees that a no-overrides user build produces a byte-for-byte identical partition to the firmware build’s output.

Yes

No

data_pos = 0

Images: assign data_offset = data_pos

advance data_pos = align4(data_pos + pixel_data_size)

palette_total = N_palettes x align4(PALETTE_BLOB_SIZE + THEME_NAME_SIZE)

lang_total = LANG_SLOT_COUNT x LANG_SLOT_SIZE

Has fonts?

avail_for_fonts =

partition_size - data_section_offset

- data_pos - palette_total - lang_total

slot_size = (avail / n_fonts) rounded down to 4 bytes

Clamp: slot_size >= max(align4(each font blob size))

Fonts: assign data_offset = data_pos

advance data_pos += slot_size per font

Lang slots: assign data_offset = data_pos

advance data_pos += LANG_SLOT_SIZE per slot

Palette slots: assign data_offset = data_pos

advance data_pos += palette_slot_size per theme

ElementSizing StrategyReason
ImagesTight packSize is deterministic from pixel dimensions + format; override always matches
FontsEqual padded slots absorbing all remaining spaceA custom font blob rarely matches the original byte size; headroom allows in-place SD update without repack
Language slotsFixed LANG_SLOT_SIZE, reserved before font splitPrevents font slot-splitting from eating into space needed for language updates
Palette slotsFixed PALETTE_BLOB_SIZE + THEME_NAME_SIZEExtra bytes allow a later SD esp3dtheme.ini update to add a display name in-place without repack

Degenerate case: if the partition is so tight that slot_size < max(font_blob_sizes), the slot is clamped to the minimum needed and a TIGHT (no headroom) warning is printed. An SD font update that grows even 1 byte would then require a full partition repack via generate_user_resources.py or build_ui_resources_from_manifest.py.


Contains stable image/font IDs and X-macros. No raw offsets or sizes — those live in the partition directory and are read at boot by esp3d_resources_init(). Automatically copied from build/<variant>/ to main/display/.

// Variant key — firmware checks this against the partition header at boot.
#define RESOURCES_VARIANT "wifi_grblhal"
// Canonical icon size for this resolution (from generic_img dimensions).
#define RESOURCES_ICON_SIZE 48U
// Image IDs (stable djb2 hash — independent of partition layout).
#define IMG_ID_OK_B 0x3A2FU // ok_b 48x48 LV_COLOR_FORMAT_I4
#define IMG_ID_BACK_B 0x1C10U // back_b 48x48 LV_COLOR_FORMAT_I4
// ...
// X-macro — iterate all images:
// #define X(name, SYM, id) ...
// ESP3D_IMAGES_LIST
// #undef X
#define ESP3D_IMAGES_LIST \
X(ok_b, OK_B, IMG_ID_OK_B) \
X(back_b, BACK_B, IMG_ID_BACK_B) \
// ...
// Font IDs.
#define FONT_ID_SMALL_FONT 0x1B7EU
// ...
// X-macro — iterate all fonts:
#define ESP3D_FONTS_LIST \
XFONT(small_font, SMALL_FONT, FONT_ID_SMALL_FONT) \
// ...

Compiled-in fallback palette values used if the partition is absent or corrupt. Generated from the same themes_default.ini used to build the palette blobs — the two can never diverge.

#define ESP3D_THEME_PALETTE_COUNT 4U
#define ESP3D_THEME_TOKEN_COUNT 26U
#define ESP3D_THEME_PALETTE_ID_THEME1 0x...U
// ...
// Brace initializer for ThemeTokens[4] — field order matches TOKEN_ORDER.
#define ESP3D_THEME_PALETTES_FALLBACK_INIT { \
{ 0xRRGGBBAA, /* ... 26 tokens */ }, /* theme1 */ \
/* theme2, theme3, theme4 ... */ \
}

Written to build/<variant>/ and copied to main/modules/buzzer/. Generated from the same resources/sounds/sounds_default.ini as the partition blob, so the compiled-in fallback sounds and the blob cannot diverge. Slots are emitted in sound_defs.EVENT_ORDER, which must match the ESP3DSoundEvent enum order.

#define ESP3D_SOUND_BLOB_ID 0x...U
#define ESP3D_SOUND_EVENT_COUNT <N>U
#define ESP3D_SOUND_SLOT_EVENTS_MAX 24U
// ESP3DSoundSlot[ESP3D_SOUND_EVENT_COUNT], in ESP3DSoundEvent order:
// { note_count, reserved, { {freq_hz, duration_ms}, ... } }
#define ESP3D_SOUND_DEFAULTS_INIT { ... }

djb2 IDs for the three language slot positions in the partition’s font directory.

#define ESP3D_LANG_SLOT_COUNT 3U
#define ESP3D_LANG_SLOT_SIZE <N>U
#define ESP3D_LANG_SLOT_ID_LANG1 0x...U
#define ESP3D_LANG_SLOT_ID_LANG2 0x...U
#define ESP3D_LANG_SLOT_ID_LANG3 0x...U
#define ESP3D_LANG_SLOT_IDS_INIT { \
ESP3D_LANG_SLOT_ID_LANG1, \
ESP3D_LANG_SLOT_ID_LANG2, \
ESP3D_LANG_SLOT_ID_LANG3 \
}

Normal Firmware Build (automatic via build_mgr.py)

Section titled “Normal Firmware Build (automatic via build_mgr.py)”
idf.py build
└─ board's build_scripts/common.py
├─ generate_resources.py -> partition binary + C headers + manifest
└─ package_user_resources_kit.py -> distributable kit folder

Customize Icons/Fonts via SD Card (repo required)

Section titled “Customize Icons/Fonts via SD Card (repo required)”
Terminal window
# Place replacement PNGs (same filename) in ~/my_icons/
python tools/build_scripts/generate_user_resources.py \
--variant 8mb_wifi_grblhal \
--resolution res_480_320 \
--partition-csv boards/dlc32_max_lcd/partitions_8mb.csv \
--overrides ~/my_icons/ \
--build-name DLC32_MAX_LCD_8MB_wifi_grblhal \
--out ui_resources.bin
# Copy ui_resources.bin to SD card root, insert, power on.

Customize via the Distributable Kit (no repo needed)

Section titled “Customize via the Distributable Kit (no repo needed)”
Terminal window
# Unzip the ui_resources_kit for your board variant.
# Place replacement files in overrides/
python build_ui_resources_from_manifest.py \
--manifest ui_resources_manifest_8MB_wifi_grblhal.json \
--defaults defaults/ \
--overrides overrides/ \
--out ui_resources.bin
# Copy ui_resources.bin to SD card root, insert, power on.
Terminal window
python tools/build_scripts/generate_single_resource.py image \
--name wifi_client \
--png ~/my_wifi.png \
--manifest build/8mb_wifi_grblhal/ui_resources_manifest_8MB_wifi_grblhal.json
# Copy wifi_client.bin to SD card /esp3dres/ — firmware patches slot on next boot.
Terminal window
python tools/build_scripts/generate_resources.py \
--variant 8mb_wifi_grblhal \
--resolution res_800_480
# Output: "Recommended size: NNNN bytes (0xNNNN) — use in partitions_*.csv"

Internal Dependencies (same tools/build_scripts/ directory)

Section titled “Internal Dependencies (same tools/build_scripts/ directory)”
ModuleUsed ByPurpose
lv_font_c_parser.pygenerate_resources.py, generate_user_resources.py, build_ui_resources_from_manifest.py, generate_single_resource.pyParse lv_font_conv .c output into a structured font object
font_blob.pySame as aboveSerialize parsed font into a position-independent FNT1 binary blob
theme_palettes.pygenerate_resources.py, generate_user_resources.py, build_ui_resources_from_manifest.py, package_user_resources_kit.pyLoad, overlay, serialize, and format theme color palettes
lang_packs.pygenerate_resources.py, generate_user_resources.py, build_ui_resources_from_manifest.pyLoad and serialize language pack .lng files; define LANG_SLOT_COUNT / LANG_SLOT_SIZE
sound_defs.pygenerate_resources.py, generate_user_resources.py, build_ui_resources_from_manifest.py, package_user_resources_kit.pyLoad sounds_default.ini, apply a partial esp3dsound.ini, check limits, build the sounds blob; defines EVENT_ORDER, MAX_NOTES, SLOT_EVENTS_MAX, SOUNDS_SLOT_SIZE
font_opts.pypackage_user_resources_kit.pyParse Opts: comments in .c files to discover source TTF filenames for kit bundling
build_mgr.py—Calls generate_resources.py and package_user_resources_kit.py as build steps
ToolSourceRequired For
LVGLImage.pycomponents/lvgl/scripts/PNG to LVGL BIN conversion (all generators)
Pillow (Python)pip install PillowPNG dimension validation in build_ui_resources_from_manifest.py and generate_single_resource.py
partitions_*.csvboards/<board>/Partition size budget read by all partition generators

The C headers emitted by this module are #included by the UI resource loader. The partition binary is mapped by esp3d_resources_init() at boot using the ID-based directory.

emits

emits

emits

writes

#include

#include

#include

SPI flash read at boot

ID-based directory lookup

lv_img_dsc_t, lv_font_t,

theme tokens, lang strings

generate_resources.py

esp3d_ui_offsets.h

esp3d_theme_palettes.h

esp3d_lang_packs.h

ui_resources flash partition

esp3d_resources.cpp

(main/display/)

UIManager and Screens

(main/display/)

Related modules: