Skip to content

Factory — SD Flash Update Actions

factory_update_actions_sd_flash is the sub-module of the factory recovery application responsible for flashing firmware and UI resources from an SD card into internal flash. It provides three functions that form the core of the SD-based update path:

FunctionPurpose
probe_sd_filesNon-destructive SD scan — detects which update files are present
action_sd_updateFlashes esp3dfw.bin from SD into a named OTA app partition
action_sd_update_resFlashes ui_resources.bin from SD into the ui_resources data partition

These functions are cross-board: the same logic is replicated inside each board’s Factory/main/main.c, with only the display driver include and a few board-specific layout constants differing between variants.


The factory recovery application is a standalone ESP-IDF app stored in a dedicated factory partition. It is entered either from the custom bootloader (boards with a physical recovery button) or via the main firmware’s [ESP444]FACTORY software trigger. See factory_app_entry.md for the full factory application entry-point and factory_update_actions_otadata.md for the OTA-data restore step that runs before the menu is shown.

factory_update_actions_sd_flash - this module

Factory App

app_main

restore_otadata_from_backup

draw_menu

User selects action

execute_selected_action

action_boot_partition

action_sd_update(label)

action_sd_update_res

probe_sd_files

Factory_Application_&_Bootloader

factory_app / factory_app_core

factory_core

factory_update_actions

factory_update_actions_otadata

factory_update_actions_sd_flash ◀ this module

factory_update_actions_dispatch

Sibling modules:


Mounts the SD card, tries to open each expected update file, and sets the global sd_has_fw / sd_has_res flags. These flags are read by draw_sd_indicators() (part of factory_visual_feedback ) to show or hide the “FW” and “RES” labels in the menu header.

fail

ok

found

not found

found

not found

probe_sd_files

sdcard_mount

return - flags stay false

fopen esp3dfw.bin

sd_has_fw = true

fclose

sd_has_fw = false

fopen ui_resources.bin

sd_has_res = true

fclose

sd_has_res = false

sdcard_unmount

Call sites:

  • app_main — called once during initialisation, before the first draw_menu.
  • action_sd_update (failure path) — called after a failed flash attempt so the re-drawn menu reflects the current SD state (the .bad rename will have removed the original .bin).
  • action_sd_update_res (failure path) — same reason.

Flashes a firmware binary from the SD card into a named OTA application partition using the ESP-IDF OTA API. The target partition label is passed by the caller ("app0" or "app1"), making the same function service both menu entries.

Snapshot (ENABLE_SNAPSHOT)Display (visual_feedback)ESP-IDF OTA APISD Cardaction_sd_updateMenu dispatchSnapshot (ENABLE_SNAPSHOT)Display (visual_feedback)ESP-IDF OTA APISD Cardaction_sd_updateMenu dispatchloop[Each 1024-byte chunk until written == fw_size]action_sd_update("app0")esp_partition_find_first(APP, label)update_partsdcard_mount()ESP_OKfopen("/sdcard/esp3dfw.bin")fw_filefseek/ftell - get fw_sizedraw_flashing_screen()snapshot_check()esp_ota_begin(update_part, fw_size)ota_handlefread(buf, 1, 1024, fw_file)read_len bytesesp_ota_write(ota_handle, buf, read_len)draw_progress(percent) [on percent change]snapshot_check()fclose(fw_file)esp_ota_end(ota_handle)esp_ota_set_boot_partition(update_part)draw_result(true, "Success! Rebooting...")snapshot_check()rename esp3dfw.bin → esp3dfw.oksdcard_unmount()esp_restart()

No

Yes

No

Yes

No

Yes

No

Yes

No

Yes

read error

write error

Yes

No

Yes

No

Yes

action_sd_update

Partition found?

show_status

Partition not found!

sdcard_mount OK?

show_status

No SD card!

fopen esp3dfw.bin OK?

show_status

No esp3dfw.bin on SD!

0 < fw_size ≤ part size?

show_status

Invalid firmware size!

esp_ota_begin OK?

show_status

OTA begin failed!

Chunk loop

All chunks written?

ok = false

esp_ota_end OK?

set_boot_partition OK?

rename → .ok

esp_restart()

esp_ota_abort

draw_result false

rename → .bad

sdcard_unmount

