Skip to content

ESP32S3-ZX3D50CE02S-USRC-4832 Factory Recovery Module

ESP32S3-ZX3D50CE02S-USRC-4832 Factory Recovery Module

Section titled “ESP32S3-ZX3D50CE02S-USRC-4832 Factory Recovery Module”

The esp32s3_zx3d50ce02s_usrc_4832_factory_app_recovery module is the recovery application logic embedded in the factory partition of the ESP32S3-ZX3D50CE02S-USRC-4832 board. It provides a minimal, touch-navigable menu that runs independently of the main firmware, allowing field recovery of the device through SD-card-based firmware and resource updates, and manual selection of boot partitions.

This module is the recovery sub-module of the parent esp32s3_zx3d50ce02s_usrc_4832_factory_app. It is implemented in two files:

FilePurpose
boards/esp32s3_zx3d50ce02s_usrc_4832/Factory/main/main.cRecovery application: menu, OTA restore, SD flash, UI rendering
boards/esp32s3_zx3d50ce02s_usrc_4832/Factory/main/factory_log.hCompile-time debug log gating and SD stack silencer

This board has no physical buttons, no encoder, and no buzzer — all corresponding GPIO pins are GPIO_NUM_NC. Navigation is exclusively touch-based using three virtual on-screen buttons rendered at the bottom of the display.

HardwarePresentDetail
Display✅ST7796, 480×320, Intel 8080 (I80) parallel bus
Touch✅FT5x06, I2C
Physical buttons❌All GPIO_NUM_NC
Rotary encoder❌All GPIO_NUM_NC
Buzzer❌GPIO_NUM_NC
SD card✅SPI bus, FAT filesystem, mounted at /sdcard
Snapshot (GPIO0)OptionalENABLE_SNAPSHOT compile flag

Shared reset line: TFT_RST_PIN resets both the ST7796 display panel and the FT5x06 touch controller simultaneously. st7796_i80_init() must always run before touch_init(). See the Display sub-module for driver details.

The button/encoder/buzzer driver code is identical to other boards (e.g., pibot_pendant_v1_0) and simply no-ops when the GPIO is GPIO_NUM_NC, so the same firmware source builds cleanly for all variants.


Storage

Input Sub-module

Display Sub-module

Recovery Module (main.c + factory_log.h)

app_main()

restore_otadata_from_backup()

Menu System

draw_menu / draw_menu_item

menu_move / menu_select

dispatch_button()

Actions

action_boot_partition()

action_sd_update()

action_sd_update_res()

touch_hint_hit_test()

show_status() / clear_status()

draw_progress() / draw_result()

Snapshot System

snapshot_take() / snapshot_check()

snap_find_next_number()

factory_log_silence_sd_stack()

gfx.c

gfx_clear / fill_rect / draw_string

gfx_hline / vline / rect

gfx_snapshot_begin / end

st7796_i80.c

st7796_i80_init()

st7796_i80_flush()

buttons.c

buttons_init()

button_wait_press()

encoder.c

encoder_init()

encoder_read()

touch.c

touch_init()

touch_read()

buzzer.c

buzzer_init()

buzzer_beep_short()

sdcard.c

sdcard_mount()

sdcard_unmount()

ESP-IDF Flash

esp_ota_begin/write/end

esp_partition_write

esp_flash_read/write


app_main() runs a strict initialization sequence. The order is critical due to the shared reset line between display and touch.

sdcard.cButtons / Encoder / Touch / Buzzergfx.cST7796 I80OTA Restorefactory_log.happ_main()FreeRTOS / ESP-IDFsdcard.cButtons / Encoder / Touch / Buzzergfx.cST7796 I80OTA Restorefactory_log.happ_main()FreeRTOS / ESP-IDFMutes sdmmc / vfs_fat / sdspi tags at runtimeReads magic at 0xB040Restores otadata at 0x10000Clears backup sectorResets both display AND touch controller(shared TFT_RST_PIN)FT5x06 via I2CMust run after st7796_i80_init()Checks /sdcard/esp3dfw.binand /sdcard/ui_resources.binapp_main() calledfactory_log_silence_sd_stack()restore_otadata_from_backup()st7796_i80_init()gpio_config(TFT_LED) + gpio_set_level HIGHgfx_init()buttons_init() - no-op (GPIO_NUM_NC)encoder_init() - no-op (GPIO_NUM_NC)touch_init()buzzer_init() - no-op (GPIO_NUM_NC)Detect partitions (app1 present?)Build menu_items[]probe_sd_files()draw_menu()Enter main event loop

