An ESP32 OTA (over-the-air) firmware update library for Arduino, supporting:
- Multi-partition updates from a single JSON manifest
- HTTP and HTTPS (WiFi and W5500 Ethernet)
- App partition rollback via ESP-IDF
- Custom HTTP headers per request
- SHA-256 binary integrity verification (optional)
- Home Assistant MQTT Update integration compatible
Loosely based on chrisjoyce911/esp32FOTA.
The following timeouts can be overridden at build time:
| Define | Default | Description |
|---|---|---|
ESP32OTA_CONNECT_TIMEOUT_MS |
10000 |
Milliseconds to wait for the server to begin responding |
ESP32OTA_STALL_TIMEOUT_MS |
5000 |
Milliseconds to wait between data chunks before aborting |
arduino-cli compile --fqbn esp32:esp32:esp32 \
--build-property "build.extra_flags=-DESP32OTA_CONNECT_TIMEOUT_MS=20000 -DESP32OTA_STALL_TIMEOUT_MS=10000" \
your_sketch.ino- ESP32 Arduino core 3.x+
- ArduinoJson 7.x
- App version and name embedded by the build system (see below)
- sdkconfig:
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
The library reads version and application name at runtime from esp_ota_get_app_description(). These are standard ESP-IDF fields populated by the build system — set them using the mechanisms below, not preprocessor defines.
Arduino CLI:
arduino-cli compile --fqbn esp32:esp32:esp32 \
--build-property "build.extra_flags=-DPROJECT_VER=\"2026.05.01\" -DPROJECT_NAME=\"My Device\"" \
your_sketch.inoESP-IDF (CMakeLists.txt):
project(my_project)
set(PROJECT_VER "2026.05.01")
set(PROJECT_NAME "My Device")If version is not embedded, checkForUpdate() will fire onError with ESP_ERR_INVALID_VERSION and return false.
{
"application_name": "My Device",
"version": "2026.05.01",
"binaries": [
{"partition": "ui", "url": "https://firmware.example.com/2026.05.01/ui.bin"},
{"partition": "app", "url": "https://firmware.example.com/2026.05.01/firmware.bin"}
],
"release_url": "https://github.com/example/device/releases/tag/2026.05.01"
}The app partition is always flashed first, regardless of its position in binaries. If app flashing fails, no other partitions are touched.
application_name is required in all JSON manifests (object or array). Entries missing it are rejected with onError(ESP_ERR_INVALID_ARG). For a forced flash with no manifest, use flashPartition(label, url) instead.
For multi-application manifests, use a top-level JSON array. The library matches the first entry whose application_name equals the running firmware's project_name from esp_app_desc_t.
Add a sha256 field to any binary entry to validate the download before activation:
{"partition": "app", "url": "https://...", "sha256": "abc123..."}The same verification is available for direct partition flashes via the three-argument form of flashPartition — see Force a single partition directly.
#include <esp32OTA.h>
#include <WiFi.h>
esp32OTA ota;
void setup() {
WiFi.begin("ssid", "password");
while (WiFi.status() != WL_CONNECTED) delay(500);
// Always call first in setup() — cancels rollback if this is a post-OTA boot.
ota.markAppValid();
ota.useBundledCerts(); // HTTPS via ESP-IDF built-in Mozilla CA bundle; no client object needed
ota.setManifestURL("https://firmware.example.com/manifest.json");
ota.addHeader("X-Device-UUID", "your-device-uuid");
ota.setBlockedPartitions({"config", "nvs"});
ota.onAvailable([](const char* version, const char* app, const char* url) {
Serial.printf("Update available: %s\n", version);
});
ota.onProgress([](const char* partition, size_t written, size_t total) {
Serial.printf("[%s] %u / %u\n", partition, written, total);
});
ota.onComplete([](bool success) {
Serial.println(success ? "OTA complete — rebooting" : "OTA failed");
});
}
void loop() {
if (ota.checkForUpdate()) {
ota.execOTA();
}
delay(86400000UL);
}JsonDocument doc;
deserializeJson(doc, manifestJsonString);
ota.execOTA(doc);// With SHA-256 verification (recommended) — aborts before activation on digest mismatch
bool ok = ota.flashPartition("ui", "https://firmware.example.com/ui.bin",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
if (!ok) {
// onError fired with the error code; partition unchanged, no restart needed
return;
}
ESP.restart();
// Without SHA-256 (skips digest check) — does not reboot, caller decides when to restart
ota.flashPartition("ui", "https://firmware.example.com/ui.bin");
ESP.restart();After an OTA update, the ESP-IDF bootloader waits for markAppValid() before permanently committing the new firmware. If the device reboots before this call, it reverts to the previous OTA slot.
void setup() {
ota.markAppValid(); // always call early in setup()
if (!hardwareCheckPassed()) {
ota.markAppInvalid(); // reboots immediately into previous slot
}
}Requires CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y in sdkconfig.
HTTPS with bundled Mozilla CA certificates (recommended):
ota.useBundledCerts();
// No client object needed — TLS is handled internally via esp_http_client.
// Works with any transport (WiFi, W5500 Ethernet, etc.).HTTPS with a custom PEM certificate (self-signed or private CA):
WiFiClientSecure client;
client.setCACert(my_root_ca_pem);
ota.setClient(&client);Plain HTTP:
WiFiClient client;
ota.setClient(&client);Use ETH.h (ESP32 Arduino core v3+), not WIZnet's standalone Ethernet.h library.
With ETH.h, the W5500 is used as a MAC/PHY only — the TCP/IP stack (lwIP) runs on the ESP32 via the ESP-IDF esp_eth SPI driver. This keeps the library on the same lwIP stack as WiFi and makes useBundledCerts() work correctly, since esp_http_client uses lwIP sockets directly.
WIZnet's Ethernet.h runs TCP/IP inside the W5500 chip and bypasses lwIP entirely. That path is not supported.
#include <ETH.h>
// Adjust pins for your board
#define W5500_CS 5
#define W5500_INT 4
#define W5500_RST -1
#define W5500_SCK 18
#define W5500_MISO 19
#define W5500_MOSI 23
Network.onEvent([](arduino_event_id_t event) {
if (event == ARDUINO_EVENT_ETH_GOT_IP) Serial.println("Ethernet up");
});
ETH.begin(ETH_PHY_W5500, 1, W5500_CS, W5500_INT, W5500_RST,
SPI3_HOST, W5500_SCK, W5500_MISO, W5500_MOSI);
// Then use useBundledCerts() exactly as you would over WiFi:
ota.useBundledCerts();See examples/periodicOTA for a complete W5500 sketch.