Upload code wirelessly to your Raspberry Pi Pico W β no USB cable needed! ESP32 support is available using the same API, but Pico W remains the primary focus.
A simple Arduino library that enables Over-The-Air (OTA) updates for Raspberry Pi Pico W (Arduino-Pico core). Optional ESP32 support uses the same API.
How it works:
- π‘ Upload your sketch once via USB - it connects to Wi-Fi and starts OTA server
- β¨ After that, upload wirelessly from Arduino IDE - no more plugging/unplugging!
- π‘ Test it by uncommenting LED blink code and uploading via OTA
| Requirement | Details |
|---|---|
| ποΈ Board | Raspberry Pi Pico W / Pico 2 W (primary), ESP32 (optional) |
| π» Software | Arduino IDE 1.8.x / 2.x + Arduino-Pico core |
| πΎ Flash Config | A partition with LittleFS filesystem (e.g., 2MB Sketch + 1MB FS) |
| πΆ Network | Device and computer on the same Wi-Fi network |
- Open Arduino IDE
- Go to
File β Preferences β Additional Board Manager URLs - Add this URL:
https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json - Go to
Tools β Board β Boards Manager - Search for "pico" and install "Raspberry Pi Pico/RP2040/2350 by Earle F. Philhower, III"
| Setting | Value | Why? |
|---|---|---|
| Board (Pico W) | Tools β Board β Raspberry Pi RP2040 Boards βRaspberry Pi Pico W (or Pico 2 W) |
Selects your hardware |
| Flash Size (Pico W) | Tools β Flash Size β2MB (Sketch: 1MB, FS: 1MB) β
|
OTA needs LittleFS space to stage updates |
| Board (ESP32, optional) | Tools β Board β ESP32 Arduino β your ESP32 board |
ESP32 support uses the same API |
| Flash Size (ESP32, optional) | Default is fine | ESP32 OTA does not require a filesystem partition |
| Port | Tools β Port βWindows: COMx (e.g., COM3)Mac/Linux: /dev/ttyACM0 or /dev/cu.usbmodem* |
For USB upload |
β οΈ Pico W: DO NOT select "2MB (No FS)" β OTA will fail without filesystem space!
Both examples have Wi-Fi credentials in secret.h (already set up for you!):
- Open the example's
secret.hfile:- Single-core:
examples/Pico_OTA_test/secret.h - Dual-core:
examples/Pico_OTA_test_with_Dual_Core/secret.h
- Single-core:
- Edit these lines with your Wi-Fi and OTA settings:
const char *ssid = "Your_SSID"; // β Your Wi-Fi network name const char *password = "Your_PASSWORD"; // β Your Wi-Fi password const char *hostname = "pico-ota"; // β Device name (shows in network ports) const char *otaPassword = "admin"; // β OTA upload password
π‘ Why
secret.h? This file is in.gitignore, so your Wi-Fi credentials stay secure and never get committed to git!
- Connect Pico W to your computer via USB cable
- Select your USB port:
Tools β Port β COMx(or/dev/ttyACM0on Linux/Mac) - Click Upload button (or press
Ctrl+U/Cmd+U) - Open Serial Monitor (
115200 baud) - π Note the IP address displayed (e.g.,
192.168.1.100)
You should see something like:
[OTA] Connecting WiFi.....
[OTA] WiFi connected, IP: 192.168.1.100
[OTA] LittleFS mounted
[OTA] Ready for OTA updates
- Go to
Tools β Port β Network Ports - You'll see your device:
pico-ota at 192.168.1.100(or your custom hostname) - Select it
- Click Upload - it uploads wirelessly! π
No USB cable needed anymore!
For more demanding applications, use the dual-core example to dedicate Core 1 exclusively to OTA while Core 0 runs your main application without blocking:
Main sketch (Pico_OTA_test_with_Dual_Core.ino):
#include "secret.h" // Contains: ssid, password, hostname, otaPassword
// Also contains setup1() and loop1() for Core 1 OTA server
// Core 0 setup - YOUR application setup code
void setup() {
Serial.begin(115200);
// Your setup code here
}
// Core 0 loop - YOUR main application (never blocks for OTA!)
void loop() {
// Your responsive application code
// Sensors, motors, real-time tasks - never blocked by OTA!
delay(100);
}OTA Server in secret.h (pre-configured, don't modify):
// Wi-Fi credentials (EDIT THESE)
const char *ssid = "Your_SSID";
const char *password = "Your_PASSWORD";
const char *hostname = "pico-ota";
const char *otaPassword = "admin";
// Core 1 setup - initializes OTA on Core 1 (automatic)
void setup1() {
Serial.println("[OTA] Core 1 OTA server initializing...");
otaSetup(ssid, password, hostname, otaPassword);
Serial.println("[OTA] Core 1 OTA ready - waiting for uploads");
}
// Core 1 loop - runs OTA server on Core 1 (automatic)
void loop1() {
otaLoop(); // Handles OTA independently
}Benefits:
- β Core 0: Dedicated to your application (sensors, motors, real-time tasks)
- β Core 1: Dedicated to OTA (never interferes with your code)
- β Better responsiveness and performance
- β True parallel processing on dual-core RP2040
To use: Open example File β Examples β PICO_OTA β Pico_OTA_test_with_Dual_Core
Want to verify OTA is really working? Add an LED blink test to your sketch:
- β Upload the example via USB first (LED code is commented out by default)
- βοΈ In the main sketch, uncomment the LED variables at the top:
// const int ledPin = LED_BUILTIN; // unsigned long lastBlink = 0; // const unsigned long blinkIntervalMs = 500;
- βοΈ In the loop() function, uncomment the LED blink logic:
// const unsigned long now = millis(); // if (now - lastBlink >= blinkIntervalMs) { // lastBlink = now; // digitalWrite(ledPin, !digitalRead(ledPin)); // }
- π‘ Upload again via OTA (wireless port) - do NOT use USB
- π‘ The LED starts blinking - OTA works!
π― Why this works: You uncommented code that blinks the LED. If you can upload it wirelessly (OTA) and see the LED blink, that proves OTA successfully updated your device!
For HTTP Pull OTA, Web Browser Upload, and GitHub Releases, you need to generate .bin firmware files. Here's how:
Pico W:
- Open your sketch in Arduino IDE
- Select
Tools β Board β Raspberry Pi RP2040 Boards β Raspberry Pi Pico W - Select
Tools β Flash Size β 2MB (Sketch: 1MB, FS: 1MB) - Go to
Sketch β Export Compiled Binary(or pressCtrl+Alt+S/Cmd+Alt+S) - β
IDE compiles and creates
.binfile in your sketch folder - π Find it at:
<sketch_folder>/<sketch_name>.ino.bin
ESP32:
- Select
Tools β Board β ESP32 Arduino β <your ESP32 board> - Go to
Sketch β Export Compiled Binary(or pressCtrl+Alt+S/Cmd+Alt+S) - β
IDE creates
.binfile in sketch folder - π Find it at:
<sketch_folder>/<sketch_name>.ino.bin
Example:
If your sketch is MyProject.ino in D:\Arduino\MyProject\, the binary will be:
D:\Arduino\MyProject\MyProject.ino.bin
Pico W:
arduino-cli compile --fqbn rp2040:rp2040:rpipicow \
--export-binaries \
--build-property "build.partitions=default_8MB" \
MyProject/Output: MyProject/build/rp2040.rp2040.rpipicow/MyProject.ino.bin
ESP32:
arduino-cli compile --fqbn esp32:esp32:esp32 \
--export-binaries \
MyProject/Output: MyProject/build/esp32.esp32.esp32/MyProject.ino.bin
For HTTP Pull OTA:
- Upload
.binto your web server - Configure firmware URL in sketch:
const char* FIRMWARE_URL = "http://your-server.com/firmware.bin"; - Device downloads and installs automatically
For Web Browser OTA:
- Navigate to
http://<device-ip>/updatein browser - Click "Choose File" and select your
.bin - Click "Update" - device installs and reboots
For GitHub Releases:
- Create a new Release on GitHub (e.g., tag
v1.1.0) - Attach your
.binfile to the release - Device checks GitHub API and downloads new version automatically
Pro Tips:
- π‘ Rename
.binfiles with version numbers:firmware-v1.2.0.bin - π‘ For GitHub, use consistent naming:
firmware-pico.bin,firmware-esp32.bin - π‘ Test
.binfiles on a spare device before production deployment β οΈ Pico W:.binfile MUST be compiled with same Flash Size settings (include FS partition)β οΈ ESP32:.binfile must match your board type (ESP32, ESP32-S2, ESP32-C3, etc.)
#include <pico_ota.h>
const char *ssid = "YourWiFi";
const char *password = "YourPassword";
void setup() {
Serial.begin(115200);
// Start OTA with optional hostname and password
otaSetup(ssid, password, "my-device", "my-ota-password");
// Your setup code here...
}
void loop() {
otaLoop(); // Must call this frequently to handle OTA requests
// Your loop code here...
}API Reference (same on both platforms):
Basic Setup:
otaSetup(ssid, password, hostname, otaPassword)- Initialize OTA (call once insetup(), uses default timeout and auto-format)otaLoop()- Handle OTA requests (call frequently inloop())
Configuration (call before otaSetup):
otaSetWifiTimeout(timeoutMs)- Set Wi-Fi connection timeout (default: 30000ms)otaSetFsAutoFormat(enabled)- Enable/disable filesystem auto-format on Pico W (default: true)
Callbacks (call before otaSetup):
otaOnStart(callback)- Called when OTA update startsotaOnProgress(callback)- Called during OTA update with progress (current, total bytes)otaOnEnd(callback)- Called when OTA update completes successfullyotaOnError(callback)- Called when OTA update fails with error code
Advanced Setup:
otaSetupWithTimeout(ssid, password, timeoutMs, hostname, otaPassword, allowFsFormat)- Full control over timeout and FS behavior (returns bool success)
Status Queries:
otaIsConnected()- Returns true if Wi-Fi is connected
For classroom and field deployments, use these reliability features:
Problem: Original code blocks forever if Wi-Fi unavailable
Solution: Configure timeout before calling otaSetup()
otaSetWifiTimeout(15000); // 15 second timeout
bool success = otaSetupWithTimeout(ssid, password, 15000, hostname, otaPassword, false);
if (!success) {
Serial.println("OTA failed to start - continuing in offline mode");
// Your fallback logic here
}Problem: No feedback during OTA updates
Solution: Register callbacks before calling otaSetup()
void onOtaStart() {
Serial.println("OTA started - do not power off!");
digitalWrite(LED_PIN, HIGH);
}
void onOtaProgress(unsigned int current, unsigned int total) {
Serial.printf("Progress: %u%%\n", (current * 100) / total);
}
void setup() {
otaOnStart(onOtaStart);
otaOnProgress(onOtaProgress);
otaOnEnd([]() { Serial.println("OTA complete!"); });
otaOnError([](int err) { Serial.printf("OTA error: %d\n", err); });
otaSetup(ssid, password, hostname, otaPassword);
}Problem: Auto-format on mount failure causes data loss
Solution: Disable auto-format for production
otaSetFsAutoFormat(false); // Prevents accidental data loss
otaSetup(ssid, password, hostname, otaPassword);void loop() {
if (otaIsConnected() && otaIsReady()) {
otaLoop(); // Only handle OTA when ready
}
// Your application code
if (otaIsConnected()) {
// Network-dependent features
}
}Complete Example: See examples/Non_Blocking_OTA/ for production-ready patterns
Download firmware updates from an HTTP server. Perfect for production deployments where devices pull updates from a central server.
#include <pico_ota.h>
void setup() {
otaSetup(ssid, password, hostname, otaPassword);
}
void loop() {
otaLoop();
// Check for updates periodically
if (shouldCheckUpdate()) {
int result = otaUpdateFromUrl("http://your-server.com/firmware.bin", "1.0.0");
if (result == OTA_UPDATE_OK) {
// Device will reboot automatically
} else if (result == OTA_UPDATE_NO_UPDATE) {
Serial.println("Already running latest version");
}
}
}API Functions:
otaUpdateFromUrl(url)- Download and install firmware from URLotaUpdateFromUrl(url, currentVersion)- With version checking (server can respond 304 Not Modified)otaUpdateFromHost(host, port, path)- Download from host:port/pathotaUpdateFromHost(host, port, path, currentVersion)- With version checking
Return Codes:
| Code | Constant | Meaning |
|---|---|---|
| 0 | OTA_UPDATE_OK |
Update successful (device will reboot) |
| 1 | OTA_UPDATE_NO_UPDATE |
Already running latest version |
| -1 | OTA_UPDATE_FAILED |
Download or install failed |
| -2 | OTA_UPDATE_NO_WIFI |
No WiFi connection |
| -3 | OTA_UPDATE_HTTP_ERROR |
HTTP request failed |
Complete Example: See examples/HTTP_Pull_OTA/
Start a web server that allows uploading firmware directly from any web browser. Great for non-technical users!
#include <pico_ota.h>
void setup() {
otaSetup(ssid, password, hostname, otaPassword);
// Optional: Enable authentication
otaSetWebCredentials("admin", "secret123");
// Start web server on port 80
otaStartWebServer(80);
Serial.print("Upload firmware at: http://");
Serial.print(WiFi.localIP());
Serial.println("/update");
}
void loop() {
otaLoop(); // Handles both ArduinoOTA and web server
}Usage:
- Open a web browser
- Navigate to
http://<device-ip>/update - Select your
.binfirmware file - Click "Update" and wait for completion
API Functions:
otaStartWebServer(port)- Start web server (default port 80)otaStopWebServer()- Stop web serverotaSetWebCredentials(username, password)- Enable HTTP authenticationotaIsWebServerRunning()- Check if web server is active
Complete Example: See examples/WebBrowser_OTA/
Automatically check GitHub Releases for new firmware versions and update when available. Perfect for open-source projects!
#include <pico_ota.h>
const char* CURRENT_VERSION = "1.0.0";
void setup() {
// Configure GitHub repository
otaSetGitHubRepo("username", "my-project");
otaSetCurrentVersion(CURRENT_VERSION);
otaSetGitHubAssetName("*.bin"); // Match any .bin file
otaSetup(ssid, password, hostname, otaPassword);
}
void loop() {
otaLoop();
// Check for updates (e.g., hourly)
if (shouldCheckUpdate()) {
char latestVersion[32];
int result = otaCheckGitHubUpdate(latestVersion, sizeof(latestVersion));
if (result == OTA_UPDATE_OK) {
Serial.printf("New version available: %s\n", latestVersion);
otaUpdateFromGitHub(); // Download and install
}
}
}GitHub Setup:
- Create a repository for your project
- Create a Release (e.g., tag
v1.1.0) - Attach your compiled
.binfirmware file to the release - Configure the repo details in your sketch
API Functions:
otaSetGitHubRepo(owner, repo)- Set GitHub owner/repo (e.g., "username", "my-project")otaSetCurrentVersion(version)- Set current firmware version for comparisonotaSetGitHubAssetName(pattern)- Asset filename pattern (*.bin,firmware-pico.bin, etc.)otaCheckGitHubUpdate(latestVersion, maxLen)- Check for new releaseotaUpdateFromGitHub()- Download and install latest releaseotaGetLatestGitHubVersion()- Get latest version string
Return Codes:
| Code | Constant | Meaning |
|---|---|---|
| 0 | OTA_UPDATE_OK |
Update available (or successful) |
| 1 | OTA_UPDATE_NO_UPDATE |
Already running latest version |
| -4 | OTA_UPDATE_PARSE_ERROR |
Failed to parse GitHub API response |
| -5 | OTA_UPDATE_NO_ASSET |
No matching firmware asset in release |
Complete Example: See examples/GitHub_OTA/
Automatically reconnect to WiFi if the connection drops. Essential for reliable IoT deployments.
#include <pico_ota.h>
void onWifiLost() {
Serial.println("WiFi disconnected!");
// Maybe blink an LED or pause network-dependent tasks
}
void onWifiRestored() {
Serial.println("WiFi reconnected!");
// Resume normal operation
}
void setup() {
// Enable auto-reconnect
otaSetAutoReconnect(true);
otaSetReconnectInterval(30000); // Try every 30 seconds
otaSetMaxReconnectAttempts(10); // Give up after 10 attempts (0 = infinite)
// Optional: Get notified of connection changes
otaOnWifiDisconnect(onWifiLost);
otaOnWifiReconnect(onWifiRestored);
otaSetup(ssid, password, hostname, otaPassword);
}
void loop() {
otaLoop(); // Handles auto-reconnect automatically
}API Functions:
otaSetAutoReconnect(enabled)- Enable/disable auto-reconnectotaSetReconnectInterval(ms)- Time between reconnect attempts (default: 30000ms)otaSetMaxReconnectAttempts(count)- Max attempts before giving up (0 = infinite)otaOnWifiDisconnect(callback)- Called when WiFi connection is lostotaOnWifiReconnect(callback)- Called when WiFi is restored
| Problem | Solution |
|---|---|
β ERR: No Filesystem (Pico W) |
Re-select Flash Size with FS partition (e.g., "2MB Sketch + 1MB FS") and re-upload via USB |
| β Device hangs at "Connecting WiFi..." | Use otaSetWifiTimeout(15000) and otaSetupWithTimeout() for graceful timeout |
| β Filesystem data lost after OTA | Call otaSetFsAutoFormat(false) before otaSetup() to prevent auto-format |
ESP32 users can use the same API (otaSetup/otaLoop) and start from the minimal example at:
examples/ESP32_OTA_test/ESP32_OTA_test.inoexamples/ESP32_OTA_test/secret.h
The example defaults to GPIO 2 for LED feedback (common on many ESP32 dev boards). Adjust the pin if your board differs.
| Problem | Solution |
|---|---|
| β Device doesn't appear in Network Ports | Check Serial Monitor for IP address, ensure same Wi-Fi network, check firewall settings |
| β OTA upload fails | Verify OTA password matches, check device is powered and connected to Wi-Fi |
| β LittleFS mount failed | Flash may be corrupt - sketch will auto-format on first run (wait ~30 seconds) |
Pico_OTA/
ββ π library.properties
ββ π src/
β ββ pico_ota.h
β ββ pico_ota.cpp
ββ π examples/
β ββ π Pico_OTA_test/ (Basic single-core example)
β β ββ Pico_OTA_test.ino
β β ββ secret.h
β ββ π Pico_OTA_test_with_Dual_Core/ (Dual-core for RP2040)
β β ββ Pico_OTA_test_with_Dual_Core.ino
β β ββ secret.h
β ββ π ESP32_OTA_test/ (ESP32 example)
β β ββ ESP32_OTA_test.ino
β β ββ secret.h
β ββ π Non_Blocking_OTA/ (Production-ready patterns)
β β ββ Non_Blocking_OTA.ino
β β ββ secret.h
β ββ π HTTP_Pull_OTA/ (Pull firmware from server)
β β ββ HTTP_Pull_OTA.ino
β β ββ secret.h
β ββ π WebBrowser_OTA/ (Browser-based upload)
β β ββ WebBrowser_OTA.ino
β β ββ secret.h
β ββ π GitHub_OTA/ (GitHub release auto-update)
β ββ GitHub_OTA.ino
β ββ secret.h
ββ π README.md
ββ π LICENSE
- π Use strong OTA passwords in production (not "admin"!)
- π« Never commit credentials - use
secret.hand add to.gitignore - π OTA only works on local network (same LAN as your computer)
- π Consider implementing additional authentication for production use
MIT License - See LICENSE for details.
This library is released under the MIT License.
Note: This library depends on the ArduinoOTA library and the Arduino-Pico core, both of which are licensed under LGPL v2.1. Binaries compiled with this library will be subject to the LGPL terms regarding the combined work.
Contributions are welcome! Please feel free to submit issues or pull requests.
Made with β€οΈ for the Raspberry Pi Pico W community
β Star this repo if you find it useful!