Skip to content

esp32s3_8048s050c_factory

The esp32s3_8048s050c_factory module is the recovery/factory partition application for the ESP32S3-8048S050C board. It runs as a standalone firmware image from a dedicated factory partition, entirely separate from the main pendant application. Its primary purpose is to provide a minimal, safe environment for:

  • Restoring OTA boot data backed up by the main firmware before it transferred control to the factory partition
  • Flashing updated firmware (esp3dfw.bin) or UI resources (ui_resources.bin) from an SD card
  • Selecting the active boot partition (app0 / app1)

This board has no physical buttons, no buzzer, and no rotary encoder in its default configuration — all three are defined as GPIO_NUM_NC. Navigation is entirely touch-based via three virtual on-screen buttons. The code is structurally identical to the reference implementation in pibot_pendant_v1_0_factory_app.md so it works unchanged if this board variant is ever populated with those physical parts.

The factory app is entered exclusively via the software [ESP444]FACTORY trigger in the main firmware. Unlike boards such as pibot_pendant_v1_0, there is no custom bootloader hook because there is no physical button available to detect at boot time. See docs/Factory/factory_app_technical_doc.md for general factory-partition design context.


PropertyValue
BoardESP32S3-8048S050C (5.0-inch RGB parallel panel, capacitive touch)
MCUESP32-S3
Partitionfactory (dedicated recovery image)
Display DriverST7262, 16-bit RGB parallel, PSRAM framebuffer
Touch ControllerGT911, I²C
Physical ButtonsNone (GPIO_NUM_NC)
Rotary EncoderNone (GPIO_NUM_NC)
BuzzerNone (GPIO_NUM_NC)
SD CardSPI interface
Custom BootloaderNot used (no physical boot-hold button on this board)
Source Pathboards/esp32s3_8048s050c/Factory/main/
FilePurpose
main.cApp entry point, OTA restore, menu system, all actions, main loop
gfx.cSoftware graphics primitives (static line buffer + optional snapshot routing)
st7262.cThin wrapper over the shared RGB parallel panel driver
touch.cGT911 capacitive touch driver wrapper with coordinate rescaling
touch.htouch_point_t structure and API
sdcard.cSD card mount/unmount via SPI VFS
buttons.cGPIO button driver (graceful no-ops when all pins are GPIO_NUM_NC)
buzzer.cBit-bang buzzer driver (graceful no-ops when BUZZER_PIN is GPIO_NUM_NC)
encoder.cPCNT-based rotary encoder (graceful no-ops when pins are GPIO_NUM_NC)
factory_log.hCompile-time log gate (FACTORY_LOGD) and SD stack silencer

ESP-IDF / Flash Layer

Shared Hardware Components

Board-Level Drivers

Factory Application · factory partition

main.c · app_main()

gfx.c · Graphics Primitives

Menu System

draw_menu / dispatch_button

OTA Manager

restore_otadata / action_boot_partition

SD Actions

action_sd_update / action_sd_update_res

Snapshot System

ENABLE_SNAPSHOT (optional)

st7262.c · RGB Parallel Display

touch.c · GT911 I²C Touch

sdcard.c · SPI SD Card

buttons.c · (GPIO_NUM_NC on this board)

buzzer.c · (GPIO_NUM_NC on this board)

encoder.c · (GPIO_NUM_NC on this board)

hardware/drivers_video_rgb/

disp_st7262

hardware/common/drivers/

touch_gt911

hardware/common/drivers/

bus_i2c

esp_ota_ops

esp_partition

esp_flash

(direct sector R/W)

esp_vfs_fat

SD VFS layer


The startup order is deliberate: OTA data is restored before the display is initialised so that even a display failure leaves the boot chain intact.

