Skip to content

tools_build_scripts_utilities

The tools_build_scripts_utilities module is a focused collection of three standalone build-time helper scripts that address specific, orthogonal concerns in the firmware release pipeline. They sit alongside—but are independent of—the larger build management system (see tools_build_scripts_build_management.md) and the UI resource pipeline (see tools_build_scripts_ui_resources.md).

ScriptPurpose
gen_dlc32_split_sdkconfig.pyDerives per-transport sdkconfig variants for the DLC32 MAX LCD board from a master BT/WiFi config
gen_ota_initial.pyGenerates the 8 KB otadata binary that tells the ESP-IDF bootloader to boot ota_0 on first power-up
png_to_lvgl_c.pyConverts PNG icon files into LVGL v9 C image sources (.c) for the statically-compiled icon sets

All three scripts are pure Python 3, require no firmware build environment, and can be invoked directly from the command line or called from CI pipelines and board-level build_scripts/ wrappers (see Board_Support_Packages.md).


Utility Scripts

Build & Development Tools

reads/writes

writes

reads

calls

produces

shared with

used by

used by

compiled into

tools_build_scripts_build_management

(build_mgr.py, size_report.py, …)

tools_build_scripts_ui_resources

(generate_resources.py, resources_config.py, …)

tools_build_scripts_utilities

← this module

gen_dlc32_split_sdkconfig.py

gen_ota_initial.py

png_to_lvgl_c.py

boards/dlc32_max_lcd/sdkconfig.*

ota_data_initial_*.bin

resources_config.py

(IMAGES list)

components/lvgl/scripts/LVGLImage.py

