ESP32 Tutorial: Over-The-Air (OTA) Web Update (Manual Method)
Abstract
This tutorial provides a foundational, library-minimal approach to implementing Over-The-Air (OTA) firmware updates using the ESP32’s built-in libraries. The ESP32 will host a simple web server that serves an HTML page with a file upload form. Upon receiving the new firmware file (.bin), it uses the built-in Update.h library to flash the new sketch onto itself, allowing for remote, wireless maintenance.
1. Introduction to Over-The-Air Updates
Over-The-Air (OTA) firmware updates are a fundamental requirement for any professional Internet of Things (IoT) deployment. They enable you to remotely install new features, fix bugs, and apply security patches to devices deployed in the field without needing physical access via a USB cable.
This tutorial details a simple, lightweight approach to implementing the OTA Manual Method using only the built-in libraries of the ESP32 Arduino Core: WiFi.h, WebServer.h, and most importantly, Update.h.
The process involves configuring the ESP32 to act as its own temporary web server. This server hosts a basic HTML form that accepts an uploaded firmware .bin file. Once the file is received via an HTTP POST request, the Update.h library takes over. It manages the complex process of writing the new firmware to the inactive application partition in the ESP32’s flash memory and safely swapping the boot reference, ensuring the device boots into the new software after a successful check. This method is crucial for reducing maintenance costs and ensuring device longevity.
2. Prerequisites and Wiring
2.1 Required Libraries
The following libraries are included with the ESP32 Arduino Core:
- WiFi.h: For network connectivity.
- WebServer.h: To create the basic web server.
- Update.h: Contains the core functions for flashing new firmware.
2.2 Firmware Binary File (.bin)
OTA updates require a compiled binary file of your new sketch, not the .ino file.
- In the Arduino IDE, open the sketch you want to upload.
- Go to Sketch > Export Compiled Binary.
- The IDE will generate a .bin file in the sketch folder. This is the file you upload via the web interface.
2.3 ESP32 Over-The-Air Update Process
This diagram outlines the key components and steps involved in an ESP32 OTA update:
- Development PC: Where you compile your new firmware.
- Cloud/Local Update Server: Hosts the new firmware image (typically a .bin file) and potentially a manifest file (e.g., json) describing the update.
- Wi-Fi Module: The ESP32’s integrated Wi-Fi for network connectivity.
- Bootloader: The initial program that runs when the ESP32 starts up. Its role in OTA is to determine which application partition to load based on the active partition table.
- Active Partition Table: A section in flash memory that tells the bootloader which application partition currently holds the running firmware and which one is designated for the next boot.
- Application Partition 0 & 1: These are where your firmware applications reside. The ESP32 usually has at least two application partitions to allow one to be updated while the other is running.
- OTA Data Partition: Stores information about the OTA process, including the currently active application partition.
- OTA Update Module (within ESP32 Device): This represents the software component on the ESP32 that handles connecting to the update server, downloading the new firmware, writing it to the inactive application partition, and managing the swap/verification process.
- Peripherals & User Application: This represents the rest of your ESP32 application, interacting with sensors, actuators, etc.
The arrows indicate the flow of data and control during the update process.
3. The ESP32 Arduino Sketch
The code defines two main functions: a simple webpage handler (handleRoot) and a firmware update handler (handleUpdate).
3.1 Libraries and Wi-Fi Setup
Libraries: The
Update.hlibrary provides the core functions for flashing, whileWebServer.hhandles the HTTP communication.ESPmDNS.hallows the board to be accessed easily via a hostname (e.g.,http://esp32.local/update) rather than a fixed IP address.Routes: The main action happens when setting up the server routes:
server.on(“/update”, HTTP_GET, handleRoot);
server.on(“/update”, HTTP_POST, [](){ /* … */ ESP.restart(); }, handleUpdate);
#include
#include
#include
#include // For accessing the board by hostname (e.g., http://esp32.local)
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// Define hostname for mDNS
const char* host = "esp32";
// Initialize the WebServer on HTTP port 80
WebServer server(80);
void setup() {
Serial.begin(115200);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();
}
// Start mDNS service for easier access
if(MDNS.begin(host)) {
Serial.println("mDNS responder started");
}
Serial.println("\nWiFi connected.");
Serial.print("Access OTA at: http://");
Serial.print(host);
Serial.println(".local/update");
Serial.print("Or by IP: http://");
Serial.print(WiFi.localIP());
Serial.println("/update");
// 1. Setup routes
server.on("/", HTTP_GET, [](){ server.send(200, "text/plain", "Firmware V1.0. Use /update to upload new firmware."); });
server.on("/update", HTTP_GET, handleRoot);
server.on("/update", HTTP_POST, [](){
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
}, handleUpdate);
// 2. Start the server
server.begin();
}
The
HTTP_GETrequest to/updatecallshandleRoot, which serves the HTML form.The
HTTP_POSTrequest to/updateis the file upload route. It utilizes a Lambda function to handle the final response and device reboot, while the core file processing is delegated tohandleUpdate(the upload handler argument).
3.2 Web Page Handler (/update)
This function sends the simple HTML form necessary for a browser to select and post the firmware file.
void handleRoot() {
server.send(200, "text/html", " ");
}
enctype='multipart/form-data': This is a standard HTML attribute required for transmitting binary files (like the.binfirmware) within an HTTP POST request. Without it, the upload handler cannot correctly parse the incoming file data.
3.3 Firmware Upload Handler (handleUpdate)
This is the core logic that processes the incoming HTTP POST request, verifies the firmware header, writes the data to the flash, and triggers a reboot. The WebServer framework automatically calls this handler repeatedly as chunks of the file are streamed from the client.
upload.status == UPLOAD_FILE_START:Update.begin(UPDATE_SIZE_UNKNOWN)initializes the OTA process. This reserves the necessary flash space and prepares the partition for writing.UPDATE_SIZE_UNKNOWNis used because the file size is not easily determined at this initial stage.
upload.status == UPLOAD_FILE_WRITE:Update.write(upload.buf, upload.currentSize)is the most critical step. It takes the received binary data buffer (upload.buf) and writes it sequentially to the inactive firmware partition in the flash memory. This happens for every chunk of data received over the network.
upload.status == UPLOAD_FILE_END:Update.end(true)finalizes the process. Thetrueparameter triggers verification checks (like checksum and firmware header validation). If verification passes, the OTA Data Partition is updated to flag the newly flashed application as the one to boot next.
Reboot: After
Update.end()succeeds, the Lambda function for theHTTP_POSTroute executesESP.restart(), causing the device to boot into the new firmware.
void handleUpdate() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update: Starting file: %s\n", upload.filename.c_str());
// Check if the firmware file size exceeds the available OTA space
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
// Write received data to the flash memory
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
// Finalize the update and verify checksum
if (Update.end(true)) {
Serial.printf("Update Success: %u bytes written.\nRebooting now...", upload.totalSize);
} else {
Update.printError(Serial);
}
}
}
void loop() {
server.handleClient();
// No deep logic needed here, as the web server handles all requests.
}
4. Source Code
The entire code for an easier copy and paste:
#include
#include
#include
#include // For accessing the board by hostname (e.g., http://esp32.local)
const char* ssid = "SSID";
const char* password = "PASSWORD";
// Define hostname for mDNS
const char* host = "esp32";
// Initialize the WebServer on HTTP port 80
WebServer server(80);
void setup() {
Serial.begin(115200);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();
}
// Start mDNS service for easier access
if(MDNS.begin(host)) {
Serial.println("mDNS responder started");
}
Serial.println("\nWiFi connected.");
Serial.print("Access OTA at: http://");
Serial.print(host);
Serial.println(".local/update");
Serial.print("Or by IP: http://");
Serial.print(WiFi.localIP());
Serial.println("/update");
// 1. Setup routes
server.on("/", HTTP_GET, [](){ server.send(200, "text/plain", "Firmware V1.0. Use /update to upload new firmware."); });
server.on("/update", HTTP_GET, handleRoot);
server.on("/update", HTTP_POST, [](){
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
}, handleUpdate);
// 2. Start the server
server.begin();
}
void handleRoot() {
server.send(200, "text/html", "");
}
void handleUpdate() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update: Starting file: %s\n", upload.filename.c_str());
// Check if the firmware file size exceeds the available OTA space
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
// Write received data to the flash memory
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
// Finalize the update and verify checksum
if (Update.end(true)) {
Serial.printf("Update Success: %u bytes written.\nRebooting now...", upload.totalSize);
} else {
Update.printError(Serial);
}
}
}
void loop() {
server.handleClient();
//MDNS.update();
// No deep logic needed here, as the web server handles all requests.
}
5. Execution Flow
Upload Initial Code: Use the USB serial port to upload this sketch for the first time.
Connect Wirelessly: Once uploaded, disconnect the USB cable and power the ESP32 separately (optional, but demonstrates true OTA).
Access Web Interface: Open a web browser and navigate to http://esp32.local/update (or the IP address printed on the Serial Monitor).

Update Firmware:
Click the Choose File:
Select your new firmware .bin:
Click Update Firmware.
Reboot: After the upload completes, the ESP32 will automatically reboot and start running the new code.
6. Hands-On Lab Recap
You’ve learned:
- Initial Requirement: The first sketch must be uploaded via serial and contain the full OTA routine.
- Core Libraries: The h and Update.h libraries are essential for handling the HTTP request and flashing the firmware, respectively.
- Web Interface: A basic HTML form is required to accept the .bin file using an HTTP POST request with enctype=’multipart/form-data’.
- Update Process: The begin(), Update.write(), and Update.end() functions are used in sequence to safely transfer and commit the new firmware to the ESP32’s flash memory.
- Persistence: To enable future OTA updates, every subsequent firmware update must also include the full Wi-Fi and Web OTA server setup code.





