Skip to content

pibot_pendant_v1_0_factory_app

The PiBot Pendant v1.0 Factory App is a standalone recovery firmware that runs from a dedicated factory flash partition, completely independent of the main application OTA partitions. It provides a touch-and-button-navigable recovery menu allowing users to:

  • Boot into app0 or app1 OTA partitions
  • Flash firmware from an SD card (esp3dfw.bin) to an OTA partition via the ESP-IDF OTA API
  • Flash UI resources from an SD card (ui_resources.bin) to the ui_resources partition
  • Restore otadata from a bootloader-written backup so that a power-off during recovery always returns to the previously active OTA partition

This module is intentionally minimal — it uses lightweight, self-contained C drivers (no LVGL, no esp3d framework) and communicates directly with hardware to keep the recovery partition small and reliable.

Related modules:

  • pibot_pendant_v1_0_bootloader — the custom bootloader that detects a recovery-entry button hold, backs up and erases otadata, and jumps to this factory app
  • pibot_pendant_v1_0_bsp — the main firmware BSP (LVGL-based); shares the same hardware but is a separate, full-featured firmware

BTN3 held at boot:

backup otadata → jump

deploy via esptool

(development / production)

Custom Bootloader

(pibot_pendant_v1_0_bootloader)

Factory App - app_main

pibot_pendant_v1_0_factory_app_main

Display Layer

pibot_pendant_v1_0_factory_app_display

Input Layer

pibot_pendant_v1_0_factory_app_input

Storage Layer

pibot_pendant_v1_0_factory_app_storage

Python Tooling

pibot_pendant_v1_0_factory_app_tools

ESP32 SPI Flash (8 MB)

Bootloader 0x0001000

otadata Backup (raw) 0x000B000

Partition Table 0x000C000

otadata 0x0010000

nvs

app0 (ota_0) 0x0020000

app1 (ota_1)

ui_resources

flashfs

factory 0x07A0000

The factory partition resides at 0x7A0000 in the 8 MB layout. On every recovery entry, the custom bootloader:

  1. Reads both otadata entries and backs them up verbatim to 0xB000 (a raw sector outside all partitions), stamped with magic word 0xAA55AA55
  2. Erases otadata to prevent the ROM from loading an OTA app before the factory app can restore it
  3. Jumps to the factory partition

On startup, app_main immediately restores otadata from the backup before initialising any hardware, so a power cycle from the recovery menu always returns to the last-known good OTA partition.


Factory App Module

Main Application

main.c

─────────────────

Menu system

OTA flash actions

Otadata restore

Snapshot capture

Display

gfx.c · ili9341.c

─────────────────

ILI9341 SPI driver

Software GFX layer

Snapshot pipe

Input Drivers

buttons.c · encoder.c

touch.c

─────────────────

Debounced buttons

PCNT encoder

FT6336U I²C touch

Storage

sdcard.c

─────────────────

SPI SD mount/unmount

OTA partition write

Python Tools

flash_all.py · flash_factory.py

generate_font.py · snap2png.py

─────────────────

Flashing · Font gen

Snapshot convert

Sub-moduleSource FilesDocumentation
Main Applicationmain.c, factory_log.hpibot_pendant_v1_0_factory_app_main.md
Displaygfx.c, ili9341.cpibot_pendant_v1_0_factory_app_display.md
Input Driversbuttons.c, encoder.c, touch.c/hpibot_pendant_v1_0_factory_app_input.md
Storagesdcard.cpibot_pendant_v1_0_factory_app_storage.md
Python Toolsflash_all.py, flash_factory.py, generate_font.py, snap2png.pypibot_pendant_v1_0_factory_app_tools.md

SD CardILI9341 DisplayESP Flashapp_mainCustom BootloaderSD CardILI9341 DisplayESP Flashapp_mainCustom BootloaderReads 0xB000, checks magic,restores entries, erases backuploop[Main Event Loop]Read otadata, write backup to 0xB000 + magic 0xAA55AA55Erase otadata (both sectors)Jump to factory partitionfactory_log_silence_sd_stack()restore_otadata_from_backup()ili9341_init() → backlight ongfx_init()buttons_init()encoder_init() [PCNT]touch_init() [FT6336U I²C]buzzer_init()sdcard_mount() → probe FW + RES filesdraw_menu()encoder_read() → menu_move()button_wait_press(100ms) → dispatch_button()touch_read() → touch_hint_hit_test() → dispatch_button()

