Skip to content

esp32s3_hmi43v3_factory_app_main

The esp32s3_hmi43v3_factory_app_main module is the application logic core of the factory/recovery firmware for the ESP32S3-HMI43V3 board. It runs from the dedicated factory flash partition and provides a touch-navigable recovery menu that allows developers and field operators to:

  • Boot a specific OTA application partition (app0 or app1)
  • Flash a new firmware image from an SD card (esp3dfw.bin → app0 or app1)
  • Flash a UI resources image from an SD card (ui_resources.bin → ui_resources partition)
  • Restore the OTA metadata backup written by the main firmware before entering recovery

Board-specific characteristics:

  • No physical buttons, encoder, or buzzer — all are GPIO_NUM_NC on this board. Navigation is exclusively through the GT911 capacitive touchscreen via three virtual button icons rendered at the bottom of the display.
  • RM68120 display driven over the Intel 8080 (i80) parallel bus, unlike most other boards in the BSP family which use SPI.
  • TCA9554 IO expander (I²C) controls the LCD backlight enable bit and the GT911 touch reset bit — it must be initialized before the display driver and the touch controller.
  • Software-only factory entry: the partition is entered only via the [ESP444]FACTORY software command from the main firmware. There is no custom bootloader hook (ENABLE_CUSTOM_BOOT_LOADER = OFF) because there is no physical BOOT button to hold at reset.
  • Logical canvas: the glass is physically 480 × 272 (landscape), but the pendant enclosure mounts it rotated 90°, so the gfx logical canvas is 272 × 480 portrait — the same topology as esp32_3248s035r and esp32s3_bzm_tft35_gt911. Layout constants are scaled ×0.85 from the 320 × 480 portrait reference.

See esp32s3_hmi43v3_bsp for the board-level BSP used by the main firmware. See display_i80_drivers for the RM68120 panel driver.


This module is a child of esp32s3_hmi43v3_factory_app and contains two source files:

FilePurpose
boards/esp32s3_hmi43v3/Factory/main/main.cApplication entry point, menu logic, OTA operations, UI rendering
boards/esp32s3_hmi43v3/Factory/main/factory_log.hCompile-time debug log gate and SD stack silencer
ModuleResponsibility
esp32s3_hmi43v3_factory_app_displayFramebuffer GFX API (gfx.c) + RM68120 flush-ready callback (rm68120.c)
esp32s3_hmi43v3_factory_app_inputButtons (no-op), encoder (no-op), GT911 touch, buzzer (no-op)
esp32s3_hmi43v3_factory_app_storageSD card mount/unmount
esp32s3_hmi43v3_factory_app_toolsHost-side flash scripts, font generator, snapshot converter

ESP-IDF OTA API

Board Hardware (I2C chain)

factory_app_storage

factory_app_input

factory_app_display

esp32s3_hmi43v3_factory_app_main (this module)

app_main

restore_otadata_from_backup

draw_menu / UI render functions

touch_hint_hit_test

dispatch_button

execute_selected_action

action_boot_partition

action_sd_update

action_sd_update_res

snapshot_take / snapshot_check (optional)

gfx_* (draw, clear, flush, snapshot)

rm68120_init / rm68120_flush_ready_cb

touch_init / touch_read - GT911

buttons_init / button_wait_press - no-op

encoder_init / encoder_read - no-op

buzzer_init / buzzer_beep_short - no-op

sdcard_mount / sdcard_unmount

bus_i2c_init

io_tca9554_configure

esp_ota_begin / write / end / set_boot_partition

esp_partition_write / erase_range

esp_flash_read / write / erase_region


The factory partition is activated by the main firmware’s ESP444 command (esp444.cpp). Before switching the boot partition, the main firmware:

  1. Reads the current OTA data sectors from flash offset 0x10000
  2. Writes them to a backup sector at 0xB000
  3. Stamps the backup with magic 0xAA55AA55 at backup offset 0x40
  4. Sets the boot partition to factory and reboots

On the very next app_main call, restore_otadata_from_backup() detects and restores this backup before any other initialization, ensuring that a power-cut during recovery returns to the correct OTA partition on the next boot.