Menu SystemSD Card (probe)Touch / Buttons / Encoder / Buzzergfx.cST7262 DisplayOTA / Flashapp_main()ESP-IDF BootloaderMenu SystemSD Card (probe)Touch / Buttons / Encoder / Buzzergfx.cST7262 DisplayOTA / Flashapp_main()ESP-IDF BootloaderRead magic at OTADATA_BACKUP_OFFSET+0x40If valid: copy entries back to 0x10000Erase backup sector regardlessmount → fopen esp3dfw.bin / ui_resources.bin → unmountTransfer control to factory partitionfactory_log_silence_sd_stack()restore_otadata_from_backup()true (restored) / false (no backup)st7262_init()ESP_OK (abort() on failure)st7262_backlight(true)gfx_init()buttons_init() [no-op - GPIO_NUM_NC]encoder_init() [no-op - GPIO_NUM_NC]touch_init() [GT911 via I²C]buzzer_init() [no-op - GPIO_NUM_NC]Detect app1 partition presenceprobe_sd_files()draw_menu()Enter main event loop

app_main() is the sole entry point. After hardware init it enters a bare while(1) that polls all three input sources and calls the unified dispatch_button(button_id_t):

[optional] snapshot_check() — GPIO0 (BOOT button), ENABLE_SNAPSHOT
encoder_read() → menu_move(±1)
button_wait_press(100 ms timeout) → dispatch_button()
touch_read()
→ touch_hint_hit_test(x, y)
→ draw_button_hint_pressed(vbtn) — visual feedback
→ buzzer_beep_short() — audible feedback (no-op here)
→ 80 ms delay
→ draw_button_hints() — restore hint bar
→ dispatch_button(vbtn)

dispatch_button() maps button IDs to menu actions:

ButtonAction
BTN_1menu_move(-1) — scroll up
BTN_2menu_move(+1) — scroll down
BTN_3execute_selected_action() — confirm

Before rendering anything, the app checks for a backup written by the main firmware’s esp444.cpp. The backup uses a 4-byte magic sentinel (0xAA55AA55) at OTADATA_BACKUP_OFFSET + BACKUP_MAGIC_OFFSET.

No

Yes

Yes

No

Yes

No

Yes

No

Read magic at 0xB040

magic == 0xAA55AA55?

Return false - no backup

Read entry1 from 0xB000

Read entry2 from 0xB020

Both entries all 0xFF?

Erase backup sector at 0xB000

Erase 0x10000 (4 KB)

Erase 0x11000 (4 KB)

entry1 not empty?

Write entry1 to 0x10000

entry2 not empty?

Write entry2 to 0x11000

Return true - restored

Flash offset table — must match esp444.cpp in the main firmware exactly:

SymbolOffsetConstraint
OTADATA_OFFSET0x10000Standard ESP-IDF OTA data partition
OTADATA_SECTOR_SIZE0x10004 KB per OTA entry slot
OTADATA_BACKUP_OFFSET0xB000After bootloader end; before partition table (0xC000)
BACKUP_MAGIC_OFFSET0x40Stored at OTADATA_BACKUP_OFFSET + 0x40
BACKUP_MAGIC0xAA55AA55Sentinel value

⚠️ CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y is required in the factory sdkconfig. esp_flash_erase_region() targets 0xB000, which is below the first partition. ESP-IDF aborts such calls by default (CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS). The factory app is an intentionally privileged recovery tool.

boot_partition(label) and action_boot_partition(label)

Section titled “boot_partition(label) and action_boot_partition(label)”

boot_partition() calls esp_partition_find_first() → esp_ota_set_boot_partition() → esp_restart(). action_boot_partition() wraps it with status display and handles the partition-not-found error case.