main/display/cnc/fw/res/images/*.c

main/display/res/images/*.c

Flash / OTA deployment

Firmware binary


png_to_lvgl_c.py

resize_png()

main()

convert_one()

batch_cnc_images()

batch_shared_images()

_convert_dir()

find_source_png()

load_image_configs()

gen_ota_initial.py

crc32()

main()

generate_ota_initial()

make_ota_entry()

gen_dlc32_split_sdkconfig.py

parse()

transform()

set_or_unset()

remove_keys()

BLE_ONLY_KEYS (constant set)

CLASSIC_ONLY_KEYS (constant set)


Location: tools/build_scripts/gen_dlc32_split_sdkconfig.py

The DLC32 MAX LCD board supports three distinct radio transport modes — Classic Bluetooth Serial, BT BLE, and plain Serial/WiFi. Each mode requires a different ESP-IDF sdkconfig because the Bluetooth controller stack options are mutually exclusive and carry a significant RAM cost. Maintaining three separate hand-edited sdkconfig files would cause drift; this script derives the bt_serial, bt_ble, and serial variants programmatically from a single authoritative source file.

Source fileMode argumentOutput file
sdkconfig.8mb.btbt_serialsdkconfig.8mb.bt_serial
sdkconfig.8mb.btbt_blesdkconfig.8mb.bt_ble
sdkconfig.8mb.wifiserialsdkconfig.8mb.serial

All paths are relative to boards/dlc32_max_lcd/.

bt_serial

bt_ble

serial

Source sdkconfig

(parse into lines + keys dict)

mode?

Disable BLE keys

Set controller: BR/EDR only

Remove BLE_ONLY_KEYS lines

Disable Classic BT keys

Set controller: BLE only

Remove CLASSIC_ONLY_KEYS lines

Disable BT entirely

Disable WiFi

Disable coexistence stack

Write output sdkconfig

parse(path: Path) → (lines, keys) Reads the sdkconfig file into a list of raw lines and a dictionary mapping each CONFIG_* key to its (state, value, line_index) tuple. Recognises both KEY=value and # CONFIG_KEY is not set forms.

transform(source, mode, out) Top-level entry point. Calls parse, applies mode-specific mutations via set_or_unset and remove_keys, then writes the result to out.

set_or_unset(lines, keys, key, enable) Patches a single line in-place: sets it to KEY=y when enable=True, or # KEY is not set when False. No-ops if the key is absent — safe for config options that may not exist in older IDF versions.

remove_keys(lines, keys, key_set) Returns a filtered line list with the lines corresponding to key_set members removed. Used to strip keys that are entirely meaningless for the selected controller mode (e.g. CONFIG_BT_GATTS_ENABLE when Classic BT is off).

ConstantContent
BLE_ONLY_KEYS~50 CONFIG_BT_* / CONFIG_BTDM_BLE_* / CONFIG_GATTS_* keys that only apply when BLE is active
CLASSIC_ONLY_KEYS~40 CONFIG_BT_SPP_* / CONFIG_BT_A2DP_* / CONFIG_BTDM_CTRL_BR_EDR_* keys that only apply when Classic BT is active
Terminal window
# Regenerate all three dlc32_max_lcd variants from their sources
python tools/build_scripts/gen_dlc32_split_sdkconfig.py

Running the script directly executes the three canonical transforms shown in the table above (the if __name__ == "__main__" block at the bottom of the file).


Location: tools/build_scripts/gen_ota_initial.py

When a board has no factory partition (the common case in this project’s partition tables), the ESP-IDF bootloader must find a valid otadata sector to know which application slot to boot. This script synthesises a minimal, correct 8 KB otadata binary that points to ota_0 (app0). Without this file the bootloader falls back to an undefined state.

The ESP-IDF otadata region is two 4 KB sectors, each holding one esp_ota_select_entry_t (32 bytes, little-endian):

Offset Size Field Notes
------ ---- ----------- -----------------------------------------------
0 4 ota_seq Sequence number. 1 → ota_0 (odd = slot 0)
4 20 seq_label Unused, filled 0xFF
24 4 ota_state OTA_IMG_VALID = 0xFFFFFFFF
28 4 crc CRC32 of bytes [0:28], Ethernet polynomial

Only sector 1 (offset 0) receives a valid entry (seq=1); sector 2 is left as 0xFF.

File Systemcrc32()make_ota_entry()generate_ota_initial()main()Command LineFile Systemcrc32()make_ota_entry()generate_ota_initial()main()Command Linealt[template provided and exists][generate fresh]--output path [--template path]shutil.copyfile(template, output)generate_ota_initial(output_path)make_ota_entry(seq=1)crc32(entry[0:28])uint32 checksum32-byte entry bytesBuild 4096-byte sector1 (entry at offset 0)Build 4096-byte sector2 (all 0xFF)write sector1 + sector2 (8192 bytes)

crc32(data: bytes) → int Implements the standard Ethernet CRC32 (polynomial 0xEDB88320) used by ESP-IDF’s OTA integrity check. Pure Python; no external dependencies.

make_ota_entry(seq: int) → bytes Builds the 32-byte esp_ota_select_entry_t struct. Sets ota_state to 0xFFFFFFFF (OTA_IMG_VALID) and appends the CRC of the first 28 bytes.

generate_ota_initial(output_path: str) Assembles the two 4 KB sectors and writes the resulting 8192-byte binary. Creates missing parent directories automatically.

main() CLI entry point. Accepts --output/-o (required) and an optional --template/-t path. The template path is useful when a board vendor supplies a pre-validated otadata image that should be used verbatim instead of a generated one.

Terminal window
# Generate a fresh otadata binary
python tools/build_scripts/gen_ota_initial.py \
--output build/ota_data_initial_8MB.bin
# Use a board-specific template if present, fall back to generation otherwise
python tools/build_scripts/gen_ota_initial.py \
--output build/ota_data_initial_8MB.bin \
--template boards/esp32_3248s035r/ota_data_template.bin
seq valueBoot slot
Odd (1, 3, …)ota_0 / app0
Even (2, 4, …)ota_1 / app1

Setting seq=1 always boots ota_0, which is the expected default after a factory flash.


Location: tools/build_scripts/png_to_lvgl_c.py

The firmware embeds two distinct sets of icon images:

  • Static (compiled-in) icons — small icon sets baked directly into the firmware image, stored under main/display/cnc/<fw>/<resolution>/images/ and main/display/<resolution>/images/. These are referenced from C++ source files as extern const lv_image_dsc_t icon_name;.
  • Dynamic (partition) resources — large sets managed by the ui_resources partition pipeline (generate_resources.py / resources_config.py).

This script handles only the static icon sets. It is a thin wrapper around the official LVGL tool components/lvgl/scripts/LVGLImage.py, adding resolution-aware batch conversion and optional PIL-based resizing.

⚠️ Do not use this script to add icons to the ui_resources partition. Use generate_resources.py / resources_config.py for that (see tools_build_scripts_ui_resources.md).

main()

--png + --out-dir

(single file)

--batch-cnc-images

+ --from-resolution

+ --to-resolution

--batch-shared-images

+ --from-resolution

+ --to-resolution

convert_one()

batch_cnc_images()

batch_shared_images()

_convert_dir()

find_source_png()

load_image_configs()

(resources_config.IMAGES)

resize_png() - Pillow

(only if --scale != 1.0)

LVGLImage.py

(subprocess)

main/display/
├── <to_resolution>/images/ ← --batch-shared-images target
│ ├── icon_a.c
│ ├── icon_b.c
│ └── conditionals/<group>/
│ └── icon_c.c
└── cnc/
├── <to_resolution>/images/ ← shared CNC icons
│ └── alarm_off_b.c
├── fluidnc/<to_resolution>/images/
├── grbl/<to_resolution>/images/
└── grblhal/<to_resolution>/images/

convert_one(png_path, out_dir, cf, scale) → Path Core conversion step. Optionally resizes the PNG in a temporary directory (via resize_png), then calls LVGLImage.py in a subprocess with --ofmt C --cf <cf>. Returns the path of the produced .c file. Exits with an error if the LVGL script is not found or the conversion fails.

resize_png(src, dst, scale) Opens the source PNG as RGBA, scales it by scale using Image.LANCZOS, and writes the result to dst. Requires Pillow.

load_image_configs() → list[ImageConfig] Imports resources_config.IMAGES from the sibling resources_config.py. Returns the image configuration list shared with the ui_resources pipeline. Used solely for PNG source path resolution — it does not write to the partition.

find_source_png(stem, from_resolution, image_configs) → Path | None Maps a .c file’s stem (e.g. alarm_off_b) to its original PNG under resources/<from_resolution>/ by matching against the output_name or source_file stem in image_configs. Case-insensitive. Returns None (with a warning) if no match is found.

batch_cnc_images(from_resolution, to_resolution, scale) Iterates over all *.c files under the CNC image directories for the source resolution, resolves their PNG sources, and regenerates them for the target resolution. The logo stem is skipped (it is a fixed-size splash image handled separately).

batch_shared_images(from_resolution, to_resolution, scale) Same as batch_cnc_images but operates on the root-level main/display/<resolution>/images/ tree, including conditionals/<group>/ subfolders (recursive scan).

_convert_dir(src_dir, dst_dir, from_resolution, image_configs, scale, skip_stems, recursive) Internal helper. Enumerates .c files in src_dir (recursively if recursive=True), resolves the PNG source for each, and calls convert_one. Preserves sub-folder structure relative to src_dir.

Terminal window
# Single file, no resize
python tools/build_scripts/png_to_lvgl_c.py \
--png resources/res_320_240/Ok_b/ok_b.png \
--out-dir /tmp/out
# Single file, scale 2x before conversion
python tools/build_scripts/png_to_lvgl_c.py \
--png resources/res_320_240/Ok_b/ok_b.png \
--out-dir /tmp/out \
--scale 2
# Batch: regenerate CNC static icons for a new resolution
python tools/build_scripts/png_to_lvgl_c.py \
--batch-cnc-images \
--from-resolution res_320_240 \
--to-resolution res_800_480 \
--scale 2
# Batch: regenerate shared (non-CNC) static icons
python tools/build_scripts/png_to_lvgl_c.py \
--batch-shared-images \
--from-resolution res_320_240 \
--to-resolution res_800_480 \
--scale 2
DependencyRequired forNotes
Pillow (PIL)--scale != 1.0Not needed for plain conversion at native size
components/lvgl/scripts/LVGLImage.pyAll conversionsMust be present in the cloned repo
resources_config.pyBatch modesRead-only; shares IMAGES list with the UI resource pipeline

reads/writes

writes

imports

subprocess

produces

tools_build_scripts_utilities

gen_dlc32_split_sdkconfig.py

gen_ota_initial.py

png_to_lvgl_c.py

boards/dlc32_max_lcd/sdkconfig.*

Board_Support_Packages

(build_scripts/build_one.py)

tools_build_scripts_build_management

ota_data_initial_*.bin

resources_config.py

(tools_build_scripts_ui_resources)

components/lvgl/scripts/LVGLImage.py

main/display/**/*.c

