Skip to content

esp32s3_8048s070c Factory App

The esp32s3_8048s070c_factory_app is the recovery partition application for the ESP32-S3 8048S070C board — a 800×480 RGB-panel pendant display with capacitive touch (GT911). It runs from a dedicated factory flash partition and provides a touch-navigable recovery menu for:

  • Selecting which OTA partition (app0 / app1) to boot into
  • Flashing updated firmware (esp3dfw.bin) from SD card to app0 or app1
  • Flashing updated UI resources (ui_resources.bin) from SD card to the ui_resources partition
  • Restoring OTA boot-selection metadata (otadata) that the main firmware backed up before entering recovery

This module follows the same source architecture as all other factory apps in this repository (e.g. pibot_pendant_v1_0_factory_app, factory_app), but has several board-specific adaptations:

  • Touch-only navigation: all physical button, encoder, and buzzer pins are GPIO_NUM_NC — their drivers are compiled in and null-safe, but no-op at runtime.
  • EK9716 RGB parallel display driver (not SPI like st7796 or ili9341 used on other boards).
  • GT911 I2C capacitive touchscreen (not resistive XPT2046 used on some other boards).
  • No custom bootloader hook: ENABLE_CUSTOM_BOOT_LOADER is OFF. Recovery is triggered exclusively via the [ESP444]FACTORY command in the main firmware, which writes the OTA backup and switches the boot partition programmatically before rebooting.

virtual button

hit-test

no-op - NC pins

no-op - NC pins

beep on touch press

(no-op - NC pin)

app_main()

restore_otadata_from_backup()

EK9716 Display Init

ek9716_init() + ek9716_backlight()

gfx.c

Software Graphics Library

Recovery Menu

Draw & Event Loop

Input Layer

touch.c

GT911 / I2C (active)

buttons.c

GPIO_NUM_NC guards - no-op

encoder.c

GPIO_NUM_NC guards - no-op

buzzer.c

GPIO_NUM_NC guard - no-op

sdcard.c

SPI SD Mount / Unmount

Menu Actions

action_boot_partition()

esp_ota_set_boot_partition()

action_sd_update()

OTA flash from SD

action_sd_update_res()

Partition write from SD

ESP OTA API

esp_ota_begin / write / end

esp_partition API

erase_range / write


boards/esp32s3_8048s070c/Factory/
├── main/
│ ├── main.c — Recovery menu, OTA logic, input dispatch, main loop
│ ├── gfx.c / gfx.h — Lightweight software graphics primitives
│ ├── touch.c / touch.h — GT911 capacitive touch polling driver
│ ├── buttons.c / buttons.h — GPIO button driver (null-safe for NC pins)
│ ├── buzzer.c / buzzer.h — Bit-bang buzzer driver (null-safe for NC pin)
│ ├── encoder.c / encoder.h — PCNT rotary encoder driver (null-safe for NC pins)
│ ├── sdcard.c / sdcard.h — SPI SD card mount / unmount (FAT over VFS)
│ └── factory_log.h — Compile-time-gated debug logging macros
└── tools/
├── flash_all.py — Complete board flash (bootloader + partitions + factory ± firmware)
└── flash_factory.py — Flash factory partition only

main.c — Recovery Application Entry Point

Section titled “main.c — Recovery Application Entry Point”

app_main() is the FreeRTOS application entry point. Its startup sequence initialises hardware and the recovery menu, then runs the main input/dispatch loop.

Menu LoopHardware InitOTA Restoreapp_main()Power-on / RebootMenu LoopHardware InitOTA Restoreapp_main()Power-on / RebootReads magic at 0xB000+0x40Restores otadata entries if validErases backup sector when doneloop[Main event loop]FreeRTOS schedules app_mainrestore_otadata_from_backup()ek9716_init() + ek9716_backlight(true)gfx_init()buttons_init() [early-exit, all NC]encoder_init() [early-exit, all NC]touch_init() [GT911 over I2C - active]buzzer_init() [early-exit, NC]probe SD partitions + probe_sd_files()draw_menu()snapshot_check() [if ENABLE_SNAPSHOT]encoder_read() [returns 0 - NC]button_wait_press(100ms) [BTN_NONE - NC]touch_read()touch_hint_hit_test(x, y)dispatch_button(vbtn)

The recovery menu is built dynamically at startup by probing available flash partitions and SD card contents:

Menu ItemActionShow Condition
Boot app0esp_ota_set_boot_partition + restartAlways
Boot app1esp_ota_set_boot_partition + restartOnly if app1 exists in flash
SD -> app0Flash esp3dfw.bin to app0 via OTA APIAlways
SD -> app1Flash esp3dfw.bin to app1 via OTA APIOnly if app1 exists
SD -> resourcesFlash ui_resources.bin to ui_resources partitionAlways

The selected item is highlighted with a blue outline box and bright-blue text. Navigation is performed through three virtual button icons drawn in a bar at the screen bottom.

┌────────────────────────────────────────────────────────────────────┐ y=0
│ Recovery vX.Y.Z (header) │ y=13
│ ───────────────────────────────────────────────────────────────── │ y=40
│ Active: app0 (OTA status) │ y=45
│ SD: FW RES (SD indicators) │ y=68
│ ───────────────────────────────────────────────────────────────── │ y=88
│ ┌──────────────────────────────────────┐ MENU_START_Y=94 │
│ │ Boot app0 │ MENU_ITEM_H=34 │
│ ├──────────────────────────────────────┤ │
│ │▓▓ SD -> app0 (selected) ▓▓▓▓▓▓▓▓│ blue highlight │
│ ├──────────────────────────────────────┤ │
│ │ SD -> resources │ │
│ └──────────────────────────────────────┘ │
│ ───────────────────────────────────────────────────────────────── │ STATUS_Y-7
│ Power off to cancel (footer / status) │ STATUS_Y
│ ───────────────────────────────────────────────────────────────── │ BTN_HINT_BASE_Y
│ ↑ (BTN_1) ↓ (BTN_2) ✓ (BTN_3) (virtual buttons) │
└────────────────────────────────────────────────────────────────────┘ y=479
x=0 x=400 x=799

Orientation note: the physical panel is 800×480 landscape, but the pendant enclosure mounts it rotated 90°. The gfx/LVGL logical canvas is therefore treated as portrait internally (matching the reference design used for other 480-wide boards). Layout constants are scaled ×0.85 from those reference boards to match this board’s wider physical canvas.


A lightweight software renderer that writes directly to the EK9716 display via ek9716_flush(). No full framebuffer is held in RAM; each drawing call immediately pushes pixels to the panel. A single static line_buf[SCREEN_WIDTH] provides a row-level scratch buffer to batch horizontal runs.

if ENABLE_SNAPSHOT

and s_snap_file != NULL

main.c

Draw calls

gfx.c

Primitives

gfx_clear / gfx_fill_rect

gfx_rect / gfx_hline / gfx_vline

gfx_draw_char / gfx_draw_string

gfx_flush() - internal wrapper

ek9716_flush()

RGB565 big-endian to RGB panel

snap_write()

RGB565 native-endian to SD file

All pixel data is byte-swapped from native to big-endian RGB565 before being sent to ek9716_flush() (the panel’s wire format). snap_write() un-swaps back to native endian for the file format.

When compiled with ENABLE_SNAPSHOT, every gfx_flush() call also writes into an open snapshot file on the SD card:

  • gfx_snapshot_begin(filepath) — opens the file, writes an 8-byte header (uint32_t width LE + uint32_t height LE), then pre-fills the pixel area with zeros.
  • Any subsequent gfx_flush() call seeks to the correct file offset and writes that region.
  • gfx_snapshot_end() — closes and finalises the file.
  • gfx_snapshot_is_capturing() — returns true while a capture is in progress.
  • In the main loop, snapshot_check() debounce-polls GPIO0 (the BOOT button) and calls snapshot_take() on press. snapshot_take() triggers a full screen redraw so the capture file contains a complete frame.
  • Files are named snap000.raw, snap001.raw, … with the counter persisting across presses within a single boot.

Snapshot file format:

Offset Size Content
0 4 uint32_t width (little-endian)
4 4 uint32_t height (little-endian)
8 W×H×2 RGB565 pixels, native endian, row-major

Thin polling wrapper over the shared touch_drivers touch_gt911 component, using the same hardware paths as the main firmware’s board_init.c.

typedef struct {
bool pressed;
int16_t x; // calibrated screen coordinates (0 … SCREEN_WIDTH-1)
int16_t y; // calibrated screen coordinates (0 … SCREEN_HEIGHT-1)
} touch_point_t;

Coordinate scaling: the GT911’s x_max / y_max read back from device registers does not match the physical 800×480 panel resolution on this hardware. Raw coordinates are rescaled in touch_read():

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