st7262.c is a thin wrapper over the shared hardware/drivers_video_rgb/disp_st7262 component (see display_rgb_drivers.md). Key properties of this integration:

  • 16-bit RGB parallel bus — 16 data GPIO lines + HSYNC / VSYNC / DE / PCLK / backlight
  • PSRAM framebuffer mandatory (fb_in_psram = true) — the RGB panel controller DMA-fetches pixels from PSRAM continuously; esp_lcd_panel_draw_bitmap() copies data into that buffer
  • Native landscape orientation — DISP_ST7262_ORIENTATION_LANDSCAPE is a pure passthrough with no swap_xy / mirror. This contrasts with esp32s3_4827s043c, where the panel is physically mounted rotated 90° and requires orientation correction in the driver
  • Byte-swap in st7262_flush() — gfx.c stores pixels in SPI big-endian RGB565 (shared convention across all boards). The RGB parallel bus requires native RGB565. st7262_flush() swaps each pixel into a local scratch[SCREEN_WIDTH] buffer before calling esp_lcd_panel_draw_bitmap(). Using a scratch buffer — rather than swapping in-place — prevents corruption on multi-row fill operations where gfx_hline() / gfx_fill_rect() reuse the same line buffer across multiple successive st7262_flush() calls
PSRAM Framebufferesp_lcd_panel_draw_bitmap()scratch[SCREEN_WIDTH]st7262_flush()gfx.c (caller)PSRAM Framebufferesp_lcd_panel_draw_bitmap()scratch[SCREEN_WIDTH]st7262_flush()gfx.c (caller)RGB panel DMA reads continuously → displaydata[] big-endian RGB565, pixel countswap each pixel to native RGB565(x0, y0, x1+1, y1+1, scratch)copy pixels to framebuffer region

gfx.c provides all drawing primitives above the panel driver. A single static line_buf[SCREEN_WIDTH] is reused for all horizontal fill operations to avoid per-row allocation.

FunctionDescription
gfx_init()No-op (snapshot is purely on-demand)
gfx_clear(color)Full-screen fill using line-buffer horizontal scan
gfx_draw_char(x,y,c,fg,bg)12×24 bitmap font character (clipped to screen bounds)
gfx_draw_string(x,y,str,fg,bg)Null-terminated string via repeated gfx_draw_char()
gfx_hline(x,y,w,color)Horizontal line — one gfx_flush() call
gfx_vline(x,y,h,color)Vertical line — one gfx_flush() per row
gfx_rect(x,y,w,h,color)Rectangle outline via four line calls
gfx_fill_rect(x,y,w,h,color)Filled rectangle via horizontal line scan
gfx_snapshot_begin(path)Open snapshot file, write 8-byte header, pre-fill with zeros
gfx_snapshot_end()Close snapshot file
snap_write(...)Route pixel data to correct byte offset in snapshot file

All draw calls funnel through the internal gfx_flush() wrapper, which calls st7262_flush() and, when a snapshot is active, also calls snap_write().


All three input sources converge on a single dispatch_button(button_id_t) call:

clicks > 0 → BTN_1

clicks < 0 → BTN_2

BTN_1 / BTN_2 / BTN_3

BTN_1

BTN_2

BTN_3

Rotary Encoder

encoder_read()

Physical Buttons

button_wait_press(100 ms)

Touch

touch_read()

touch_hint_hit_test(x, y)

dispatch_button()

menu_move(-1)

menu_move(+1)

execute_selected_action()

Wraps the shared touch_gt911 component (see touch_drivers.md) and bus_i2c. The GT911 reports its own x_max / y_max from internal configuration registers. On this board, these values do not match the physical panel resolution (measured approximately 468×253 on real hardware). Raw coordinates are rescaled in touch_read():

pt.x = (int)data.x * SCREEN_WIDTH / touch_gt911_get_x_max();
pt.y = (int)data.y * SCREEN_HEIGHT / touch_gt911_get_y_max();

touch_hint_hit_test(x, y) — Virtual Button Touch Zones

Section titled “touch_hint_hit_test(x, y) — Virtual Button Touch Zones”

Three virtual buttons are drawn at BTN_HINT_CX1, BTN_HINT_CX2, and BTN_HINT_CX3 (clustered around horizontal screen centre). Hit zones span the full hint bar height. Column boundaries use midpoints between adjacent button centres — not equal screen-width thirds — because equal thirds would map the wrong touch column to each button on a wide landscape canvas where all icons are clustered near centre:

