Skip to content

Upgrading to 1.0.0

Arduino SimpleOTAClient 1.0.0 and ESP-IDF simpleota 1.0.0 share one update core and the same simpleota.h API. Inside simpleota_apply(), and before any download, both refuse an offer whose build number is not strictly above the running build and report that refusal as failed with reason downgrade_refused. simpleota_check() still returns the offer. SimpleOTA is roll-forward only: to move the fleet back to older code, upload that binary again so the platform assigns a new, higher build number and deploy it.

Trial-boot rollback still works the same way, so a new image that isn't confirmed within the timeout returns to the previous one. Every OTA apply runs as a trial on both platforms when bootloader or software rollback is enabled.

Install 1.0.0 from the Arduino Library Manager or the Arduino releases page, and on ESP-IDF with idf.py add-dependency "wassie-dev/simpleota^1.0.0". The snippets below describe the API at those versions.

Shared: image_build

When firmware is flashed over USB, or installed any other way than by SimpleOTA, the client stores the running image ELF SHA-256 in NVS. On boot, if that fingerprint changed and no pending SimpleOTA trial explains it, the device reports image_build from config (default 0) instead of the build number left from an earlier OTA. Reflashing the same image keeps the stored build; image_build does not override that.

In release firmware you also ship by serial, set it to the build number SimpleOTA assigned when you uploaded that release, so anti-rollback and the server see the correct build. Set cfg.image_build in simpleota_config_t before simpleota_init() on both Arduino and ESP-IDF.

If you USB-flash a device back to a pre-1.0 client, that client keeps reporting the build number the 1.0.0 client stored in NVS until SimpleOTA installs a newer build. Flashing an old client back while a 1.0 trial is still open is not supported.

Arduino: 0.6.0 to 1.0.0

1.0.0 replaces the SimpleOTAClient C++ class with the same C API as ESP-IDF. #include <simpleota.h>, fill a simpleota_config_t, and call simpleota_init() early in setup().

0.6.x (SimpleOTAClient) 1.0.0 (simpleota.h)
SimpleOTAClient ota(token, chip, ...) cfg.token, cfg.chip_family, cfg.device_id, ... then simpleota_init(&cfg)
ota.begin(interval, onResult, isConnected[, delayMs]) cfg.check_interval_s, cfg.event_cb, cfg.is_connected, cfg.initial_delay_ms, then simpleota_start()
ota.end() simpleota_stop()
ota.checkNow() simpleota_check_now()
ota.check() / ota.apply() simpleota_check(&update) / simpleota_apply()
ota.confirm() simpleota_confirm()
ota.setImageBuild(n) cfg.image_build
ota.setServerUrl(url) cfg.base_url or simpleota_set_server_url(url) while stopped
ota.setCACert(pem) cfg.cert_pem (TLS stays on; use cert_pem to pin a CA)
ota.onEvent(cb) cfg.event_cb with simpleota_event_t / SIMPLEOTA_EVENT_*
OTAResult / OTA_* simpleota_result_t / SIMPLEOTA_*
SECURITY_MODE_BASIC, SECURITY_MODE_SIGNED SIMPLEOTA_SECURITY_BASIC, SIMPLEOTA_SECURITY_SIGNED on cfg.security_mode
setSigningPublicKey / addSigningPublicKey cfg.signing_keys / cfg.num_signing_keys
SimpleOTAClient::setDebug(true) esp_log_level_set("simpleota", ESP_LOG_DEBUG)

Managed mode

Before (0.6.0):

#include <SimpleOTAClient.h>
SimpleOTAClient ota(token);
ota.begin(3600, nullptr, wifiUp);

After (1.0.0):

#include <simpleota.h>
simpleota_config_t cfg = {};
cfg.token = token;
cfg.is_connected = wifiUp;
cfg.check_interval_s = 3600;
simpleota_init(&cfg);
simpleota_start();

Events instead of begin(onResult)

Before (0.6.0):

