Skip to content

esp32s3_bzm_tft35_gt911_factory_app_recovery

esp32s3_bzm_tft35_gt911_factory_app_recovery

Section titled “esp32s3_bzm_tft35_gt911_factory_app_recovery”

Recovery partition application for the ESP32-S3 BZM TFT35 GT911 board — a 320×480 portrait-format panel with a GT911 capacitive touchscreen. This module contains the core recovery logic (main.c) and the compile-time debug-logging gate (factory_log.h). It is one of four child modules of esp32s3_bzm_tft35_gt911_factory_app.

The recovery app presents a touch-navigable menu that lets an operator:

  • Boot directly into app0 or app1
  • Flash a new firmware image (esp3dfw.bin) from SD card to app0 or app1
  • Flash new UI resources (ui_resources.bin) from SD card to the ui_resources partition

Because this board has no physical buttons or encoder, navigation is done exclusively through three large virtual touch zones rendered in the bottom bar of the screen (↑ / ↓ / ✓).


  1. Module Architecture
  2. Hardware Context
  3. Entry Point & Startup Sequence
  4. OTA Backup & Restore Mechanism
  5. Menu System
  6. UI Layout & Rendering
  7. Input Handling
  8. SD Card Update Flow
  9. Snapshot Feature (Debug)
  10. Logging System
  11. Key Constants & Configuration
  12. Component Dependency Map

This recovery module is the application-level orchestrator. It depends on three peer sibling modules plus shared ESP-IDF APIs:

esp32s3_bzm_tft35_gt911_factory_app (parent)

st7796_init/backlight/flush

gfx_init/draw_*/gfx_snapshot_*

buttons_init / button_wait_press

encoder_init / encoder_read

touch_init / touch_read

sdcard_mount / sdcard_unmount

esp_ota_* / esp_flash_*

esp_partition_*

Flashes factory image

over USB/UART

esp32s3_bzm_tft35_gt911_factory_app_recovery

(main.c + factory_log.h)

← THIS MODULE

esp32s3_bzm_tft35_gt911_factory_app_display

(st7796.c + gfx.c)

esp32s3_bzm_tft35_gt911_factory_app_input

(buttons.c + encoder.c

+ touch.c / touch.h)

esp32s3_bzm_tft35_gt911_factory_app_flash_tools

(flash_all.py + flash_factory.py)

ESP-IDF OTA & Flash APIs

ModuleFilesResponsibility
recovery (this module)main.c, factory_log.hApp entry point, menu logic, OTA backup/restore, SD flash, touch virtual buttons
displayst7796.c, gfx.cST7796 SPI init, pixel rendering, snapshot capture
inputbuttons.c, encoder.c, touch.c, touch.hPhysical buttons (no-ops), quadrature encoder (no-ops), GT911 capacitive touch
flash_toolsflash_all.py, flash_factory.pyPC-side scripts to flash the factory partition over USB

See esp32s3_bzm_tft35_gt911_factory_app_display.md and esp32s3_bzm_tft35_gt911_factory_app_input.md for sibling module details.


FeatureDetail
SoCESP32-S3
Display3.5″ TFT, 320×480 px, portrait
Display controllerST7796 via SPI
Touch controllerGT911 capacitive multi-touch (I2C)
Physical buttonsNone — all BUTTON_n_PIN = GPIO_NUM_NC
Rotary encoderNone — ENCODER_A/B_PIN = GPIO_NUM_NC
BuzzerPresent in init; pin may be NC
SD cardSPI-attached microSD via sdcard.c
Screen coordinatesOrigin top-left; rotation via SCREEN_ROTATION MADCTL byte

Key implication: The board’s entire user interaction path runs through the GT911 touch layer. buttons_init() and encoder_init() are called for cross-board code compatibility but both silently skip GPIO setup when pins are GPIO_NUM_NC.

Layout scaling: The factory app layout is scaled ×4/3 relative to the 240×320 reference board (pibot_pendant_v1_0 / esp32_2432s028r). For example:

  • Font size: 8×16 → 11×21 px
  • Button circle radius: 20 → 27 px
  • Menu item height: 30 → 40 px

