Skip to content

UI Studio — Developer Customization Tool

UI Studio — Developer Customization Tool

Section titled “UI Studio — Developer Customization Tool”

tools/ui_studio/ is a local Flask web application that gives developers and end-users a browser-based GUI to customize the three kinds of UI resources baked into the pendant’s ui_resources flash partition: theme colors, images, and fonts.

It runs entirely on the developer’s machine — no cloud, no device connection needed. After editing, it produces the artefacts consumed by the UI resources pipeline:

Output artefactWhere it goes
esp3dtheme.iniSD card root → applied at boot; or saved into the kit for full rebuild
<name>.binSD card for individual image patch
<name>.fntSD card for individual font patch
ui_resources.binFlash via USB (full rebuild including all customizations)

Related documentation


Generated Artefacts

Developer's Machine

Flask Server (app.py + kit_api.py)

Browser (localhost:5000)

POST /api/colors/import

POST /api/export

GET|POST|DELETE /api/kit/override

POST /api/kit/generate_one

POST /api/kit/convert_font

POST /api/kit/generate_one

GET /api/kit/resources

POST /api/kit/generate_all

export

generate_one

generate_one

generate_all

(subprocess)

convert_font

(subprocess)

Colors Tab

(main.js)

Images Tab

(images.js)

Fonts Tab

(fonts.js)

Kit Bar

(kit.js - shared)

app.py

Colors API + page rendering

kit_api.py Blueprint

