Skip to content

pibot_pendant_v1_0_build

Build system scripts for the PiBot CNC Pendant v1.0 board. This module is the single authoritative place for defining which firmware configurations exist for this board, how they are compiled, and how all output artifacts are assembled into an installer-ready directory. It wraps ESP-IDF’s idf.py with board-specific logic: variant flag resolution, UI-resource generation, factory-artifact injection, user-kit packaging, and flash-map generation.


  1. Module Location and File Map
  2. Architecture Overview
  3. Variant System
  4. Build Pipeline
  5. Factory Artifact Management
  6. Installer Output Structure
  7. Partition Layout Reference
  8. Board Configuration Constants
  9. Tool Integration
  10. Usage Reference
  11. Component Relationships

boards/pibot_pendant_v1_0/
├── build_scripts/
│ ├── build_one.py ← CLI entry point for single-variant builds
│ ├── common.py ← Build orchestration: pipeline, helpers, naming
│ └── variants.py ← All variant definitions (flags + paths)
├── board_config.cmake ← CMake board profile (IDF_TARGET, sdkconfig selection, hw flags)
├── flash_params.json ← Chip/flash protocol parameters for flash_mgr
├── partitions_4mb.csv ← Flash partition table for 4 MB builds
├── partitions_8mb.csv ← Flash partition table for 8 MB builds
├── sdkconfig.<mem>.<radio> ← Per-radio sdkconfig defaults (applied once at first configure)
├── resources/ ← Board-specific UI resource overrides (boot logo, etc.)
├── Factory/ ← Factory test app (see pibot_pendant_v1_0_factory_app)
└── components/bsp/ ← Board support package (see pibot_pendant_v1_0_bsp)

The build module is a pure Python layer that sits between the developer and ESP-IDF. It never compiles code directly — instead it drives idf.py with the correct flags, coordinates pre- and post-build steps, and ensures all output lands in the right installer directory.

installer//

Repo Tools

ESP-IDF 5.4.3

pibot_pendant_v1_0_build module

variant name

--clean / --check

config dict

cmake flags + paths

--variant --resolution

--partition-csv --board-resources

idf.py -B build_dir + cmake flags

idf.py -B build_dir

--variant --manifest

--variant-dir --generate

copy factory artifacts

Developer / build_mgr.py

build_one.py

main()

common.py

build_variant()

variants.py

VARIANTS / FACTORY_VARIANTS

idf.py build

idf.py reconfigure

idf.py size

generate_resources.py

UI resources partition

package_user_resources_kit.py

standalone kit

flash_mgr.py

flash map JSON

.bin / .elf

firmware

bootloader_*.bin

partitions_*.bin

factory artifacts

ui_resources_*.bin

ui_resources_kit/

config_name.json

flash map

installer_history.log


Every variant is a Python dictionary with four keys:

KeyTypeDescription
namestrUnique variant identifier (also the CLI argument)
cmakelist[str]Flat list of -D flag=value arguments passed to idf.py
cwdstrWorking directory for idf.py (REPO_ROOT for regular, FACTORY_DIR for factory)
build_dirstrAbsolute path to the isolated build directory

3.2 Flag Composition — make_variant_args()

Section titled “3.2 Flag Composition — make_variant_args()”

variants.py defines ROOT_CMAKE_OPTIONS: a master list of every CMake option the project recognises. DEFAULT_OFF_CMAKE_ARGS is generated from this list with every flag set to OFF. make_variant_args(*on_flags) copies that baseline and enables only the listed flags:

# Result is a clean, deterministic cmake arg list with no stale options
make_variant_args("ESP32_PIBOT_CNC_PENDANT_V1=ON", "MEMORY_8_MB=ON", "WIFI_SERVICE=ON", ...)
# → [..., "-D", "ESP32_PIBOT_CNC_PENDANT_V1=ON", ..., "-D", "MEMORY_8_MB=ON",
# ..., "-D", "WIFI_SERVICE=ON", ...]

This “deny-all, allow-selected” pattern prevents stale flags from a previous build leaking into a different variant’s configuration.

Factory

factory_4mb

factory_8mb

8 MB Flash - grblHAL

