All articles
3 min read

ESP32 & ESP8266 MicroPython OTA Updates Using Python Server

Implement over-the-air (OTA) firmware updates for ESP32/ESP8266 running MicroPython using a Flask server. Covers the server setup, device-side update script, and applying the new firmware.

Over-the-Air (OTA) updates let you push new firmware to ESP32/ESP8266 devices over Wi-Fi without physical access. This guide builds a simple OTA system: a Flask server serves the firmware file, and the device downloads and applies it.

Prerequisites

  • ESP32 or ESP8266 with MicroPython installed
  • Flask for the update server
  • Both the device and server on the same network (or server accessible via internet)

Part 1: The Flask Update Server

The server hosts the firmware file and serves it on demand.

Install Flask

pip install flask

Create app.py

from flask import Flask, send_file, jsonify
import os

app = Flask(__name__)

FIRMWARE_PATH = "firmware.py"   # path to your firmware file
FIRMWARE_VERSION = "1.2.0"

@app.route("/version")
def version():
    """Returns the current firmware version."""
    return jsonify({"version": FIRMWARE_VERSION})

@app.route("/update")
def update_firmware():
    """Serves the firmware file for download."""
    if not os.path.exists(FIRMWARE_PATH):
        return jsonify({"error": "Firmware not found"}), 404
    return send_file(FIRMWARE_PATH, as_attachment=True)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Run the Server

python app.py

Place your updated firmware.py (the MicroPython script to deploy) in the same directory. The server exposes:

  • GET /version — returns the available firmware version
  • GET /update — downloads the firmware file

Part 2: The Device Update Script

This runs on the ESP32/ESP8266. It connects to Wi-Fi, checks for a new version, downloads it, replaces the current main.py, and reboots.

Create updater.py and upload it to the device:

import network
import urequests
import os
import machine
import time

WIFI_SSID = "your_wifi_ssid"
WIFI_PASSWORD = "your_wifi_password"
SERVER_URL = "http://192.168.1.100:5000"   # your server's IP
CURRENT_VERSION = "1.1.0"                   # this device's current version

def connect_wifi():
    sta = network.WLAN(network.STA_IF)
    sta.active(True)
    if not sta.isconnected():
        sta.connect(WIFI_SSID, WIFI_PASSWORD)
        timeout = 15
        while not sta.isconnected() and timeout > 0:
            time.sleep(1)
            timeout -= 1
    if sta.isconnected():
        print("Connected:", sta.ifconfig()[0])
        return True
    print("Wi-Fi connection failed")
    return False

def check_for_update():
    try:
        response = urequests.get(SERVER_URL + "/version", timeout=10)
        data = response.json()
        response.close()
        return data.get("version")
    except Exception as e:
        print("Version check failed:", e)
        return None

def download_and_apply(filename="main.py"):
    try:
        print("Downloading firmware...")
        response = urequests.get(SERVER_URL + "/update", timeout=30)

        if response.status_code != 200:
            print("Download failed, status:", response.status_code)
            response.close()
            return False

        # Write firmware to a temporary file first
        with open("firmware_new.py", "wb") as f:
            f.write(response.content)
        response.close()

        # Verify the file was written successfully
        stat = os.stat("firmware_new.py")
        if stat[6] == 0:
            print("Downloaded file is empty, aborting")
            os.remove("firmware_new.py")
            return False

        # Atomically replace current firmware
        try:
            os.remove(filename)
        except OSError:
            pass
        os.rename("firmware_new.py", filename)

        print("Firmware updated successfully")
        return True

    except Exception as e:
        print("Update failed:", e)
        # Clean up temp file if it exists
        try:
            os.remove("firmware_new.py")
        except OSError:
            pass
        return False

def run_ota():
    if not connect_wifi():
        return

    server_version = check_for_update()
    if not server_version:
        print("Could not reach update server")
        return

    if server_version == CURRENT_VERSION:
        print("Already up to date:", CURRENT_VERSION)
        return

    print(f"Update available: {CURRENT_VERSION} → {server_version}")

    if download_and_apply():
        print("Rebooting in 3 seconds...")
        time.sleep(3)
        machine.reset()
    else:
        print("Update failed, keeping current firmware")

# Call at startup or on a schedule
run_ota()

Running OTA at Boot

To check for updates on every boot, call run_ota() from your main.py before your main application logic:

# main.py
import updater
updater.run_ota()

# ... rest of your application ...

Or check for updates only every N boots to save bandwidth:

import updater
import machine

# Check every 10 boots
boot_count = 0
try:
    with open("boot_count.txt") as f:
        boot_count = int(f.read())
except OSError:
    pass

boot_count = (boot_count + 1) % 10
with open("boot_count.txt", "w") as f:
    f.write(str(boot_count))

if boot_count == 0:
    updater.run_ota()

Deploying to the Device

Upload updater.py via ampy, rshell, or Thonny:

# Using ampy
ampy --port /dev/ttyUSB0 put updater.py

# Using rshell
rshell cp updater.py /pyboard/

Conclusion

This OTA approach covers the core pattern: version check, conditional download, safe write-then-rename, and reboot. For production deployments, add HTTPS to the Flask server and verify firmware integrity with a checksum or HMAC signature before applying updates.