This is the first critical operation performed at startup. The main firmware (esp444.cpp) backs up the OTA partition table entries to a reserved flash region before switching to the factory partition. The recovery app restores this backup on startup so that power-cycling from recovery always returns the device to the correct firmware slot.

No

Yes

Yes

No

app_main starts

Read 4 bytes at 0xB000 + 0x40

OTADATA_BACKUP_OFFSET + BACKUP_MAGIC_OFFSET

magic == 0xAA55AA55?

Log: No backup found

Return false

Read entry1[32] from 0xB000

Read entry2[32] from 0xB020

Both entries

0xFF filled?

Skip restore → goto clear_backup

Erase otadata sector 1 at 0x10000

Erase otadata sector 2 at 0x11000

Write entry1 to 0x10000

Write entry2 to 0x11000

(if not empty)

Erase backup sector at 0xB000

Continue startup

Flash layout constants — must match esp444.cpp in the main firmware exactly:

ConstantValueDescription
OTADATA_OFFSET0x10000Live OTA data region start
OTADATA_SECTOR_SIZE0x10004 KB per OTA entry sector
OTADATA_ENTRY_SIZE32Bytes per OTA entry
OTADATA_BACKUP_OFFSET0xB000Backup region (pre-partition space, below 0xC000)
BACKUP_MAGIC_OFFSET0x40Byte offset of magic word within backup sector
BACKUP_MAGIC0xAA55AA55Presence sentinel value

⚠️ CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y is required in the factory sdkconfig. Erasing 0xB000 (below the first partition at 0xD000) is aborted by default. The factory app is a privileged recovery tool that manages flash directly, making this opt-in intentional.


The menu is a simple indexed list with selection highlight, rendered directly via the GFX layer — no LVGL, no FreeRTOS scheduler dependency beyond vTaskDelay. All menu state is stored in module-level globals.

typedef struct {
const char *label; // Display text
menu_action_t action; // Action identifier (enum)
uint16_t color; // Text color when not selected
} menu_item_t;
static menu_item_t menu_items[MENU_MAX_ITEMS]; // Max 8 items
static int menu_count = 0;
static int menu_selected = 0;
Menu Item LabelConditionAction Enum
Boot app0AlwaysMENU_ACTION_BOOT_APP0
Boot app1Only if app1 partition foundMENU_ACTION_BOOT_APP1
SD -> app0AlwaysMENU_ACTION_SD_UPDATE_APP0
SD -> app1Only if app1 partition foundMENU_ACTION_SD_UPDATE_APP1
SD -> resourcesAlwaysMENU_ACTION_SD_UPDATE_RES

This board ships with only app0. app1 items appear only if the partition table includes it.

┌──────────────────────────────────────────────────────────────┐ y=0
│ Recovery vX.Y.Z │
│ │ y=40
├───────────────────────────────────────────────────────────────┤
│ Active: app0 │ y=45
│ SD: FW RES │ y=68
├───────────────────────────────────────────────────────────────┤ y=88
│ ▌ Boot app0 (selected - highlight box) │ y=94
│ Boot app1 │ y=128
│ SD -> app0 │ y=162
│ SD -> app1 │ y=196
│ SD -> resources │ y=230
├───────────────────────────────────────────────────────────────┤ STATUS_Y-7
│ Power off to cancel │ STATUS_Y
├───────────────────────────────────────────────────────────────┤ BTN_HINT_BASE_Y
│ (▲) (▼) (✓) │
└──────────────────────────────────────────────────────────────┘ y=320

Layout constants (480×320 canvas, 8×16 font):