8mb_wifi_grblhal

+ MDNS + SocketClient

8mb_serial_grblhal

8mb_bt_serial_grblhal

8mb_bt_ble_grblhal

8 MB Flash - grbl

8mb_serial_grbl

8mb_bt_serial_grbl

8mb_bt_ble_grbl

8 MB Flash - FluidNC

8mb_wifi_fluidnc

WiFi + MDNS + SocketClient

8mb_serial_fluidnc

Serial only

8mb_bt_serial_fluidnc

8mb_bt_ble_fluidnc

4 MB Flash

4mb_wifi_fluidnc

WiFi + MDNS + SocketClient

4mb_serial_fluidnc

Serial only

4mb_bt_serial_fluidnc

Bluetooth SPP

4mb_bt_ble_fluidnc

Bluetooth BLE

Flag4MB WiFi4MB Serial4MB BT-SPP4MB BT-BLE8MB WiFi8MB Serial8MB BT
SERIAL_SERVICE✓✓✓✓✓✓✓
UART_EXT_SERVICE✓✓✓✓✓✓✓
TFT_UI_SERVICE✓✓✓✓✓✓✓
TFT_TOUCH_SERVICE✓✓✓✓✓✓✓
SD_CARD_SERVICE✓✓✓✓✓✓✓
BUZZER_SERVICE✓✓✓✓✓✓✓
UPDATE_SERVICE✓✓✓✓✓✓✓
FACTORY_SERVICE✓✓✓✓✓✓✓
WIFI_SERVICE✓———✓——
MDNS_SERVICE✓———✓¹——
SOCKET_CLIENT_SERVICE✓———✓¹——
BT_SERVICE——✓✓——✓
BT_SERIAL_SERVICE——✓———✓²
BT_BLE_SERVICE———✓——✓²

¹ WiFi variants use SOCKET_CLIENT_SERVICE (CNC over TCP) + MDNS_SERVICE. This matches the WiFi-as-CNC-transport model described in feature_resource_matrix.md.
² BT_SERIAL_SERVICE and BT_BLE_SERVICE are mutually exclusive — never both ON.

The installer output directory name is derived by build_config_name():

ESP32_PIBOT_CNC_PENDANT_V1_{MEMORY}{radio}_{firmware}
ComponentValues
{MEMORY}4MB, 8MB
{radio}_wifi, _serial, _bt_serial, _bt_ble, _bt
{firmware}fluidnc, grblhal, grbl

Examples:

ESP32_PIBOT_CNC_PENDANT_V1_8MB_wifi_fluidnc
ESP32_PIBOT_CNC_PENDANT_V1_4MB_bt_serial_fluidnc
ESP32_PIBOT_CNC_PENDANT_V1_8MB_serial_grblhal

The resource variant string (used by generate_resources.py) uses a shorter form:

{N}mb_{transport}_{firmware} e.g. 8mb_wifi_fluidnc

Factory variants produce no resource variant string — they carry no ui_resources partition.


4.1 Full Sequence — build_variant(config)

Section titled “4.1 Full Sequence — build_variant(config)”

Yes

No

fail

ok

fail

ok

No - factory variant

Yes

fail

ok

build_variant called

--clean flag?

clean_build_dir

clean installer_dir

clean parent build if factory

return 0

prepare_build_dir

_ensure_clean_build_dir: clear if IDF_TARGET changed

os.makedirs

write .idf_target marker

generate_resources

generate_resources.py

--variant Nmb_transport_fw

--resolution res_320_240

--partition-csv partitions_Nmb.csv

--out build_dir

--board-resources if overrides exist

return non-zero

run_cmake_build

idf.py -B build_dir

cmake flags

-D PROD_BUILD=ON

IDF_TARGET=esp32

CMAKE_BUILD_PARALLEL_LEVEL=N

return non-zero

_show_size_report

idf.py -B build_dir size

cwd == REPO_ROOT?

regular variant?

return 0

copy_factory_artifacts

Locate ESP3D-FACTORY_4MB or _8MB

Auto-build factory variant if missing

Copy bootloader + partition bins

Append FACTORY lines to installer_history.log

return 1