int boundary_1_2 = (BTN_HINT_CX1 + BTN_HINT_CX2) / 2;
int boundary_2_3 = (BTN_HINT_CX2 + BTN_HINT_CX3) / 2;

Computes a combined GPIO pin mask at runtime with GPIO_IS_VALID_GPIO(). If all three button pins are GPIO_NUM_NC, the mask is 0 and buttons_init() returns immediately without configuring any GPIO. button_wait_press() then times out on every poll cycle, adding only the configured timeout latency to the main loop.

Guards against GPIO_NUM_NC on both encoder pins before creating a PCNT unit. When populated, uses full quadrature decoding with PULSES_PER_DETENT = 4 and clears the hardware counter after each read. The 100 ms button_wait_press() timeout in the main loop sets the effective encoder polling interval.

Guards against GPIO_NUM_NC in both buzzer_init() and buzzer_beep_short(). When populated, generates a 2700 Hz square wave for 40 ms using esp_rom_delay_us() — no LEDC/PWM peripheral is required.


Mounts to /sdcard via esp_vfs_fat_sdspi_mount(). The SPI bus is initialised once and kept active across mount/unmount cycles — releasing and re-initialising it can interfere with the display SPI bus on shared-bus boards. The card pointer is set to NULL on unmount to permit clean remounting after card swaps.

Files managed by the factory app:

PathRole
/sdcard/esp3dfw.binMain firmware image to flash
/sdcard/esp3dfw.okRenamed from .bin after a successful firmware flash
/sdcard/esp3dfw.badRenamed from .bin after a failed firmware flash
/sdcard/ui_resources.binUI resources partition image
/sdcard/ui_resources.okRenamed from .bin after a successful resources flash
/sdcard/ui_resources.badRenamed from .bin after a failed resources flash
/sdcard/snap###.rawScreen snapshots (optional, ENABLE_SNAPSHOT)

probe_sd_files() mounts, checks for both files, then unmounts. The resulting sd_has_fw and sd_has_res flags drive the SD indicator line in the menu header.


The menu is a flat list of menu_item_t entries with an immediate-mode highlighted cursor. There is no retained widget state.

menu_item_t

+const char* label

+menu_action_t action

+uint16_t color

«enumeration»

menu_action_t

MENU_ACTION_BOOT_APP0

MENU_ACTION_BOOT_APP1

MENU_ACTION_SD_UPDATE_APP0

MENU_ACTION_SD_UPDATE_APP1

MENU_ACTION_SD_UPDATE_RES

Screen Layout (logical canvas, portrait orientation in pendant enclosure)

Section titled “Screen Layout (logical canvas, portrait orientation in pendant enclosure)”
┌──────────────────────────────────────┐ y=0
│ Recovery vX.Y.Z │ Title (CYAN)
│ ──────────────────────────────── │ y=40 separator
│ Active: app0 │ y=45 active partition (YELLOW)
│ SD: FW RES │ y=68 SD file indicators
│ ──────────────────────────────── │ y=88 separator
│ ┌──────────────────────────────┐ │ y=94 MENU_START_Y
│ │ Boot app0 │ │ 34 px per item
│ ├──────────────────────────────┤ │
│ │ SD -> app0 ◀ HIGHLIGHTED │ │ selected item
│ ├──────────────────────────────┤ │
│ │ SD -> resources │ │
│ └──────────────────────────────┘ │
│ ──────────────────────────────── │ STATUS_Y − 7 separator
│ Power off to cancel │ STATUS_Y footer / status message
│ ──────────────────────────────── │ BTN_HINT_BASE_Y separator
│ [↑ circle] [↓ circle] [✓ circle] │ Virtual button hints
└──────────────────────────────────────┘ y=SCREEN_HEIGHT

Menu items are built dynamically from partition detection results:

Item labelCondition
Boot app0Always present
Boot app1Only if app1 partition exists in flash
SD -> app0Always present
SD -> app1Only if app1 partition exists in flash
SD -> resourcesAlways present

action_sd_update(target_label) — Firmware Flash Flow

