Skip to main content

OTA firmware updates

OTA is the feature you cannot retrofit. A fleet without working updates is a fleet you cannot fix, so get this working before you ship anything — including the rollback path.

RainMaker Neo OTA is built on AWS IoT Jobs. The cloud creates a job for a node or a group; the node receives the job document, downloads the image over MQTT file streams, verifies it, flashes it, reboots, and reports status back at every step.

Enabling it

Link the esp_rmaker_neo_ota component, then enable it before esp_rmaker_start():

#include <esp_rmaker_ota.h>

esp_rmaker_ota_config_t ota_config = {
.ota_cb = NULL, /* NULL → the built-in callback (recommended) */
.ota_diag = ota_diag_fn, /* rollback diagnostics — see below */
.priv = NULL,
};
esp_rmaker_ota_enable(&ota_config);

Leaving ota_cb as NULL selects the built-in callback and its image-reference validator. That is what you want unless you have a specific reason to intervene.

Disable it again with esp_rmaker_ota_disable(), which also frees the OTA resources.

If you never link esp_rmaker_neo_ota, no OTA code is compiled in at all.

Set the firmware version

The node reports a firmware version, and the version must increase relative to what is running or the update may be rejected. This trips people up constantly: they build a new image, start a job, and the node rejects it because both are 1.0.0.

CMake (both platforms)

Set PROJECT_VER before the project() call:

set(PROJECT_VER "1.1.0" CACHE STRING "Firmware version")

project(light C ASM) # or project(my_app) for ESP-IDF

Rebuild after changing it.

ESP-IDF Kconfig alternative

Instead of PROJECT_VER you can drive the version from sdkconfig:

CONFIG_APP_PROJECT_VER_FROM_CONFIG=y
CONFIG_APP_PROJECT_VER="1.1.0"

Set them via idf.py menuconfig (under Application manager) or in sdkconfig.defaults, then rebuild.

Which file to upload

PlatformPath, relative to the project's build/ directory
ESP-IDF<project_name>.bin — the basename matches project(<name>) in the root CMakeLists.txt. For the light example: build/light.bin.
POSIXpartitions/ota_0 — the OTA slot image, no file extension. Only produced for projects that link esp_rmaker_neo_ota.

Rolling it out

Upload the binary under OTA → Images, then create a job under OTA → Jobs targeting a node or a node group. Fill in Firmware version and Platform honestly — they are how you avoid pushing an esp32c3 build to an esp32c6 node. Step-by-step, with screenshots: OTA from the dashboard.

For scripted flows, tools/ota_helper/ drives the same AWS APIs from the command line — see Tools reference.

Transport

The image is downloaded over AWS IoT MQTT file streams (CONFIG_RMNG_OTA_TRANSPORT_MQTT), which reuses the node's existing MQTT connection — no extra socket, no HTTP client, no second TLS handshake.

One consequence: an OTA download and ordinary node traffic share the same connection, so MQTT budgeting would starve the download. Kconfig therefore defaults CONFIG_ESP_RMAKER_MQTT_ENABLE_BUDGETING to n here. Leave it off.

OptionDefaultNotes
CONFIG_RMNG_OTA_MQTT_BLOCK_SIZE3072 (2304 on esp32c2)~75% of the MQTT input buffer, leaving headroom for other traffic
CONFIG_RMNG_OTA_MQTT_BLOCKS_PER_REQUEST40Must stay below the work queue's task queue size
CONFIG_RMNG_OTA_MQTT_DATA_TYPE_CBOR / _JSONCBORCBOR is more compact

