SimpleOTA
All posts
6 min read Alexander Wasserman

SimpleOTA client libraries 1.0 for Arduino and ESP-IDF

The Arduino and ESP-IDF client libraries hit 1.0 and now share one core. Here is what changed, what you need to update, and how your devices get there over the air.

releases arduino esp-idf

Both SimpleOTA client libraries just hit 1.0. SimpleOTAClient 1.0.0 for Arduino and simpleota 1.0.0 for ESP-IDF now run on the same update core behind the same simpleota.h. Whichever framework you use, you get the same API and the same behaviour, so if you’re starting new firmware, start here.

The short version for existing users: if your Arduino sketch creates a SimpleOTAClient object, you’ll need to change a few lines before it builds. Your devices in the field are fine, though. Anything running Arduino 0.6.0 or earlier, or ESP-IDF 0.2.x, can take the 1.0 image over the air.

Getting it

On Arduino, grab SimpleOTAClient 1.0.0 from the Library Manager, or use the PlatformIO package xanderwasserman/SimpleOTAClient. On ESP-IDF, add the component wassie-dev/simpleota:

idf.py add-dependency "wassie-dev/simpleota^1.0.0"

Heads up if you’re on ESP-IDF 0.2.x: your project depends on xanderwasserman/simpleota, and that name stays on 0.2.x for good. Swap it for wassie-dev/simpleota in your idf_component.yml to get 1.0.

Either way, you include simpleota.h (<simpleota.h> on Arduino, "simpleota.h" on ESP-IDF). Both libraries are tagged v1.0.0 on GitHub.

Upgrading from 0.x

The good news first: devices on Arduino 0.6.0 or earlier, or on ESP-IDF 0.2.x, can update to 1.0 over the air. 1.0 keeps the NVS keys those releases already write and adds one new key, sota_image, which older releases simply ignore. If the 1.0 trial fails, the device boots back into the old image just like it would for any other update.

On Arduino, 1.0 is a breaking change. The SimpleOTAClient class is replaced by the C API, so you fill in a simpleota_config_t and call simpleota_init(). If you used begin() on 0.6, its arguments map across like this:

  • interval is now cfg.check_interval_s.
  • onResult is now cfg.event_cb.
  • isConnected is now cfg.is_connected, and the callback takes a void *.
  • initialDelayMs is now cfg.initial_delay_ms, and you call simpleota_start() to run the task.

You can delete any calls to report(), because the library posts every status itself.

security_mode is an enum on both clients now, simpleota_security_t. Set it to SIMPLEOTA_SECURITY_BASIC or SIMPLEOTA_SECURITY_SIGNED, or leave it at zero to report none. ESP-IDF code that already uses those two names compiles as is. SIMPLEOTA_SECURITY_TOKEN and string values won’t compile, and the platform treats a device that reports token as reporting none.

One more thing if you use signed firmware: SIMPLEOTA_SECURITY_SIGNED needs at least one key in cfg.signing_keys. Without one, simpleota_init() returns ESP_ERR_INVALID_ARG on both clients. That way a device in signed mode always has a pinned key to check signatures against.

Downgrades and rollbacks

Build numbers only go up, and 1.0 holds devices to that. A device won’t install a build at or below the one it’s running, so you can’t fix a bad release by pointing the deployment back at an older build. Roll forward instead:

  1. Pause the deployment that’s offering the bad build.
  2. Upload the known-good binary again. SimpleOTA gives it the next build number, so 13 if 12 was the last one.
  3. Deploy that new build.

Pausing deployments walks through it in more detail.

Builds 11, 12 and 13 on a line that only goes up. Re-targeting build 11 on a device running 12 is refused with downgrade_refused, while build 13, the build 11 binary uploaded again, is accepted. Same bytes as an older release, but a higher build number.

If you poll, simpleota_check() reports the offer as usual. simpleota_apply() then sees that build_number isn’t above the running build, posts failed with reason downgrade_refused to /status/, and returns SIMPLEOTA_DOWNGRADE. There’s no override in the public API. The device posts that status once per boot, so applying the same offer again in the same boot just returns SIMPLEOTA_DOWNGRADE quietly.

#include <esp_log.h>

bool update = false;
if (simpleota_check(&update) == ESP_OK && update) {
  if (simpleota_apply() == SIMPLEOTA_DOWNGRADE) {
    ESP_LOGW("simpleota", "offered build is not above the running one");
  }
}

Rolling back a bad update is a different thing, and it’s automatic. When a newer build is flashed, the previous image stays on flash until the new one is confirmed. If the confirm timeout runs out, or the new image resets for any reason before it’s confirmed (a crash, a watchdog, a brownout, a power cut, a restart or a wake from deep sleep), the device goes back to the previous image. So on both clients, confirm before you restart or sleep on purpose.

A new image boots as a trial while the previous image stays on flash. A confirmation keeps it. Any reset before the confirmation, or the confirm timeout, boots the previous image and reports the reason. The rolled_back report tells you what ended the trial.

On Arduino, every update in 1.0 runs as a trial, so setRollbackEnabled() is gone. On ESP-IDF, turning on CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE also catches an image that crashes before simpleota_init() even runs. Keep in mind that this setting lives in the bootloader, and an OTA update only replaces the app. It only takes effect on devices you flash over USB with it turned on, and devices already in the field keep the bootloader they shipped with. If bootloader rollback is off, CONFIG_SIMPLEOTA_SOFTWARE_ROLLBACK is on by default, and the client rolls the trial back itself from simpleota_init() onwards.

USB flashing and image_build

SimpleOTA has no way of knowing when you flash an image over USB. To spot it, the client stores a fingerprint of the running image, its ELF SHA-256. If the fingerprint changes and no SimpleOTA trial explains why, the device reports the image_build from your simpleota_config_t, which defaults to 0. Reflashing the same image keeps the stored build, and an image SimpleOTA installed keeps the build number from its offer. If a device reports a build number the project never issued, the server compares it as 0 but still records the number the device sent.

At init the client compares the running image's ELF SHA-256 with the stored sota_image. The same image keeps the stored build, an image a SimpleOTA trial installed reports the offer's build, and any other image reports cfg.image_build, 0 by default. A USB-flashed image reports image_build, so the server offers it the current release.

So if you ever flash a release over USB, set cfg.image_build to the build number SimpleOTA gave that exact binary when you uploaded it, before you call simpleota_init(). On Arduino, start from an empty initializer and set each field on its own line, which builds on every C++ version Arduino-ESP32 uses:

#include <simpleota.h>

simpleota_config_t cfg = {};
cfg.token = "YOUR_PROJECT_TOKEN";
cfg.image_build = 15;
simpleota_init(&cfg);

In an ESP-IDF C file you can name the fields instead:

#include "simpleota.h"

simpleota_config_t cfg = {
    .token = "YOUR_PROJECT_TOKEN",
    .image_build = 15,
};
simpleota_init(&cfg);

For how a successful OTA writes the offer’s build into NVS, see build numbers.

Where to go next

Whichever client you’re on, handle SIMPLEOTA_DOWNGRADE, then follow Upgrading to 1.0.0 for the rest, step by step.

New to SimpleOTA? Start with getting started and create an account. The Arduino guide takes a 1.0.0 sketch all the way from install to your first deployment. You’ll find the packages on PlatformIO and the Espressif Component Registry.