The menu is built dynamically at startup based on detected flash partitions and SD card contents:

Menu ItemShown WhenAction
Boot app0Alwaysesp_ota_set_boot_partition("app0") → esp_restart()
Boot app1app1 partition existsesp_ota_set_boot_partition("app1") → esp_restart()
SD → app0AlwaysFlash esp3dfw.bin from SD to app0 via OTA API
SD → app1app1 partition existsFlash esp3dfw.bin from SD to app1 via OTA API
SD → resourcesAlwaysFlash ui_resources.bin from SD to ui_resources partition

BTN1 ▲ Up

menu_move(-1)

BTN2 ▼ Down

menu_move(+1)

BTN3 ✓ Select

execute_selected_action()

Encoder CW

Encoder CCW

Touch tap on virtual button bar

touch_hint_hit_test(x,y)

dispatch_button(vbtn)

SD filePurposeOn successOn failure
esp3dfw.binFirmware binaryRenamed → esp3dfw.okRenamed → esp3dfw.bad
ui_resources.binUI resource packRenamed → ui_resources.okRenamed → ui_resources.bad

The rename approach provides a clear audit trail: after a successful flash the original file is preserved with a .ok extension, so re-inserting the same SD card does not accidentally trigger another flash.


The factory app uses a self-contained software GFX layer (gfx.c) that writes pixel rows directly to the ILI9341 over SPI polling. This keeps the factory partition small and avoids any dependency on heap-heavy LVGL task infrastructure.

The bootloader backs up otadata to a raw flash sector at 0xB000 (between the bootloader end and the partition table, outside all partition entries) and marks it with magic word 0xAA55AA55. The factory app reads this backup on startup and restores otadata before doing anything else. Two invariants are maintained:

  • Power-off safety: a power cycle during recovery returns to the correct OTA partition because otadata is restored in the first lines of app_main
  • Single-restore: after restoring, the backup sector is immediately erased so a subsequent cold boot into the factory app does not double-restore

⚠️ OTADATA_BACKUP_OFFSET (0xB000) must be identical in both main.c and hooks.c. When porting to a new board, recalculate this value for the new bootloader size and update both files together.

CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y must be set in the factory sdkconfig. The factory app erases the 0xB000 backup sector — an address below the first partition entry — which ESP-IDF rejects by default. This is intentional: the factory app is a privileged recovery tool that manages flash directly.

When compiled with ENABLE_SNAPSHOT, pressing GPIO0 (the physical BOOT button) captures the current screen state to a .raw file on the SD card. The gfx.c layer writes all draw calls to both the display and the open capture file simultaneously (row-by-row as drawing happens), making it possible to capture screens mid-flash-operation. The companion Python tool snap2png.py converts .raw → .png for review. See pibot_pendant_v1_0_factory_app_display.md for file format details.

Reading and writing flash happens in 1024-byte static chunks (uint8_t buf[1024]). This bounds stack usage, avoids hitting SPI DMA single-transfer limits, and enables incremental progress display.


All pin assignments are defined in hw_config.h (board-specific, referenced by every driver). Key assignments for the PiBot Pendant v1.0:

Signal GroupPinsDriver
TFT SPI (ILI9341)TFT_MOSI, TFT_CLK, TFT_CS, TFT_DC, TFT_LEDili9341.c
SD card SPISD_MOSI, SD_MISO, SD_CLK, SD_CSsdcard.c
Rotary encoderENCODER_A_PIN, ENCODER_B_PINencoder.c (PCNT)
Push buttonsBUTTON_1_PIN, BUTTON_2_PIN, BUTTON_3_PINbuttons.c (active-low, pull-up)
Touch I²CTOUCH_SDA_PIN, TOUCH_SCL_PINtouch.c (FT6336U, polling)
BuzzerBUZZER_PINbuzzer.c (bit-bang square wave)
Snapshot triggerGPIO_NUM_0 (BOOT button)main.c (ENABLE_SNAPSHOT only)

main.c - app_main

ili9341.c

SPI display driver

gfx.c

Software GFX + snapshot

buttons.c

Debounced GPIO

encoder.c

PCNT quadrature

touch.c

FT6336U I²C polling

buzzer.c

Bit-bang square wave

sdcard.c

SPI VFS FAT mount

factory_log.h

Compile-time log gate

hw_config.h

Pin definitions