void onResult(OTAResult r) {
    if (r == OTA_FLASH_FAIL) { /* ... */ }
}
ota.begin(3600, onResult, wifiUp);

After (1.0.0):

void onOtaEvent(const simpleota_event_t *e, void *) {
    if (e->id == SIMPLEOTA_EVENT_UPDATE_FAILED && e->reason) {
        Serial.println(e->reason);
    }
}
cfg.event_cb = onOtaEvent;
simpleota_start();

Remove report() and setRollbackEnabled()

The library now posts lifecycle status itself, so delete your report() calls, and delete setRollbackEnabled() too, because every apply on Arduino 1.0 runs as a trial.

Confirm and trial helpers

Before (0.6.0): confirmRunning(), isTrialInstall()

After (1.0.0): simpleota_confirm(), simpleota_is_trial_boot()

HTTP timeout build flag

On 1.0.0 the knob is CONFIG_SIMPLEOTA_HTTP_TIMEOUT_MS (default 15000), set as a -D compile flag or in ESP-IDF Kconfig. CONFIG_SIMPLEOTA_TASK_STACK_SIZE (default 12288) sizes the managed task and the short-lived tasks that run simpleota_check() and simpleota_apply() in polling mode. Map from 0.6.x as follows:

0.6.x 1.0.0
SIMPLEOTA_TIMEOUT_MS -DCONFIG_SIMPLEOTA_HTTP_TIMEOUT_MS=...
SIMPLEOTA_HTTP_TIMEOUT_MS -DCONFIG_SIMPLEOTA_HTTP_TIMEOUT_MS=...

TLS is always verified

setCACert(nullptr) on 0.6.x disabled verification. On 1.0.0 TLS stays on; pin trust with cfg.cert_pem when needed.

Handle SIMPLEOTA_DOWNGRADE

After (1.0.0):

bool update = false;
esp_err_t err = simpleota_check(&update);
if (err == ESP_ERR_NO_MEM) { /* try again later */ }
else if (err == ESP_OK && update) {
    switch (simpleota_apply()) {
        case SIMPLEOTA_OK: break;
        case SIMPLEOTA_NO_OFFER: break;
        case SIMPLEOTA_FLASH_FAIL: break;  // network, URL, flash or state failure; offer cleared unless the apply task could not be created
        case SIMPLEOTA_DOWNGRADE: break;  // server gets failed / downgrade_refused
    }
}

Do not mix polling and managed mode

While simpleota_start() runs, simpleota_check() returns ESP_ERR_INVALID_STATE and simpleota_apply() returns SIMPLEOTA_NO_OFFER. Call simpleota_stop() before polling, or never call simpleota_start().

simpleota_set_server_url() only when stopped

cfg.base_url = "https://simpleota.com";  // before init, or:
simpleota_stop();
simpleota_set_server_url("https://simpleota.com");

http:// URLs are rejected. Firmware downloads follow up to three redirects, each of which must be https://.

One client instance

A second simpleota_init() returns ESP_ERR_INVALID_STATE.

simpleota_stop() waits for the current step

simpleota_stop() blocks up to about one minute for the current check, download, or flash step to finish.

Chip family

Before (0.6.0): SimpleOTAClient::CHIP_ESP32S3

After (1.0.0): cfg.chip_family = "esp32s3" (defaults to the build target when omitted).

ESP-IDF: 0.2.1 to 1.0.0

In idf_component.yml, replace xanderwasserman/simpleota with wassie-dev/simpleota.

security_mode is simpleota_security_t

Before (0.2.x): security_mode is const char *. The macros are string literals (SIMPLEOTA_SECURITY_BASIC is "basic", SIMPLEOTA_SECURITY_SIGNED is "signed", SIMPLEOTA_SECURITY_TOKEN is "token").

cfg.security_mode = SIMPLEOTA_SECURITY_SIGNED; /* const char * "signed" */
cfg.security_mode = "basic";