Verify bootloader_mem.bin present

print its size

_copy_ui_resources_bin

Copy ui_resources_*.bin

build_dir to installer_dir

_package_user_resources_kit

package_user_resources_kit.py

--variant --manifest --out

non-fatal on failure

_log_firmware_artifacts

Append FIRMWARE section

to installer_history.log

generate_flash_map

flash_mgr.py

--variant-dir installer_dir

--generate

--flash-params flash_params.json

return 0

4.2 Check-Only Path — check_variant(config)

Section titled “4.2 Check-Only Path — check_variant(config)”

Used by build_one.py --check and build_mgr.py --check_*. Runs only the CMake configure step — no compilation — so it validates flag legality and sanity_check.cmake rules quickly:

idf.py -B build_dir [cmake flags] -D PROD_BUILD=ON reconfigure

4.3 IDF Target Guard — prepare_build_dir()

Section titled “4.3 IDF Target Guard — prepare_build_dir()”

Each build directory stores a .idf_target marker file. On every invocation, _ensure_clean_build_dir() reads this file and wipes the directory if the stored value differs from EXPECTED_IDF_TARGET ("esp32"). This prevents cross-target contamination when the same repo builds both esp32 and esp32s3 boards.

board_config.cmake selects the correct sdkconfig defaults file at CMake configure time. The selection applies only once when build/<variant>/sdkconfig does not yet exist, preserving subsequent menuconfig changes:

4MB

8MB

BT_SERIAL only

BT_BLE only

both BT flags

WiFi

none

Memory?

_SDK_MEM = 4mb

_SDK_MEM = 8mb

Radio?

sdkconfig.Nmb.bt_serial

sdkconfig.Nmb.bt_ble

sdkconfig.Nmb.bt

sdkconfig.Nmb.wifi

sdkconfig.Nmb.serial

Parallel compilation is controlled via the CMAKE_BUILD_PARALLEL_LEVEL environment variable (idf.py has no -j CLI option). Pass --jobs=N through build_mgr.py:

Terminal window
python tools/build_scripts/build_mgr.py --build_board pibot_pendant_v1_0 --jobs=8

common.py extracts --jobs=N from sys.argv and injects the value into the subprocess environment.


Regular firmware variants depend on factory-app artifacts (custom bootloader binary, partition-table binaries) produced by the factory build. These are managed by copy_factory_artifacts().

installer/<config_name>/build_variant(factory_4/8mb)Factory/installer/ESP3D-FACTORY_4or8MB/copy_factory_artifacts()build_variant()installer/<config_name>/build_variant(factory_4/8mb)Factory/installer/ESP3D-FACTORY_4or8MB/copy_factory_artifacts()build_variant()alt[Artifacts present][Missing or empty directory]cmake_args_factory_artifacts_complete()\ndir exists AND bootloader_*.bin present?copy bootloader_*.bin, partitions_*.bin, otadata_*.binappend FACTORY entries to installer_history.log_build_missing_factory()produce artifactscopy artifacts

Completeness check: _factory_artifacts_complete() verifies that at least one bootloader_*.bin is present inside the directory. CMake’s file(MAKE_DIRECTORY) can leave an empty directory behind on a failed factory build — directory existence alone is not sufficient.

The factory installer’s size_report.txt is not copied to avoid overwriting the regular variant’s own size report.

For details on the factory app itself, see pibot_pendant_v1_0_factory_app.md.
For the custom bootloader hooks, see pibot_pendant_v1_0_bootloader.md.


After a successful regular-variant build, the installer directory is self-contained for flashing:

installer/
└── ESP32_PIBOT_CNC_PENDANT_V1_8MB_wifi_fluidnc/
├── bootloader_8MB.bin ← FACTORY
├── partitions_8MB.bin ← FACTORY
├── otadata_initial_8MB.bin ← FACTORY
├── ESP32_PIBOT_CNC_PENDANT_V1_8MB_wifi_fluidnc.bin ← FIRMWARE — main app
├── ESP32_PIBOT_CNC_PENDANT_V1_8MB_wifi_fluidnc.elf ← FIRMWARE — debug symbols
├── ui_resources_8mb_wifi_fluidnc.bin ← FIRMWARE — UI partition image
├── ESP32_PIBOT_CNC_PENDANT_V1_8MB_wifi_fluidnc.json ← flash map (addresses + files)
├── installer_history.log ← timestamped audit log
├── size_report.txt ← ROM/RAM usage summary
└── ui_resources_kit/
├── README.md ← end-user customisation guide
├── build_ui_resources_from_manifest.py ← standalone builder script
├── ui_resources_manifest_8mb_wifi_fluidnc.json ← source file list + offsets
└── res_320_240/ ← default PNG/font sources