(UI_Framework_and_Screens)


boards/dlc32_max_lcd/gen_dlc32_split_sdkconfig.pyDeveloper / CIboards/dlc32_max_lcd/gen_dlc32_split_sdkconfig.pyDeveloper / CIDisable BLE, set BR/EDR only,remove BLE_ONLY_KEYSDisable Classic BT, set BLE only,remove CLASSIC_ONLY_KEYSDisable BT + WiFi,disable coexistence stackpython gen_dlc32_split_sdkconfig.pyparse(sdkconfig.8mb.bt)lines[], keys{}transform(..., "bt_serial", ...)write sdkconfig.8mb.bt_serialtransform(..., "bt_ble", ...)write sdkconfig.8mb.bt_bleparse(sdkconfig.8mb.wifi)lines[], keys{}transform(..., "serial", ...)write sdkconfig.8mb.serial

yes, file exists

no / file missing

gen_ota_initial.py --output path

--template

provided?

shutil.copyfile(template, output)

generate_ota_initial(output)

Allocate 4096-byte sector1 (0xFF)

make_ota_entry(seq=1)

crc32(entry[0:28])

pack entry into sector1[0:32]

Allocate 4096-byte sector2 (0xFF)

write sector1 + sector2 to 8192 bytes

Output: ota_data_initial_*.bin

