Why OTA Matters
Once an ESP32 sensor is mounted behind a wall, embedded in a product enclosure, or deployed to a remote location, reprogramming it by USB cable is impractical. OTA lets you push bug fixes and new features wirelessly from your laptop — or even automate firmware updates from a cloud server.
Method 1 — ArduinoOTA (Arduino IDE Upload via Network)
The simplest approach: the ESP32 appears as a network port in the Arduino IDE. Select it, click Upload, and the IDE sends the firmware over Wi-Fi.
#include <WiFi.h>
#include <ArduinoOTA.h>
const char* ssid = "YourSSID";
const char* password = "YourPassword";
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println("\nIP: " + WiFi.localIP().toString());
ArduinoOTA.setHostname("esp32-sensor"); // appears as this in IDE
ArduinoOTA.setPassword("ota-password"); // optional but recommended
ArduinoOTA.onStart([]() {
String type = (ArduinoOTA.getCommand() == U_FLASH) ? "firmware" : "filesystem";
Serial.println("OTA start: " + type);
});
ArduinoOTA.onEnd([]() { Serial.println("\nOTA done — rebooting"); });
ArduinoOTA.onError([](ota_error_t e) {
Serial.printf("OTA error[%u]: ", e);
if (e == OTA_AUTH_ERROR) Serial.println("Auth failed");
else if (e == OTA_BEGIN_ERROR) Serial.println("Begin failed");
else if (e == OTA_CONNECT_ERROR) Serial.println("Connect failed");
else if (e == OTA_RECEIVE_ERROR) Serial.println("Receive failed");
else if (e == OTA_END_ERROR) Serial.println("End failed");
});
ArduinoOTA.onProgress([](unsigned int prog, unsigned int total) {
Serial.printf("Progress: %u%%\r", (prog / (total / 100)));
});
ArduinoOTA.begin();
Serial.println("ArduinoOTA ready — select 'esp32-sensor' in IDE > Tools > Port");
}
void loop() {
ArduinoOTA.handle(); // must call every loop — checks for incoming update
// your application code here
}
Method 2 — Web Browser OTA (Upload .bin via HTML Form)
No Arduino IDE required — upload firmware from any web browser by browsing to the ESP32’s IP and selecting the .bin file.
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Update.h>
const char* ssid = "YourSSID";
const char* password = "YourPassword";
AsyncWebServer server(80);
const char OTA_PAGE[] PROGMEM = R"(
<!DOCTYPE html><html><head><title>OTA Update</title></head><body>
<h1>OTA Firmware Update</h1>
<form method='POST' action='/update' enctype='multipart/form-data'>
<input type='file' name='firmware' accept='.bin'>
<input type='submit' value='Update'>
</form>
</body></html>
)";
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println("\nIP: " + WiFi.localIP().toString());
server.on("/", HTTP_GET, [](AsyncWebServerRequest* req) {
req->send_P(200, "text/html", OTA_PAGE);
});
server.on("/update", HTTP_POST,
[](AsyncWebServerRequest* req) {
bool ok = !Update.hasError();
AsyncWebServerResponse* response = req->beginResponse(
200, "text/plain", ok ? "Update OK — rebooting" : "Update FAILED");
response->addHeader("Connection", "close");
req->send(response);
if (ok) ESP.restart();
},
[](AsyncWebServerRequest* req, String filename, size_t index,
uint8_t* data, size_t len, bool final) {
if (!index) {
Serial.printf("OTA start: %s\n", filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
Update.printError(Serial);
}
}
if (Update.write(data, len) != len) Update.printError(Serial);
if (final) {
if (Update.end(true)) {
Serial.printf("OTA complete: %u bytes\n", index + len);
} else {
Update.printError(Serial);
}
}
}
);
server.begin();
Serial.println("Web OTA server ready at http://" + WiFi.localIP().toString());
}
void loop() {}
Method 3 — Automatic OTA from HTTP Server
The ESP32 periodically checks a version file on your server and downloads the firmware binary if a newer version is available:
#include <WiFi.h>
#include <HTTPClient.h>
#include <Update.h>
#define CURRENT_VERSION "1.0.2"
#define VERSION_URL "http://your-server.com/esp32/version.txt"
#define FIRMWARE_URL "http://your-server.com/esp32/firmware.bin"
void checkForUpdate() {
HTTPClient http;
http.begin(VERSION_URL);
if (http.GET() == HTTP_CODE_OK) {
String latest = http.getString();
latest.trim();
if (latest != CURRENT_VERSION) {
Serial.printf("New version %s available (have %s) — downloading\n",
latest.c_str(), CURRENT_VERSION);
downloadAndApply();
} else {
Serial.println("Firmware up to date: " + String(CURRENT_VERSION));
}
}
http.end();
}
void downloadAndApply() {
HTTPClient http;
http.begin(FIRMWARE_URL);
int code = http.GET();
if (code == HTTP_CODE_OK) {
int total = http.getSize();
if (!Update.begin(total > 0 ? total : UPDATE_SIZE_UNKNOWN)) {
Update.printError(Serial); return;
}
WiFiClient* stream = http.getStreamPtr();
size_t written = Update.writeStream(*stream);
if (Update.end()) {
Serial.printf("Updated: %u bytes — rebooting\n", written);
ESP.restart();
} else {
Update.printError(Serial);
}
}
http.end();
}
void loop() {
static unsigned long last = 0;
if (millis() - last >= 3600000) { // check every hour
last = millis();
checkForUpdate();
}
// your application code
}
Partition Scheme for OTA
OTA requires the correct partition scheme. In Arduino IDE: Tools → Partition Scheme → select one with “OTA” in the name (e.g. “Default with OTA (1.3 MB APP / 1.5 MB SPIFFS)”).
Rollback Safety Pattern
#include <esp_ota_ops.h>
// In setup(), after all hardware initialised successfully:
esp_ota_img_states_t state;
const esp_partition_t* running = esp_ota_get_running_partition();
if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
if (state == ESP_OTA_IMG_PENDING_VERIFY) {
// Mark this boot as valid — do NOT call this if something is wrong
esp_ota_mark_app_valid_cancel_rollback();
Serial.println("OTA: firmware marked as valid");
}
}
// If setup() crashes before reaching this line, the watchdog timer
// triggers a reboot and the bootloader rolls back to the previous image.