Skip to content

Factory App Entry (`factory_app_entry`)

The factory_app_entry module is the application entry point for the factory recovery firmware. It implements app_main() — the function ESP-IDF calls immediately after the RTOS scheduler starts — for every supported board variant. Its sole job is to wire together all factory subsystems and run the interactive recovery menu loop.

This module is a leaf of the factory_core hierarchy. It depends on every other factory sub-module but contains no reusable logic of its own; all menu, flash, visual, and input logic lives in the sibling modules documented in factory_app.md.


Factory_Application_&_Bootloader
└── factory_app
└── factory_core
├── factory_app_entry ← THIS MODULE (app_main per board)
├── factory_menu_system
├── factory_update_actions
├── factory_visual_feedback
├── factory_snapshot
└── factory_input_dispatch

Each board has its own boards/<board>/Factory/main/main.c that compiles into an independent binary. The entry-point logic is structurally identical across all boards; differences are limited to the LCD driver, screen geometry, and optional IO expander initialization.

BoardDisplay DriverLogical ResolutionSpecial Hardware
esp32_2432s028rILI9341 (SPI)240 × 320 portrait—
esp32_3248s035cST7796 (SPI)320 × 480 portrait—
esp32_3248s035rST7796 (SPI)320 × 480 portrait—
esp32s3_4827s043cILI9485 (SPI)272 × 480 (glass rotated)—
esp32s3_8048s043cST7262 (RGB)480 × 272 (glass rotated)—
esp32s3_8048s050cST7262 (RGB)480 × 272 (glass rotated)—
esp32s3_8048s070cST7262 (RGB)480 × 272 (glass rotated)—
esp32s3_8048_touch_lcd_7ST7262 (RGB)800 × 480CH422G IO expander (SD_CS via EXIO3)
esp32s3_bzm_tft35_gt911ST7796 (SPI)320 × 480 portrait—
esp32s3_hmi43v3RM68120 (I80)variesI80 parallel bus
esp32s3_zx3d50ce02s_usrc_4832ST7796 (I80)variesI80 parallel bus
pibot_pendant_v1_0ILI9341 (SPI)240 × 320 portraitPhysical buttons + encoder + potentiometer

The factory recovery app runs from the ESP-IDF factory partition — a dedicated flash region separate from app0/app1. There are two entry mechanisms:

Yes (pibot_pendant_v1_0)

Button held

Button not held

No (all other boards)

User sends [ESP444]FACTORY

Power On / Reset

Custom bootloader

present?

Bootloader hook

checks physical button

Bootloader erases otadata

→ factory becomes default boot

Normal boot

→ main firmware

Main firmware running

esp444.cpp:

1. Back up otadata → 0xB000

2. Set factory as boot partition

3. esp_restart()

factory app_main starts

Key invariant: The main firmware always backs up the current otadata to flash offset 0xB000 before switching the boot partition to factory. This ensures that power-cycling from inside recovery returns to the correct OTA slot, not just app0.


One of the first acts of app_main() — before display, before any user interaction — is restore_otadata_from_backup(). This function directly reads and writes raw flash using the esp_flash_* APIs.

0x0000 ┌──────────────────────────────┐
│ Bootloader │
│ ... │
0xB000 ├──────────────────────────────┤ ← OTADATA_BACKUP_OFFSET (4 KB sector)
│ [0x00 – 0x1F] OTA Entry 1 │ (32 bytes)
│ [0x20 – 0x3F] OTA Entry 2 │ (32 bytes)
│ [0x40 – 0x43] BACKUP_MAGIC │ = 0xAA55AA55
│ ... │
0xC000 ├──────────────────────────────┤ Partition table
0xD000 ├──────────────────────────────┤ NVS
│ ... │
0x10000 ├──────────────────────────────┤ ← OTADATA_OFFSET
│ OTA data sector 0 (4 KB) │ slot 0 descriptor
0x11000 ├──────────────────────────────┤
│ OTA data sector 1 (4 KB) │ slot 1 descriptor
│ ... │

≠ 0xAA55AA55

== 0xAA55AA55

Yes

No

restore_otadata_from_backup

Read magic at

0xB040

No valid backup - return false

Read Entry1 from 0xB000

Read Entry2 from 0xB020

Both entries

all 0xFF?

Skip write - flash already clean

Erase otadata sectors

0x10000 and 0x11000

Write Entry1 → 0x10000

Write Entry2 → 0x11000

Erase backup sector 0xB000

so it cannot retrigger

return true