Leaving ota_config.ota_cb as NULL selects the built-in MQTT callback and its image-reference validator (esp_rmaker_ota_mqtt_validate_image_ref, which rejects stream IDs too long for the AWS MQTT file downloader's fixed topic buffer). Set ota_config.ota_cb explicitly only when you need to wrap or gate that callback — see Status reporting.

Rollback and diagnostics

This is the safety net that stops a bad update bricking a fleet.

With CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE set — which CONFIG_RMNG_OTA_FORCE_ENABLE_ROLLBACK does by default on ESP-IDF — a freshly flashed image boots in a pending state. It must be explicitly marked valid, or the bootloader reverts to the previous image on the next reset.

Your diagnostics callback decides:

static esp_rmaker_ota_diag_status_t ota_diag_fn(esp_rmaker_ota_diag_priv_t *priv, void *p)
{
switch (priv->state) {
case OTA_DIAG_STATE_INIT:
/* Runs inside esp_rmaker_ota_enable(), on the first boot after an OTA. */
return OTA_DIAG_STATUS_SUCCESS;
case OTA_DIAG_STATE_POST_MQTT:
/* Runs again once MQTT has connected. */
return OTA_DIAG_STATUS_SUCCESS;
default:
return OTA_DIAG_STATUS_FAIL;
}
}
ReturnAt OTA_DIAG_STATE_INITAt OTA_DIAG_STATE_POST_MQTT
OTA_DIAG_STATUS_FAILRoll back immediatelyRoll back immediately
OTA_DIAG_STATUS_SUCCESSContinue bootingMark the image valid
OTA_DIAG_STATUS_PENDINGContinue bootingDefer — you must later call esp_rmaker_ota_mark_valid() or esp_rmaker_ota_mark_invalid()

priv->rmaker_ota tells you whether the OTA that triggered this was a RainMaker one, which matters only if your application has its own update path too.

Two escape hatches: esp_rmaker_ota_mark_valid() and esp_rmaker_ota_mark_invalid() can be called at any point, so you can validate against your own criteria — a successful sensor read, a completed self-test — rather than just "MQTT connected".

If nothing marks the image valid within CONFIG_RMNG_OTA_ROLLBACK_WAIT_PERIOD (default 90 s, range 30–600), the firmware is marked invalid and the older image boots.

caution
Passing ota_diag = NULL disables the check

With no diagnostics callback the new firmware is assumed fine and no rollback is ever performed. That is convenient during development and a liability in production — a firmware that boots but cannot connect will never be rolled back. Provide a callback that verifies something real.

To handle the reboot yourself — to finish a wash cycle first, say — set CONFIG_RMNG_OTA_DISABLE_AUTO_REBOOT=y and act on the OTA events.

Signature verification

CONFIG_RMNG_OTA_SIGNATURE_VERIFY_ENABLE (default n) makes the node verify the downloaded image's signature before flashing it.

Turning it on has two prerequisites:

  1. A codesign certificate must be present in the factory NVS credentials (codesign_cert). See Factory NVS.
  2. The OTA job document must carry a valid signature.

Enable this in production builds. Without it, a node will flash any image the job document points at.

Resume

CONFIG_RMNG_OTA_RESUME (default y) lets an interrupted download continue instead of restarting from byte 0. It activates only when the job document declares a file_md5 — the image identity. Download progress is persisted to NVS as a block bitmap and reused only for the exact same image; the full-image MD5 is verified end to end on completion.

Resume is always best-effort: any inconsistency falls back to a full download, so enabling it cannot make an otherwise-successful update fail.

Update windows

CONFIG_RMNG_OTA_TIME_SUPPORT (default y) honours the optional time metadata in a job document — "update between 1 and 10 December, only between 02:00 and 05:00". This depends on the node having a valid clock; see Time and timezone.

Reliability tuning

OptionDefaultNotes
CONFIG_RMNG_OTA_MAX_RETRIES_DIVIDE_FACTOR10Max retries = floor(total requests / factor). A fully successful request resets the count.
CONFIG_RMNG_OTA_REQUEST_TIMEOUT_MS10000Per-request timeout; range 1000–60000. Raise it on high-latency links.
CONFIG_RMNG_OTA_PROGRESS_CHECKPOINTS10Progress reports every 100/N%. Applies to the default callbacks only.
CONFIG_RMNG_OTA_EVENT_POOL_SIZE8Preallocated slots for payload-free state-machine events, so transitions and error recovery never stall on heap exhaustion. Range 4–32.

OTA events

The RMAKER_OTA_EVENT base carries the state machine:

EventMeaning
RMAKER_OTA_EVENT_STARTINGA job was accepted and the download is about to begin
RMAKER_OTA_EVENT_RESUMEDAn interrupted download resumed
RMAKER_OTA_EVENT_IN_PROGRESSA progress checkpoint
RMAKER_OTA_EVENT_SUCCESSFULImage flashed and verified
RMAKER_OTA_EVENT_FAILEDThe job failed
RMAKER_OTA_EVENT_REJECTEDThe job was rejected (wrong version, wrong model, bad image reference)
RMAKER_OTA_EVENT_DELAYEDDeferred, e.g. outside the allowed update window
RMAKER_OTA_EVENT_REQ_FOR_REBOOTReboot needed — relevant when auto-reboot is disabled
RMAKER_OTA_EVENT_ERROR_OCCURREDAn error state was entered; esp_rmaker_ota_request_recovery() can resume from it
RMAKER_OTA_EVENT_FETCH_REQUEST_IGNOREDAn esp_rmaker_ota_fetch() was dropped, e.g. a job is already running

Use these to drive user feedback. The ota-sim test simulator mirrors the whole state machine into reported parameters, which is a good pattern to copy.

About recovery

esp_rmaker_ota_request_recovery() does not fix the underlying problem — it just resumes the state machine from the recovery state. Calling it immediately on RMAKER_OTA_EVENT_ERROR_OCCURRED will very likely produce an infinite error loop. Fix the cause first, or back off.

Triggering a check from the device

esp_rmaker_ota_fetch(); /* ask the backend now */
esp_rmaker_ota_fetch_with_delay(60); /* ask in 60 seconds */

Useful for a "check for updates" button, or to retry after a network problem.

Custom jobs and filetypes

Two extension points for pushing things that are not app firmware — a host-MCU image, a resource bundle, a config blob.

Custom filetypes. Set ota_config.custom_filetype_handler_lookup to a function that resolves a filetype from the job document to a handler context. The ota-custom example implements this. See Firmware specifications → Custom OTA Filetypes.

Custom job documents. With CONFIG_RMNG_OTA_CUSTOM_JOB_SUPPORT=y, set ota_config.custom_job_cb and any job document the OTA engine does not recognise is handed to you whole. Report the outcome with esp_rmaker_ota_report_custom_job_status():

static esp_rmaker_error_t custom_job_cb(const char *job_doc, size_t job_doc_len)
{
/* Keep this minimal — copy what you need and return. */
/* The job document does not persist after the callback returns. */
return ESP_RMAKER_OK;
}

Rules worth knowing: don't reuse keys from the default OTA job document (a document with the right keys but bad values is treated as a default OTA job and auto-rejected), custom job execution posts no events, and returning ESP_RMAKER_INVALID_ARG auto-reports Rejected while any other error auto-reports Failed.

See Firmware specifications → Job Document Format.

Status reporting

If you write your own ota_cb, you own status reporting via esp_rmaker_ota_report_status():

StatusWhen
OTA_STATUS_IN_PROGRESSRepeatedly, as the update progresses
OTA_STATUS_SUCCESSOnce, at the end
OTA_STATUS_FAILEDOnce, at the end
OTA_STATUS_DELAYEDThe application postponed the update
OTA_STATUS_REJECTEDWrong project, wrong version, unusable image reference

You can still call esp_rmaker_ota_default_cb() from inside your own callback — a good way to gate when an OTA may proceed while leaving the mechanics to the SDK.

See Firmware specifications → Status Details for the JSON shapes.

Troubleshooting

SymptomCause
Job rejected immediatelyVersion not higher than what is running, or the model/platform doesn't match
Job rejected with "Image reference failed"The MQTT stream ID exceeded the downloader's limit
Download starts and stallsRequest timeout too low for the link, or MQTT budgeting is enabled
Update succeeds, then the old firmware comes backNothing marked the image valid within the rollback wait period — check your ota_diag callback
Signature verification failsNo codesign_cert in factory NVS, or the job document has no signature
Nothing happens at allesp_rmaker_neo_ota not linked, or esp_rmaker_ota_enable() called after esp_rmaker_start()