OTA updates
Updating firmware over Wi-Fi instead of over a cable, without bricking the board when the power fails halfway. The mechanism is simple - two copies of the firmware - and it costs you half your flash.
Two slots, one running
markValid(), which is what saves you from bricking a device on a roof.Push or pull
Push — ArduinoOTA, the Update over network entry in the IDE. Convenient on your desk, useless in the field, because you have to be able to reach the board.
Pull — the board checks a URL on a schedule. This is what a product does: it works through NAT, it works when the device is on somebody else's network, and it scales to more than one board.
Three things a real update needs
- A version check, so a board does not re-download the same image every hour.
- Rollback, so a firmware that cannot connect is undone automatically. That
is
esp_ota_mark_app_valid_cancel_rollback()and the choice of when to call it is the whole design. - Signature verification, if the update goes over plain HTTP. Otherwise anyone who can answer that URL owns your device.
The code
Pull rather than push. The board checks a URL for a version number, downloads only if it is newer, and marks the new image valid once it has proved it can connect. That last step is what rollback needs.
#include <WiFi.h>
#include <HTTPClient.h>
#include <HTTPUpdate.h>
#include <esp_ota_ops.h>
const char *URL = "http://example.com/firmware.bin";
const int VERSION = 7;
void setup() {
Serial.begin(115200);
WiFi.begin("your-network", "your-password");
while (WiFi.status() != WL_CONNECTED) delay(250);
// We are connected and running, so the current image is good
esp_ota_mark_app_valid_cancel_rollback();
WiFiClient client;
httpUpdate.rebootOnUpdate(true);
t_httpUpdate_return r = httpUpdate.update(client, URL,
String(VERSION));
if (r == HTTP_UPDATE_FAILED)
Serial.printf("failed %d: %s\n", httpUpdate.getLastError(),
httpUpdate.getLastErrorString().c_str());
}
void loop() {}Without markAppValid the bootloader assumes the update failed and switches back on the next reboot. That is the safety net - do not call it at the top of setup.
MicroPython updates are usually files rather than firmware images - fetch main.py and reboot. Simpler, and it does not protect you from a bad download the way a dual-slot firmware update does.
import urequests, os, machine
def update(url):
r = urequests.get(url)
if r.status_code != 200:
r.close()
return False
with open('main.new', 'w') as f:
f.write(r.text)
r.close()
os.rename('main.new', 'main.py') # atomic - all or nothing
machine.reset()
update('http://example.com/main.py')Download to a temporary name and rename only after the whole file has arrived. A truncated main.py is a board that boot-loops and needs a cable.
When it does not work
Two app slots means each one is half the app space. Pick a partition scheme with a bigger app region, or a board with 8 MB of flash.
Rollback fired. The new image never called markAppValid, so the bootloader concluded it was broken and switched back. Call it once the new firmware has proved itself.
An HTTPS update needs a large contiguous heap for the TLS handshake on top of the download buffer. Do the update early, before allocating display buffers, or update over plain HTTP with a signature check.
Weak Wi-Fi. A megabyte over a marginal link times out. Retry with backoff, and log the failure somewhere you can see it.
Updating over the air assumes air. When the install is in a plant room with no signal, there is a cable.
Ethernet instead of Wi-Fi →Edit this page — content/esp32/ota-updates.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.