touch_hint_hit_test(x, y) maps a touch coordinate to one of three virtual navigation buttons. The column boundaries are the midpoints between the actual icon centre positions (BTN_HINT_CX1, BTN_HINT_CX2, BTN_HINT_CX3) rather than thirds of the screen width — the icons are clustered around the horizontal centre of the wide landscape canvas, so equal-width thirds would misalign the tap targets.

touch_read()

x, y, pressed

touch_hint_hit_test(x, y)

BTN_NONE

y is above BTN_HINT_BASE_Y

BTN_1 up-arrow

x is left of boundary_1_2

BTN_2 down-arrow

between boundary_1_2 and boundary_2_3

BTN_3 check-mark

x is right of boundary_2_3

On a confirmed touch in the hint bar:

  1. draw_button_hint_pressed(vbtn) redraws the icon in BTN_PRESSED_COLOR (violet).
  2. buzzer_beep_short() is called (no-op on this board).
  3. 80 ms delay for visual feedback.
  4. draw_button_hints() redraws the bar in its normal colours.
  5. dispatch_button(vbtn) executes the mapped action.

Standard active-low GPIO input driver with 50 ms software debounce and release detection. The pin_bit() helper returns 0ULL for any GPIO_NUM_NC pin, preventing invalid bitmask construction. buttons_init() exits immediately when the composite pin mask is zero (all three buttons are NC on this board), so no GPIO is misconfigured.

button_wait_press(timeout_ms):

  • Polls in 20 ms increments up to timeout_ms.
  • On a press, waits an additional 50 ms debounce, re-checks, then waits for release + 50 ms release debounce.
  • Called with timeout_ms = 100 in the main loop to remain encoder-responsive without blocking.

Uses the ESP32 PCNT (Pulse Counter) peripheral for full-quadrature decoding. ENCODER_A_PIN and ENCODER_B_PIN are both GPIO_NUM_NC on this board; encoder_init() skips PCNT setup and encoder_read() returns 0 unconditionally.

When pins are populated on other boards:

  • High/low PCNT limits of ±1000 prevent counter wrap-around.
  • 1000 ns glitch filter matches the main firmware setting.
  • Quadrature decoding: Channel A edges on pin A, level on pin B; Channel B edges on pin B, level on pin A.
  • 4 pulses per detent (PULSES_PER_DETENT).
  • Counter is cleared after each non-zero read; any sub-detent remainder is discarded (acceptable at ~100 ms polling intervals).
  • CW rotation → menu_move(-1) (up); CCW → menu_move(+1) (down).

Bit-banged square-wave output at 2700 Hz for 40 ms using esp_rom_delay_us(). BUZZER_PIN is GPIO_NUM_NC on this board; both buzzer_init() and buzzer_beep_short() return early without touching GPIO. When populated on other boards, the buzzer provides audible confirmation feedback on virtual button taps.


SPI-based SD card using ESP-IDF’s esp_vfs_fat_sdspi_mount() / esp_vfs_fat_sdcard_unmount(). A static spi_bus_inited flag ensures the SPI bus is initialised only once, even across repeated mount / unmount cycles. The SPI bus is intentionally kept initialised after sdcard_unmount() to avoid interfering with the TFT display’s SPI bus.

Mount configuration highlights:

  • format_if_mount_failed = false — never format an unrecognised card.
  • max_files = 4 — sufficient for firmware read + rename operations.
  • disk_status_check_enable = true — detect card removal.

ON

OFF

called in app_main regardless

called in app_main regardless

CMakeLists.txt

ENABLE_FACTORY_DEBUG_LOG

FACTORY_LOG_LEVEL = 1

FACTORY_LOGD maps to ESP_LOGI(...)

FACTORY_LOG_LEVEL = 0

FACTORY_LOGD compiles to nothing

factory_log_silence_sd_stack()

esp_log_level_set per tag to NONE

sdmmc / vfs_fat_sdmmc / sdmmc_periph

sdmmc_req / sdmmc_common

fatfs / sdspi / sd_diskio

ESP_LOGW / ESP_LOGE are always active regardless of FACTORY_LOG_LEVEL. ESP-IDF internal startup logs (emitted before app_main()) are silenced by a separate sdkconfig.prod_log overlay applied at build time when ENABLE_FACTORY_DEBUG_LOG is OFF. factory_log_silence_sd_stack() mutes the SD/FAT driver tags at runtime; in debug builds (FACTORY_LOG_LEVEL=1) the sdkconfig ceiling is raised to INFO for everything, which would otherwise flood output with SD stack internals.


