Skip to content

esp32_3248s035c — Factory Recovery Application

esp32_3248s035c — Factory Recovery Application

Section titled “esp32_3248s035c — Factory Recovery Application”

The esp32_3248s035c_factory module is the standalone recovery firmware for the ESP32-3248S035C board — a 320×480 capacitive-touch TFT panel. It lives entirely inside boards/esp32_3248s035c/Factory/ and is compiled as a separate ESP-IDF project, completely independent of the main pendant firmware.

Unlike boards with physical recovery buttons (e.g. pibot_pendant_v1_0), the ESP32-3248S035C has no physical buttons wired at GPIO level (BUTTON_n_PIN = GPIO_NUM_NC). Recovery is entered exclusively via the [ESP444]FACTORY software command from the main firmware, which backs up OTA data and switches the boot partition. The factory app then restores that backup on startup before presenting the touch-navigated menu.

Key responsibilities:

  • Restore OTA partition data written by the main firmware before entry
  • Present a touch-navigated recovery menu on the ST7796 display
  • Flash firmware updates from SD card (esp3dfw.bin → app0 / app1)
  • Flash UI resource updates from SD card (ui_resources.bin → ui_resources)
  • Reboot into the selected OTA partition on demand

FeatureDetail
SoCESP32 (single-core PCNT, no PSRAM)
DisplayST7796 SPI TFT — 320 × 480 px, portrait
TouchGT911 capacitive (I2C) — coordinates pre-calibrated by IC
Physical buttonsNone — BUTTON_1/2/3_PIN = GPIO_NUM_NC
Rotary encoderNone — ENCODER_A/B_PIN = GPIO_NUM_NC
BuzzerNone — BUZZER_PIN = GPIO_NUM_NC
SD cardSPI (separate host from display SPI)
FontBitmap 11 × 21 px (font11x21)
Custom bootloaderNo — boot entry is software-only via [ESP444]

Peripheral stub pattern: The buttons, encoder, and buzzer drivers are initialized and polled in app_main exactly as on boards that do have those parts. Each driver guards against GPIO_NUM_NC and becomes a safe no-op. This lets the Factory source tree stay identical across board variants with minimal #ifdef overhead.


ESP-IDF

Shared Hardware Drivers

Factory App - boards/esp32_3248s035c/Factory/

main.c

app_main · menu system

OTA actions · UI draw

gfx.c

Pixel rendering

Snapshot capture

st7796.c

SPI display driver

touch.c

GT911 polling wrapper

sdcard.c

SPI SD VFS mount

buttons.c

GPIO stubs (NC)

encoder.c

PCNT stubs (NC)

buzzer.c

GPIO stubs (NC)

factory_log.h

Debug log gate

touch_gt911

hardware/common/drivers

bus_i2c

hardware/common/drivers

esp_ota_ops

esp_flash / esp_partition

SPI master driver

esp_vfs_fat / sdspi

pulse_cnt


Input Pipeline

Main loop

encoder_read()

button_wait_press()

touch_read()

touch_hint_hit_test()

dispatch_button()

Rendering Pipeline

ENABLE_SNAPSHOT

draw_menu()

draw_progress()

draw_result()

gfx_clear / gfx_fill_rect

gfx_rect / gfx_hline / gfx_vline

gfx_draw_string

Static line_buf[320]

(avoids heap alloc)

gfx_flush()

(internal)

st7796_flush()

SPI chunked write

snap_write()

File pixel mirror


The app_main function follows a strict ordering. OTA data must be restored before any display or peripheral initialization to guarantee correct partition state even if the device is power-cycled during the recovery session.

app_mainBootloaderMain Firmware(esp444.cpp)app_mainBootloaderMain Firmware(esp444.cpp)Read magic @ 0xB040Restore → 0x10000Erase backup sectorMount SD, checkesp3dfw.bin & ui_resources.binUnmount SDBackup otadata → 0xB000esp_ota_set_boot_partition("factory")esp_restart()Boot factory partitionfactory_log_silence_sd_stack()restore_otadata_from_backup()st7796_init() + backlight ONgfx_init()buttons_init()encoder_init()touch_init() [GT911 via I2C]buzzer_init()Detect app0/app1 partitionsprobe_sd_files()Build menu_items[]draw_menu()Enter main event loop

This mechanism ensures the board can return to the correct OTA application even after power loss during recovery. The main firmware writes the backup; the factory app reads and restores it.

No

Yes

Yes

No

Main firmware receives

[ESP444]FACTORY

esp444.cpp:

Read otadata → 0x10000

Write backup → 0xB000

Write magic 0xAA55AA55 @ 0xB040

esp_ota_set_boot_partition('factory')

esp_restart()

Factory app_main():

restore_otadata_from_backup()

Magic @ 0xB040

== 0xAA55AA55?