Section titled “action_sd_update(target_label) — Firmware Flash Flow”

No

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Yes

Find partition by label

Partition found?

show_status: Partition not found

sdcard_mount()

Mounted?

show_status: No SD card!

fopen /sdcard/esp3dfw.bin

File exists?

show_status: No esp3dfw.bin on SD!

fseek → get file size

Valid size

> 0 and ≤ partition size?

show_status: Invalid firmware size!

draw_flashing_screen()

esp_ota_begin()

OTA begin OK?

show_status: OTA begin failed!

Read/write loop: 1 KB chunks

draw_progress(percent)

All bytes written?

esp_ota_end()

esp_ota_set_boot_partition()

All OK?

rename → .bad

draw_result(false)

probe_sd_files / draw_menu

rename → .ok

draw_result(true)

esp_restart()

action_sd_update_res() — Resources Flash

Section titled “action_sd_update_res() — Resources Flash”

Follows the same structure but targets the ui_resources data partition using esp_partition_erase_range() + esp_partition_write() instead of the OTA API. Before erasing, it reads and logs an optional 16-byte build header (magic ESP3 + 12-char variant string written by generate_resources.py) to enable early detection of variant mismatches without destructive side effects.


Snapshot System (Optional — ENABLE_SNAPSHOT)

Section titled “Snapshot System (Optional — ENABLE_SNAPSHOT)”

When enabled, GPIO0 (the BOOT button) acts as a snapshot trigger. This pin is always available on ESP32-S3 modules regardless of board peripheral population.

Raw file format:

BytesContent
0–3SCREEN_WIDTH as little-endian uint32_t
4–7SCREEN_HEIGHT as little-endian uint32_t
8 … endRaw RGB565 pixels, native byte order, row-major

Capture mechanism: gfx_snapshot_begin() opens the output file, writes the 8-byte header, and pre-fills the pixel area with zeros. The caller then performs a full screen redraw — every gfx_flush() call routes pixel data concurrently to the display and to snap_write(). snap_write() reverses the big-endian byte convention used internally by gfx.c back to native RGB565 before writing at the correct file offset for each row.