probe_sd_files

draw_menu

return


Flashes a UI resources binary from the SD card into the ui_resources DATA partition. This path uses the raw partition API (esp_partition_erase_range + esp_partition_write) because ui_resources is a DATA partition, not an OTA app partition, and has no OTA integrity metadata.

Snapshot (ENABLE_SNAPSHOT)Display (visual_feedback)ESP-IDF Partition APISD Cardaction_sd_update_resMenu dispatchSnapshot (ENABLE_SNAPSHOT)Display (visual_feedback)ESP-IDF Partition APISD Cardaction_sd_update_resMenu dispatchValidate 'ESP3' magic + log variant string(warning only, does not abort on mismatch)loop[Each 1024-byte chunk until written == res_size]action_sd_update_res()esp_partition_find_first(DATA, "ui_resources")res_partsdcard_mount()ESP_OKfopen("/sdcard/ui_resources.bin")res_filefread(bin_hdr, 16) - read build headerfseek(0, SEEK_END) / ftell - get res_sizedraw_flashing_screen()snapshot_check()esp_partition_erase_range(res_part, 0, res_part->size)fread(buf, 1, 1024, res_file)read_len bytesesp_partition_write(res_part, offset, buf, read_len)draw_progress(percent) [on percent change]snapshot_check()fclose(res_file)draw_result(ok, "Done! Rebooting...")snapshot_check()rename ui_resources.bin → ui_resources.oksdcard_unmount()esp_restart()

The module defines a deterministic rename protocol that serves as a persistent outcome marker surviving any subsequent reboot:

File placed on SD card

action_sd_update called

Flash succeeded\nrename .bin → .ok

Flash failed\nrename .bin → .bad

User renames .ok → .bin\n(manual retry)

User renames .bad → .bin\n(manual retry)

esp3dfw.bin present

Flashing in progress

esp3dfw.ok - success

esp3dfw.bad - failure

The identical state machine applies to ui_resources.bin / .ok / .bad.

#define FW_FILENAME "/sdcard/esp3dfw.bin"
#define FW_OK_FILENAME "/sdcard/esp3dfw.ok"
#define FW_BAD_FILENAME "/sdcard/esp3dfw.bad"
#define RES_FILENAME "/sdcard/ui_resources.bin"
#define RES_OK_FILENAME "/sdcard/ui_resources.ok"
#define RES_BAD_FILENAME "/sdcard/ui_resources.bad"
PropertyBenefit
Re-entry safetyAfter a reboot back into the factory app, the .bin file is absent so no automatic re-flash occurs
Post-mortem visibilityA technician can check the SD card to determine the last flash outcome without UART logs
Manual retryRenaming .bad → .bin on a host machine queues a retry without re-copying the original binary

action_sd_update_res reads the first 16 bytes of ui_resources.bin before any erase:

Offset Size Content
0 4 Magic bytes: 'E' 'S' 'P' '3'
4 12 Variant string (null-padded, e.g. "fluidnc_ser")

The variant string (written by generate_resources.py since the header was introduced) encodes the firmware type and transport combination. A mismatch between the binary variant and the target board’s configuration is logged as a warning but does not abort the flash — this preserves compatibility with older binaries that pre-date the header.

See ui_resources/development.md for the full binary format specification and the generate_resources.py pipeline.


Fixed 1024-byte Stack Buffer — No Heap Allocation

Section titled “Fixed 1024-byte Stack Buffer — No Heap Allocation”
uint8_t buf[1024];

Both flash functions use a fixed stack-allocated 1024-byte chunk buffer. This is intentional:

  • The factory recovery app runs without LVGL, WiFi, or BT stacks — heap is larger than in production firmware but still constrained and potentially fragmented after SD mount.
  • A malloc failure mid-flash would leave the partition in a half-written state with no graceful recovery path.
  • 1024 bytes is within the project-wide safe stack buffer limit (see esp32_memory_constraints.md).
Target partitionAPI usedReason
app0 / app1esp_ota_begin / esp_ota_write / esp_ota_endApp partitions need OTA integrity metadata for ESP-IDF rollback tracking
ui_resourcesesp_partition_erase_range + esp_partition_writeDATA partition — no OTA metadata, raw write is correct and simpler

