Wirelessly update ESP32 firmware using Bluetooth Low Energy (BLE) with this Arduino-compatible library for IoT and robotics projects.
Updating ESP32 devices in the field can be challenging without USB or Wi-Fi access. Traditional Wi-Fi-based OTA (Over-The-Air) updates require network connectivity, but what if your project is offline? My ESP32 BLE OTA Library simplifies Bluetooth firmware updates for Arduino and PlatformIO, enabling wireless updates via mobile apps or BLE devices.
Table of Contents
- Why Bluetooth OTA for ESP32?
- Installation
- How to Use
- Writing a Cross-Platform OTA Client
- Conclusion
- Contributing
- Further Reading
Why Bluetooth OTA for ESP32? 🔧
This Arduino-compatible library enables ESP32 OTA tutorials for IoT, robotics, and consumer electronics projects. Key benefits include:
- ✅ No Wi-Fi Required: Updates work offline, ideal for remote devices.
- ✅ Simple Integration: Add OTA with minimal code in Arduino IDE or PlatformIO.
- ✅ Configurable UUIDs: Customize BLE service and characteristic UUIDs.
- ✅ Cross-Platform: Supports Android, iOS, and desktop BLE clients.
- ✅ Secure & Reliable: Ensures data integrity with progress monitoring and error recovery.
Installation 📦
Install via Arduino Library Manager:
- Open Arduino IDE.
- Go to Tools > Manage Libraries.
- Search for BLE OTA Update.
- Select the latest version and click Install.
For PlatformIO, add to platformio.ini
:
lib_deps = Raghav117/bluetooth_ota_firmware_update
How to Use 🖥️
This Bluetooth firmware update Arduino example shows how to set up OTA and control an LED.
Basic Setup
Include the library and define custom UUIDs:
#include <BLEOtaUpdate.h>
const char* CUSTOM_SERVICE_UUID = "12345678-1234-1234-1234-123456789ABC";
const char* CUSTOM_OTA_CHAR_UUID = "87654321-4321-4321-4321-CBA987654321";
const char* CUSTOM_COMMAND_CHAR_UUID = "11111111-2222-3333-4444-555555555555";
const char* CUSTOM_STATUS_CHAR_UUID = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE";
BLEOtaUpdate bleOta(CUSTOM_SERVICE_UUID, CUSTOM_OTA_CHAR_UUID, CUSTOM_COMMAND_CHAR_UUID, CUSTOM_STATUS_CHAR_UUID);
const int LED_PIN = 2;
UUID Purposes:
- CUSTOM_SERVICE_UUID: Groups OTA-related characteristics (container).
- CUSTOM_OTA_CHAR_UUID: Transfers firmware binary in chunks.
- CUSTOM_COMMAND_CHAR_UUID: Sends non-OTA commands (e.g., "LED_ON").
- CUSTOM_STATUS_CHAR_UUID: Reports OTA progress and status.
Callbacks
Set up callbacks to monitor OTA and handle commands:
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
// Set callbacks
bleOta.setOtaProgressCallback(onOtaProgress);
bleOta.setOtaStatusCallback(onOtaStatus);
bleOta.setCommandCallback(onCommand);
bleOta.setConnectionCallback(onConnection);
// Start OTA service
bleOta.begin("ESP32-OTA-Device");
Serial.println("Ready for OTA update via Bluetooth!");
}
void onOtaProgress(int progress) {
Serial.printf("OTA Progress: %d%%\n", progress);
digitalWrite(LED_PIN, progress % 2); // Blink LED
}
void onOtaStatus(OtaStatus status) {
if (status == OTA_COMPLETED) {
Serial.println("OTA Update Finished!");
digitalWrite(LED_PIN, HIGH);
} else if (status == OTA_FAILED) {
Serial.println("OTA Update Failed!");
digitalWrite(LED_PIN, LOW);
}
}
void onCommand(String cmd) {
Serial.print("Received Command: ");
Serial.println(cmd);
if (cmd == "LED_ON") digitalWrite(LED_PIN, HIGH);
else if (cmd == "LED_OFF") digitalWrite(LED_PIN, LOW);
}
void onConnection(bool connected) {
Serial.println(connected ? "Device connected!" : "Device disconnected!");
}
- Serial.begin(115200): Starts serial communication for logs.
- pinMode(LED_PIN, OUTPUT): Configures LED for feedback.
- Callbacks: Monitor progress, status, commands, and connections.
Example Workflow
A minimal example for beginners:
#include <BLEOtaUpdate.h>
BLEOtaUpdate bleOta;
const int LED_PIN = 2;
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
bleOta.setOtaStatusCallback([](OtaStatus status) {
Serial.println(status == OTA_COMPLETED ? "OTA Success!" : "OTA Failed!");
digitalWrite(LED_PIN, status == OTA_COMPLETED ? HIGH : LOW);
});
bleOta.begin("ESP32-OTA-Device");
}
void loop() {
// No code needed; OTA is handled by the library
}
More examples (minimal, advanced, UUID customization) are in the Examples Folder.
Writing a Cross-Platform OTA Client 📱
Create a BLE client (e.g., mobile app) to update ESP32 firmware. MTU (Maximum Transmission Unit) is the max data size per BLE packet; chunk size splits firmware for transfer.
Step 0: Define UUIDs
Match UUIDs with ESP32 firmware:
const SERVICE_UUID = "12345678-1234-5678-9ABC-DEF012345678"; // OTA Service
const OTA_CHAR_UUID = "87654321-4321-8765-CBA9-FEDCBA987654"; // Firmware transfer
const COMMAND_CHAR_UUID = "11111111-2222-3333-4444-555555555555"; // Commands
const STATUS_CHAR_UUID = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"; // Status
Step 1: Enable Bluetooth & Permissions
enableBluetooth();
requestPermissions();
Step 2: Scan for OTA-Enabled Device
startScan({ services: [SERVICE_UUID] });
const device = await userSelectsDeviceFromResults();
Step 3: Connect to Device
await device.connect({ timeout: 15000 });
await device.requestMtu(247); // Max packet size
Step 4: Discover Services & Characteristics
const service = await device.getPrimaryService(SERVICE_UUID);
const otaChar = await service.getCharacteristic(OTA_CHAR_UUID);
const cmdChar = await service.getCharacteristic(COMMAND_CHAR_UUID);
const stsChar = await service.getCharacteristic(STATUS_CHAR_UUID);
Step 5: Select Firmware File
const firmware = await pickFile({ extensions: [".bin"] });
if (!firmware) throw new Error("No firmware selected");
Step 6: Compute Chunk Size
const chunkSize = (device.currentMtu > 0 ? device.currentMtu : 23) - 3;
Step 7: Start OTA Transfer
await otaChar.writeWithoutResponse(new TextEncoder().encode("OPEN"));
await otaChar.writeWithoutResponse(
new DataView(new ArrayBuffer(4)).setUint32(0, firmware.length, false).buffer
);
Step 8: Send Firmware in Chunks
for (const chunk of splitArray(firmware.bytes, chunkSize)) {
await otaChar.writeWithoutResponse(chunk);
await new Promise(resolve => setTimeout(resolve, 25)); // Avoid overflow
}
await otaChar.writeWithResponse(new TextEncoder().encode("DONE"));
Bonus: Sending Non-OTA Commands
Send custom commands:
await cmdChar.writeWithoutResponse(new TextEncoder().encode("LED_ON"));
await cmdChar.writeWithoutResponse(new TextEncoder().encode("SPEED_80"));
Summary:
- Scan, connect, discover services.
- Select and split firmware into chunks.
- Send OTA, confirm completion, disconnect.
- Use Command Characteristic for device control (e.g., LEDs, motors).
See a full OTA client in the GitHub Examples.
Conclusion 🏁
Bluetooth OTA transforms IoT development by enabling wireless ESP32 firmware updates without USB or Wi-Fi. This library simplifies the process with:
- Ready-to-use OTA service and characteristics.
- Flexible UUID configuration.
- Command-based control alongside OTA.
Whether building smart gadgets, robotics, or prototypes, this library keeps your ESP32 devices up-to-date and future-proof. Try it using the GitHub repo examples.
Contributing 🤝
Feedback and contributions are welcome! To contribute:
- Open an issue on GitHub.
- Discuss on X or Arduino Forums.
- Submit a pull request.
Check the Changelog for updates.
Further Reading 📖
- GitHub Wiki: ESP32 BLE OTA
- Medium: ESP32 Bluetooth OTA
- Hashnode: Bluetooth OTA for ESP32
- Arduino BLE Guide
- Espressif BLE Documentation
- PlatformIO Documentation