The recovery app is stored in the factory OTA partition and is booted by the main firmware via the [ESP444]FACTORY command (esp444.cpp). Unlike other boards, this board has no custom bootloader hook (no physical button to hold at power-on). The ENABLE_CUSTOM_BOOT_LOADER flag is not set in its CMakeLists.txt.

app_main()ESP-IDF Bootloaderesp444.cppMain Firmwareapp_main()ESP-IDF Bootloaderesp444.cppMain Firmware[ESP444]FACTORY commandWrite otadata backup to 0xB000esp_ota_set_boot_partition("factory")esp_restart()Boot factory partitionfactory_log_silence_sd_stack()restore_otadata_from_backup()st7796_init() + st7796_backlight(true)gfx_init()buttons_init() [no-op on this board]encoder_init() [no-op on this board]touch_init() [GT911 via I2C]buzzer_init()Detect app0 / app1 partitionsprobe_sd_files()draw_menu()Enter main event loop
  1. Silence SD log chatter — calls factory_log_silence_sd_stack() before any SD mount operations.
  2. Restore otadata — calls restore_otadata_from_backup() to reinstate the original boot partition metadata. This ensures a power-cycle from inside the recovery app returns to the correct OTA slot, not the factory partition.
  3. Display init — st7796_init() configures the SPI bus, sends the MADCTL / pixel-format / gamma init sequence to the ST7796 controller, then st7796_backlight(true) enables the backlight. Aborts on failure (200 ms delay before abort() to flush UART).
  4. GFX init — gfx_init() (lightweight; snapshot file state is on-demand).
  5. Peripheral init — buttons, encoder, touch, buzzer — all safe no-ops if pins are GPIO_NUM_NC.
  6. Partition discovery — esp_partition_find_first() checks for app1; sets has_app1.
  7. Menu build — Constructs menu_items[] array: always Boot app0, SD→app0, SD→resources; adds app1 variants when present.
  8. SD probe — probe_sd_files() mounts/unmounts SD to check for esp3dfw.bin and ui_resources.bin.
  9. Draw menu — draw_menu() renders the full screen.
  10. Main loop — polls encoder, buttons (via button_wait_press(100 ms)), and touch; dispatches to dispatch_button().

When the main firmware executes [ESP444]FACTORY, it:

  1. Writes the two active OTA data entries (each 32 bytes) into a backup sector at flash offset 0xB000.
  2. Writes a magic word 0xAA55AA55 at backup_offset + 0x40.
  3. Calls esp_ota_set_boot_partition("factory") and reboots.

The recovery app restores this backup at startup, before any user interaction, so that:

  • A power cycle during or after recovery boots back into the original OTA partition.
  • Explicitly booting app0/app1 from the recovery menu calls esp_ota_set_boot_partition() again before rebooting — this is a separate, intentional operation.
0x0000 Bootloader
...
0xB000 ← OTADATA_BACKUP_OFFSET (must sit in this gap, 4 KB-aligned)
...
0xC000 Partition table (CONFIG_PARTITION_TABLE_OFFSET)
0xD000 NVS partition (first app partition)
...
0x10000 OTA data (OTADATA_OFFSET) — two consecutive 4 KB sectors

Warning: esp_flash_erase_region(NULL, OTADATA_BACKUP_OFFSET, ...) targets an address below the first partition. ESP-IDF aborts this by default (CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS). The factory sdkconfig must set CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y. This is intentional — the factory app is a privileged recovery tool that manages flash directly.

Porting note: When adapting to another board, recalculate OTADATA_BACKUP_OFFSET for its partition layout and update both this file and the main firmware’s esp444.cpp with the same value.

≠ 0xAA55AA55

or read error

== 0xAA55AA55

Yes

No

Start restore_otadata_from_backup

Read magic word

at 0xB000 + 0x40

Return false

no backup present

Read 2 × 32-byte OTA

entries from 0xB000

Both entries

all 0xFF?

Skip restore

goto clear_backup

Erase two otadata sectors

0x10000 and 0x11000

Write back non-empty entries

Erase backup sector 0xB000

Return true


typedef struct {
const char *label; // Display string
menu_action_t action; // One of MENU_ACTION_*
uint16_t color; // Foreground text color when not selected
} menu_item_t;

menu_action_t values:

