ESP-IDF integration¶
The recommended path is the first-party component
xanderwasserman/simpleota
from the Espressif Component Registry. It implements the full SimpleOTA
device contract: periodic checks, streaming download and flash with SHA-256
and Ed25519 signature verification, complete lifecycle status reporting, and
bootloader-rollback-aware trial installs. It is the sibling of the Arduino
client and behaves identically on the wire.
If you prefer to hand-roll your integration, the manual integration appendix below documents the raw API flow.
Install¶
Project prerequisites¶
In your project's sdkconfig.defaults:
CONFIG_PARTITION_TABLE_TWO_OTA=y
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
Two OTA app slots are required for any OTA scheme. Bootloader rollback is strongly recommended: with it, a freshly flashed image gets exactly one attempt (crash, watchdog, or power loss before confirmation returns the device to the previous build automatically). Both settings live in the bootloader and partition table, so they must ship with the first serial flash; they cannot be enabled later over the air.
Each two-OTA app slot is 1 MB. A size-optimized build leaves room in it: at the default optimization level, an app with Wi-Fi and SimpleOTA can exceed 1 MB, and the build stops with "All app partitions are too small".
ESP-IDF reads sdkconfig.defaults only when it generates sdkconfig, so
delete an existing sdkconfig after adding these lines.
Minimal integration¶
#include "simpleota.h"
void app_main(void) {
// nvs_flash_init(), then bring up Wi-Fi (or Ethernet) and wait for an IP:
// the first check runs as soon as simpleota_start() is called.
simpleota_config_t cfg = {
.token = "YOUR_PROJECT_TOKEN", // Project -> API tokens (device scope)
.board_id = "my-board-r1", // match your artifact constraints
};
ESP_ERROR_CHECK(simpleota_init(&cfg));
ESP_ERROR_CHECK(simpleota_start());
}
esp_wifi_start() and esp_wifi_connect() return before the station has
an IP, so wait for IP_EVENT_STA_GOT_IP before calling simpleota_start().
A check without an IP fails, the component retries it once after 2 s, and
the next attempt comes a full check interval later (default one hour). To
make every check wait for the link, set the optional .is_connected
callback as well: while it returns false the component polls it every
second, and the check runs as soon as it returns true. The basic example
does both.
The component starts a background task that checks on an interval (default
hourly), downloads and flashes offered builds, reboots into them as a
supervised trial, and reports the full lifecycle
(download_started through confirmed / failed / rolled_back) so the
dashboard timeline is complete. The device identifies itself with its Wi-Fi
MAC by default and reports framework: "esp_idf" plus the chip family of
the build target automatically.
Rollback¶
With CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE, a freshly flashed image boots
as PENDING_VERIFY and the bootloader itself guarantees exactly one attempt:
a crash, watchdog reset, or power loss before confirmation returns the
device to the previous build automatically. On top of that the component
runs a confirm timeout (default 300 s): an image that runs but never
completes a successful check-in is marked invalid and rolled back, reported
as rolled_back / confirm_timeout. After any rollback the previous build
number is restored in NVS so the server can re-offer a fixed build.
Confirmation is automatic on the first successful check-in after a trial
boot. If your application has its own health checks, set
.manual_confirm = true and call simpleota_confirm() once they pass.
Signed firmware¶
Pin your project's public key and enforce signed updates:
static const char *PUBKEY_PEM =
"-----BEGIN PUBLIC KEY-----\n"
"...from the dashboard's Signing keys section...\n"
"-----END PUBLIC KEY-----\n";
const simpleota_signing_key_t keys[] = {{.key_id = "prod-2026", .pem = PUBKEY_PEM}};
simpleota_config_t cfg = {
.token = "...",
.security_mode = SIMPLEOTA_SECURITY_SIGNED,
.signing_keys = keys,
.num_signing_keys = 1,
};
Every offered image is then verified against the pinned key during
download, before the partition is marked bootable. A missing, stripped, or
invalid signature rejects the update (failed / signature_invalid) and
the device stays on its current build. See the
signed firmware guide for key creation, CI signing,
rotation, and the basic -> signed fleet migration recipe (identical for
Arduino and ESP-IDF).
TLS¶
The component uses the ESP-IDF certificate bundle by default
(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE, on by default), so there is no CA
certificate to embed or rotate. Pin a specific CA with .cert_pem if your
policy requires it.
Examples¶
The component ships two complete example projects, also usable as
templates via
idf.py create-project-from-example "xanderwasserman/simpleota:basic":
basic: managed updates with bootloader rollback.signed: on-device Ed25519 verification with a pinned key.
Appendix: manual integration¶
The raw device API is small and hand-rolling a client is entirely supported; this is what the component does internally.
POST /api/v1/ota/check/withAuthorization: Bearer <token>and a JSON body carryingdevice_id,framework: "esp_idf",chip_family,current_build_number(0 for a factory-fresh device), and any compatibility fields your artifacts constrain (board_id,hardware_revision,partition_profile,nvs_schema_version,security_mode,channel).- When the response has
update_available: true, stream the pre-signedurlinto the inactive OTA partition (esp_ota_begin/esp_ota_write), hashing as you go, and compare againstchecksum(SHA-256 hex) beforeesp_ota_end+esp_ota_set_boot_partition. Do not send the bearer token to the download URL. - Report lifecycle events to
POST /api/v1/ota/status/(device_id,deployment_idfrom the offer,event,build_number, optionalreason). The event vocabulary, ordering, and reason tokens are documented in the device API reference; note thatconfirmed(notsuccess) is the terminal success event, and unknown events are rejected with HTTP 400. - Persist the offered
build_number(NVS) and send it ascurrent_build_numberon subsequent checks; after a rollback, restore the previous build number or the server will consider the device up to date and never re-offer a fix. - For rollback, prefer
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE+esp_ota_mark_app_valid_cancel_rollback()once the new image proves healthy, andesp_ota_mark_app_invalid_rollback_and_reboot()when it does not.
For signed firmware in a hand-rolled client, see the manual verification appendix of the signed firmware guide.