Skip restore

(no backup present)

Read entries from 0xB000

Both entries

all 0xFF?

Skip write

(backup was empty)

Erase 0x10000 (2 sectors)

Write entries back

Erase backup sector 0xB000

(prevent double-restore)

Continue init

Flash address constraints (do not change without recalculating):

SymbolValueRole
OTADATA_BACKUP_OFFSET0xB000Where backup is stored
OTADATA_OFFSET0x10000Real OTA data location
BACKUP_MAGIC_OFFSET0x40Magic word offset within backup sector
BACKUP_MAGIC0xAA55AA55Sentinel to detect valid backup

The backup sector sits between bootloader end and the partition table (0xC000). The factory sdkconfig must set CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y because erasing below the first partition is normally blocked by ESP-IDF. These values must match esp444.cpp in the main firmware exactly.


The menu is a simple indexed list of menu_item_t entries, built dynamically at startup based on detected partitions and SD content.

typedef struct {
const char *label; // display text
menu_action_t action; // enum: BOOT_APP0 | BOOT_APP1 | SD_UPDATE_APP0 | ...
uint16_t color; // RGB565 text color (when not selected)
} menu_item_t;
ConditionItem added
AlwaysBoot app0
app1 partition existsBoot app1
AlwaysSD -> app0
app1 partition existsSD -> app1
AlwaysSD -> resources
┌──────────────────────────────┐ ← y=0
│ Recovery v1.x.x │ y=13 (title, cyan)
│──────────────────────────────│ y=40 (separator)
│ Active: app0 │ y=53 (yellow)
│ SD: FW RES │ y=81 (SD indicators)
│──────────────────────────────│ y=104 (separator)
│ ▶ Boot app0 │ y=111 (menu item 0, highlighted)
│ Boot app1 │ y=151 (menu item 1)
│ SD -> app0 │ y=191 (menu item 2)
│ SD -> app1 │ y=231 (menu item 3)
│ SD -> resources │ y=271 (menu item 4)
│ │
│──────────────────────────────│ y=377 (separator before status)
│ Power off to cancel │ y=384 (status / footer zone)
│──────────────────────────────│ y=417 (button bar separator)
│ [↑] [↓] [✓] │ y=449 (circle icon centers)
└──────────────────────────────┘ ← y=480

Because this board has no physical buttons, three virtual button zones span the full bottom bar (y ≥ 417). The tappable columns are much wider than the drawn circles to ensure reliable touch targeting.

Bottom bar (y ≥ 417)

Column 0..106

BTN_1 = UP ↑

Column 107..213

BTN_2 = DOWN ↓

Column 214..320

BTN_3 = OK ✓

touch_read

touch_hint_hit_test

dispatch_button

menu_move

execute_selected_action

Touch press feedback sequence:

  1. Redraw pressed circle in BTN_PRESSED_COLOR (violet)
  2. Call buzzer_beep_short() — silent on this board (buzzer NC)
  3. 80 ms delay for perceptual confirmation
  4. Restore hint bar to normal colors
  5. Call dispatch_button(vbtn)

Both operations follow the same pattern: mount → verify → flash in 1 KB chunks → rename result file → reboot.

Resources Flash - action_sd_update_res()

No

Yes

fail

sdcard_mount()

ui_resources.bin

exists?

show_status: No ui_resources.bin

Read & log 16-byte

build header

('ESP3' + variant string)

Validate size ≤ partition size

draw_flashing_screen()

esp_partition_erase_range()

(entire ui_resources partition)

Loop: fread 1 KB

→ esp_partition_write()

→ draw_progress()

rename → ui_resources.ok

esp_restart()

rename → ui_resources.bad

draw_menu()

Firmware Flash - action_sd_update(label)

No

Yes

fail

sdcard_mount()

esp3dfw.bin

exists?

show_status: No esp3dfw.bin

Validate size ≤ partition size

draw_flashing_screen()

esp_ota_begin()

Loop: fread 1 KB

→ esp_ota_write()

→ draw_progress()

esp_ota_end()

esp_ota_set_boot_partition(label)

rename → esp3dfw.ok

esp_restart()

esp_ota_abort()

rename → esp3dfw.bad

draw_menu()

SD file lifecycle:

SD FileOn successOn failure
esp3dfw.binRenamed → esp3dfw.okRenamed → esp3dfw.bad
ui_resources.binRenamed → ui_resources.okRenamed → ui_resources.bad

The rename-on-result pattern prevents accidental re-flash on the next recovery session while preserving the binary for post-mortem inspection.

ui_resources.bin build header: The resource flash action reads the first 16 bytes of the file before flashing. If the file begins with ESP3, the next 12 bytes are logged as the variant string (FACTORY_LOGD). This allows early detection of a wrong-variant binary (wrong CNC firmware type or transport) without aborting the flash — it is a log-only warning at this time.