installer_history.log tags each file as FACTORY or FIRMWARE on every build run, creating a timestamped audit trail useful for diagnosing partial or mismatched builds.

copy_factory_artifacts

cmake postbuild

_copy_ui_resources_bin

_package_user_resources_kit

generate_flash_map

Factory/installer/

ESP3D-FACTORY_8MB/

build/8mb_wifi_fluidnc/

idf.py output

generate_resources.py

ui_resources binary

package_user_resources_kit.py

ui_resources_kit/

flash_mgr.py

flash map JSON

installer/

ESP32_PIBOT_CNC_PENDANT_V1_8MB_wifi_fluidnc/


Both layouts use a custom bootloader that requires CONFIG_PARTITION_TABLE_OFFSET=0xC000 (44 KB bootloader, vs the ESP-IDF default 32 KB). The ui_resources partition (custom subtype 0x42) is the UI resource store — see ui_resources/development.md.

PartitionTypeOffsetSizeNotes
nvsdata/nvs0xD00012 KBNVS-backed settings (ESP3DSettings)
otadatadata/ota0x100008 KBOTA slot selector
app0app/ota_00x200002,560 KBSingle app slot — OTA overwrites in place
ui_resourcesdata/0x420x2A0000256 KBUI icons, fonts, theme colors, language packs, sounds
flashfsdata/spiffs0x2E0000768 KBLittleFS user file storage
(factory)app/factory0x3A0000384 KBCommented out in the CSV; the factory image is flashed there
PartitionTypeOffsetSizeNotes
nvsdata/nvs0xD00012 KBNVS-backed settings
otadatadata/ota0x100008 KBOTA slot selector
app0app/ota_00x200003,072 KBOTA slot A
app1app/ota_10x3200003,072 KBOTA slot B
ui_resourcesdata/0x420x620000256 KBUI icons, fonts, theme colors, language packs, sounds
flashfsdata/spiffs0x6600001,152 KBLittleFS user file storage
(factory)app/factory0x780000512 KBCommented out in the CSV; the factory image is flashed there

4 MB vs 8 MB: The 4 MB layout has a single app slot — OTA overwrites it in place. The 8 MB layout adds a full app1 slot for true atomic A/B OTA. Both use 256 KB for ui_resources (layout redesigned 2026-10-21; a full ui_resources is expected, since the font slots absorb the free space). The factory partition line is commented out in both CSVs: the factory app sits at the end of the flash, outside the table.

  • The installer directory name comes from CMake (cmake/build_config_name.cmake, which appends the log level _LOG_<LEVEL>): installer_config_name(cmake_args, build_dir) reads <build_dir>/build_config_name.txt and falls back to build_config_name() before any configure.
  • build_config_name() (no log suffix) is passed to generate_resources.py as --build-name (build trailer, see tools_build_scripts_ui_resources.md); copy_factory_artifacts(cmake_args, build_dir) uses the CMake installer name.

Defined across common.py and board_config.cmake:

ConstantValueWhere set
EXPECTED_IDF_TARGETesp32common.py
RESOLUTIONres_320_240common.py → board_config.cmake
IDF_PATH defaultC:\Users\luc\esp\v5.4.3\esp-idfcommon.py (override via IDF_PATH env var)
TFT_TARGETESP32_PIBOT_CNC_PENDANT_V1board_config.cmake
DEFAULT_ORIENTATIONPORTRAITboard_config.cmake
DYNAMIC_ROTATION_FEATUREOFFboard_config.cmake
FIXED_UIONboard_config.cmake
HARDWARE_BUTTONSONboard_config.cmake
HARDWARE_ENCODERONboard_config.cmake
HARDWARE_SWITCHONboard_config.cmake
HARDWARE_POTENTIOMETERONboard_config.cmake
TFT_BRIGHTNESS_CONTROLONboard_config.cmake
SHOW_MENU_FACTORYOFFboard_config.cmake
ESP3D_HOSTNAMEPIBOT-CNC-PENDANTboard_config.cmake
ESP3D_FALLBACK_MODE1 (stay in WiFi STA on failure)board_config.cmake
Flash chipesp32, dio, 80 MHzflash_params.json

