Over-the-air MicroPython script updates for ESP32 via Bluetooth LE, using the Nordic UART Service (NUS) as the transport. No custom GATT service, no re-pairing — OTA traffic rides alongside your existing NUS application protocol.
Phone / laptop ──BLE NUS──► ESP32 MicroPython
OTA:START ────────────────►
◄──────────────── OTA:READY
OTA:DATA:0 ────────────────►
◄──────────────── OTA:ACK:0
... ...
OTA:COMMIT ────────────────►
◄──────────────── OTA:OK → machine.reset()
| Folder | What it is |
|---|---|
esp32/ |
MicroPython firmware — OTAManager class + wiring demo |
python-client/ |
Desktop/laptop CLI tool (Python + bleak) |
flutter-client/ |
Android Flutter app with OTA screen |
docs/ |
Protocol reference, wiring guide |
OTA messages are newline-terminated ASCII lines prefixed with OTA:,
multiplexed over the standard NUS RX/TX characteristics. Your existing NUS
on_rx handler routes lines that start with OTA: to the OTAManager
and passes everything else to your application as before.
A full transfer has three phases:
-
Handshake — client sends
OTA:STARTwith filename, size, CRC32, and optional version/token. Device validates and repliesOTA:READYorOTA:REJECT:<reason>. -
Transfer — client sends hex-encoded chunks as
OTA:DATA:<seq>:<hex>. Device repliesOTA:ACK:<seq>orOTA:ERR:<seq>. Lost ACKs and out-of-order chunks are handled automatically. -
Commit — client sends
OTA:COMMIT. Device verifies CRC32, renames the temp file to the target atomically, backs up the old file, repliesOTA:OK, and callsmachine.reset().
See esp32/docs/protocol.md for the full
specification.
Copy esp32/ota_manager.py to your device, then add two lines to your
existing NUS handler:
from ota_manager import OTAManager
class NUSService:
def __init__(self):
self.ota = OTAManager(reply_fn=self._nus_send)
def _on_rx(self, data):
line = data.decode('utf-8', 'ignore').strip()
if line.startswith("OTA:"):
self.ota.handle(line) # ← OTA traffic
else:
self._app_handle(line) # ← your existing logicEdit OTAManager.ALLOWED_FILES to list the scripts you permit to be updated.
pip install bleak
python python-client/ota_client.py --scan
python python-client/ota_client.py --device "ESP32-OTA" --file main.py// Already connected via your existing BLE code:
Navigator.push(context, MaterialPageRoute(
builder: (_) => OtaScreen(connectedDevice: yourBluetoothDevice),
));See flutter-client/README.md for full setup.
- Allowlist — only files explicitly listed in
ALLOWED_FILEScan be written. Path traversal attempts are rejected before any file I/O. - CRC32 verification — the full file CRC is checked before the target file is overwritten.
- Atomic rename —
_ota_tmp→target.pyonly happens after CRC passes. - Backup — the previous version is preserved as
target.py.bak. - Optional auth token — a pre-shared token field in
OTA:STARTprevents casual updates from unknown clients.
pip install pytest pytest-asyncio bleak
pytest esp32/tests/ python-client/tests/ -vSee esp32/tests/ and python-client/tests/
for details.
Pull requests welcome. Please run the test suite before opening a PR. New protocol features should include tests for both the ESP32 and client sides.
GPL-3.0 — see LICENSE.