ConstantTriggered action
MENU_ACTION_BOOT_APP0action_boot_partition("app0")
MENU_ACTION_BOOT_APP1action_boot_partition("app1")
MENU_ACTION_SD_UPDATE_APP0action_sd_update("app0")
MENU_ACTION_SD_UPDATE_APP1action_sd_update("app1")
MENU_ACTION_SD_UPDATE_RESaction_sd_update_res()

draw_menu()

BTN_2 / encoder CCW\nmenu_move(+1)

BTN_3 / touch OK\nexecute_selected_action()

BOOT_APP0 / BOOT_APP1

SD_UPDATE_APP0 / SD_UPDATE_APP1

SD_UPDATE_RES

esp_restart()

partition not found\ndraw_menu() + show_status()

Flash OK → esp_restart()

Flash failed → draw_menu()

Flash OK → esp_restart()

Flash failed → draw_menu()

MenuIdle

ActionRunning

BootAction

SDUpdate

ResUpdate

FunctionBehaviour
menu_move(direction)Adjusts menu_selected with wrap-around; clears any status message; redraws old and new items
menu_select(index)Sets menu_selected; clears status if set; redraws old and new items only (partial update)
execute_selected_action()Dispatches to the appropriate action function via switch on menu_items[menu_selected].action
dispatch_button(btn)Maps BTN_1→up, BTN_2→down, BTN_3→execute; unified entry point for physical and touch events

┌────────────────────────────────┐ y=0
│ Double border (2 px) │
│ "Recovery <version>" │ y=13
│────────────────────────────────│ y=40 (separator)
│ "Active: <ota_label>" │ y=53
│ "SD: FW RES" (indicators) │ y=81
│────────────────────────────────│ y=104 (separator)
│ │
│ Menu item 0 (highlighted) │ y=111
│ Menu item 1 │ y=151
│ Menu item 2 │ y=191
│ Menu item 3 │ y=231
│ Menu item 4 │ y=271
│ │
│────────────────────────────────│ STATUS_Y - 7
│ Status / "Power off..." │ STATUS_Y
│────────────────────────────────│ BTN_HINT_BASE_Y
│ [↑ BTN_1] [↓ BTN_2] [✓ BTN_3]│ circle cy = BTN_HINT_CY
└────────────────────────────────┘ y=480

STATUS_Y = SCREEN_HEIGHT - 33 - BTN_HINT_H

BTN_HINT_H = 2×27 + 9 = 63 px

FunctionDraws
draw_header()Clears screen, double border, title string, separator at y=40
draw_menu()Full screen: header + active OTA label + SD indicators + all menu items + footer + button hints
draw_menu_item(index)Single item: fills background, optional highlight box, label text
draw_sd_indicators()”SD: FW RES” with per-token colors (FW=green, RES=cyan)
draw_footer_zone()Active status message, or grey “Power off to cancel” when no status is set
draw_button_hints()Grey separator line + three circle-with-icon virtual buttons
draw_button_hint_at(cx, cy, btn, color)Double-outline circle + directional arrow or checkmark icon
draw_button_hint_pressed(btn)Redraws that button hint in violet for transient press feedback
draw_flashing_screen()Header + “Flashing…” title + “Do NOT power off!” warning
draw_progress(percent)Outlined progress bar filled in green + percentage text
draw_result(success, msg)”Success! Rebooting…” (green) or “FAILED! Power cycle.” (red)
show_status(msg, color)Updates last_status_msg / last_status_color and redraws footer
clear_status()Clears last_status_msg and redraws footer

Icons are drawn with raw gfx_hline() calls — no font or image resources are used, making the factory app fully self-contained with no dependency on the ui_resources partition:

ButtonIconConstruction
BTN_1 (↑)Up arrow7-row widening triangle head + 10-row 7 px-wide stem
BTN_2 (↓)Down arrow10-row stem + 7-row narrowing triangle head
BTN_3 (✓)CheckmarkShort left arm to vertex + long right arm, 4 px thick

The circle outline uses the integer midpoint (Bresenham) algorithm via draw_circle(). Each button draws a double outline (radius r and r-1) for visual weight.

SymbolRGB565 valueUsage
MENU_HIGHLIGHTRGB(0, 80, 160)Selected item background box
MENU_HIGHLIGHT_TXTRGB(80, 160, 255)Selected item text
BTN_NAV_COLORRGB(100, 160, 255)Up/Down button circle outlines
BTN_OK_COLORRGB(100, 220, 100)Confirm button circle outline
BTN_PRESSED_COLORRGB(180, 80, 220)Transient press feedback (violet)

