Arduino integration¶
Add SimpleOTA to an Arduino sketch using the first-party SimpleOTAClient library. It handles polling, download, SHA-256 verification, reboot, and automatic trial-install rollback in a few lines of code.
SimpleOTAClient library (recommended)¶
Repository: https://github.com/xanderwasserman/SimpleOTAClient-Arduino
The library README is the canonical reference for the full API, timing knobs, callback hooks, and limitations. The quick-start below is the minimum to get a device receiving updates.
Install¶
In the IDE: Sketch > Include Library > Manage Libraries... Search for SimpleOTAClient and click Install.
To install a specific version, or if the Library Manager is unavailable, download the zip from the Releases page and use Sketch > Include Library > Add .ZIP Library...
Managed-mode sketch¶
The library polls on a configurable timer and handles everything
automatically. begin() defaults to a one-hour interval.
In managed mode, the library auto-confirms the trial install on the
first successful /check/ response after a new image boots. This means
rollback protection works without any extra code in the default case.
#include <WiFi.h>
#include <SimpleOTAClient.h>
// Replace these with your values.
const char* PROJECT_TOKEN = "soto_proj_xxxxxxxxxxxxxxxx";
const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASS = "your-password";
#define FIRMWARE_VERSION "1.0.0"
// Pick the constant matching your hardware:
// CHIP_ESP32, CHIP_ESP32S2, CHIP_ESP32S3,
// CHIP_ESP32C3, CHIP_ESP32C6, CHIP_ESP32H2
SimpleOTAClient ota(PROJECT_TOKEN, SimpleOTAClient::CHIP_ESP32);
static bool wifiUp() { return WiFi.status() == WL_CONNECTED; }
void setup() {
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
ota.setVersionLabel(FIRMWARE_VERSION);
ota.begin(3600, nullptr, wifiUp); // hourly check, auto-apply, auto-reboot
}
void loop() {}
Polling mode¶
Use check() / apply() when you want to control exactly when updates
happen (for example, only while on mains power). In polling mode you
must call confirmRunning() yourself, otherwise every OTA will roll
back after the confirm timeout (default 300 s):
// Call once per loop iteration, before ota.check().
// It is a no-op when no trial is in progress.
ota.confirmRunning();
if (ota.check()) {
ota.apply(); // download, verify, flash, reboot
}
Targeting specific hardware¶
Optional constructor arguments let deployments target a device by stable ID, board, or hardware revision:
SimpleOTAClient ota(
PROJECT_TOKEN,
SimpleOTAClient::CHIP_ESP32S3,
/* deviceId */ "kitchen-sensor-01",
/* boardId */ "feather-s3",
/* hardwareRevision */ "rev-b"
);
Rollback (v0.2.0+)¶
SimpleOTAClient v0.2.0 adds trial-install rollback: after apply()
the library snapshots the previous partition and marks the new image as a
trial. The new firmware must call confirmRunning() within a timeout
(default 300 s) or the library reboots the device back into the previous
partition. The rolled_back event is POSTed to the server on the next
/check/ so the dashboard reflects the outcome automatically. By default
the deployment also pauses automatically when the first rolled_back
event arrives, stopping any further offers while you investigate.
This mechanism operates entirely inside your application firmware. It does
not depend on the ESP32 bootloader's rollback flag or esp_ota_mark_app_valid_cancel_rollback().
Managed mode (default: auto-confirm)¶
No extra code needed. The library confirms the trial as soon as the OTA
background task gets its first 2xx /check/ response, proving that boot,
connectivity, TLS, and token auth all succeeded.
To gate confirmation on your own health check instead, disable
auto-confirm before calling begin():
ota.setManagedAutoConfirm(false);
// Now call ota.confirmRunning() yourself once your app is healthy.
Polling mode (explicit confirm required)¶
In polling mode the library never auto-confirms. Call confirmRunning()
once per loop, at a point where you are confident the application is
working:
void loop() {
// Confirm the trial install. No-op when not in a trial.
ota.confirmRunning();
if (ota.check()) {
ota.apply();
}
delay((uint32_t)SIMPLEOTA_CHECK_INTERVAL_S * 1000UL);
}
Failing to call this means the device will roll back after the timeout expires on every OTA, regardless of whether the new firmware is healthy.
Application health gate (manual confirm in managed mode)¶
When you want to confirm only after your own sensors, servers, or application logic are proven working:
// In setup():
ota.setManagedAutoConfirm(false);
ota.setConfirmTimeout(120); // seconds; raise for cellular / slow-boot devices
ota.begin(3600, nullptr, wifiUp);
// In loop() - confirmed guards against re-running the check on subsequent iterations:
static bool confirmed = false;
if (!confirmed && ota.isTrialInstall() && applicationHealthy()) {
ota.confirmRunning();
confirmed = true;
}
See the RollbackOTA example in the library repository for the full
pattern.
Key methods¶
| Method | Description |
|---|---|
confirmRunning() |
Mark the running image as confirmed. No-op outside a trial. Returns true on success. |
isTrialInstall() |
Returns true if the current boot is an unconfirmed trial install. |
setManagedAutoConfirm(bool) |
Enable/disable managed-mode auto-confirm (default true). |
setConfirmTimeout(uint32_t s) |
Override the confirm timeout (default 300 s). Must be called before begin(). |
Limitations¶
- Pre-library crashes are not caught. The rollback timer is armed inside
begin()or the firstcheck()/isTrialInstall()/confirmRunning()call. If the firmware crashes or hangs before any of these run (for example in a global constructor or duringSerial.begin()), the timer is never armed and the device reboot-loops into the bad image. This is a fundamental difference from bootloader-level rollback. - The rollback mechanism targets the active OTA slot. Devices with only one OTA partition cannot roll back (apply() fails before the trial state is written, so no trial is started).
For more detail see Rollback and the Arduino library README.
Signed firmware (v0.4.0+)¶
The library verifies Ed25519 firmware signatures on the device: signed artifacts are checked against your pinned project public key, streamed during download, before the image is ever marked bootable. Even a compromised delivery path cannot make the device flash an unapproved image.
ota.setSecurityMode(SimpleOTAClient::SECURITY_MODE_SIGNED);
ota.setSigningPublicKey(
"-----BEGIN PUBLIC KEY-----\n"
"MCowBQYDK2VwAyEA...your project public key...\n"
"-----END PUBLIC KEY-----\n");
Get the public key from your project's "Firmware signing keys" section
(Advanced mode). On verification failure, apply() returns
OTA_SIGNATURE_FAIL, the device keeps running its current firmware, and
the dashboard shows the device as failed / signature_invalid.
Secure Boot is not available on Arduino
Stock Arduino-ESP32 cores ship precompiled bootloaders built without ESP32 Secure Boot, so hardware-rooted boot verification (protection against physical reflashing over UART/USB) cannot be enabled on the Arduino path. OTA-level signing as described here is the strongest firmware-integrity protection available on Arduino; it protects the update delivery path, not the device against physical access. If your threat model includes physical attackers, build under ESP-IDF with Secure Boot v2. See the threat model.
Key methods¶
| Method | Description |
|---|---|
setSigningPublicKey(pem) |
Pin the project public key (replaces pinned keys). Returns false for invalid PEM. |
addSigningPublicKey(keyId, pem) |
Pin a second key for rotation windows (max two). |
Migrating an existing fleet¶
Keys are compiled into the firmware, so ship one final basic upload whose
code pins the key and sets SECURITY_MODE_SIGNED. Devices running it
report signed mode and verify every subsequent update.
Two rules of the server's mode matching
to keep in mind: a device reporting basic is never offered a signed
artifact (so the migration build itself must be uploaded as basic), and
once devices report signed they are never offered basic artifacts (so
every upload after the migration must be signed). Blocked devices do not
error; they silently stay on their current build, so check the device list
for stragglers still reporting basic after the rollout.
Full walkthrough (key creation, CI signing, rotation): Signed firmware guide.