/api/kit/*

repo/resources/

(TTF fonts for preview)

docs/ui_resources/theme-*.ini

(default palettes)

tools/ui_studio/standalone/

(esp3d_ui_simulator.html)

ui_resources_kit/

(user-provided)

kit/custom/

(staged overrides)

kit/defaults/

(original assets)

kit/ui_resources_manifest_*.json

kit/*.py

(bundled scripts)

esp3dtheme.ini

name.bin (image patch)

name.fnt (font patch)

ui_resources.bin (full rebuild)


tools/ui_studio/
├── app.py ← Flask entry point + Colors tab backend
├── kit_api.py ← Blueprint: Images & Fonts tab backend (/api/kit/*)
├── templates/
│ └── index.html ← Jinja2 template: three-tab layout
├── static/
│ └── js/
│ ├── main.js ← Colors tab: token editing, import/export, live preview
│ ├── kit.js ← Shared: kit path selection, "Generate all" button
│ ├── images.js ← Images tab: grid rendering, override upload, patch gen
│ └── fonts.js ← Fonts tab: grid rendering, TTF conversion, patch gen
└── standalone/
└── esp3d_ui_simulator.html ← Embedded live preview iframe (Colors tab)

app.py — Main Application + Colors Backend

Section titled “app.py — Main Application + Colors Backend”

The Flask application root. Handles:

  1. Page rendering (GET /) — Reads docs/ui_resources/theme-1.ini as the default palette, passes the 26 token definitions and their default values into the Jinja2 template.

  2. Static asset serving for the live preview:

    • GET /preview/<filename> → tools/ui_studio/standalone/ (hosts esp3d_ui_simulator.html)
    • GET /resources/<filename> → repo/resources/ (the repository’s TTF assets referenced by the simulator)
  3. Colors API:

    • POST /api/colors/import — Accepts an uploaded esp3dtheme.ini, parses it with configparser, returns the token dict and detected theme number as JSON.
    • POST /api/export — Accepts a JSON body { tokens: {...}, theme: 1..4 }, validates all 26 tokens, normalizes each to 0xRRGGBBAA format, and streams back an esp3dtheme.ini file attachment.
  4. Jinja2 filters: csshex (→ #rrggbb) and alphapct (→ 0-100) for template use.

The 26 semantic tokens are organized into 8 named groups matching esp3dtheme.ini.example:

26 Semantic Color Tokens

Backgrounds (4)

bg_main · bg_panel · bg_overlay · bg_disabled

Text (3)

text_primary · text_secondary · text_disabled

Icons (1)

icon_idle

Borders (2)

border_idle · border_focus

Accent BG (4)

accent_select · accent_active · accent_alert · accent_action

Text-on-accent (4)

text_on_select · text_on_active · text_on_alert · text_on_action

Glows (4)

glow_select · glow_active · glow_alert · glow_action

State indicators (4)

indicator_success · indicator_info · indicator_error · indicator_warning

See theme_palette.md for the full token reference with per-theme values and design conventions.


kit_api.py — Images & Fonts Kit Blueprint

Section titled “kit_api.py — Images & Fonts Kit Blueprint”

Registered at /api/kit. Operates on a user-provided ui_resources_kit/ folder (the standalone kit shipped alongside each firmware release — see development.md for the kit packaging process).

The kit folder contract:

PathRole
ui_resources_manifest_*.jsonExactly one manifest (image + font list, dimensions, slot sizes)
defaults/Flat directory of default PNG images and lv_font_conv .c files
custom/Created by the tool; staged overrides (PNG, .c, .fnt, esp3dtheme.ini)
generate_single_resource.pyBundled kit script: single resource → patch blob
build_ui_resources_from_manifest.pyBundled kit script: full ui_resources.bin rebuild
ttf_to_fnt.pyBundled kit script: TTF/OTF/WOFF → .fnt blob

Design principle: kit_api.py is a thin orchestration wrapper. It stages files and shells out to the kit’s own bundled scripts for all conversion and validation logic. It never duplicates binary-format or font-rendering logic — the kit scripts fail loudly on any mismatch, which is surfaced to the user verbatim.

HelperPurpose
_resolve_kit(path_str)Validate kit folder, locate unique manifest, return (kit_dir, manifest, manifest_path)
_entries(manifest, kind)Return manifest["images"] or manifest["fonts"]
_font_fit(entry, blob_size)Compute patch-vs-slot size feedback: patchable bool + byte sizes
_parse_font_opts(kit_dir, source_file)Parse the Opts: comment in a default .c file to recover original size/bpp/sources
_extra_bundled_fonts(defaults_dir, used)Discover additional font files in defaults/ not already used by a font entry (e.g. icon fonts)
_font_override_paths(custom_dir, source_file)Return [stem.fnt, source_file] — the .fnt wins when both exist
_remember_kit_path(kit_dir)Persist last kit path to .work/last_kit_path.txt
_script_error(result)Extract ERROR lines from subprocess output for readable error messages

Defines the shared singleton kitState = { path, manifest } and the kitListeners subscriber list used by both images.js and fonts.js.

fonts.jsimages.js/api/kit/*kit.js (initKitBar)Userfonts.jsimages.js/api/kit/*kit.js (initKitBar)UserEnter/browse kit path → "Load"GET /api/kit/resources?path=…{ variant, fw, transport, resolution, images[], fonts[] }Update kitState.manifestnotifyKitChange() → renderImageGrid()notifyKitChange() → renderFontGrid()"Generate all"POST /api/kit/generate_all { path }subprocess: build_ui_resources_from_manifest.pyui_resources.bin (octet-stream)Browser download

Startup behavior: On page load, initKitBar() calls GET /api/kit/last. If a remembered path exists it auto-loads silently (errors are logged, not alerted — the remembered path may have been deleted or moved since last session).


Manages the 26-token color editor. Three synchronized controls per token:

[color picker] ↔ [alpha % input] ↔ [hex input (0xRRGGBBAA)]
↑ source of truth on export
FunctionPurpose
iniHexToCssHex(iniHex)0xRRGGBB[AA] → #rrggbb for <input type=color>
cssHexToIniHex(cssHex)#rrggbb → 0xRRGGBB (RGB part only)
alphaPercentFromIniHex(iniHex)Extract alpha byte as 0–100%
setHexAlphaPercent(hexInput, percent)Overwrite alpha byte in a hex input
buildIniHex(rgb6, alpha)Combine RGB + alpha byte → 0xRRGGBBAA
parseIniHex(iniHex)Parse to { rgb, alpha } — tolerates missing alpha (treats as FF)

An <iframe> embeds esp3d_ui_simulator.html from tools/ui_studio/standalone/. Token changes are pushed into the iframe via postMessage:

previewFrame.contentWindow.postMessage({ type: "esp3d-preview-update", tokens }, "*");

The iframe’s script listens for this message and redraws the simulated pendant UI using the supplied palette — no page reload required.

The “Save to kit” button (save-theme-kit-btn) calls POST /api/kit/theme_ini to stage the current palette as custom/esp3dtheme.ini inside the loaded kit folder. This file is then picked up automatically by “Generate all” and baked into ui_resources.bin.

Multiple themes can be accumulated: each POST merges the new [themeN] section into the existing file, so themes 1–4 can each be customized in separate editing sessions.


Renders a card grid, one card per image entry in kitState.manifest.images.

Each card shows:

  • A thumbnail loaded from GET /api/kit/default_file?kind=image&source_file=…
  • Image name, dimensions, and format
  • A file upload input (.png only)
  • Override status (Default / Custom)
  • “Generate patch (.bin)” — invokes POST /api/kit/generate_one and triggers a browser download of the resulting .bin patch file

On upload, the preview thumbnail is immediately replaced using URL.createObjectURL() so the user sees the change before generating the patch.


Renders a card grid, one card per font entry in kitState.manifest.fonts. More complex than images because fonts can be supplied in three forms:

  1. lv_font_conv .c replacement — upload the raw C file produced by lv_font_conv
  2. Pre-serialized .fnt blob — upload a .fnt file from ttf_to_fnt.py / font_c_to_fnt.py
  3. Convert from TTF/WOFF/OTF in-browser — fill in the “Convert from TTF/WOFF…” panel and click Convert & stage

For patch-via-SD, the generated .fnt blob must fit inside the slot reserved in the already-flashed partition. If it doesn’t fit, the patch cannot be applied via SD card — only a full ui_resources.bin rebuild (via “Generate all”) will work, as it recomputes slot sizes. The fontFitLabel() function surfaces this distinction visually:

orbitron_14 — 18.3 KB / 24.0 KB slot (SD patch OK)
orbitron_14 — 26.1 KB exceeds the 24.0 KB slot: use Generate all (full rebuild)

Pre-filled from the original generation parameters (Opts: comment in the default .c), the panel exposes:

  • Size (px) — free to adjust; the tool will report blob size vs. slot after conversion
  • bpp — 1 / 2 / 4 / 8
  • Up to 2 source rows, each with:
    • A “Keep the bundled <font>” checkbox (for fonts already shipping in defaults/)
    • A file upload for a replacement TTF/OTF/WOFF
    • A Unicode ranges field (e.g. 32-126,176,192-253)

An optional second row is offered for any additional font files discovered in defaults/ (e.g. an icon font), enabling users to merge symbol glyphs into any font.


Import .ini

Edit token

Export .ini

Save to kit

Open UI Studio

Load theme-1.ini defaults

from docs/ui_resources/

Render 26 token rows

color picker + alpha + hex

User action

POST /api/colors/import

parse configparser

Populate all token inputs

Sync color picker + alpha% + hex

postMessage to preview iframe

POST /api/export

normalize to 0xRRGGBBAA

Download esp3dtheme.ini

POST /api/kit/theme_ini

merge into custom/esp3dtheme.ini

Palette staged for Generate all

Upload .png

Generate patch

Clear override

Select kit folder

GET /api/kit/resources

parse manifest JSON

Render image grid

load default thumbnails

User action

POST /api/kit/override

saved to kit/custom/source_file.png

Update thumbnail preview

status: Custom

POST /api/kit/generate_one

subprocess: generate_single_resource.py image

Download name.bin

DELETE /api/kit/override

delete kit/custom/source_file.png

status: Default

Upload .c or .fnt

Convert from TTF

Generate patch

Clear override

Select kit folder

GET /api/kit/resources

parse font Opts: comments

Render font grid

pre-fill size/bpp/ranges

User action

POST /api/kit/override

check slot fit if .fnt

status: Custom + slot fit label

POST /api/kit/convert_font

subprocess: ttf_to_fnt.py

Saved as custom/stem.fnt

Return slot fit feedback + log tail

POST /api/kit/generate_one

subprocess: generate_single_resource.py font

Download name.fnt

DELETE /api/kit/override

delete .fnt and/or .c

status: Default

Yes

No

Yes

No

All customizations staged in kit/custom/

POST /api/kit/generate_all

custom/ dir

non-empty?

subprocess: build_ui_resources_from_manifest.py

--manifest ...

--defaults kit/defaults/

--overrides kit/custom/

subprocess: build_ui_resources_from_manifest.py

--manifest ...

--defaults kit/defaults/

Exit code 0

and output exists?

Download ui_resources.bin

Return error: script stdout/stderr


MethodPathRequestResponse
GET/—HTML page (index.html with token defaults)
GET/preview/<filename>—Static file from standalone/
GET/resources/<filename>—Static file from repo resources/
POST/api/colors/importmultipart file (.ini){ tokens: {…}, theme: 1..4 }
POST/api/export{ tokens: {…}, theme: 1..4 }esp3dtheme.ini attachment

Kit Endpoints (kit_api.py — prefix /api/kit)

Section titled “Kit Endpoints (kit_api.py — prefix /api/kit)”
MethodPathRequestResponse
GET/last—{ path: string|null }
POST/browse—{ path: string|null } (native OS folder picker)
GET/resources?path=…—{ variant, fw, transport, resolution, images[], fonts[] }
GET/default_file?path=…&kind=…&source_file=…—Raw file (PNG or .c text)
POST/overridemultipart path, kind, source_file, file{ ok, fit? }
DELETE/override?path=…&source_file=…—{ ok }
POST/generate_one{ path, kind, name }.bin or .fnt attachment
POST/convert_fontmultipart path, source_file, size, bpp, file0/1, ranges0/1, default_font0/1{ ok, stored_as, fit, log[] }
GET/theme_ini?path=…—{ sections: ["themeN", …] }
POST/theme_ini{ path, theme: 1..4, tokens: {…} }{ ok, sections }
DELETE/theme_ini?path=…—{ ok }
POST/generate_all{ path }ui_resources.bin attachment

All color values use the 0xRRGGBBAA format defined in theme_palette.md:

0xRRGGBBAA
^^^^^^^^
||||||||
||||++++── Alpha: last byte = LVGL opacity (0x00 = transparent, 0xFF = opaque)
++++────── RGB: standard 6-digit hex

The tool’s color inputs (<input type=color>) only expose the RGB portion. A separate alpha percentage slider controls the AA byte. The hex input accepts the full 0xRRGGBBAA string and is the single source of truth used on export.

Conversion chain (bidirectional):

<input type=color> "#3ca0ff"
↕ cssHexToIniHex / iniHexToCssHex
hex input "0x3CA0FFCC"
↕ alphaPercentFromIniHex / setHexAlphaPercent
alpha input "80" (percent)

Individual SD patches are applied to an already-flashed ui_resources.bin whose slot sizes are fixed. A font .fnt blob that exceeds the slot reserved for it cannot be applied as a patch — but it is still valid for a full rebuild (where slots are recomputed from actual sizes).

Yes

No

slot_size = 0

Generated .fnt blob

blob_size le slot_size?

SD patch OK

Copy name.fnt to SD card root

Too large for SD patch

Use Generate all → flash ui_resources.bin

Pre-slot manifest:

exact match required

The slot_size field comes from the manifest JSON. A value of 0 means the kit was built before slot-based patchability was introduced; in that case, the blob must exactly match the stored data_size.


Terminal window
# From the repo root
cd tools/ui_studio
python app.py
# Serving on http://127.0.0.1:5000

Dependencies: Flask (server), tkinter (optional — native OS folder picker for the Images/Fonts kit bar).

For font conversion via the Fonts tab, the kit’s requirements_font_tools.txt dependencies must also be installed in this Python environment (freetype-py, fonttools). No Node.js or lv_font_conv is needed.


shells out to kit copies of

shells out to kit copies of

reads defaults from

produces artefacts consumed by

packages scripts into kit/

packages scripts into kit/

tools_ui_studio

(this module)

tools_build_scripts

(generate_resources.py,

build_ui_resources_from_manifest.py,

resources_config.py)

tools_resource_generation

(ttf_to_fnt.py, font_c_to_fnt.py,

image_c_to_bin.py)

docs/ui_resources/

development.md - binary format

theme_palette.md - token reference

ui_style_guide.md - LVGL usage

UI_Framework_Screens

(esp3d_resources.cpp - partition reader

esp3d_ui.cpp - ThemeStyles/ThemeColors)

The scripts bundled inside each ui_resources_kit/ are copies (packaged at kit-build time by package_user_resources_kit.py in tools_build_scripts) of the repo’s tools/build_scripts/ and tools/fonts|images/ scripts. UI Studio always calls the kit’s own copies, never the repo’s originals, so the tool remains self-contained and works without a full repo clone on the end-user’s machine.


DecisionRationale
Local Flask server (not a desktop app)Works on any OS without a native GUI framework; the browser provides a rich UI with no extra desktop dependencies
Kit-centric for Images/FontsThe kit resolves all variant-specific paths and includes all scripts; UI Studio needs no knowledge of board configs or build systems
custom/ lives inside the user’s kit dirCustomizations persist across sessions and are visible/manageable outside the tool; no hidden app state
Subprocess for all conversionAvoids duplicating binary-format or font-rendering logic; errors from kit scripts are surfaced verbatim
Colors tab reads repo docs/ui_resources/ directlyThe repo defaults are canonical; no duplication in the tool itself
postMessage for live previewThe iframe loads the same simulator HTML used for design validation; token changes are pushed without a page reload
.fnt override wins over .cConsistent with build_ui_resources_from_manifest.py; a pre-serialized blob skips re-parsing the C source, matching the flashing pipeline’s own precedence rule
theme_ini merges sectionsLets the user customize themes 1–4 across separate editing sessions into a single custom/esp3dtheme.ini that “Generate all” will pick up