Skip to content

Update Service

The Update Service (ESP3DUpdateService) is the central orchestrator for all SD-card-based update operations on the pendant firmware. It handles six distinct update types — firmware (OTA), configuration settings (INI), UI resources (images + fonts partition), individual resource patches, theme colour palettes, and language packs — all sourced from the SD card.

The service runs as a synchronous, blocking operation during boot (before the LVGL UI task starts), and exposes a progress callback so the Update Screen can render live feedback via the ESP3DValues observable system.

A global singleton esp3dUpdateService is declared in the header and is the only instance used throughout the firmware.


Post-Update

Progress Feedback

Flash Targets

Update Execution

Boot Sequence (Core 0)

File scan

ESP3DUpdateType

callback(percent, msg)

main.cpp

app_main()

esp3dUpdateService.detect()

SD Card

(ESP3DSD)

esp3dUpdateService.begin()

executeFirmwareUpdate()

→ updateFW()

executeConfigUpdate()

→ updateConfig()

executeResourcesUpdate()

→ updateResources()

executeResourcePatches()

→ esp3d_resources_patch_sd()

executeThemeColorsUpdate()

→ esp3d_theme_colors_update_from_sd()

executeLangPacksUpdate()

→ esp3d_lang_packs_update_from_sd()

ESP-IDF OTA

(ota_1 partition)

NVS Settings

(ESP3DSettings)

ui_resources partition

(raw flash write)

ESP3DUpdateProgressCallback

ESP3DValues

update_progress

update_status

Update Screen

(LVGL)

rename .bin → .ok / .bad

rename .ini → .ok / .bad

rename .bin → .ok / .bad

rename .ini → .ok / .bad

esp_restart()


FileRole
main/modules/update/esp3d_update_service.hClass declaration, enums, file path macros
main/modules/update/esp3d_update_service.cppAll update logic implementation

The ESP3DUpdateType enum defines the priority-ordered set of updates the service can detect and apply:

Detection Priority (highest → lowest)

① firmware

esp3dfw.bin

② config

esp3dcnf.ini

③ resources

ui_resources.bin

④ theme_colors

esp3dtheme.ini

⑤ language_packs

/ui_*.lng

⑥ sounds

esp3dsound.ini

⑦ resource_patches