Factory app_mainESP-IDF BootloaderMain Firmware (esp444.cpp)Factory app_mainESP-IDF BootloaderMain Firmware (esp444.cpp)Backup otadata to 0xB000 + magic stampesp_ota_set_boot_partition("factory")esp_restart()Start factory partitionfactory_log_silence_sd_stack()restore_otadata_from_backup()Detect magic at 0xB040Erase 0x10000, write back OTA entriesErase backup sector (0xB000)bus_i2c_init() - shared I2C busio_tca9554_configure() - backlight ON + touch reset releasedrm68120_init() + gfx_init()buttons / encoder / touch / buzzer initDetect partitions, build menu itemsprobe_sd_files()draw_menu()Enter main event loop

I2C init ordering is critical on this board: the TCA9554 IO expander controls the LCD backlight enable bit and the GT911 touch reset bit. Initializing it before rm68120_init() ensures the backlight is on when the first frame is drawn, and the touch IC has been released from reset before touch_init().


Flash Map (relevant regions)

backed up by main firmware

(magic + entry1 + entry2)

restored by factory app_main

on startup

0x0000 Bootloader

0x10000 otadata (2 x 4 KB)

0xB000 Backup sector (4 KB)

0xC000 Partition table

0xD000 NVS

ConstraintValueReason
OTADATA_BACKUP_OFFSET0xB000After bootloader end, before partition table (0xC000), 4 KB-aligned
OTADATA_OFFSET0x10000Fixed ESP-IDF otadata location
OTADATA_ENTRY_SIZE32 bytesOne ESP-IDF OTA state record
BACKUP_MAGIC0xAA55AA55 at +0x40Presence check before restore
CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWEDMust be yesp_flash_erase_region targets address below first partition

Porting note: When adapting this factory app to a board with a different flash layout, recalculate OTADATA_BACKUP_OFFSET and update both this file and the main firmware’s esp444.cpp with the same value.


typedef struct {
const char *label; /* Display label */
menu_action_t action; /* Action dispatched by execute_selected_action */
uint16_t color; /* Text color when item is not selected */
} menu_item_t;

Menu items are built dynamically at startup based on detected partitions:

LabelActionShown when
Boot app0MENU_ACTION_BOOT_APP0Always
Boot app1MENU_ACTION_BOOT_APP1app1 partition detected
SD -> app0MENU_ACTION_SD_UPDATE_APP0Always
SD -> app1MENU_ACTION_SD_UPDATE_APP1app1 partition detected
SD -> resourcesMENU_ACTION_SD_UPDATE_RESAlways

Screen Layout (272 x 480 portrait logical canvas)

Section titled “Screen Layout (272 x 480 portrait logical canvas)”

Layout constants are scaled x0.85 from the 320 x 480 portrait reference (esp32_3248s035r / esp32s3_bzm_tft35_gt911). Border and outline thickness literals (1-2 px insets) are left unscaled.

+------------------------------+ <- Y=0
| Recovery vX.Y.Z (title) | Y=13 CYAN
| --------------------------- | Y=40 separator
| Active: <partition> | Y=45 YELLOW
| SD: FW RES | Y=68 SD indicators
| --------------------------- | Y=88 separator
| |
| [ Boot app0 ] | Y=94 (MENU_START_Y)
| [ Boot app1 ] | Y=128 each item H=34
| [ SD -> app0 ] | Y=162
| [ SD -> app1 ] | Y=196
| [ SD -> resources ] | Y=230
| |
| --------------------------- | STATUS_Y-7 separator
| <status / "Power off"> | STATUS_Y footer zone
| --------------------------- | BTN_HINT_BASE_Y separator
| (UP) (DOWN) (OK) | BTN_HINT_CY virtual buttons
+------------------------------+ <- Y=479

Physical buttons are unpopulated (GPIO_NUM_NC). Three virtual buttons are drawn as double-circle icons (radius 30 px) with centered arrow/checkmark icons at the bottom of the screen.

Touch hit zones use midpoints between the icon centers as column boundaries — not screen-width thirds. This is necessary because on a 272 px wide canvas the three icons (at W/2-100, W/2, W/2+100 = x=36, x=136, x=236) are clustered in the middle. Using screen-width thirds would mis-map BTN_1 and BTN_3 to the wrong columns:

Screen width: 272 px
| x < 86 | 86 <= x < 186 | x >= 186 |
| BTN_1 UP | BTN_2 DOWN | BTN_3 OK |
| CX1=36 | CX2=136 | CX3=236 |
| BLUE | BLUE | GREEN |

