Skip to content

Rollback

SimpleOTAClient v0.2.0 adds trial-install rollback for Arduino. After a successful apply() the new firmware is marked as a trial: it must call confirmRunning() within a timeout or the library boots the device back into the previous partition. The outcome (confirmed or rolled_back) is reported to the server automatically on the next /check/.

This guide covers the mechanism in detail. For the quick-start see Arduino integration.


How a trial install works

%%{init: {"flowchart": {"rankSpacing": 35, "nodeSpacing": 20}}}%%
flowchart TD
    classDef device  fill:#e0e7ff,color:#312e81,stroke:#6366f1
    classDef lib     fill:#e0f2fe,color:#075985,stroke:#0ea5e9
    classDef ok      fill:#dcfce7,color:#166534,stroke:#16a34a
    classDef bad     fill:#fee2e2,color:#991b1b,stroke:#dc2626

    A["<code>apply()</code> called"]:::device --> B["<code>download_started</code>, <code>downloaded</code><br/>SHA-256 verified"]:::lib
    B --> C["<code>flashed</code><br/>written to OTA partition"]:::lib
    C --> D["<code>validated</code><br/><code>sota_trial=1</code> saved to NVS"]:::lib
    D --> E["<code>reboot</code> event, <code>esp_restart()</code>"]:::lib

    E --> F(["New image boots"]):::device
    F --> G["<code>processBootValidation()</code><br/>detects <code>sota_trial=1</code>"]:::lib
    G --> H["Timer armed on first <code>begin()</code>, <code>check()</code>,<br/><code>isTrialInstall()</code>, or <code>confirmRunning()</code>"]:::lib

    H --> I{"<code>confirmRunning()</code> called<br/>within timeout?"}

    I -- Yes --> J["Trial state cleared from NVS"]:::ok
    J --> K["next <code>check()</code>: POST /status/<br/>event=confirmed"]:::ok
    K --> L(["Build recorded as stable"]):::ok

    I -- "No: timer fires" --> M["<code>performRollback()</code><br/><code>sota_trial=2</code>, <code>esp_restart()</code>"]:::bad
    M --> N(["Previous image boots"]):::bad
    N --> O["<code>processBootValidation()</code><br/>detects <code>sota_trial=2</code>"]:::bad
    O --> P["next <code>check()</code>: POST /status/<br/>event=rolled_back"]:::bad
    P --> Q(["Deployment auto-paused"]):::bad

Colours: indigo = app/device layer; blue = library automatic behaviour; green = confirmation path; red = rollback path.

The library operates at the application layer: it manages the OTA partition swap and the confirm window independently of the ESP32 bootloader's rollback flag. No bootloader configuration is required.


Confirm modes

Managed mode, auto-confirm (default)

The OTA background task (started by begin()) confirms the trial as soon as it gets a 2xx response from /check/. This proves that boot, network, TLS, and token auth are all working.

No extra code is needed:

ota.begin(3600, nullptr, wifiUp);
// Trial is confirmed automatically on the first successful /check/.

Managed mode, manual confirm

Disable auto-confirm to gate the trial on your own application health:

// In setup():
ota.setManagedAutoConfirm(false);
ota.setConfirmTimeout(120);   // seconds
ota.begin(3600, nullptr, wifiUp);

// In loop():
static bool confirmed = false;
if (!confirmed && ota.isTrialInstall() && applicationHealthy()) {
    ota.confirmRunning();
    confirmed = true;
}

isTrialInstall() lets you skip the health-check work entirely on normal (non-trial) boots, keeping the loop lightweight.

Polling mode, explicit confirm

The library never auto-confirms in polling mode. Call confirmRunning() once per loop, before check(), at a point where you are confident the application is working:

void loop() {
    ota.confirmRunning();   // no-op outside a trial

    if (ota.check()) {
        ota.apply();        // reboots on success by default
    }
    delay((uint32_t)SIMPLEOTA_CHECK_INTERVAL_S * 1000UL);
}

Omitting this call means the device rolls back after the timeout on every OTA, regardless of whether the new firmware is healthy.


Timeout

The confirm window starts when the library initialises (on the first call to begin(), check(), isTrialInstall(), or confirmRunning()), not when apply() was called on the previous boot.

Default: 300 seconds. Override with:

ota.setConfirmTimeout(seconds);   // call before begin() or the first check()

Guidelines:

Transport Suggested timeout
Wi-Fi (fast home network) 60-120 s
Wi-Fi (enterprise / captive portal) 180-300 s
Cellular (LTE-M / NB-IoT) 300-600 s

Set the timeout to the worst-case time from device power-on to your application's healthy state, with margin. If in doubt, use the default.


Dashboard visibility

When a trial is confirmed the device's status moves to confirmed in the deployment progress view. When a trial rolls back:

  • The device reverts to its previous build number.
  • The rolled_back event appears in the device event log.
  • The deployment progress counter does not count the device as confirmed.

Automatic deployment pause

By default, the deployment pauses automatically the moment the first rolled_back event is received. From that point, new /check/ requests behave as if the deployment does not exist, so no additional devices are offered the update while you investigate.

This behaviour is controlled by two settings on the deployment, both configurable from the deployment form and the deployment detail page:

  • Auto-pause on rollback (default: on): whether to pause at all.
  • Rollback pause threshold (default: 1): number of rolled_back events required before the pause fires. Raise this if you expect transient rollbacks (for example, a cellular fleet where some devices may time out on slow modem registration) and want to require multiple failures before stopping the rollout.

After the automatic pause you can resume the deployment (if the rollback was a known transient) or cancel it and deploy a corrected build.


Limitations

  • Pre-library crashes are not caught. The rollback timer is armed lazily inside begin(), or the first call to check(), isTrialInstall(), or confirmRunning(). If the firmware crashes before any of these run (for example in a global constructor or during Serial.begin()), the timer is never armed and the device reboot-loops into the bad image indefinitely. This is a fundamental difference from bootloader-level rollback and the most important limitation to understand.
  • Single OTA partition: apply() fails with OTA_FLASH_FAIL before trial state is written to NVS, so no trial is started and no rollback occurs. The net effect is the same as having rollback disabled.
  • ESP-IDF: the trial-install mechanism is specific to SimpleOTAClient (Arduino). For ESP-IDF, use esp_ota_mark_app_valid_cancel_rollback() after your own health check. See ESP-IDF integration.