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
| Platform | Path, 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. |
| POSIX | partitions/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.
| Option | Default | Notes |
|---|---|---|
CONFIG_RMNG_OTA_MQTT_BLOCK_SIZE | 3072 (2304 on esp32c2) | ~75% of the MQTT input buffer, leaving headroom for other traffic |
CONFIG_RMNG_OTA_MQTT_BLOCKS_PER_REQUEST | 40 | Must stay below the work queue's task queue size |
CONFIG_RMNG_OTA_MQTT_DATA_TYPE_CBOR / _JSON | CBOR | CBOR 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;
}
}
| Return | At OTA_DIAG_STATE_INIT | At OTA_DIAG_STATE_POST_MQTT |
|---|---|---|
OTA_DIAG_STATUS_FAIL | Roll back immediately | Roll back immediately |
OTA_DIAG_STATUS_SUCCESS | Continue booting | Mark the image valid |
OTA_DIAG_STATUS_PENDING | Continue booting | Defer — 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.
ota_diag = NULL disables the checkWith 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:
- A codesign certificate must be present in the factory NVS credentials (
codesign_cert). See Factory NVS. - 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
| Option | Default | Notes |
|---|---|---|
CONFIG_RMNG_OTA_MAX_RETRIES_DIVIDE_FACTOR | 10 | Max retries = floor(total requests / factor). A fully successful request resets the count. |
CONFIG_RMNG_OTA_REQUEST_TIMEOUT_MS | 10000 | Per-request timeout; range 1000–60000. Raise it on high-latency links. |
CONFIG_RMNG_OTA_PROGRESS_CHECKPOINTS | 10 | Progress reports every 100/N%. Applies to the default callbacks only. |
CONFIG_RMNG_OTA_EVENT_POOL_SIZE | 8 | Preallocated 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:
| Event | Meaning |
|---|---|
RMAKER_OTA_EVENT_STARTING | A job was accepted and the download is about to begin |
RMAKER_OTA_EVENT_RESUMED | An interrupted download resumed |
RMAKER_OTA_EVENT_IN_PROGRESS | A progress checkpoint |
RMAKER_OTA_EVENT_SUCCESSFUL | Image flashed and verified |
RMAKER_OTA_EVENT_FAILED | The job failed |
RMAKER_OTA_EVENT_REJECTED | The job was rejected (wrong version, wrong model, bad image reference) |
RMAKER_OTA_EVENT_DELAYED | Deferred, e.g. outside the allowed update window |
RMAKER_OTA_EVENT_REQ_FOR_REBOOT | Reboot needed — relevant when auto-reboot is disabled |
RMAKER_OTA_EVENT_ERROR_OCCURRED | An error state was entered; esp_rmaker_ota_request_recovery() can resume from it |
RMAKER_OTA_EVENT_FETCH_REQUEST_IGNORED | An 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.
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():
| Status | When |
|---|---|
OTA_STATUS_IN_PROGRESS | Repeatedly, as the update progresses |
OTA_STATUS_SUCCESS | Once, at the end |
OTA_STATUS_FAILED | Once, at the end |
OTA_STATUS_DELAYED | The application postponed the update |
OTA_STATUS_REJECTED | Wrong 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
| Symptom | Cause |
|---|---|
| Job rejected immediately | Version 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 stalls | Request timeout too low for the link, or MQTT budgeting is enabled |
| Update succeeds, then the old firmware comes back | Nothing marked the image valid within the rollback wait period — check your ota_diag callback |
| Signature verification fails | No codesign_cert in factory NVS, or the job document has no signature |
| Nothing happens at all | esp_rmaker_neo_ota not linked, or esp_rmaker_ota_enable() called after esp_rmaker_start() |
Related
- Firmware specifications → OTA — job documents, status details, custom filetypes
- Tools reference — scripting jobs from the CLI
- Product overview → OTA
- Pre-production checklist