← All projects
IoT / Web Beginner

Over-the-Air (OTA) Updates: Flash Your ESP32 Without a USB Cable

Flash over USB once — after that new versions go over Wi-Fi straight from Arduino IDE or PlatformIO. Indispensable once the board is mounted on a ceiling or in a panel.

Difficulty
Beginner
Time
30 min
Category
IoT / Web

Why

The device is already installed: a sensor in the attic, a controller in a junction box, a weather station on the roof. Taking it down to fix one bug is no fun. OTA (Over-The-Air) lets you upload new firmware over Wi-Fi just like over a cable.

The ArduinoOTA library ships with the arduino-esp32 core — nothing to install.

How it works

The ESP32's flash is split into partitions. The default scheme has two app partitions (app0 and app1). New firmware is written to the inactive one, verified, and only then does the board switch to it. If the transfer is interrupted, the board simply keeps running the old version.

Partition schemes marked No OTA (e.g. Huge APP) have a single app partition — OTA won't work with them.

Code template

Add these lines to any of your sketches:

#include <WiFi.h>
#include <ArduinoOTA.h>

const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";

void setupOTA() {
  ArduinoOTA.setHostname("esp32-workshop");   // name shown in the IDE port list
  ArduinoOTA.setPassword("change-me");        // without it anyone on the network can flash

  ArduinoOTA
    .onStart([]() {
      Serial.println(ArduinoOTA.getCommand() == U_FLASH ? "OTA: firmware" : "OTA: filesystem");
    })
    .onProgress([](unsigned int done, unsigned int total) {
      Serial.printf("OTA: %u%%\r", done * 100 / total);
    })
    .onEnd([]() {
      Serial.println("\nOTA: done, rebooting");
    })
    .onError([](ota_error_t error) {
      Serial.printf("OTA error %u\n", error);
    });

  ArduinoOTA.begin();
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.printf("IP: %s\n", WiFi.localIP().toString().c_str());

  setupOTA();
  pinMode(2, OUTPUT);
}

void loop() {
  ArduinoOTA.handle();   // must be called continuously

  // your code — no long delay() calls
  static unsigned long last = 0;
  if (millis() - last > 500) {
    last = millis();
    digitalWrite(2, !digitalRead(2));
  }
}

First upload

  1. Flash the sketch over USB. Make sure Tools → Partition Scheme is Default (or another one with OTA).
  2. Unplug the cable and power the board from any USB charger.

Updating from Arduino IDE

  1. Change something in the code (e.g. the blink rate).
  2. Tools → Port — under Network ports you'll see esp32-workshop at 192.168.x.x. Select it.
  3. Click Upload and enter the password change-me.

No network port? Restart the IDE; the computer and board must be on the same subnet and your firewall must allow mDNS (UDP 5353).

Updating from PlatformIO

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
upload_protocol = espota
upload_port = esp32-workshop.local
upload_flags = --auth=change-me

Rules so OTA doesn't break

  • Every new firmware must include the OTA code too. Otherwise the next update has to go over a cable again.
  • Don't block loop() with long delay() calls or endless loops — ArduinoOTA.handle() must run at least a few times per second.
  • Change the password and don't publish it with your code.

Alternatives

  • Web OTA — a page with an "Upload .bin" button (the core's OTAWebUpdater example or the ElegantOTA library).
  • Update from a server (HTTPUpdate) — the device pulls a new version on a schedule; handy for a fleet of identical devices.