touch_hint_hit_test(x, y) returns BTN_NONE for any y < BTN_HINT_BASE_Y, so taps on menu items are never misinterpreted as button presses.


yes

no

clicks > 0

clicks < 0

0

BTN_NONE

BTN_1/2/3

pressed AND not was_pressed

else

BTN_NONE

BTN_1/2/3

BTN_1

BTN_2

BTN_3

Main Loop

ENABLE_SNAPSHOT?

snapshot_check GPIO0

encoder_read - no-op on this board

menu_move -1 UP

menu_move +1 DOWN

button_wait_press 100ms - no-op

touch_read GT911

dispatch_button

touch_hint_hit_test x y

draw_button_hint_pressed

buzzer_beep_short

80ms delay

draw_button_hints

menu_move -1

menu_move +1

execute_selected_action

Loop timing: button_wait_press(100) blocks for up to 100 ms. On this board, physical buttons return BTN_NONE immediately, so the loop runs at approximately 100 ms cadence — responsive for touch polling. The touch handler uses leading-edge detection (pressed && !touch_was_pressed) to fire exactly once per tap.


ESP-IDF OTAMenu UIESP-IDF OTAMenu UIalt[partition found][not found]show_status("Booting label...", CYAN)vTaskDelay 500msesp_partition_find_first(label)partition handle or NULLesp_ota_set_boot_partition(part)esp_restart()show_status("label not found!", RED)redraw menu

SD Firmware Flash Action (action_sd_update)

Section titled “SD Firmware Flash Action (action_sd_update)”
ESP-IDF OTA APISD CardMenu UIESP-IDF OTA APISD CardMenu UIloop[Read 1 KB chunks until EOF]alt[success][failure]sdcard_mount()fopen("/sdcard/esp3dfw.bin")fseek to get file size, validate <= partition sizedraw_flashing_screen()esp_ota_begin(target_part, fw_size, &handle)fread(buf, 1024)esp_ota_write(handle, buf, len)draw_progress(percent)esp_ota_end(handle)esp_ota_set_boot_partition(target_part)rename esp3dfw.bin to esp3dfw.oksdcard_unmount()draw_result(true, "Success! Rebooting...")esp_restart()rename esp3dfw.bin to esp3dfw.badsdcard_unmount()draw_result(false, ...)probe_sd_files() + draw_menu()

SD Resources Flash Action (action_sd_update_res)

Section titled “SD Resources Flash Action (action_sd_update_res)”

Follows the same SD mount → open → validate → erase → write → rename pattern, but targets the ui_resources DATA partition using esp_partition_erase_range() + esp_partition_write() directly (no OTA handle needed).

One additional step: a 16-byte build header is read from the start of ui_resources.bin and logged before flashing. The header format is "ESP3" (4 bytes) + 12-char variant string. This allows early detection of variant mismatches (wrong firmware type or display resolution). Old binaries without the header emit a warning and continue.


All drawing calls route through the GFX API defined in esp32s3_hmi43v3_factory_app_display.

