Serial OTA application firmware update solution for MicroPython
Project description
OTAmpy — Over-The-Air Update Suite for MicroPython
OTAmpy is a lightweight Over-The-Air (OTA) file and system management suite for MicroPython microcontrollers (Raspberry Pi Pico, ESP32, ESP8266, and compatible boards). It provides robust remote firmware updates and device control over wireless serial modules (such as XBee) or any transparent UART connection.
OTAmpy is built on top of the Universal Reliable Serial Transport (URST) protocol, which guarantees error-free framing, integrity checking, and automatic recovery even over noisy or high-latency wireless links.
DISCLAIMER This is not intended as professional grade OTA. Use at your own risk.
Features
- Transactional OTA firmware updates — files are staged, SHA-256 verified, and atomically committed; a failed transfer leaves the running firmware untouched.
- Interactive file management — upload (
cp,upd), view (cat), or remove (rm) files on the device. - Recovery protection —
rmrefuses to delete boot, main, configuration, OTAmpy, or URST files needed for future wireless maintenance; no force option exists. - Remote reboot and reset — trigger a hard reboot (
rb) or MicroPython soft reset (sr) over the air. - Diagnostic commands —
pinghealth checks andmemRAM/flash queries. - Port management —
portslists and selects adapters;OTAMPY_PORTand persistent~/.config/otampy/config.jsonsettings avoid repeating--porton every command. - Target-matched bytecode deployment —
--bytecodecompiles OTAmpy and URST to.mpyusing the connected device's exact.mpyformat and small-int width. - Fail-safe CLI — destructive commands display a confirmation prompt before contacting the device.
Repository Structure
otampy/
├── src/otampy/ # Host CLI package (Python ≥ 3.12)
│ ├── cli.py # Click-based command-line interface
│ ├── deploy.py # Deploy command implementation
│ └── device/ # MicroPython device library (bundled in releases)
│ ├── lib/otampy/ # Device-side OTA library
│ └── examples/ # Example boot.py, main.py, config files
├── docs/ # Deployment, release, architecture, and protocol guides
├── tests/ # Host-side pytest suite
└── scripts/ # Automated release gate
The published package contains both the CLI and a read-only copy of the device library. The canonical device source lives in src/otampy/device/; see the deployment guide for how it is bundled.
Installation
Assumes your project will be using UV
uv add otampy
otampy init
# Edit configota.py
During development you can use pipx install git+https://github.com/simonl65/otampy.git@develop --force to install the latest development version.
init creates boot.py, main.py, and configota.py in your project at the location (device-dir) of your choosing. Edit configota.py to set the UART pins, baud rate, and timeout for your board. init will not overwrite existing files but will prompt you; use --force only when intentionally replacing all three.
Preview then perform the initial USB deployment:
WARNING
deployerases the device filesystem before copying files. Back up any application data and configuration first.
# Edit configota.py before deployment
otampy deploy --port /dev/ttyACM0 --dry-run
otampy deploy --port /dev/ttyACM0
The installed package contains the versioned OTAmpy device library and the templates used by init. Your project owns boot.py, main.py, and configota.py; upgrading the package does not overwrite them.
Developer installation from this repository
Ensure uv is installed, then:
cd /path/to/otampy
uv sync
uv tool install -e .
Device library setup (repository checkout)
-
Copy
src/otampy/device/examples/config.example.pytosrc/otampy/device/examples/configota.py(do not rename) and set the UART pins and baud rate for your board. -
Deploy the device library, example scripts, and URST to your device:
otampy deploy --port /dev/ttyACM0
-
Optionally add
--with-loggerto install thelog-to-filedevelopment logger. The default profile runs silently viaNullLogger. -
For a smaller, target-matched deployment, install
mpy-crossand pass--bytecode. OTAmpy queries the connected firmware's.mpyformat before erasing anything.uv tool install mpy-cross otampy deploy --port /dev/ttyACM0 --bytecode
-
Call
ota.poll()inside your application main loop to enable background OTA listening.
See the deployment guide and device integration guide for the complete set of deployment options, logger injection, and integration examples.
Usage
All commands follow this pattern:
otampy [global-options] <command> [command-options]
Global options
| Option | Description |
|---|---|
-p, --port PORT |
Select the OTA UART adapter (e.g. /dev/ttyUSB0, COM3). |
-b, --baud RATE |
Set the baud rate. |
--log-level LEVEL |
Host CLI logging: DEBUG, INFO, WARNING, ERROR, or CRITICAL. |
The default log level is ERROR. When --log-level is supplied, the CLI offers to retain the setting permanently (p), for the current shell session (s), or only for the current command (c).
Permanent port and log-level settings are stored together in ~/.config/otampy/config.json:
{
"default_port": "/dev/ttyUSB0",
"log_level": "DEBUG"
}
Session-only selections use shell-specific files in the operating system's temporary directory (normally /tmp on Linux) and do not alter the permanent configuration. OTAMPY_PORT and OTAMPY_LOG_LEVEL environment variables override saved settings.
Commands
| Command | Arguments | Description |
|---|---|---|
cat |
file |
Print a file from the device. |
cp |
source[:dest] [...] |
Copy files or folders to the device without rebooting. |
deploy |
(see below) | Erase and deploy the full device library over USB. |
device-dir |
— | Show or manage the saved project directory for deploy. |
init |
[directory] |
Scaffold boot.py, main.py, and configota.py. |
log-level |
Show or manage the saved CLI log level. | |
ls |
[path] |
List device directory contents. |
mem |
— | Query device RAM and flash utilisation. |
ping |
— | Connection health check - should receive PONG. |
ports |
— | List available serial adapters; mark and store a selection. |
rb |
— | Hard reboot the device (with confirmation). |
rm |
path [...] |
Remove paths from the device (with confirmation - not recoverable). |
sr |
— | MicroPython soft reset (with confirmation). |
upd |
[source[:dest] ...] |
Transactional OTA firmware update.1 |
1 Updates take place after the device has rebooted; the update process is handled by boot.py. With no sources specified, upd selects main.py and all Python files under lib/ in the current directory.
Deployment Options
| Option | Effect |
|---|---|
-p, --port |
Select the USB/serial device used by mpremote. |
--device-dir |
Select the directory containing device/ (boot.py, etc.) |
--with-logger |
Install the optional log-to-file package. |
--bytecode, --mpy |
Compile OTAmpy and URST into target-matched .mpy files. |
--mpy-cross |
Select the mpy-cross executable or command. |
--no-mip |
Install neither URST nor the optional logger. |
--no-reset |
Leave the board without a final reset. |
--dry-run |
Print the complete mpremote command without running it. |
--mpremote |
Use a specific mpremote executable. |
Common examples
Select a port for subsequent commands (avoids repeating --port):
otampy ports
NOTE: The following commands assume you've set the port.
Check device connection health:
otampy ping
List device files:
otampy ls
otampy ls /lib
Query device memory and storage:
otampy mem
Copy files or folders to the device (without reboot):
otampy cp settings.json:config/settings.json
otampy cp assets:assets/
otampy cp 'device/lib/*:lib/'
cp accepts multiple sources, folders, and local wildcard patterns (*, ?, [], **). Folder contents are copied recursively; empty folders are not created. Files are streamed to checksum-verified staging files and committed individually while the device continues running. Copies targeting root /boot.py or /main.py produce a reminder that the replacement will take effect on the next restart.
Remove files or directories (recovery paths are protected):
otampy rm old.py config.old
otampy rm 'lib/plugins/*.py'
Quote wildcards to prevent the host shell from expanding them locally (e.g., otampy rm '*'). Prefix an argument with : (e.g., :notes.txt) or use --literal-remote-paths if the filename also exists locally to prevent protection/verification aborts. Removing a non-empty directory requires a confirmation prompt.
To preserve remote recovery, rm cannot remove root /boot.py, /main.py, /configota.py, anything under /lib/otampy or /lib/urst, or an ancestor such as /lib or /.
Trigger an OTA firmware update (defaults to main.py and all lib/ Python files):
otampy upd
Update specific files or mapped paths:
otampy upd main.py configota.py
otampy upd 'device/lib/something/*.py:lib/something/'
Directories include all Python files recursively. Local *, ?, [], and ** patterns are supported. A pattern matching multiple files must map to a destination ending in /. If any source or pattern has no matches, the update stops before contacting the device.
Enable host-side diagnostics for a single command:
otampy --log-level DEBUG ping
Device Integration
Call ota.poll() in your application loop:
from otampy import OTA
from machine import UART
uart = UART(1, baudrate=9600, tx=Pin(4), rx=Pin(5))
ota = OTA(uart, config=config)
while True:
ota.poll()
# ... application logic
Important! - If your device needs to be set to a safe state before a reboot/reset you can add a callback function to the ota.poll() call:
def prepare_for_shutdown():
# ... application safety logic
while True:
ota.poll(callback=prepare_for_shutdown)
# ... application logic
This will invoke prepare_for_shutdown() before continuing to reboot/reset.
Inject any logger that implements debug, info, warning, error, and critical:
from log_to_file import Logger
from otampy import OTA
logger = Logger("/logs/ota.log", "some identifier", level="DEBUG")
ota = OTA(uart, config=config, logger=logger)
When no logger is provided, OTA uses an allocation-light NullLogger. NullLogger is also importable directly for applications that want an unconditional logger variable:
from otampy import NullLogger
logger = NullLogger()
See the device integration guide for the full configuration reference.
Contributing
Contributions are welcome. Please follow the process below to keep the codebase consistent and the release gate green.
Getting started
-
Fork the repository and create a feature branch from
develop:git checkout develop git checkout -b feature/your-feature-name
-
Install the development dependencies:
uv sync --group dev
-
Make your changes. Match the existing code style; the project uses Ruff for linting and formatting.
Running tests
uv run pytest
The test suite requires no connected hardware. Coverage is reported automatically.
Run the linter:
uv run ruff check .
uv run ruff format --check .
Submitting a pull request
- Target the
developbranch, notmain. - Ensure
ruff checkandpytestboth pass before opening a PR. - Describe what the change does, why it is needed, and how it was tested.
- For changes to the device library, note any impact on flash or RAM footprint.
- Keep commits focused and write clear, semantic and scoped commit messages (e.g.
feat(core): add ability to walk on water).
Reporting issues
Open a GitHub issue and include:
- A minimal reproduction case.
- The host OS, Python version, and OTAmpy version (
otampy --version). - The target board, MicroPython version, and transport (XBee model, direct UART, etc.).
- Any relevant host CLI and/or device logs (For CLI use:
--log-level DEBUG. For device setLOG_LEVEL="DEBUG"inconfigota.py).
Release process
Releases are cut by maintainers using the automated gate described in the release guide. Do not build publishable artifacts manually.
Documentation
| Document | Description |
|---|---|
| Deployment guide | All deploy options, logging profiles, and bytecode deployment. |
| Device integration guide | Device-side setup, logger injection, and runtime file copy. |
| Protocol specification | URST framing, packet types, and error recovery. |
| Architecture overview | Component relationships and design decisions. |
| Release guide | Versioning, the automated release gate, and publishing. |
License
OTAmpy is released under the Sustainable Use License.
Non-commercial and internal business use is permitted free of charge. Commercial redistribution or monetisation requires a separate commercial licence. Contact oss@codeability.co.uk for commercial licensing enquiries.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file otampy-2.0.1.tar.gz.
File metadata
- Download URL: otampy-2.0.1.tar.gz
- Upload date:
- Size: 49.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Zorin OS","version":"18","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df421cd4a12910c465d03f4693398b073d0767db8f5df6023e81f839473820d0
|
|
| MD5 |
eb3eadcd20ff31ebd33e9d1c39cd758a
|
|
| BLAKE2b-256 |
f234babd96f9a8e3499122c3693d5482a403fda9ec971de2984c8c5fa3f56739
|
File details
Details for the file otampy-2.0.1-py3-none-any.whl.
File metadata
- Download URL: otampy-2.0.1-py3-none-any.whl
- Upload date:
- Size: 61.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Zorin OS","version":"18","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b5367d023e50d68f132258a9509c64b9ac4ed4567919f15aefb3db073744c2a
|
|
| MD5 |
a826c328c07e7e6cac7ad4b1be39d652
|
|
| BLAKE2b-256 |
1488c048b6fb4565cdba09e34b2a98700b5bb33b35f9ed61eb3dc9f18e0c186b
|