ConstantValueDescription
SCREEN_WIDTH480From hw_config.h
SCREEN_HEIGHT320From hw_config.h
MENU_START_Y94Y-coordinate of first menu item
MENU_ITEM_H34Pixels per menu item row
MENU_PAD_X20Left/right padding
FONT_WIDTH8Glyph width in pixels
FONT_HEIGHT16Glyph height in pixels
BTN_CIRCLE_R22Virtual button circle radius
BTN_HINT_H2*R+9Total height of the button hint bar
STATUS_YSCREEN_HEIGHT - 28 - BTN_HINT_HFooter / status text Y
MENU_HIGHLIGHTRGB565(0,80,160)Dark blue selection box fill
MENU_HIGHLIGHT_TXTRGB565(80,160,255)Bright blue selected label

The main loop polls three input sources concurrently: encoder, physical buttons, and touchscreen. All three converge on dispatch_button() with a button_id_t, keeping action logic unified regardless of input source.

Yes

No

Yes

No

BTN_1

BTN_2

BTN_3

BTN_NONE

No

Yes

No

Yes

BTN_1

BTN_2

BTN_3

Main loop iteration

snapshot_check()

#ifdef ENABLE_SNAPSHOT

encoder_read()

enc > 0?

enc < 0?

button_wait_press(100ms)

touch_read()

pressed &&

not prev pressed?

touch_hint_hit_test(x, y)

vbtn != BTN_NONE?

draw_button_hint_pressed(vbtn)

buzzer_beep_short() - no-op

vTaskDelay(80ms)

draw_button_hints()

dispatch_button(btn / vbtn)

menu_move(-1) → Up

menu_move(+1) → Down

execute_selected_action()

The three virtual buttons (up / down / ok) are horizontally centered on the screen. The hit-test divides the screen using midpoints between the icon centers, not screen-width thirds, to correctly account for the non-uniform icon spacing.

Screen width: 480 px
BTN_HINT_CX1 = 240 - 100 = 140 (▲ up)
BTN_HINT_CX2 = 240 = 240 (▼ down)
BTN_HINT_CX3 = 240 + 100 = 340 (✓ ok)
boundary_1_2 = (140 + 240) / 2 = 190
boundary_2_3 = (240 + 340) / 2 = 290
Touch zones (y >= BTN_HINT_BASE_Y only):
x < 190 -> BTN_1 (up)
190 <= x < 290 -> BTN_2 (down)
x >= 290 -> BTN_3 (ok)

action_sd_update(target_label) flashes /sdcard/esp3dfw.bin into the named OTA application partition using the ESP-IDF OTA API.

SPI FlashESP-IDF OTA APIsdcard.cmain.cUser (BTN_3)SPI FlashESP-IDF OTA APIsdcard.cmain.cUser (BTN_3)loop[1 KB chunks]alt[Success][Failure]execute_selected_action() -> action_sd_update("app0")esp_partition_find_first(TYPE_APP, ANY, "app0")sdcard_mount()fopen("/sdcard/esp3dfw.bin", "rb")Validate size: > 0 and <= partition sizedraw_flashing_screen()esp_ota_begin(update_part, fw_size, &handle)fread(buf, 1, 1024, file)esp_ota_write(handle, buf, read_len)draw_progress(percent)esp_ota_end(handle)esp_ota_set_boot_partition(update_part)rename esp3dfw.bin to esp3dfw.ok (or .bad on failure)sdcard_unmount()draw_result(true, "Success! Rebooting...")esp_restart()draw_result(false, "FAILED! Power cycle.")probe_sd_files() + draw_menu()

SD file naming conventions:

FileRole
/sdcard/esp3dfw.binInput — firmware binary to flash
/sdcard/esp3dfw.okOutput — renamed from .bin on success
/sdcard/esp3dfw.badOutput — renamed from .bin on failure
/sdcard/ui_resources.binInput — UI resources binary to flash
/sdcard/ui_resources.okOutput — renamed from .bin on success
/sdcard/ui_resources.badOutput — renamed from .bin on failure

action_sd_update_res() flashes /sdcard/ui_resources.bin into the ui_resources data partition using direct partition erase + write (not the OTA API, since this is a data partition).

