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
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.
Minimal integration¶
#include "simpleota.h"
void app_main(void) {
// nvs_flash_init() and your Wi-Fi/Ethernet bring-up first...
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());
}
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.