ConstantValueDescription
SCREEN_WIDTH272Logical portrait width (pixels)
SCREEN_HEIGHT480Logical portrait height (pixels)
FONT_WIDTH12Pixels per character column
FONT_HEIGHT24Pixels per character row
MENU_START_Y94Y coordinate of first menu item
MENU_ITEM_H34Height of each menu item row
MENU_PAD_X20Horizontal padding for menu items
BTN_CIRCLE_R30Virtual button circle radius
BTN_HINT_H69Total height of button hint bar
BTN_HINT_BASE_YSCREEN_HEIGHT - BTN_HINT_HTop edge of button hint bar
BTN_HINT_CX1/2/3W/2-100, W/2, W/2+100Button icon center X positions
STATUS_YSCREEN_HEIGHT - 28 - BTN_HINT_HFooter message Y coordinate
FunctionDescription
draw_header()Clears screen, double border, version title (CYAN), horizontal separator
draw_menu()Full menu repaint: header + active partition (YELLOW) + SD indicators + all items + footer + button hints
draw_menu_item(index)Redraws one menu item with or without dark-blue selection highlight and bright-blue text
draw_sd_indicators()Shows FW (green) and/or RES (cyan) badges if files detected on SD
draw_footer_zone()Renders active status message, or "Power off to cancel" (dark gray) if none
draw_flashing_screen()Full-screen flash-in-progress overlay: title (YELLOW) + "Do NOT power off!" (RED)
draw_progress(percent)Outlined white progress bar filled green + percentage text below
draw_result(success, msg)Green success string or red "FAILED! Power cycle." result line
draw_button_hints()Horizontal separator + three double-circle icons with up/down/check icons
draw_button_hint_pressed(btn)Redraws one button in violet for touch-press visual feedback
draw_circle(cx, cy, r, color)Midpoint circle algorithm, outline only
draw_up_arrow(cx, cy, color)Upward arrow centered at (cx, cy), scaled for r=30
draw_down_arrow(cx, cy, color)Downward arrow centered at (cx, cy), scaled for r=30
draw_check_mark(cx, cy, color)Checkmark, 4 px thick strokes, scaled for r=30
ConstantRGBUsed for
MENU_HIGHLIGHTDark blue (0, 80, 160)Selection highlight box background
MENU_HIGHLIGHT_TXTBright blue (80, 160, 255)Selected item text
BTN_NAV_COLORBlue (100, 160, 255)Up/Down button icons at rest
BTN_OK_COLORGreen (100, 220, 100)OK/Select button icon at rest
BTN_PRESSED_COLORViolet (180, 80, 220)Any button during press feedback

When compiled with ENABLE_SNAPSHOT, pressing GPIO0 (BOOT button on evaluation breakout boards) triggers a full screen capture saved to the SD card as a raw pixel file. Used during development and QA to capture factory UI states without a USB connection.

yes

no

GPIO0 LOW detected in snapshot_check

50ms debounce re-check

sdcard_mount()

snap_find_next_number()

scan snap000.raw to snap999.raw

return first missing index

gfx_snapshot_begin('/sdcard/snapNNN.raw')

Flash in

progress?

draw_flashing_screen()

draw_progress(last_percent)

draw_menu()

show_status(if any)

gfx_snapshot_end() - fclose

s_snapshot_count++

Wait for GPIO0 release + 50ms debounce

Raw files can be converted to PNG using snap2png.py in esp32s3_hmi43v3_factory_app_tools.


Controlled by FACTORY_LOG_LEVEL (set via ENABLE_FACTORY_DEBUG_LOG in CMakeLists.txt), independent of the sdkconfig log ceiling:

FACTORY_LOG_LEVELFACTORY_LOGD behavior
0 (default / production)Expands to no-op; only ESP_LOGW / ESP_LOGE remain active
1 (debug build)Expands to ESP_LOGI; all log levels active

ESP-IDF internal logs (startup banner, spi_flash, sdmmc, …) are silenced in production by a separate sdkconfig.prod_log overlay applied at build time — these fire before app_main() and cannot be gated at runtime.

Called first in app_main(). Dynamically mutes the SD/FAT driver log tags at runtime using esp_log_level_set(..., ESP_LOG_NONE) for: sdmmc, vfs_fat_sdmmc, sdmmc_periph, sdmmc_req, sdmmc_common, fatfs, sdspi, sd_diskio. This prevents the SD stack from becoming verbose in debug builds where FACTORY_LOG_LEVEL=1 raises the sdkconfig ceiling to INFO for everything.


Key Differences vs. Reference Board (pibot_pendant_v1_0)

Section titled “Key Differences vs. Reference Board (pibot_pendant_v1_0)”
Aspectpibot_pendant_v1_0esp32s3_hmi43v3 (this module)
Display busSPI (ILI9341)Intel 8080 parallel (RM68120)
IO expanderNoneTCA9554 (backlight + touch reset)
I2C initIn touch_init() onlyExplicit bus_i2c_init() before display
Physical buttonsYes (populated)No (GPIO_NUM_NC)
Custom bootloaderYesNo
Factory entryBOOT button hold or ESP444ESP444 software command only
Canvas size240 x 320 portrait272 x 480 portrait (rotated 90 degrees)
Layout scaleReference (x1.0)x0.85 horizontal from 320 px reference
Touch hit zonesColumn thirdsIcon-center midpoints
Snapshot GPIOGPIO0GPIO0 (same)

For boards with the same factory logic pattern, also see esp32s3_bzm_tft35_gt911_factory_app_main and esp32s3_8048s070c_factory_app_main.