ESP-IDF Partition APIsdcard.cmain.cESP-IDF Partition APIsdcard.cmain.cBytes 0-3: 'ESP3' magicBytes 4-15: 12-char variant stringLog variant or warn if no headerloop[1 KB chunks]alt[Success][Failure]esp_partition_find_first(TYPE_DATA, ANY, "ui_resources")sdcard_mount()fopen("/sdcard/ui_resources.bin", "rb")Read 16-byte build headerValidate file size: > 0 and <= partition sizedraw_flashing_screen()esp_partition_erase_range(res_part, 0, res_part->size)fread(buf, 1, 1024, file)esp_partition_write(res_part, offset, buf, read_len)draw_progress(percent)rename ui_resources.bin to .ok or .badsdcard_unmount()draw_result(true, "Done! Rebooting...")esp_restart()draw_result(false, "FAILED! Power cycle.")probe_sd_files() + draw_menu()

Build header: ui_resources.bin files generated by generate_resources.py carry a 16-byte prefix: bytes 0–3 are "ESP3", bytes 4–15 are a 12-character null-padded variant string identifying firmware type and transport. This enables early detection of wrong-variant binaries. Legacy binaries without the header are accepted with a log warning.


action_boot_partition(label) uses esp_ota_set_boot_partition() to redirect the next boot, then immediately restarts.

Found

OK

Not found

Fails

User selects

Boot app0 or Boot app1

show_status('Booting appX...')

vTaskDelay(500 ms)

esp_partition_find_first(label)

esp_ota_set_boot_partition()

esp_restart()

show_status('appX not found!', RED)

Return to menu


Before rendering the initial menu, probe_sd_files() mounts the SD card, checks for both firmware and resource binaries, then unmounts. Two global flags store the result:

static bool sd_has_fw = false; // /sdcard/esp3dfw.bin exists
static bool sd_has_res = false; // /sdcard/ui_resources.bin exists

draw_sd_indicators() renders a one-line status at y=68 — FW in green when firmware is present, RES in cyan when resources are present — giving the user immediate visual confirmation before selecting an update action.


When compiled with ENABLE_SNAPSHOT, pressing the physical BOOT button (GPIO0) saves a raw screenshot to the SD card. This is a development and diagnostics aid — it works during both the menu display and mid-flash progress screens.

snapshot_take() internals

No

Yes

No

Yes

snapshot_check()

called each loop iteration

GPIO0 low?

vTaskDelay(50 ms) debounce

Still low?

snapshot_take()

Wait for GPIO0 release

+ 50 ms debounce

Return

sdcard_mount()

snap_find_next_number()

Scans snap000.raw to snap999.raw

gfx_snapshot_begin('/sdcard/snapNNN.raw')

Writes header: width(u32-LE) + height(u32-LE)

Pre-fills remaining bytes with zeros

Full screen redraw

draw_menu() or draw_flashing_screen()

draw_progress() if mid-flash

All gfx_* calls also write via snap_write()

gfx_snapshot_end() - close file

Raw file format: 4-byte LE width + 4-byte LE height + width × height × 2 bytes of RGB565 pixel data in row-major order (no compression; 480×320 = 300 KB per file).


factory_log.h provides a two-level logging system with independent compile-time and runtime controls.

ON

OFF

ENABLE_FACTORY_DEBUG_LOG

set in CMakeLists.txt

FACTORY_LOG_LEVEL = 1

FACTORY_LOGD maps to ESP_LOGI

sdkconfig ceiling raised to INFO

FACTORY_LOG_LEVEL = 0

FACTORY_LOGD is a no-op

sdkconfig.prod_log suppresses

IDF startup banners

factory_log_silence_sd_stack() at runtime

esp_log_level_set to NONE for:

sdmmc / vfs_fat_sdmmc / sdmmc_periph

sdmmc_req / sdmmc_common / fatfs

sdspi / sd_diskio

ESP_LOGW and ESP_LOGE are always active regardless of FACTORY_LOG_LEVEL. Only routine info/progress logs (FACTORY_LOGD) are gated. The SD stack silencer is always called because debug builds raise the sdkconfig ceiling globally — making IDF-internal SD chatter visible again — while the factory app only cares about its own logic.