After (1.0.0): the field is simpleota_security_t (simpleota.h). SIMPLEOTA_SECURITY_BASIC and SIMPLEOTA_SECURITY_SIGNED still compile, as enum constants. Assigning a string does not compile. SIMPLEOTA_SECURITY_TOKEN is removed. SIMPLEOTA_SECURITY_NONE (the zero default) reports no mode.

cfg.security_mode = SIMPLEOTA_SECURITY_SIGNED;

base_url must be https:// with a host

Before (0.2.1): base_url was stored as given. A plain http:// URL was accepted.

After (1.0.0): omit base_url to use https://simpleota.com, or set an https:// URL that has a host. simpleota_init() returns ESP_ERR_INVALID_ARG otherwise. Plain http:// is rejected as not_https. An empty URL, one with no host, one longer than 256 characters, or one with spaces or control characters is rejected as malformed.

simpleota_config_t cfg = {
    .token = "YOUR_PROJECT_TOKEN",
    .base_url = "https://simpleota.com",  // https:// with a host, or omit
};
ESP_ERROR_CHECK(simpleota_init(&cfg));

simpleota_result_t and polling API

Before (0.2.1): polling was not in the public header.

After (1.0.0):

bool update = false;
esp_err_t err = simpleota_check(&update);
if (err == ESP_ERR_NO_MEM) { /* try again later */ }
else if (err == ESP_OK && update) {
    simpleota_result_t r = simpleota_apply();
    if (r == SIMPLEOTA_DOWNGRADE) { /* refused before download */ }
    if (r == SIMPLEOTA_FLASH_FAIL) { /* network, URL, flash or state failure; offer cleared unless the apply task could not be created */ }
}

While the managed task runs, simpleota_check() returns ESP_ERR_INVALID_STATE and simpleota_apply() returns SIMPLEOTA_NO_OFFER.

Polling mode always needs simpleota_confirm()

.manual_confirm applies to managed mode only. In polling mode you must call simpleota_confirm() after your health checks, on every trial boot.

After (1.0.0):

if (simpleota_is_trial_boot()) {
    ESP_ERROR_CHECK(simpleota_confirm());
}

Software rollback when bootloader rollback is off

After (1.0.0): CONFIG_SIMPLEOTA_SOFTWARE_ROLLBACK defaults to y. A reset after simpleota_init() on the new image (crash, deep sleep, or esp_restart()) before confirm rolls back. Confirm (or managed auto-confirm) before you call esp_restart() on purpose, or set CONFIG_SIMPLEOTA_SOFTWARE_ROLLBACK=n, and with bootloader rollback also off, a new image is then accepted on first boot as in 0.2.x. Software rollback can't catch a crash loop that never reaches simpleota_init(), so turn on CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE on new devices.

simpleota_confirm() return value

After (1.0.0):

esp_err_t err = simpleota_confirm();
if (err == ESP_FAIL) {
    /* image not marked valid; trial continues; call again */
}

simpleota_deinit() return values

After (1.0.0):

esp_err_t err = simpleota_deinit();
if (err == ESP_ERR_TIMEOUT) { /* task did not stop; resources kept */ }
if (err == ESP_ERR_INVALID_STATE) { /* called from event_cb on the managed task */ }

image_build in simpleota_config_t

The field sits between version_label and nvs_schema_version, so use designated initializers rather than positional ones.

After (1.0.0):

simpleota_config_t cfg = {
    .token = "YOUR_PROJECT_TOKEN",
    .version_label = "1.4.0",
    .image_build = 15,
    .nvs_schema_version = 1,
};

simpleota_set_server_url() and simpleota_last_offered_version()

simpleota_set_server_url() follows the same https:// with a host rule as base_url and returns ESP_ERR_INVALID_ARG when the URL does not match. It returns ESP_ERR_INVALID_STATE before init or while the managed task runs.

ESP_ERROR_CHECK(simpleota_set_server_url("https://simpleota.com"));
const char *ver = simpleota_last_offered_version();