/esp3dres/*.bin|*.fnt

Only one update type is executed per boot cycle. Once a higher-priority update file is found, lower-priority files are ignored until the next boot.

Enum ValuePrioritySource File(s)Flash Target
firmware1 (highest)/esp3dfw.binOTA partition (ota_1)
config2/esp3dcnf.iniNVS (all settings)
resources3/ui_resources.binui_resources data partition
theme_colors4/esp3dtheme.iniui_resources partition (palette slots)
language_packs5/ui_*.lng (any matching file)ui_resources partition (language slots)
sounds (a14)6/esp3dsound.ini (partial: events it does not mention keep their sound)ui_resources partition (sounds slot), via esp3d_sound_update_from_sd()
resource_patches7 (lowest)/esp3dres/*.bin, /esp3dres/*.fntui_resources partition (individual entries)
none———

Note: firmware updates require a valid second OTA slot (ota_1). Boards with a single-app-slot layout cannot self-update firmware — the service detects this via canUpdate() and skips the firmware check entirely, but still applies resource/config/theme/language updates.


After an update, the service renames the source file to signal the outcome. If a .ok or .bad file already exists from a previous run, the old file is first renamed with a numeric suffix (*.ok1, *.ok2, …) to preserve history.

Source file found on SD

begin() called

Flash write verified

Error at any stage

rename → .ok\n(secrets scrambled for config)

rename → .bad

esp_restart()

Returns false to caller

Detected

InProgress

Success

Failure

OkFile

BadFile

Reboot

SourceOn SuccessOn Failure
/esp3dfw.bin/esp3dfw.ok/esp3dfw.bad
/esp3dcnf.ini/esp3dcnf.ok (secrets scrambled)/esp3dcnf.bad
/ui_resources.bin/ui_resources.ok/ui_resources.bad
/esp3dtheme.ini/esp3dtheme.ok/esp3dtheme.bad
/ui_*.lng/ui_*.ok (renamed by translation service)/ui_*.bad
/esp3dsound.ini/esp3dsound.ok/esp3dsound.bad

accessFS / releaseFS

open / stat / rename

readdir / closedir

esp_ota_begin/write/end

esp_ota_set_boot_partition

esp_partition_find_first

esp_partition_erase_range

esp_partition_write

processFile / revokeFile

findSettingByIni

writeByte / writeString

writeUint32 / writeIPString

esp3d_resources_patch_sd()

esp3d_theme_colors_update_from_sd()

esp3d_lang_packs_update_from_sd()

set_value(update_progress)

set_value(update_status)

subscription callback

ESP3DUpdateService

ESP3DSD

(filesystem/esp3d_sd.h)

ESP-IDF OTA &

Partition APIs

ESP3DConfigFile

(config_file/esp3d_config_file.h)

ESP3DSettings

(core/includes/esp3d_settings.h)

esp3d_resources

(display/esp3d_resources.cpp)

UIManager / ThemeConfig

(display/esp3d_ui.cpp)

ESP3DTranslationService

(modules/translations/)

ESP3DValues

(modules/values/esp3d_values.h)

Update Screen

(display/screens/update_screen.cpp)


No

Yes

No

Yes

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

detect() called

SD card

accessible?

return none

check_update_on_sd

setting enabled?

canUpdate() &&

esp3dfw.bin exists

and valid size?

_pendingUpdate = firmware

esp3dcnf.ini

exists?

_pendingUpdate = config

ui_resources.bin

exists and valid size?

_pendingUpdate = resources

esp3dtheme.ini

exists?

_pendingUpdate = theme_colors

any /ui_*.lng

in SD root?

_pendingUpdate = language_packs

esp3dsound.ini

exists?

_pendingUpdate = sounds

/esp3dres/*.bin

or *.fnt found?

_pendingUpdate = resource_patches

sd.releaseFS()

return type


Firmware updates use the ESP-IDF OTA API. The service validates that a genuine second OTA slot exists (single-slot boards are rejected by canUpdate()) and that the binary fits within the partition before touching flash.

ESP-IDF OTAESP3DSDESP3DUpdateServiceESP-IDF OTAESP3DSDESP3DUpdateServiceValidate fileSize > 0 && fileSize ≤ partition.sizeloop[Read 1 KB chunks]accessFS()stat("/esp3dfw.bin")esp_ota_get_next_update_partition()esp_ota_begin(partition, OTA_WITH_SEQUENTIAL_WRITES)fread(chunk, 1024)esp_ota_write(handle, chunk, size)callback(progress%, "Writing...")esp_ota_end(handle)esp_ota_set_boot_partition(partition)rename("esp3dfw.bin", "esp3dfw.ok")releaseFS()esp_restart()

Memory note: The 1 KB chunk buffer is heap-allocated (malloc(CHUNK_BUFFER_SIZE)) rather than stack-allocated, to avoid stack overflow. It is freed immediately after the write loop.


The ui_resources partition holds all images, fonts, theme data, and language slots used by LVGL. Before erasing the partition the binary is validated against an ESP3 magic header and the board-specific RESOURCES_VARIANT key to prevent flashing a wrong-board image onto a healthy partition.

IDFui_resources partitionESP3DSDESP3DUpdateServiceIDFui_resources partitionESP3DSDESP3DUpdateServiceValidate size, read first 16 bytesalt[Header invalid or variant mismatch]a14: file size must equal the partition sizealt[No trailer, or name / version differ from RESOURCES_BUILD_NAME /ESP3D_X_VERSION]loop[Read 1 KB chunks]accessFS()stat("/ui_resources.bin")esp_partition_find_first(..., "ui_resources")Check magic "ESP3" (bytes 0-3)Check RESOURCES_VARIANT (bytes 4-15)rename → .badreturn false (no erase)Read the last 96 bytes: 'ESPB' + build name + fw versionrename → .badreturn false (no erase)esp_partition_erase_range(0, partition.size)fread(chunk, 1024)esp_partition_write(offset, chunk, size)callback(progress%, "Writing resources...")rename("ui_resources.bin", "ui_resources.ok")releaseFS()esp_restart()

Build trailer (a14): the variant key does not tell boards, resolutions or releases apart, so the last 96 bytes of the file carry the exact build (installer variant name and firmware version). A file without trailer (“kit from an older firmware?”) fails with Resources file not built for this firmware; a mismatch with Resources file built for another firmware. A kit therefore only works for one variant and one firmware version; see tools_build_scripts_ui_resources.md.

For details on the binary format validated by this header check, see UI Resources — Development Reference.


Config updates parse an INI-formatted file (/esp3dcnf.ini) and write each key/value pair into NVS via ESP3DSettings. After a successful parse, revokeFile() renames the INI to .ok and scrambles the values of protected security keys so credentials are not left in plain text on the SD card.

Yes

No

string_t

byte_t

integer_t

ip_t

unknown

executeConfigUpdate()

sd.accessFS()

new ESP3DConfigFile

(path, callback, okPath, protectedKeys)

processFile()

→ for each section/key/value

processSpecialValue?

(radio_mode, TargetFW, output…)

string → enum → writeByte()

findSettingByIni(section, key)

Setting

type?

writeString()

atoi / bool parse → writeByte()

atol → writeUint32()

writeIPString()

skip (not an error)

revokeFile()

rename .ini → .ok

scramble protected keys

sd.releaseFS()

esp_restart()

The following settings are automatically obfuscated when the .ini is renamed to .ok, so sensitive credentials are never stored in clear text on the SD card after a successful config update:

KeySetting
NOTIF_TOKEN1Notification service token 1
NOTIF_TOKEN2Notification service token 2
AP_PasswordWi-Fi access-point password
STA_PasswordWi-Fi station password
ADMIN_PASSWORDHTTP admin password
USER_PASSWORDHTTP user password
BT_PinBluetooth PIN
BLE_PasskeyBLE passkey

Certain settings require string-to-enum translation before being written to NVS. processSpecialValue() handles these before the generic findSettingByIni() lookup. Values equal to the scramble sentinel (HIDDEN_SETTING_VALUE) are silently skipped.

SectionKeyAccepted String Values
networkradio_modeBT, STA, AP, SETUP, OFF
networksta_fallbackBT, WIFI-SETUP, OFF
networkSTA_IP_modeDHCP, STATIC
systemTargetFWGRBL, GRBLHAL, MARLIN, SMOOTHIEWARE, REPETIER, HP_GL, FLUIDNC, None
systemoutputUSB-OTG (USB kept as alias for INI files written before 2026-10-30), USB-CDC (USB CDC device builds, a14), SERIAL, UARTEXT / UART_EXT / EXTERNAL_MODULE
servicesNOTIF_TYPENone, PushOver, Email, Telegram, IFTTT

Boolean strings (yes/no, on/off, true/false, 1/0) are accepted for all byte_t settings. Unknown section/key pairs are logged and silently skipped — they are not treated as errors.


The service communicates update progress to the LVGL Update Screen through the ESP3DValues observable layer, keeping the update logic fully decoupled from the display stack.

Update Screen (LVGL / Core 1)ESP3DValuesupdateCallbackWithDisplay()ESP3DUpdateServiceUpdate Screen (LVGL / Core 1)ESP3DValuesupdateCallbackWithDisplay()ESP3DUpdateServicecallback(percent, statusMsg)set_value(update_progress, "42")set_value(update_status, statusMsg)onUpdateValueChange(update_progress, "42")lv_bar_set_value(progress_bar, 42)lv_label_set_text(percent_label, "42%")checkCompletionState()

Font safety during resources update: The update screen uses only the built-in lv_font_montserrat_14 (embedded in the firmware binary) for all labels. It never uses XIP fonts from the ui_resources partition because that partition may be partially erased during a resources update, making its flash-mapped data unsafe to read.

The ESP3DUpdateWaitBeforeRebootFn hook lets the update screen gate the reboot on its final render cycle completing, instead of the service performing a blind esp3d_hal::wait() sleep of fixed duration.


ESP3DUpdateService constructed

detect() called

No update found / type set in _pendingUpdate

begin() / executeXxxUpdate() called

Flash write verified,\nfile renamed .ok

Any error,\nfile renamed .bad (where applicable)

esp_restart()

Returns false to caller\n(auto_reboot_on_fail also triggers restart)

end() called (reset to idle)

idle

detecting

in_progress

success

failed


// Lifecycle
bool begin(ESP3DUpdateProgressCallback callback = nullptr,
bool detectType = true,
bool auto_reboot_on_fail = true,
uint16_t delay = 1000,
ESP3DUpdateWaitBeforeRebootFn waitBeforeReboot = nullptr);
void handle(); // reserved for periodic tasks, currently no-op
void end(); // resets status and pendingUpdate to idle/none
// Detection (does NOT execute the update)
ESP3DUpdateType detect();
// Individual executors (called internally by begin())
bool executeFirmwareUpdate(ESP3DUpdateProgressCallback callback = nullptr);
bool executeConfigUpdate(ESP3DUpdateProgressCallback callback = nullptr);
bool executeResourcesUpdate(ESP3DUpdateProgressCallback callback = nullptr);
bool executeResourcePatches(ESP3DUpdateProgressCallback callback = nullptr);
bool executeThemeColorsUpdate(ESP3DUpdateProgressCallback callback = nullptr);
bool executeLangPacksUpdate(ESP3DUpdateProgressCallback callback = nullptr);
// Status queries
ESP3DUpdateType getPendingUpdate() const; // set after detect()
ESP3DUpdateStatus getStatus() const;
size_t getFirmwareSize() const; // valid after detect() if firmware pending
size_t getResourcesSize() const; // valid after detect() if resources pending
const char* getErrorMessage() const; // last error string
// Partition capability checks
bool canUpdate(); // safe OTA slot available and distinct from running
bool canUpdateResources(); // ui_resources partition present in partition table
size_t maxUpdateSize(); // OTA partition size in bytes (0 if none)
size_t maxResourcesUpdateSize(); // ui_resources partition size in bytes (0 if absent)
// INI file processing callback (static; passed to ESP3DConfigFile)
static bool processingFileFunction(const char* section,
const char* key,
const char* value);
// Progress notification: percent 0–100, human-readable status string
using ESP3DUpdateProgressCallback = std::function<void(uint8_t percent, const char* status)>;
// Optional hook to delay/gate reboot after a successful update
// (e.g. wait for LVGL to render the final completion screen)
using ESP3DUpdateWaitBeforeRebootFn = std::function<void(uint16_t delay_ms)>;
MacroValue
ESP3D_FW_FILE/esp3dfw.bin
ESP3D_FW_FILE_OK/esp3dfw.ok
ESP3D_FW_FILE_BAD/esp3dfw.bad
ESP3D_CONFIG_FILE/esp3dcnf.ini
ESP3D_CONFIG_FILE_OK/esp3dcnf.ok
ESP3D_CONFIG_FILE_BAD/esp3dcnf.bad
ESP3D_RESOURCES_FILE/ui_resources.bin
ESP3D_RESOURCES_FILE_OK/ui_resources.ok
ESP3D_RESOURCES_FILE_BAD/ui_resources.bad
ESP3D_THEME_FILE/esp3dtheme.ini
ESP3D_THEME_FILE_OK/esp3dtheme.ok
ESP3D_SOUND_FILE / _OK / _BAD (a14)/esp3dsound.ini / .ok / .bad
ESP3D_THEME_FILE_BAD/esp3dtheme.bad

The update service is conditionally compiled based on CMakeLists.txt feature options:

GuardControlled Behaviour
ESP3D_SD_CARD_FEATUREAll SD-sourced update paths (firmware, config, resources, theme, language, patches)
ESP3D_DISPLAY_FEATUREResource patches, theme colours, language packs (require esp3d_resources and UIManager)
ESP3D_WIFI_FEATUREWi-Fi-specific INI keys (STA_IP_mode, sta_fallback, STA/AP/SETUP radio modes)
ESP3D_NOTIFICATIONS_FEATURENOTIF_TYPE INI key mapping
ESP3D_USB_SERIAL_FEATUREUSB output client INI value
ESP3D_UART_EXT_FEATUREUARTEXT / UART_EXT / EXTERNAL_MODULE output client INI values

  • The chunk buffer (CHUNK_BUFFER_SIZE = 1024 bytes) is heap-allocated and freed immediately after the write loop.
  • No large stack allocations exist in any update path.
  • std::string is used only for file-suffix collision avoidance in renameToOk() / renameToBad(), which run after the main flash write — not in the hot path.
  • All malloc() calls are guarded; a nullptr result produces a logged error and returns false immediately.

For general memory budget guidance see ESP32 Memory Constraints.