This board does not use a custom bootloader hook. Recovery is entered when the main firmware executes the [ESP444]FACTORY command (esp444.cpp), which:

  1. Backs up the two 32-byte otadata entries to OTADATA_BACKUP_OFFSET (0xB000).
  2. Writes the magic word 0xAA55AA55 at OTADATA_BACKUP_OFFSET + 0x40.
  3. Calls esp_ota_set_boot_partition(factory) and reboots.

On every startup the factory app checks for and processes this backup before touching the display:

Flash at 0x10000 (otadata)Flash at 0xB000 (backup sector)factory app_mainFlash at 0x10000 (otadata)Flash at 0xB000 (backup sector)factory app_mainSkip write - otadata stays erasedalt[both entries non-empty (not all 0xFF)][entries are empty]alt[magic == 0xAA55AA55][no valid magic]esp_flash_read(magic at offset +0x40, 4 bytes)valid backup presentread entry1 (32 bytes at +0x00)read entry2 (32 bytes at +0x20)esp_flash_erase_region(0x10000, 4 KB)esp_flash_erase_region(0x11000, 4 KB)esp_flash_write(entry1 to 0x10000)esp_flash_write(entry2 to 0x11000)esp_flash_erase_region(0xB000, 4 KB) to clear magicreturns trueskip restore, returns false

⚠️ Critical constraints for OTADATA_BACKUP_OFFSET (0xB000):

  • Must be after the bootloader end.
  • Must be before the partition table (CONFIG_PARTITION_TABLE_OFFSET = 0xC000).
  • Must be 4 KB-aligned.
  • Must not overlap any partition.

The factory sdkconfig must set CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y because esp_flash_erase_region(NULL, 0xB000, …) targets an address below the first data partition, which ESP-IDF rejects by default. This is intentional: the factory app is a privileged recovery tool.

When porting to a new board, recalculate OTADATA_BACKUP_OFFSET and update both this file and main/core/commands/esp444.cpp with the same value.


fail

ok

fail

ok

invalid

valid

fail

ok

read or write error

all bytes written

fail

ok

execute_selected_action()

sdcard_mount()

fopen esp3dfw.bin or ui_resources.bin

Validate: 0 < size <= partition size

draw_flashing_screen()

Do NOT power off!

esp_ota_begin() for firmware

or esp_partition_erase_range() for resources

Read 1024 B chunks

fread then esp_ota_write or esp_partition_write

draw_progress(percent)

esp_ota_end() + esp_ota_set_boot_partition()

firmware path only

draw_result success

rename to .ok

vTaskDelay 2s

esp_restart()

draw_result failure

rename to .bad

vTaskDelay 3s

probe_sd_files() + draw_menu()

esp_ota_abort(ota_handle)

SD probe: probe_sd_files() mounts the SD card, checks for esp3dfw.bin and ui_resources.bin, sets sd_has_fw / sd_has_res flags, then unmounts. These flags drive the SD indicator row (draw_sd_indicators()), but not whether SD menu items appear — those are always present regardless of card content.

Resources variant check: when flashing ui_resources.bin, the first 16 bytes are inspected for the ESP3 magic. If present, the embedded 12-byte variant string is logged so firmware/resource mismatches can be caught before the flash completes.


enc greater than 0

enc less than 0

enc equals 0

not pressed or outside hint bar

pressed in hint bar

Main Loop Iteration

snapshot_check()

GPIO0 debounce poll - ENABLE_SNAPSHOT only

encoder_read()

returns 0 on this board - NC pins

button_wait_press(100ms)

returns BTN_NONE on this board - NC pins

touch_read()

GT911 polling - active on this board

touch_hint_hit_test(x, y)

virtual button column mapping

draw_button_hint_pressed()

buzzer_beep_short() no-op

vTaskDelay 80ms

draw_button_hints()

dispatch_button(btn_id)

menu_move(-1)

BTN_1 or CW encoder

menu_move(+1)

BTN_2 or CCW encoder

execute_selected_action()

BTN_3


The factory app is an independent ESP-IDF project rooted at boards/esp32s3_8048s070c/Factory/. It shares build-script infrastructure with the main firmware targets on this board; see esp32s3_8048s070c_build for the full variant list and build pipeline.