All three input sources converge on dispatch_button():

CW → BTN_1

CCW → BTN_2

BTN_1/2/3

(or BTN_NONE after timeout)

touch_hint_hit_test(x,y)

→ BTN_1 / BTN_2 / BTN_3

BTN_1

BTN_2

BTN_3

Rotary Encoder

encoder_read()

[returns 0 on this board]

Physical Buttons

button_wait_press(100ms)

[returns BTN_NONE]

GT911 Touch

touch_read()

dispatch_button(btn)

Visual + buzzer feedback

(80 ms)

menu_move(-1) ↑

menu_move(+1) ↓

execute_selected_action()

touch_hint_hit_test(x, y) maps the full-width hint bar into three equal tap zones. The tap area is deliberately wider than the drawn circles for easier touch on the small panel:

┌──────────────┬──────────────┬──────────────┐
│ x < 107 │ 107 ≤ x < 213│ x ≥ 213 │
│ BTN_1 │ BTN_2 │ BTN_3 │
│ ↑ │ ↓ │ ✓ │
└──────────────┴──────────────┴──────────────┘
y ≥ BTN_HINT_BASE_Y (entire hint-bar height)

Any touch with y < BTN_HINT_BASE_Y returns BTN_NONE.

Touch press-and-hold is prevented by tracking touch_was_pressed across loop iterations. Only the transition from not-pressed to pressed dispatches an action, preventing repeated triggers while a finger rests on the screen.

Physical Button and Encoder Behaviour on This Board

Section titled “Physical Button and Encoder Behaviour on This Board”

buttons_init() detects that all BUTTON_n_PIN = GPIO_NUM_NC (pin mask == 0) and skips GPIO config entirely. button_wait_press(100 ms) therefore always returns BTN_NONE after the timeout, acting only as a 100 ms poll delay that keeps the encoder check responsive.

encoder_init() similarly detects invalid pins (!GPIO_IS_VALID_GPIO) and leaves s_initialized = false; encoder_read() returns 0 unconditionally. The loop’s encoder branch has zero hardware overhead on this board.


SD pathPurposeRenamed after flash
/sdcard/esp3dfw.binFirmware image to flash→ esp3dfw.ok (success) or esp3dfw.bad (failure)
/sdcard/esp3dfw.okMarker of last successful firmware flash—
/sdcard/ui_resources.binUI resources image to flash→ ui_resources.ok or ui_resources.bad

action_sd_update(target_label) — Firmware Flash

Section titled “action_sd_update(target_label) — Firmware Flash”

Uses the ESP-IDF OTA API (esp_ota_begin / esp_ota_write / esp_ota_end). Calls esp_ota_abort if any step fails.

not found

found

fail

ok

fail

ok

invalid

valid

fail

ok

no

yes

fail

ok

fail

ok

action_sd_update

Find APP partition

by label

show_status: Partition not found

sdcard_mount

show_status: No SD card!

fopen esp3dfw.bin

show_status: No esp3dfw.bin!

Get file size

validate ≤ partition size

show_status: Invalid firmware size!

draw_flashing_screen

esp_ota_begin

show_status: OTA begin failed!

Read + write loop

1 KB chunks

draw_progress per %

All written OK?

esp_ota_abort

draw_result false

esp_ota_end

esp_ota_set_boot_partition

draw_result true

rename .bin → .ok

sdcard_unmount

esp_restart

rename .bin → .bad

sdcard_unmount

delay 3 s

probe_sd_files

draw_menu

action_sd_update_res() — Resources Flash

Section titled “action_sd_update_res() — Resources Flash”

Uses esp_partition_erase_range() + esp_partition_write() directly instead of the OTA API, because the ui_resources partition is a data partition, not an app partition. The same file-size validation, 1 KB chunk loop, progress bar, and rename-on-success/failure pattern applies.

Build variant check: The first 16 bytes of ui_resources.bin are read before flashing. If the first 4 bytes are "ESP3", bytes 4–15 contain the build variant string (written by generate_resources.py). This is logged via FACTORY_LOGD to enable early detection of mismatched resource archives — a wrong variant will flash without error but may render incorrectly in the main firmware.