When compiled with ENABLE_SNAPSHOT, both flash functions call snapshot_check() at three points:

  1. Immediately after draw_flashing_screen() — captures initial screen state.
  2. Inside the chunk loop, once per percentage-point change (s_flash_last_percent caches the last value so a full-redraw after a snap is consistent with the actual progress at snapshot time).
  3. After draw_result() — captures the final success or failure screen.

The s_snap_in_flash flag tells the snapshot subsystem which screen layout to redraw during a capture (draw_flashing_screen + draw_progress vs draw_menu).

See factory_snapshot.md for the full snapshot mechanism.


ESP-IDF

factory_snapshot (ENABLE_SNAPSHOT)

factory_visual_feedback

factory_sdcard

factory_update_actions_sd_flash

probe_sd_files

action_sd_update

action_sd_update_res

sdcard_mount

sdcard_unmount

draw_flashing_screen

draw_progress

draw_result

draw_menu

show_status

snapshot_check

esp_ota_begin / write / end / abort

esp_ota_set_boot_partition

esp_partition_find_first

esp_partition_erase_range

esp_partition_write

esp_restart

esp_get_free_heap_size


The three functions are identical in logic across all boards. Board differences are confined to:

  • The display driver #include (st7796.h, st7262.h, ili9341.h, etc.)
  • Layout constants (MENU_START_Y, FONT_WIDTH, BTN_CIRCLE_R, etc.) that control only the visual feedback helpers, not the flash logic.

One board-specific pre-condition exists outside this module: on esp32s3_8048_touch_lcd_7, the CH422G IO expander must be initialised and its SD_CS line (EXIO3) asserted before any call to sdcard_mount(). This is handled in app_main via io_ch422g_configure() which runs after touch_init() brings up the shared I2C bus. The functions in this module call sdcard_mount() directly and rely on that initialisation having already occurred.

BoardFirmware FlashResources Flash
esp32_3248s035c✓✓
esp32_3248s035r✓✓
esp32s3_4827s043c✓✓
esp32s3_8048_touch_lcd_7✓✓
esp32s3_8048s043c✓✓
esp32s3_8048s050c✓✓
esp32s3_8048s070c✓✓
esp32s3_bzm_tft35_gt911✓✓
esp32s3_hmi43v3✓✓
esp32s3_zx3d50ce02s_usrc_4832✓✓
pibot_pendant_v1_0✓✓

The factory sdkconfig must set CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y. action_sd_update_res calls esp_partition_erase_range which may target flash addresses that ESP-IDF’s default CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS policy would reject. This is intentional — the factory app is a privileged recovery tool with explicit flash management authority.

OTADATA_BACKUP_OFFSET and Partition Boundaries

Section titled “OTADATA_BACKUP_OFFSET and Partition Boundaries”

The OTA-data backup sector (written by the main firmware’s esp444.cpp) sits in the region below the partition table. The firmware flash path must not overlap this region. OTADATA_BACKUP_OFFSET (0xB000) and OTADATA_OFFSET (0x10000) are defined to be well below the first application partition and are kept in sync between this factory app and the main firmware. When porting to a new board, verify both values remain valid for the target flash layout.

Factory logging uses the FACTORY_LOGD macro from factory_log.h. The SD stack (FatFS/SDMMC) is silenced via factory_log_silence_sd_stack() called in app_main to reduce UART noise during the flash progress display. Debug heap values are logged at flash entry and after SD mount via esp_get_free_heap_size() for diagnostic purposes.

See esp3d_log_guide.md for the general logging convention. FACTORY_LOGD is a bare-C adaptation of the same pattern used in the main firmware.


DocumentTopic
factory_app_entry.mdFactory app entry point (app_main) — hardware init and menu bootstrap
factory_update_actions.mdParent module — all update actions together
factory_update_actions_otadata.mdOTA-data backup restore before the menu
factory_menu_system.mdMenu draw, navigation, SD indicators
factory_sdcard.mdSD card mount / unmount implementation
factory_snapshot.mdScreen snapshot capture to SD (debug feature)
ui_resources/development.mdui_resources binary format and build pipeline
guides/tools.mdHost-side flash helpers (flash_factory.py, flash_all.py)
guides/esp32_memory_constraints.mdHeap and fragmentation constraints