Production-ready ESP32 firmware template for OTA updates over Bluetooth Low Energy (BLE) using the Nordic Legacy DFU profile.
This repository focuses on the ESP32 firmware side.
The companion iOS app is maintained as a separate app/repository and is only included here as a development reference under iOS/ESP32-DFU.
- BLE DFU service compatible with Nordic Legacy DFU UUID set.
- Dual-slot OTA partition layout for safer updates.
- Firmware streaming through DFU Packet characteristic.
- Pre-commit validation by image size and CRC32 before activation.
- Packet Receipt Notification (PRN) support.
- Verbose serial diagnostics for protocol-level debugging.
- Clean modular structure for long-term maintainability.
- BLE library:
NimBLE-Arduino - Role: GATT server on ESP32
- Service UUID:
00001530-1212-EFDE-1523-785FEABCD123 - Characteristics:
- Control Point (
1531): command/response path - Packet (
1532): firmware and init payload stream - Version (
1534): DFU protocol version information
- Control Point (
0x01Start DFU0x02Init DFU Parameters0x03Receive Firmware Image0x04Validate0x05Activate and Reset0x06Reset0x08Packet Receipt Notification Request
Before finalizing an update, firmware validates:
- expected image size (if provided)
- expected CRC32 (if provided in init packet)
Only after validation passes, Update.end(true) is called and new image is committed.
platformio.ini— PlatformIO environment and dependencies.partitions_ota.csv— OTA partition table withotadata,app0,app1,spiffs,coredump.src/main.cpp— firmware entrypoint (setup()/loop()).src/dfu_ble.h— DFU module public interface.src/dfu_ble.cpp— DFU BLE protocol implementation and OTA flow.
Template uses dual app slots:
- active app runs from one slot
- incoming firmware is written to the inactive slot
- boot metadata in
otadataswitches active slot only after successful validation
This minimizes risk of bricking during interrupted updates.
- ESP32 board supported by
espressif32PlatformIO platform - PlatformIO Core (CLI or VS Code extension)
- BLE DFU client (mobile app or custom client implementing Legacy DFU flow)
- Clone repository.
- Open the folder in VS Code with PlatformIO extension.
- Build firmware:
pio run- Flash firmware:
pio run -t upload- Open serial monitor:
pio device monitor- Connect to ESP32 DFU peripheral.
Start DFUwith expected app size.Init DFU Paramsstart, send init payload, then complete init.Receive FW Image, stream binary in chunks.Validateand confirm success response.Activate and Reset.
- iOS app (SwiftUI + CoreBluetooth) acting as Legacy DFU client.
- Android app with BLE GATT write/notify implementation.
- Desktop updater script using BLE library and custom opcode sequence.
- Keep protocol constants explicit and close to implementation.
- Use structured serial prefixes (
[DFU][STATE],[DFU][ERROR]) for fast log filtering. - Keep
main.cppminimal; place protocol logic in dedicated modules. - Prefer deterministic state transitions over implicit side effects.
- Build fails for missing deps:
- run
pio pkg update
- run
- Device not visible in scanner:
- verify BLE advertising started in serial log
- Validate fails:
- check expected size/CRC in init packet versus transmitted image
- OTA boot issues:
- confirm partition table is exactly
partitions_ota.csv
- confirm partition table is exactly
This template implements protocol-level integrity checks (size and CRC32), but does not include cryptographic signature verification by default. For production deployments, add signed artifacts and authenticity checks in bootloader or app-level policy.
For releases, publish the mobile DFU client as a separate app/repository with its own versioning, signing, and release notes. Keep this repository focused on firmware template quality and embedded protocol behavior.
This project is licensed under the MIT License.