Skip to content

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

idf.py add-dependency "xanderwasserman/simpleota"

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.

  1. POST /api/v1/ota/check/ with Authorization: Bearer <token> and a JSON body carrying device_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).
  2. When the response has update_available: true, stream the pre-signed url into the inactive OTA partition (esp_ota_begin / esp_ota_write), hashing as you go, and compare against checksum (SHA-256 hex) before esp_ota_end + esp_ota_set_boot_partition. Do not send the bearer token to the download URL.
  3. Report lifecycle events to POST /api/v1/ota/status/ (device_id, deployment_id from the offer, event, build_number, optional reason). The event vocabulary, ordering, and reason tokens are documented in the device API reference; note that confirmed (not success) is the terminal success event, and unknown events are rejected with HTTP 400.
  4. Persist the offered build_number (NVS) and send it as current_build_number on 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.
  5. For rollback, prefer CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE + esp_ota_mark_app_valid_cancel_rollback() once the new image proves healthy, and esp_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.