Why FIXED_UI=ON?
The physical buttons, encoder, selector switch, and potentiometer are wired to fixed positions on the PCB enclosure. Unlike touch-only boards where the UI layout can be rotated freely, the on-screen virtual controls must mirror the physical controls’ positions, so runtime orientation change is disabled.

Why SHOW_MENU_FACTORY=OFF?
Factory mode is accessible via the [ESP400]FACTORY command and a physical boot-button hold. The on-screen Settings menu entry that other boards rely on is redundant here and is hidden.

Why ESP3D_FALLBACK_MODE=1?
When a WiFi STA connection fails, the board stays in wifi_sta mode rather than falling back to AP mode. This lets the user reconnect manually via the connection status screen — appropriate for a pendant that is always within range of the machine’s own network.


Invoked before idf.py build so the ui_resources partition binary and esp3d_ui_offsets.h header are available when the firmware compiles. This guarantees that the compiled firmware’s offset constants always match the partition being flashed.

Terminal window
generate_resources.py
--variant 8mb_wifi_fluidnc
--resolution res_320_240
--partition-csv boards/pibot_pendant_v1_0/partitions_8mb.csv
--out build/8mb_wifi_fluidnc/
[--board-resources boards/pibot_pendant_v1_0/resources/] # only when dir exists

The --board-resources flag layers board-specific overrides (PiBot branded boot logo, one per firmware target) on top of the shared resources/ defaults. See ui_resources/development.md for the override mechanism.

Assembles a self-contained kit that end users can run without cloning the full repo to customise icons, fonts, or theme colors and rebuild the ui_resources partition over SD card.

Terminal window
package_user_resources_kit.py
--variant 8mb_wifi_fluidnc
--manifest build/8mb_wifi_fluidnc/ui_resources_manifest_8mb_wifi_fluidnc.json
--out installer/ESP32_PIBOT_CNC_PENDANT_V1_8MB_wifi_fluidnc/ui_resources_kit/

Failure is non-fatal — the build logs WARN and continues.

Generates <config_name>.json describing the complete flash map (addresses + binary file paths). This file is consumed by the web installer and by flash_mgr.py --flash.

Terminal window
flash_mgr.py
--variant-dir installer/ESP32_PIBOT_CNC_PENDANT_V1_8MB_wifi_fluidnc/
--generate
--flash-params boards/pibot_pendant_v1_0/flash_params.json