draw_progress(percent) is only called when the integer percentage changes (percent != last_percent), minimising SPI bus traffic during the write loop. The function always redraws the progress bar outline to support full-screen redraws triggered by the optional snapshot feature.


Controlled at compile time by ENABLE_SNAPSHOT in Factory/CMakeLists.txt. When enabled, GPIO0 (the BOOT button on the ESP32-S3 module) acts as a snapshot trigger.

SD cardgfx.csnapshot_take()snapshot_check()GPIO0 (BOOT button)SD cardgfx.csnapshot_take()snapshot_check()GPIO0 (BOOT button)Every gfx_hline/fill_rect/draw_string call\nwrites pixels to both display AND the open fileLevel = 0 (pressed)50 ms debounce + re-checksnapshot_take()sdcard_mount()snap_find_next_number() → Ngfx_snapshot_begin("/sdcard/snapNNN.raw")Write 8-byte header (width, height)Pre-fill with zeros (full frame)Full redraw (draw_menu or draw_flashing_screen + draw_progress)gfx_snapshot_end()fclose()Wait for GPIO0 release + 50 ms debounce

Raw binary, no compression:

OffsetSizeContent
04 bytesScreen width (little-endian uint32)
44 bytesScreen height (little-endian uint32)
8width × height × 2 bytesRGB565 pixels, row-major order

Files are named snap000.raw, snap001.raw, … snap_find_next_number() scans for the first missing index at the start of the first snapshot to avoid overwriting earlier captures.

s_snap_in_flash stateWhat gets redrawn
false (menu is visible)draw_menu() + active status message (if any)
true (SD flash in progress)draw_flashing_screen() + draw_progress(s_flash_last_percent)

snapshot_check() is non-blocking when GPIO0 is high, so it can be called freely in the main loop and inside the flash write loop without impacting throughput when the button is not pressed.


Two-level log gate, independent of the sdkconfig log level:

FACTORY_LOG_LEVELFACTORY_LOGD(tag, ...) expands to
0 (production, default)do {} while (0) — zero runtime overhead
≠ 0 (debug)ESP_LOGI(tag, ...)

FACTORY_LOG_LEVEL is injected by -DFACTORY_LOG_LEVEL=1 in Factory/CMakeLists.txt when the ENABLE_FACTORY_DEBUG_LOG option is ON.

ESP_LOGW / ESP_LOGE calls throughout main.c are always active and never gated, ensuring unexpected errors are always visible regardless of build mode.

Called first in app_main(), before any SD operation. Silences the following ESP-IDF log tags at runtime via esp_log_level_set(..., ESP_LOG_NONE):

sdmmc, vfs_fat_sdmmc, sdmmc_periph, sdmmc_req, sdmmc_common, fatfs, sdspi, sd_diskio

Why this is needed in debug builds: Setting FACTORY_LOG_LEVEL=1 raises the sdkconfig compile-time log ceiling to INFO for all tags. Without this runtime suppression, every SD mount/unmount floods the UART with internal SD-stack messages that obscure the recovery app’s own logic.

Complementary mechanism: ESP-IDF internal logs emitted before app_main() (SPI flash scan, partition table, startup banner) cannot be silenced at runtime. These are handled separately in production builds by sdkconfig.prod_log, applied via cmake/targets.cmake.


ConstantValueDescription
OTADATA_OFFSET0x10000Base address of the two 4 KB OTA data sectors
OTADATA_SECTOR_SIZE0x1000Size of one OTA data sector (4 KB)
OTADATA_ENTRY_SIZE32Size of one OTA data entry in bytes
OTADATA_BACKUP_OFFSET0xB000Sector used as backup (must match esp444.cpp)
BACKUP_MAGIC_OFFSET0x40Byte offset of the magic word within the backup sector
BACKUP_MAGIC0xAA55AA55Sentinel value confirming a valid backup exists

Critical: OTADATA_BACKUP_OFFSET must be kept in sync with the main firmware’s esp444.cpp. Both files must use identical values.