All drawing is done via a single static line_buf[320] buffer — no dynamic allocation. Each draw primitive fills the buffer and calls gfx_flush(), which serializes to the ST7796 via SPI and optionally mirrors to the snapshot file.

s_snap_file != NULL

gfx_hline / gfx_fill_rect

gfx_draw_char / gfx_rect

line_buf[320]

big-endian RGB565

gfx_flush(x0,y0,x1,y1,data,count)

st7796_flush()

SPI DMA chunked

snap_write()

fseek + fwrite

per-pixel un-swap

Byte-swap note: RGB565 pixels are stored big-endian for the SPI wire ((color >> 8) | (color << 8)). snap_write reverses this swap before writing to the file so the raw capture file contains native little-endian RGB565 pixels.

The snapshot feature is a debug-only build option. When enabled, pressing GPIO0 (the physical BOOT button, separate from the on-screen virtual buttons) triggers a full-screen capture to the SD card.

SD card fileFull redrawgfx_snapshot_begin/endsnapshot_check()GPIO0 (BOOT btn)SD card fileFull redrawgfx_snapshot_begin/endsnapshot_check()GPIO0 (BOOT btn)Level LOW detected50ms debouncesdcard_mount()snap_find_next_number()gfx_snapshot_begin("/sdcard/snap001.raw")Write header: [width:u32][height:u32]Pre-fill SCREEN_W×SCREEN_H×2 bytes with 0x00draw_menu() OR draw_flashing_screen()+draw_progress()All gfx_flush() calls also invoke snap_write()gfx_snapshot_end()fclose()Wait for release + 50ms debounce

Raw file format:

Offset Size Content
0 4 bytes width (uint32_t, little-endian) = 320
4 4 bytes height (uint32_t, little-endian) = 480
8 307200 B Raw RGB565 pixels, native endian, row-major
Total: 8 + 320×480×2 = 307,208 bytes

Files can be converted to PNG with the boards/pibot_pendant_v1_0/Factory/tools/raw2png/snap2png.py utility (shared across boards).

During a flash operation, s_snap_in_flash = true and s_flash_last_percent track the current state so a snapshot taken mid-flash captures a meaningful progress view rather than a stale menu frame.


The driver in st7796.c is reimplemented standalone — no esp_lcd or esp3d_log dependency — for minimal factory app footprint. The init sequence mirrors the shared hardware/common/drivers/disp_st7796 component used by the main firmware. See display_spi_drivers for the main firmware equivalent.

ST7796 panelESP-IDF SPI masterst7796_init()ST7796 panelESP-IDF SPI masterst7796_init()alt[TFT_RST is valid GPIO][TFT_RST == GPIO_NUM_NC]spi_bus_free() [clear prior state from main fw]spi_bus_initialize(TFT_HOST)spi_bus_add_device(freq=TFT_SPI_FREQ_HZ)RST LOW 10ms → HIGH 120msCMD 0x01 (software reset) + 150msCMD 0x11 (sleep out) + 120msCMD 0x36 MADCTL = 0x48 (portrait, BGR)CMD 0x3A pixel format = 0x55 (RGB565)CMD 0xF0 / 0xE0 / 0xE1 (gamma tuning)CMD 0x29 (display ON) + 50ms

Pixel write (st7796_flush):

  • Sets column address range (0x2A) and row address range (0x2B)
  • Sends memory write command (0x2C)
  • Transmits pixel data in ≤4096-byte SPI polling chunks

Reset handling: On this board variant TFT_RST may be GPIO_NUM_NC. The driver detects this at runtime via GPIO_IS_VALID_GPIO() and falls back to the ST7796 software reset command (0x01), matching the main firmware’s shared driver behavior.


touch.c is a thin polling wrapper over the shared hardware/common/drivers/touch_gt911 component. Unlike the resistive sibling (esp32_3248s035r) which requires runtime calibration via calibrate() and raw read_reg12() reads, the GT911 reports pre-calibrated, pixel-accurate coordinates directly. See touch_drivers for the full driver API.

touch_init

bus_i2c_init

touch_gt911_configure

touch_read

touch_gt911_read

touch_point_t

{pressed, x, y}

The touch_gt911_config_t passes TOUCH_SWAP_XY_FLAG, TOUCH_MIRROR_X_FLAG, and TOUCH_MIRROR_Y_FLAG from hw_config.h directly to the shared driver — no factory-app-level coordinate remapping is needed.


The ESP32-3248S035C ships without physical buttons, encoder, or buzzer. All three drivers protect against GPIO_NUM_NC with the same pin_bit() guard pattern used throughout the codebase:

static uint64_t pin_bit(gpio_num_t pin) {
return GPIO_IS_VALID_GPIO(pin) ? (1ULL << pin) : 0ULL;
}