stem in skip_stems

not skipped

not found

found

yes

no

done

png_to_lvgl_c.py --batch-cnc-images

load_image_configs()

(resources_config.IMAGES)

Enumerate *.c in

main/display/cnc/from_res/...

Next .c file

Skip

find_source_png(stem, from_res, configs)

Log warning, skip

scale != 1.0?

resize_png() via Pillow

into temp dir

Use original PNG

subprocess: LVGLImage.py

--ofmt C --cf I4

.c file written to

main/display/cnc/to_res/...

All icons converted


These utilities are invoked at specific, infrequent points in the overall build lifecycle:

Preparation (permaster-configchange)Developer or CIrungen_dlc32_split_sdkconfig.pyAsset Generation(per new resolution)Developerrun png_to_lvgl_c.py--batch-cnc-imagesPer-Variant Buildbuild_mgr.pyinvokes boardbuild_scripts/build_one.pybuild_one.py callsidf.py buildFlash Bundle Assembly (per variant)build_mgr.py / CIrungen_ota_initial.py--outputota_data_initial.binesptoolflashes firmware +otadata binaryFirmware Build Lifecycle - Utility Script Touch Points

gen_dlc32_split_sdkconfig.py and png_to_lvgl_c.py are infrequently run (only when their authoritative sources change). gen_ota_initial.py is run per build variant as part of assembling the flash bundle, driven by build_mgr.py (see tools_build_scripts_build_management.md).


DocumentRelationship
tools_build_scripts_build_management.mdOrchestrates multi-variant builds; consumes the OTA binary from gen_ota_initial.py and the sdkconfigs from gen_dlc32_split_sdkconfig.py
tools_build_scripts_ui_resources.mdManages the ui_resources partition pipeline; shares resources_config.IMAGES read by png_to_lvgl_c.py
Board_Support_Packages.mdEach board’s build_scripts/ wrappers trigger the build chain that depends on these utility outputs
dlc32_max_lcd.mdDLC32 MAX LCD board — the only board that uses gen_dlc32_split_sdkconfig.py
UI_Framework_&_Screens.mdConsumes the static .c icon files produced by png_to_lvgl_c.py