⚠ PORTING NOTE: OTADATA_BACKUP_OFFSET (0xB000) must be 4 KB-aligned, situated after the bootloader and before the partition table. This value must exactly match the constant used in the main firmware’s esp444.cpp. The factory sdkconfig must include CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y because this sector lies below the first user partition.

All boards share the same values. These must remain in sync with esp444.cpp:

ConstantValueDescription
OTADATA_OFFSET0x10000Flash address of the live OTA data partition
OTADATA_SECTOR_SIZE0x1000Size of one OTA data sector (4 KB)
OTADATA_ENTRY_SIZE32Size of one OTA slot descriptor (bytes)
OTADATA_BACKUP_OFFSET0xB000Flash address of the backup sector
BACKUP_MAGIC_OFFSET0x40Offset within backup sector where the magic word lives
BACKUP_MAGIC0xAA55AA55Magic word confirming a valid backup exists

app_main() performs a strict, ordered initialization. The sequence is structurally identical across all boards; board-specific driver calls appear at the display init step.

SD Card / MenuCH422G IO ExpanderButtons / Encoder / Touch / Buzzergfx layerBoard LCD Driveresp_flash / OTA APIapp_main()FreeRTOSSD Card / MenuCH422G IO ExpanderButtons / Encoder / Touch / Buzzergfx layerBoard LCD Driveresp_flash / OTA APIapp_main()FreeRTOSMust be first - protects otadata before any other actionabort() if this returns != ESP_OKMust run after touch_init() (I2C bus) and before SD accessopt[Board requires IO expander (esp32s3_8048_touch_lcd_7)]opt[ENABLE_SNAPSHOT defined]app_main() invokedfactory_log_silence_sd_stack()restore_otadata_from_backup()backup restored (or absent - both are safe)<board>_init()<board>_backlight(true)gfx_init()buttons_init()encoder_init()touch_init()io_ch422g_configure()buzzer_init()esp_partition_find_first(APP, "app1")probe_sd_files()draw_menu()gpio_config(GPIO_NUM_0) - BOOT button as snapshot triggerenter infinite event loop

No

Yes

ESP-IDF calls app_main

Silence SD stack logs

restore_otadata_from_backup

board_init - LCD init + backlight on

Init OK?

delay 200 ms → abort()

gfx_init

buttons_init

encoder_init

touch_init

io_ch422g_configure()

if board requires it

buzzer_init

esp_partition_find_first('app1')

→ has_app1 flag

Build menu_items array

probe_sd_files

draw_menu

gpio_config(GPIO_NUM_0)

if ENABLE_SNAPSHOT

Main event loop - infinite


After initialization, app_main() enters an infinite polling loop. No additional RTOS tasks are created; the entire factory app runs within the single app_main task.

Yes

No

CW > 0

CCW < 0

0

BTN_1 / BTN_2 / BTN_3

BTN_NONE

pressed && !was_pressed

no new press

BTN_NONE

vbtn

Loop start

ENABLE_SNAPSHOT?

snapshot_check()

GPIO0 debounce + raw capture to SD

encoder_read() - non-blocking

menu_move(-1) ↑

menu_move(+1) ↓

button_wait_press(100 ms)

dispatch_button(btn)

touch_read() - non-blocking

touch_hint_hit_test(x, y)

Update touch_was_pressed

draw_button_hint_pressed(vbtn)

buzzer_beep_short()

vTaskDelay(80 ms)

draw_button_hints()

dispatch_button(vbtn)

SourcePoll methodMax latency
BOOT button (snapshot only)GPIO direct read + software debounce~50 ms
Rotary encoderencoder_read() non-blocking< 1 ms
Physical buttonsbutton_wait_press(100 ms) blocking100 ms
Touchscreen (virtual buttons)touch_read() non-blocking + hit-test< 1 ms

Boards without physical buttons or encoder still call buttons_init() / encoder_init() with all pins configured as GPIO_NUM_NC, making those calls safe no-ops. This keeps app_main() structurally identical across board families.


All input sources funnel through a single dispatch_button() call that maps a logical button ID to a menu action:

BTN_1

BTN_2

BTN_3

Encoder CW → BTN_1

dispatch_button(btn)

Encoder CCW → BTN_2

Physical BTN_1 / BTN_2 / BTN_3

Touch → hit-test → vbtn

menu_move(-1) ↑

menu_move(+1) ↓

execute_selected_action()

factory_update_actions

See factory_app.md → factory_input_dispatch and factory_update_actions for the downstream action logic.


The menu item list is built dynamically in app_main() based on detected hardware (dual-slot vs. single-slot OTA) and SD card probe results:

Yes

No

Yes

No

Start menu construction

Always add: Boot app0