renders via

reads input via

reads / writes SD

manages partitions

deployed by

part of

esp32s3_zx3d50ce02s_usrc_4832

_factory_app_recovery

main.c + factory_log.h

Display sub-module

esp32s3_zx3d50ce02s_usrc_4832

_factory_app_display

st7796_i80.c + gfx.c

Input sub-module

esp32s3_zx3d50ce02s_usrc_4832

_factory_app_input

buttons.c + encoder.c

touch.c + buzzer.c

Flash Tools sub-module

esp32s3_zx3d50ce02s_usrc_4832

_factory_app_flash_tools

flash_all.py + flash_factory.py

Parent module

esp32s3_zx3d50ce02s_usrc_4832

_factory_app

sdcard.c + all sub-modules

ESP-IDF APIs

esp_ota_ops

esp_partition

esp_flash

esp_vfs_fat / sdmmc

See also:


FunctionFileDescription
app_main()main.cRecovery entry point; full hardware init + event loop
restore_otadata_from_backup()main.cRestores OTA table from flash backup; first call in app_main
boot_partition(label)main.cSets OTA boot target by label and triggers esp_restart()
action_boot_partition(label)main.cUI wrapper: shows status message, calls boot_partition()
action_sd_update(label)main.cFull SD → OTA flash flow for application partitions
action_sd_update_res()main.cFull SD → direct-write flash flow for ui_resources data partition
probe_sd_files()main.cMounts SD, checks for esp3dfw.bin / ui_resources.bin, unmounts
draw_menu()main.cFull screen redraw: header + partition info + items + footer + button bar
draw_menu_item(index)main.cRenders one menu row with optional selection highlight box
draw_header()main.cClears screen, draws double border, title string, separator
draw_footer_zone()main.cRenders status message or default “Power off to cancel” text
draw_sd_indicators()main.cRenders FW (green) / RES (cyan) labels based on SD probe state
draw_button_hints()main.cRenders 3 virtual button circles with icons and separator line
draw_button_hint_at(cx,cy,btn,color)main.cDraws one button circle + icon at given center coordinates
draw_button_hint_pressed(btn)main.cBriefly recolors one virtual button for press feedback
draw_progress(percent)main.cUpdates the progress bar during flash operations
draw_result(ok, msg)main.cShows success or failure result after a flash operation
draw_flashing_screen()main.cShows “Flashing… Do NOT power off!” full-screen warning
menu_move(direction)main.cMoves selection by ±1 with wrap-around; clears status
menu_select(index)main.cSets selection index, clears status, redraws affected rows
dispatch_button(btn)main.cMaps BTN_1→up, BTN_2→down, BTN_3→execute; single dispatch point
execute_selected_action()main.cInvokes the action of the currently highlighted menu item
touch_hint_hit_test(x, y)main.cMaps touch coordinates to virtual BTN_1/2/3 or BTN_NONE
show_status(msg, color)main.cStores and renders a status message in the footer zone
clear_status()main.cClears status message; footer reverts to “Power off to cancel”
snapshot_take()main.cSaves a raw RGB565 screenshot to SD (#ifdef ENABLE_SNAPSHOT)
snapshot_check()main.cNon-blocking GPIO0 poll; triggers snapshot_take() on press
snap_find_next_number()main.cScans SD for next available snapNNN.raw filename
factory_log_silence_sd_stack()factory_log.hSilences ESP-IDF SD/FAT log tags at runtime via esp_log_level_set

Unlike boards with a physical boot-hold button (e.g., pibot_pendant_v1_0 with its custom bootloader hook), this board has no custom bootloader. Entry to the recovery partition is triggered exclusively by the main firmware’s [ESP444]FACTORY command (esp444.cpp), which:

  1. Backs up the current OTA partition table entries to flash at 0xB000
  2. Calls esp_ota_set_boot_partition() targeting the factory partition
  3. Restarts the device

On the next boot, the ESP-IDF bootloader loads the factory app, and app_main() restores the OTA table backup before rendering the recovery menu. Power-cycling from recovery therefore returns the device to its original firmware slot without any further user action.