The repo-level build manager (tools/build_scripts/build_mgr.py) discovers this module automatically by scanning boards/*/build_scripts/variants.py and delegates single-variant builds to build_one.py. Use build_mgr.py for multi-variant or multi-board batch operations; use build_one.py for targeted single-variant work.


Terminal window
# Activate the ESP-IDF environment first
. $IDF_PATH/export.sh # Linux/macOS
# $IDF_PATH/export.ps1 # Windows PowerShell
cd boards/pibot_pendant_v1_0/build_scripts
# Build one variant
python build_one.py 8mb_wifi_fluidnc
# Build with a fresh build directory
python build_one.py 8mb_wifi_fluidnc --clean
# Validate cmake flags only (no compilation)
python build_one.py 8mb_wifi_fluidnc --check
# List all available variants
python build_one.py
Terminal window
# Build all variants for this board
python tools/build_scripts/build_mgr.py --build_board pibot_pendant_v1_0
# Build a specific variant (board-qualified to avoid ambiguity)
python tools/build_scripts/build_mgr.py --build_variant pibot_pendant_v1_0/8mb_wifi_fluidnc
# Check all pibot variants — cmake configure only, fast
python tools/build_scripts/build_mgr.py --check_board pibot_pendant_v1_0
# Clean all pibot build + installer directories
python tools/build_scripts/build_mgr.py --clean_board pibot_pendant_v1_0
# Parallel build with 8 threads
python tools/build_scripts/build_mgr.py --build_board pibot_pendant_v1_0 --jobs=8
Terminal window
# Full flash via flash map (factory + firmware + ui_resources)
python tools/flash_scripts/flash_mgr.py \
--variant-dir installer/ESP32_PIBOT_CNC_PENDANT_V1_8MB_wifi_fluidnc/ \
--port /dev/ttyUSB0
# Flash factory app only
python boards/pibot_pendant_v1_0/Factory/tools/flash_factory.py
# Flash complete image (factory + firmware) in one step
python boards/pibot_pendant_v1_0/Factory/tools/flash_all.py

Add --dev to skip the PROD_BUILD=ON cmake flag, keeping verbose logging active in the firmware:

Terminal window
python build_one.py 8mb_serial_grblhal --dev
Terminal window
export IDF_PATH=/opt/esp-idf-v5.4.3
python build_one.py 8mb_wifi_fluidnc

This build module sits at the intersection of several sibling modules. It consumes artifacts from the factory sub-modules and drives compilation of the BSP and main firmware.

Main Firmware

Repo-level Tools

Factory Sub-modules

Board Support

pibot_pendant_v1_0_build (this module)

--build_board / --build_variant

idf.py compiles

idf.py compiles

build factory_4/8mb variant

build factory_4/8mb variant

invokes pre-build

invokes post-build

invokes post-build

bootloader_*.bin artifact

factory app artifact

build_scripts

build_one.py · common.py · variants.py

pibot_pendant_v1_0_bsp

board_init.c · control_event.h

control_types.c · esp3d_snapshot.h

encoder / buttons / touch / switch / potentiometer / LVGL init

pibot_pendant_v1_0_factory_app

Factory/main

ili9341 · touch · buttons

buzzer · sdcard · gfx · encoder

pibot_pendant_v1_0_bootloader

Factory/custom_bootloader/hooks.c

OTA backup · buzzer tones

physical button detection

tools/build_scripts/build_mgr.py

multi-board orchestration

tools/build_scripts/generate_resources.py

UI partition binary

tools/build_scripts/package_user_resources_kit.py

standalone user kit

tools/flash_scripts/flash_mgr.py

flash map + flashing

main

entry_point · core_application

ui_core · cnc_shared_screens

fluidnc_screens · grbl_screens · grblhal_screens

Related moduleDocumentationRelationship
pibot_pendant_v1_0_bsppibot_pendant_v1_0_bsp.mdBSP component (encoder, buttons, switch, potentiometer, LVGL init, touch) compiled into firmware by this build module
pibot_pendant_v1_0_factory_apppibot_pendant_v1_0_factory_app.mdFactory test app built by factory_4mb/factory_8mb variants; its bootloader artifact is automatically injected into regular variants
pibot_pendant_v1_0_bootloaderpibot_pendant_v1_0_bootloader.mdCustom bootloader hooks: backup_and_erase_otadata, buzzer confirmation tones, physical button detection at boot
build_systembuild_system.mdSame three-file pattern (build_one.py, common.py, variants.py) shared by all boards; pibot adds unique steps (physical-input flags, board-resources overrides, factory auto-rebuild guard)
factory_toolsfactory_tools.mdflash_all.py and flash_factory.py in Factory/tools/ — low-level flash scripts for direct chip programming, bypassing flash_mgr
UI resourcesui_resources/development.mdui_resources partition generated pre-build; board-specific overrides in boards/pibot_pendant_v1_0/resources/ (PiBot boot logo per firmware target)
Feature matrixfeatures/feature_resource_matrix.mdDocuments WiFi-as-CNC-transport vs WiFi-as-remote; pibot WiFi variants use SOCKET_CLIENT_SERVICE=ON (CNC over TCP)
Memory constraintsguides/esp32_memory_constraints.mdESP32 without PSRAM; drives the 4 MB FluidNC-only restriction