ConstantValueDescription
MENU_START_Y111Y coordinate of the first menu item’s top edge
MENU_ITEM_H40Height of each menu item slot in pixels
MENU_PAD_X20Left/right padding for menu items
FONT_WIDTH11Pixels per character column (8×16 font scaled ×4/3)
FONT_HEIGHT21Pixels per character row
BTN_CIRCLE_R27Virtual button circle radius in pixels
BTN_HINT_H63Total height of the virtual button bar (2×27+9)
BTN_HINT_BASE_YSCREEN_HEIGHT - 63Top Y of the virtual button bar
BTN_HINT_CX1SCREEN_WIDTH/2 - 100X centre of BTN_1 circle
BTN_HINT_CX2SCREEN_WIDTH/2X centre of BTN_2 circle
BTN_HINT_CX3SCREEN_WIDTH/2 + 100X centre of BTN_3 circle
STATUS_YSCREEN_HEIGHT - 33 - BTN_HINT_HY of the status / footer text
ConstantValue
FW_FILENAME/sdcard/esp3dfw.bin
FW_OK_FILENAME/sdcard/esp3dfw.ok
FW_BAD_FILENAME/sdcard/esp3dfw.bad
RES_FILENAME/sdcard/ui_resources.bin
RES_OK_FILENAME/sdcard/ui_resources.ok
RES_BAD_FILENAME/sdcard/ui_resources.bad

ESP-IDF APIs

esp32s3_bzm_tft35_gt911_factory_app_input

esp32s3_bzm_tft35_gt911_factory_app_display

esp32s3_bzm_tft35_gt911_factory_app_recovery

draw_* calls

hw init + backlight

pixel data via SPI

main.c

app_main

restore_otadata_from_backup

action_boot_partition

action_sd_update

action_sd_update_res

menu_move / menu_select

draw_menu / draw_*

dispatch_button

touch_hint_hit_test

probe_sd_files

snapshot_* / snap_find_next_number

factory_log.h

FACTORY_LOGD macro

factory_log_silence_sd_stack()

st7796.c

st7796_init

st7796_backlight

st7796_flush

lcd_cmd / lcd_data / lcd_data_byte

gfx.c

gfx_init / gfx_clear

gfx_draw_string / gfx_fill_rect

gfx_hline / gfx_rect / gfx_flush

gfx_snapshot_begin / end / is_capturing

buttons.c

buttons_init

button_wait_press

button_is_pressed

encoder.c

encoder_init

encoder_read

touch.c + touch.h

touch_init

touch_read

touch_point_t

esp_ota_ops.h

esp_ota_begin / write / end

esp_ota_abort

esp_ota_set_boot_partition

esp_ota_get_boot_partition

esp_flash.h

esp_flash_read / write

esp_flash_erase_region

esp_partition.h

esp_partition_find_first

esp_partition_erase_range

esp_partition_write

sdcard.c

sdcard_mount

sdcard_unmount

buzzer.c

buzzer_init

buzzer_beep_short

Data Flow: Touch Event → Recovery Action

Section titled “Data Flow: Touch Event → Recovery Action”
gfx.c / st7796.cAction (boot / flash)dispatch_button()main.c (loop)touch.cGT911 Hardwaregfx.c / st7796.cAction (boot / flash)dispatch_button()main.c (loop)touch.cGT911 HardwareI2C touch data readytouch_read() → {pressed=true, x, y}Leading-edge check (touch_was_pressed == false)touch_hint_hit_test(x, y) → BTN_3draw_button_hint_pressed(BTN_3) [violet]buzzer_beep_short() + vTaskDelay(80 ms)draw_button_hints() [restore colors]dispatch_button(BTN_3)execute_selected_action()draw_flashing_screen()draw_progress(%) [per KB written]esp_restart() on success\nor draw_menu() on failure
DocumentDescription
esp32s3_bzm_tft35_gt911_factory_app.mdParent module — full factory app overview
esp32s3_bzm_tft35_gt911_factory_app_display.mdST7796 driver + GFX rendering layer
esp32s3_bzm_tft35_gt911_factory_app_input.mdGT911 touch, buttons, encoder drivers
esp32s3_bzm_tft35_gt911_factory_app_flash_tools.mdPC-side flash scripts
esp32s3_bzm_tft35_gt911_bsp.mdBoard Support Package (main firmware context)
factory_bootloader.mdCustom bootloader hooks (other boards — not applicable here)