app1 partition

exists?

Add: Boot app1

Always add: Flash firmware from SD → app0

app1 exists?

Add: Flash firmware from SD → app1

Always add: Flash UI resources from SD

probe_sd_files()

→ sets fw_found / res_found flags

draw_menu() - render full screen

SD file presence indicators (FW / RES) appear in the menu header. They are re-evaluated each time the SD card is remounted (e.g. after a failed flash attempt).


Each board calls a different driver pair at startup:

Board group_init() callBus type
esp32_2432s028r, pibot_pendant_v1_0ili9341_init()SPI
esp32_3248s035c/r, esp32s3_bzm_tft35_gt911st7796_init()SPI
esp32s3_4827s043cili9485_init()SPI
esp32s3_8048s043c/050c/070c, esp32s3_8048_touch_lcd_7st7262_init()RGB parallel
esp32s3_hmi43v3RM68120 initI80 parallel
esp32s3_zx3d50ce02s_usrc_4832ST7796 I80 initI80 parallel

If <board>_init() returns anything other than ESP_OK, app_main() logs the error, waits 200 ms for the UART buffer to flush, then calls abort(). There is no recovery path from a display failure inside the factory app.

CH422G IO Expander (esp32s3_8048_touch_lcd_7 only)

Section titled “CH422G IO Expander (esp32s3_8048_touch_lcd_7 only)”

This board routes SD_CS through a CH422G I2C IO expander (EXIO3). The factory app_main() must call io_ch422g_configure() after touch_init() (which brings up the shared I2C bus) and before the first SD access via probe_sd_files(). The CH422G_INITIAL_OUTPUT bitmask (0x2E & ~(1<<3)) must exactly match board_init.c in the main firmware to avoid bus conflicts.

The three virtual button circles (BTN_1 = ↑, BTN_2 = ↓, BTN_3 = ✓) are drawn at the bottom of the screen. The hit-test divides the touch area into zones:

Screen widthStrategy
≤ 320 px (portrait)Simple thirds: x < W/3 → BTN_1, x < 2W/3 → BTN_2, else BTN_3
≥ 480 px (landscape / large)Midpoint boundaries between actual button-center X coordinates — avoids zone mismapping on wide screens where a simple third-split would not align with the rendered circles

Rendering constants are scaled per-board to maintain consistent proportions across screen sizes:

Board / Logical ResolutionFont sizeBTN_CIRCLE_RMENU_START_YMENU_ITEM_H
240 × 320 (ILI9341)8 × 1620defaultdefault
320 × 480 (ST7796 SPI)11 × 21 (×4/3)2711140
272 × 480 (ILI9485)8 × 16229434
480 × 272 / 800 × 480 (ST7262)12 × 2430board-specificboard-specific

ConstantPathState
FW_FILENAME/sdcard/esp3dfw.binInput: firmware binary to flash
FW_OK_FILENAME/sdcard/esp3dfw.okRenamed after successful flash
FW_BAD_FILENAME/sdcard/esp3dfw.badRenamed after failed flash
RES_FILENAME/sdcard/ui_resources.binInput: UI resources binary
RES_OK_FILENAME/sdcard/ui_resources.okRenamed after successful flash
RES_BAD_FILENAME/sdcard/ui_resources.badRenamed after failed flash

factory_app_entry

app_main()

factory_logging

factory_log_silence_sd_stack

ESP-IDF esp_flash / esp_ota_ops

restore_otadata_from_backup

Board LCD drivers

board_init / board_backlight

factory_graphics

gfx_init

factory_buttons

buttons_init / button_wait_press

factory_encoder

encoder_init / encoder_read

factory_touch

touch_init / touch_read

factory_buzzer

buzzer_init / buzzer_beep_short

CH422G BSP driver

io_ch422g_configure

board-specific only

factory_menu_system

draw_menu / menu_move

factory_update_actions

execute_selected_action

factory_visual_feedback

draw_flashing_screen / draw_progress

factory_snapshot

snapshot_check

factory_input_dispatch

dispatch_button / touch_hint_hit_test


DocumentRelationship
factory_app.mdParent module — describes the full factory app scope and all sibling sub-modules
docs/Factory/Supplementary factory app design notes and bootloader integration
docs/architecture/screens_architecture.mdContrasts with the main firmware LVGL screen model; factory uses bare-metal GFX with no LVGL
docs/features/feature_resource_matrix.mdPartition layout and OTA slot strategy that the backup/restore mechanism depends on
docs/hardware/pibot-cnc-pendant-hardware-documentation.mdPhysical button and encoder wiring for pibot_pendant_v1_0