Displaygfx.c (all draw calls)SD Cardsnapshot_take()snapshot_check()GPIO0 (BOOT button)Displaygfx.c (all draw calls)SD Cardsnapshot_take()snapshot_check()GPIO0 (BOOT button)File opened, 8-byte header written, zeros pre-filledFile closed - s_snapshot_count++level == 0 (pressed)50 ms debouncecall snapshot_take()sdcard_mount()gfx_snapshot_begin("/sdcard/snapFull redraw (draw_menu or draw_flashing_screen + draw_progress)st7262_flush() per regionsnap_write() per region (un-swap bytes, fseek to row offset, fwrite)gfx_snapshot_end()Wait for GPIO0 release + 50 ms debounce

Snapshots can be converted to PNG using snap2png.py from a board that ships the tool (e.g. boards/esp32s3_8048s070c/Factory/tools/raw2png/snap2png.py).


factory_log.h provides a two-tier log gate independent of sdkconfig log levels:

Macro / CallFACTORY_LOG_LEVEL == 1FACTORY_LOG_LEVEL == 0
FACTORY_LOGD(tag, fmt, …)→ ESP_LOGI()→ empty do {} while(0)
ESP_LOGE / ESP_LOGWAlways activeAlways active
factory_log_silence_sd_stack()Silences 8 SD/FAT tags at runtimeSilences 8 SD/FAT tags at runtime

FACTORY_LOG_LEVEL is set at compile time by ENABLE_FACTORY_DEBUG_LOG in Factory/CMakeLists.txt. ESP-IDF internal logs emitted before app_main() (startup banner, partition scan, flash driver init) are silenced separately via sdkconfig.prod_log, applied by cmake/targets.cmake when debug logging is off — they cannot be gated at runtime because they fire before app_main() executes.

See esp3d_log_guide.md for the main firmware logging patterns.


Internal Flash

CPU (stack buffer)

SD Card (SPI VFS)

fread

esp_ota_write

esp_ota_set_boot_partition + esp_restart()

/sdcard/esp3dfw.bin

binary read · 1 KB chunks

uint8_t buf[1024]

app0 / app1

OTA partition

PSRAM

st7262.c · st7262_flush()

gfx.c

gfx_flush() passes pointer

(v >> 8) | (v << 8) per pixel

line_buf[SCREEN_WIDTH]

static · big-endian RGB565

scratch[SCREEN_WIDTH]

native RGB565 after byte-swap

esp_lcd_panel_draw_bitmap()

RGB Panel Framebuffer

continuous DMA to LCD

main.c

touch.c

GT911 Hardware

Raw x / y

0..x_max / 0..y_max

(~468 × 253 reported by device)

x = raw_x × SCREEN_WIDTH / x_max

y = raw_y × SCREEN_HEIGHT / y_max

touch_hint_hit_test(x, y)

→ BTN_1 / BTN_2 / BTN_3 / BTN_NONE


CMake Feature Flags (Factory/CMakeLists.txt)

Section titled “CMake Feature Flags (Factory/CMakeLists.txt)”
FlagEffect
ENABLE_FACTORY_DEBUG_LOGFACTORY_LOG_LEVEL=1; raises sdkconfig log ceiling to INFO
ENABLE_SNAPSHOTAdds GPIO0-triggered screen capture to SD
ENABLE_CUSTOM_BOOT_LOADERNot used on this board
KeyRequired ValueReason
CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWEDyesp_flash_erase_region() targets address below first partition (0xB000)
CONFIG_SPIRAMyST7262 RGB panel mandates PSRAM framebuffer (fb_in_psram = true)

ModuleRelationship
esp32s3_8048s050c_bsp.mdSibling — BSP for the main firmware on the same board; shares hw_config.h, disp_st7262, and touch_gt911 but occupies a different flash partition
esp32s3_8048s043c_factory.mdNear-identical sibling — same factory architecture (ST7262 + GT911, native landscape) for the 4.3-inch variant
pibot_pendant_v1_0_factory_app.mdReference implementation — structurally identical; differs only in panel driver (ILI9341 vs ST7262), touch controller (XPT2046 vs GT911), and peripheral population
esp32s3_8048s070c_factory_app.mdSibling — same factory pattern for the 7-inch variant
display_rgb_drivers.mdDependency — disp_st7262 component wrapped by st7262.c
touch_drivers.mdDependency — touch_gt911 and bus_i2c components wrapped by touch.c
esp32s3_8048s050c_build_scripts.mdSibling — Python build scripts for this board’s firmware variants

Boards with a physical BOOT/RESET button (e.g. pibot_pendant_v1_0) detect a long-press in a custom bootloader hook to autonomously switch to the factory partition. The ESP32S3-8048S050C has no such button, so the factory partition is entered only via the main firmware’s [ESP444]FACTORY command, which:

  1. Backs up the current OTA data to OTADATA_BACKUP_OFFSET
  2. Calls esp_ota_set_boot_partition("factory") + esp_restart()

The restore_otadata_from_backup() logic is identical across all boards regardless of how they enter the factory partition.

Double Byte-Swap Between gfx.c and st7262.c

Section titled “Double Byte-Swap Between gfx.c and st7262.c”

gfx.c is shared across all factory apps in this project. It applies the SPI big-endian byte swap once, matching the native pixel expectation of every SPI-connected panel (ILI9341, ST7796, …). The ST7262 RGB parallel bus is the only panel type requiring native little-endian RGB565, so st7262.c reverses the swap locally in st7262_flush() — keeping gfx.c unchanged across all board variants.

SD update menu items are always built regardless of whether probe_sd_files() found the files at startup. The SD probe is a best-effort check; the user may insert the SD card after boot. When a file is absent at action time, action_sd_update() and action_sd_update_res() display a clear error status and return without any destructive side effects.