This prevents a compile-time negative shift warning on GPIO_NUM_NC (= -1) when the value is used in 1ULL << pin expressions. The drivers remain fully functional if pins are ever populated.

Behaviorbuttons_init()encoder_init()buzzer_init()
Pin mask = 0Return early, no gpio_configReturn early (!GPIO_IS_VALID_GPIO)Return early
Read callbutton_wait_press() → BTN_NONE after timeoutencoder_read() → 0buzzer_beep_short() → immediate return

factory_log.h provides a compile-time debug gate independent of sdkconfig log levels, controlled by the ENABLE_FACTORY_DEBUG_LOG CMake option.

Build modeFACTORY_LOG_LEVELFACTORY_LOGD behavior
Production (OFF)0Compiled out (zero overhead)
Debug (ON)1Expands to ESP_LOGI

factory_log_silence_sd_stack() is the very first call in app_main. It mutes chatty ESP-IDF SD subsystem tags at runtime, preventing them from polluting the UART output even when FACTORY_LOG_LEVEL=1 raises the sdkconfig log ceiling to INFO globally.

Silenced tagSubsystem
sdmmc, sdmmc_periph, sdmmc_req, sdmmc_commonSD host driver
sdspi, sd_diskioSPI SD interface
vfs_fat_sdmmc, fatfsFAT VFS layer

Yes

No

enc gt 0

enc lt 0

enc eq 0

BTN_NONE

BTN_1 or BTN_2 or BTN_3

pressed and new press

BTN_NONE

BTN_1 or BTN_2 or BTN_3

not pressed or held

BTN_1

BTN_2

BTN_3

Enter main loop

ENABLE_SNAPSHOT:

GPIO0 pressed?

encoder_read()

returns ±1 click

button_wait_press(100ms)

returns BTN_NONE on this board

touch_read() - non-blocking

touch_hint_hit_test(x, y)

Draw pressed circle

buzzer_beep_short()

80ms delay

Restore hints

dispatch_button(btn)

menu_move(-1)

menu_move(+1)

execute_selected_action()

snapshot_take

The 100 ms button_wait_press timeout on a board with no physical buttons is intentional — it provides a non-blocking poll interval that keeps the encoder responsive while the button driver returns BTN_NONE immediately on each call.


Featureesp32_3248s035c (this)esp32_3248s035rpibot_pendant_v1_0
DisplayST7796 SPI 320×480ST7796 SPI 320×480ILI9341 SPI 240×320
TouchGT911 capacitive (no calibration)XPT2046 resistive + calibrate()FT5x06 capacitive
Physical buttonsNone (NC)None (NC)Physical GPIO
Rotary encoderNone (NC)None (NC)Physical PCNT
BuzzerNone (NC)None (NC)Physical GPIO
Custom bootloaderNoNoYes
Recovery entry[ESP444] software only[ESP444] software onlyButton hold at boot OR [ESP444]
Font size11 × 21 px11 × 21 px8 × 16 px (reference scale)
Snapshot triggerGPIO0 (BOOT btn)GPIO0 (BOOT btn)GPIO0 (BOOT btn)

FilePurpose
Factory/main/main.cEntry point, menu system, OTA actions, all UI drawing
Factory/main/gfx.cGraphics primitives, font rendering, snapshot capture
Factory/main/st7796.cStandalone ST7796 SPI driver (init, flush, backlight)
Factory/main/touch.cGT911 I2C polling wrapper
Factory/main/touch.htouch_point_t type definition
Factory/main/sdcard.cSD card SPI mount/unmount via VFS FAT
Factory/main/buttons.cGPIO button driver (no-op stub on this board)
Factory/main/buzzer.cBit-banged buzzer driver (no-op stub on this board)
Factory/main/encoder.cPCNT quadrature decoder (no-op stub on this board)
Factory/main/factory_log.hCompile-time debug gate + SD stack silencer
Factory/main/hw_config.hBoard-specific GPIO/SPI/I2C pin definitions
Factory/main/version.hFirmware version string

  • esp32_3248s035c_bsp — BSP for the main pendant firmware on this board (LVGL, GT911 touch, control events)
  • esp32_3248s035c_build_scripts — Build variant scripts for this board
  • esp32_3248s035r_factory — Resistive-touch sibling with XPT2046 calibration
  • pibot_pendant_v1_0_factory_app — Reference implementation with custom bootloader, ILI9341, and physical buttons
  • display_spi_drivers — Shared ST7796 / ILI9341 drivers used by the main firmware
  • touch_drivers — Shared GT911 / XPT2046 driver components
  • docs/Factory/ — Factory app and bootloader design notes
  • docs/guides/board_build_guidelines.md — Board build and flash procedures
  • docs/guides/ui_resources_guide.md — How to build and flash ui_resources.bin