Terminal window
# From the repository root — using the board's build script:
python boards/esp32s3_8048s070c/build_scripts/build_one.py factory
python boards/esp32s3_8048s070c/build_scripts/build_one.py factory --clean
# Check-only (CMake configure without building):
python boards/esp32s3_8048s070c/build_scripts/build_one.py factory --check
# Directly with idf.py (from the factory app directory):
cd boards/esp32s3_8048s070c/Factory
idf.py -B build/factory build
idf.py -B build/factory flash monitor
FlagEffect
ENABLE_FACTORY_DEBUG_LOGSets FACTORY_LOG_LEVEL=1; enables FACTORY_LOGD → ESP_LOGI output
ENABLE_SNAPSHOTAdds GPIO0 snapshot capture; requires SD card accessible during use
ENABLE_CUSTOM_BOOT_LOADERAlways OFF on this board — no bootloader hook needed

tools/flash_all.py — Complete Board Flash

Section titled “tools/flash_all.py — Complete Board Flash”

Orchestrates a full or partial flash using esptool via subprocess. Three mutually exclusive modes:

ModeCommandFiles Flashed
--recoveryFirst-time / replacement setupbootloader.bin + partitions.bin + factory.bin
--full --fw <bin>Complete image including firmwareAll of --recovery + firmware at app0
--fw <bin>Dev iteration: firmware onlyesp3dfw.bin at 0x20000

Fixed flash address map:

OffsetContent
0x1000bootloader.bin
0xC000partitions.bin (44 KB bootloader region — larger than ESP-IDF’s default 0x8000)
0x20000app0 firmware
auto-detectedfactory.bin (parsed from partitions*.csv; defaults to 0x7A0000 for 8 MB layouts)

Port is auto-detected by scanning for CP210x, CH340, CH910, FTDI USB-UART descriptions; --port overrides.

tools/flash_factory.py — Factory Partition Only

Section titled “tools/flash_factory.py — Factory Partition Only”

Useful for updating the recovery app without disturbing the main firmware. Reads the factory partition offset from the partition table CSV automatically.

Terminal window
python flash_factory.py # auto-detect port and offset
python flash_factory.py --port /dev/ttyUSB0 # specify port
python flash_factory.py --port COM3 --offset 0x7A0000 # manual offset
python flash_factory.py --bin custom_factory.bin # custom binary

ModuleRelationship
esp32s3_8048s070c_bspShares the same physical hardware (EK9716 panel, GT911 touch, hw_config.h pin definitions), but the factory app drives peripherals directly without LVGL
pibot_pendant_v1_0_factory_appPrimary reference design — identical menu architecture and OTA backup mechanism; differs in ILI9341 SPI display, resistive XPT2046 touch, and physical buttons / encoder / buzzer
factory_appCross-board pattern documentation — covers the shared source layout common to all factory apps
esp32s3_8048s070c_buildBuild scripts shared by this factory app and all main firmware variants for this board
touch_driverstouch_gt911 component reused as-is from hardware/common/drivers/touch_gt911
display_drivers_rgbEK9716 RGB parallel panel driver referenced by gfx.c via ek9716_flush()

ConstantValueDescription
OTADATA_OFFSET0x10000OTA data partition flash address
OTADATA_SECTOR_SIZE0x1000Size of each otadata sector (4 KB)
OTADATA_ENTRY_SIZE32Size of one otadata entry in bytes
OTADATA_BACKUP_OFFSET0xB000Backup sector address (between bootloader and partition table)
BACKUP_MAGIC_OFFSET0x40Offset within backup sector for the magic word
BACKUP_MAGIC0xAA55AA55Magic value confirming a valid backup is present
SCREEN_WIDTH800Logical screen width in pixels
SCREEN_HEIGHT480Logical screen height in pixels
FONT_WIDTH12Glyph width in pixels (font12x24)
FONT_HEIGHT24Glyph height in pixels (font12x24)
MENU_START_Y94Y-coordinate of the first menu item
MENU_ITEM_H34Height of each menu item row in pixels
BTN_CIRCLE_R30Radius of virtual button circle icons
BEEP_FREQ_HZ2700Buzzer output frequency
BEEP_DURATION_MS40Buzzer beep duration
DEBOUNCE_MS50Button press / release debounce period
PULSES_PER_DETENT4PCNT raw pulses per encoder detent
FW_FILENAME/sdcard/esp3dfw.binFirmware binary expected on the SD card
RES_FILENAME/sdcard/ui_resources.binUI resources binary expected on the SD card
FW_OK_FILENAME/sdcard/esp3dfw.okFirmware binary renamed to on successful flash
FW_BAD_FILENAME/sdcard/esp3dfw.badFirmware binary renamed to on failed flash