Skip to main content

OTAmpy — Over-The-Air Update Suite for MicroPython

Static Badge

License: SUL-1.0 Ruff PyPI version

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.

View the latest version at https://otampy.codeability.co.uk/

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 — rm refuses 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.
  • Trial-boot updates with automatic rollback — a committed update stays on trial until confirmed; a reboot within OTA_TRIAL_BOOTS boots (a crash, panic, or watchdog) restores the previous generation automatically, with no host involvement. otampy upd auto-confirms once the device answers healthy; otampy confirm/otampy state manage or query trial state directly.
  • Radio rollback — otampy rollback reverts a confirmed (or trialling) update to the one retained previous generation, over the radio, no USB required.
  • Boot-time recovery window — otampy upd --recover / otampy rollback --recover can reach a device stranded before ota.poll() — one that never made it back after a bad update — by retrying into a brief window boot.py opens on every boot. Widens automatically after a boot that looks risky.
  • Diagnostic commands — ping health checks and mem RAM/flash queries.
  • Port management — ports lists and selects adapters; OTAMPY_PORT and persistent ~/.config/otampy/config.json settings avoid repeating --port on every command.
  • Shared-UART (channel-mux) mode — --mux (or otampy mux --enable) wraps every URST frame in a channel-mux outer frame so one UART can carry OTAmpy traffic alongside the device's own stream. Off by default; must match the device's SerialMux (scaffold it with otampy init --mux). See docs/protocol.md §1.3.
  • Target-matched bytecode deployment — --bytecode compiles OTAmpy and optional user/logger code to .mpy using the connected device's exact .mpy format and small-int width; URST remains device-native source.
  • 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
│   ├── minify.py         # Safe, staged source minification
│   ├── utils/            # Host-side logging helpers
│   └── device/           # MicroPython device library (bundled in releases)
│       ├── lib/otampy/   # Device-side OTA library
│       └── examples/     # Direct-mode scaffold; shared-uart/ = channel-mux set
├── 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.

By default these are direct-mode scaffolds — OTAmpy owns the UART. Pass otampy init --mux to scaffold the shared-UART (channel-mux) set instead; use it only when your application shares the OTA UART, and note the host CLI then needs --mux too (see docs/protocol.md §1.3).

Preview then perform the initial USB deployment:

WARNING
deploy erases 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)

  1. Copy src/otampy/device/examples/config.example.py to src/otampy/device/examples/configota.py (do not rename) and set the UART pins and baud rate for your board.

  2. Deploy the device library, example scripts, and URST to your device:

    otampy deploy --port /dev/ttyACM0
    
  3. Optionally add --with-logger to install the log-to-file development logger. The default profile runs silently via NullLogger.

  4. For a smaller, target-matched deployment, install mpy-cross and pass --bytecode. OTAmpy queries the connected firmware's .mpy format before erasing anything.

    uv tool install mpy-cross
    otampy deploy --port /dev/ttyACM0 --bytecode
    
  5. 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 settings are stored per project and log-level settings globally in ~/.config/otampy/config.json. Advanced host settings managed by otampy config are also stored per project:

{
  "projects": {
    "/path/to/project": {
      "default_port": "/dev/ttyUSB0",
      "serial_timeout_seconds": 2.0,
      "query_retries": 3,
      "query_retry_backoff_seconds": 0.25,
      "update_ready_timeout_seconds": 10.0,
      "transfer_chunk_size": 256
    }
  },
  "global": {
    "log_level": "DEBUG"
  }
}

Session-only selections use files in the operating system's temporary directory (normally /tmp on Linux) and do not alter the permanent configuration. On POSIX, the session is identified by the parent process's PID, which is stable for an interactive shell but not for automation that forks a fresh subprocess per command (each invocation then gets a different parent PID and misses the session file a prior command wrote). Such automation should set OTAMPY_SESSION_ID to a fixed value once, so every invocation resolves the same session file regardless of process ancestry. On Windows, session selections apply to the active Windows logon session. OTAMPY_PORT and OTAMPY_LOG_LEVEL environment variables override saved settings. Advanced settings have matching overrides: OTAMPY_SERIAL_TIMEOUT, OTAMPY_QUERY_RETRIES, OTAMPY_QUERY_RETRY_BACKOFF, OTAMPY_UPDATE_READY_TIMEOUT, OTAMPY_TRANSFER_CHUNK_SIZE, OTAMPY_MUX, OTAMPY_POST_COMMIT_READY_TIMEOUT (health-check timeout after a commit or rollback), and OTAMPY_RECOVERY_WAIT (how long --recover keeps retrying into the boot-time recovery window while you power-cycle the device).

Commands

Command Arguments Description
cat file Print a file from the device.
confirm — Take the running update candidate off trial.
config — Show or manage advanced host configuration.
cp source[:dest] [...] / device:path[:dest] Copy files or folders to the device without rebooting, or a single file back from it.
deploy (see below) Erase and deploy the full device library over USB.
device-dir — Show or manage the saved project directory for deploy and updates.
init [directory] [--mux] Scaffold boot.py, main.py, configota.py (--mux = shared-UART set).
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 [--no-rtc] Hard reboot the device (with confirmation).
rm path [...] Remove paths from the device (with confirmation - not recoverable).
rollback [--recover] Revert to the retained previous version over the radio (with confirmation).2
rtc — Display the current device RTC timestamp without rebooting.
sr [--no-rtc] MicroPython soft reset (with confirmation).
state — Report whether the running build is on trial or confirmed (stable).
upd [--bytecode] [--keep-user-source] [--mpy-cross COMMAND] [--minify] [--no-rtc] [--no-progress] [--no-confirm] [--recover] [--all-files] [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 boot.py, main.py, configota.py, and all Python files under lib/ in the saved device directory. The update stays on trial until confirmed unless --no-confirm is passed; --recover blind-retries into the boot-time recovery window for a device stranded before ota.poll().
2 One-shot — only one previous generation is retained. --recover targets the boot-time recovery window and refuses outright if the device is already reachable via a plain PING.

Deployment Options

Option Effect
-p, --port Select the USB/serial device used by mpremote.
--device-dir Select the directory containing boot.py, main.py, and configota.py.
--with-logger Install the optional log-to-file package.
--urst-branch BRANCH Install URST-mpy from a Git branch, such as develop.
--bytecode, --mpy Compile OTAmpy and selected user code into target-matched .mpy files.
--all-files Deploy every deployable file and directory in the device directory.
--keep-user-source With --bytecode, deploy user Python files as source instead of .mpy.
--minify Remove comments and redundant blank lines from deployed Python source.
--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.
--no-rtc Skip setting the device RTC from the host during final boot.
--dry-run Print the complete mpremote command without running it.
--verbose Show raw mpremote commands and output during deployment.
--mpremote Use a specific mpremote executable.

--all-files recursively copies user directories such as lib/; it excludes configota.example.py so only the active configota.py configuration is installed.

Common examples

Select a port for subsequent commands (avoids repeating --port):

otampy ports

Show or tune advanced host communication settings:

otampy config --show
otampy config --set serial-timeout 3.5
otampy config --session --set query-retries 5
otampy config --clear update-ready-timeout

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/'
otampy cp --minify main.py

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. Use --minify to stage Python files without comments or redundant blank lines; it never alters local source files.

Copy a single file from the device back to the host with a device: prefix:

otampy cp device:main.py
otampy cp device:config/settings.json ./settings.json

The device path must name a single file (no wildcards or directories); the host target defaults to the file's basename in the current directory. --minify does not apply to device-to-host copies.

Local cp and upd source paths are resolved from the project root: /device/*:/ copies <project-root>/device/* to the device filesystem root. The path after : is always a remote device path.

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 boot.py, main.py, configota.py, and all lib/ Python files in the saved device directory):

otampy upd

To update every file in that directory (including non-Python assets), use --all-files. OTAmpy lists the files and asks for confirmation before contacting the device:

otampy upd --all-files

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.

Use --bytecode to compile selected updates for the connected target; it accepts --mpy-cross and --keep-user-source just like deploy. If a source update would be shadowed by matching deployed bytecode, OTAmpy offers to switch to bytecode or remove the stale artifact as part of the transaction.

Enable host-side diagnostics for a single command:

otampy --log-level DEBUG ping

Confirm a trial update, check its state, or roll back to the previous version:

otampy upd --no-confirm main.py   # leave the candidate on trial
otampy state                      # STATE_OK:trial:1 (or :stable)
otampy confirm                    # take it off trial
otampy rollback                   # revert to the retained previous generation

Recover a device that never made it back to ota.poll() (e.g. after a bad update), by retrying into the brief window boot.py opens on every boot — you will be prompted to power-cycle the device:

otampy upd --recover
otampy rollback --recover

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

  1. Fork the repository and create a feature branch from develop:

    git checkout develop
    git checkout -b feature/your-feature-name
    
  2. Install the development dependencies:

    uv sync --group dev
    
  3. 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 develop branch, not main.
  • Ensure ruff check and pytest both 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 set LOG_LEVEL="DEBUG" in configota.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.

Metadata

Release files for otampy 4.9.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for otampy 4.9.0
File Size Uploaded
otampy-4.9.0.tar.gz 131.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for otampy 4.9.0
File Interpreter ABI Platform
otampy-4.9.0-py3-none-any.whl Python 3 none any Details

Total release size: 287.7 kB

Release files / otampy-4.9.0.tar.gz

Download URL otampy-4.9.0.tar.gz
Size 131.3 kB
Tags Source
SHA-256 checksum
How to use checksums
1294d32e18c95ba88769038578a4212e0f7c50087606cfd5128625d79e88550a
BLAKE2b-256 checksum
How to use checksums
f943912dd5fa5f8208f59931579fba6d1ce97dfbea4768c823da3a73a754a51b
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / otampy-4.9.0-py3-none-any.whl

Download URL otampy-4.9.0-py3-none-any.whl
Size 156.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3e0e80e127d9dd1bcdaef3038ce278cab839a6d5938bb91cf68f34a1cfa778ec
BLAKE2b-256 checksum
How to use checksums
b18e1d8cf55d8f699ca079406c7ae1c38dbec89f22bcc8b2bd44efab0b68311e
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

4.9.3

2 release files

4.9.2

2 release files

4.9.1

2 release files

This release

4.9.0 This release

2 release files

4.8.0

2 release files

4.7.0

2 release files

4.6.0

2 release files

4.5.0

2 release files

4.4.0

2 release files

4.3.1

2 release files

4.3.0

2 release files

4.2.4

2 release files

4.2.3

2 release files

4.2.2

2 release files

4.2.1

2 release files

4.2.0

2 release files

4.1.1

2 release files

4.1.0

2 release files

4.0.0

2 release files

3.1.1

2 release files

3.1.0

2 release files

3.0.2

2 release files

3.0.1

2 release files

3.0.0

2 release files

2.7.1

2 release files

2.7.0

2 release files

2.6.0

2 release files

2.5.0

2 release files

2.4.1

2 release files

2.4.0

2 release files

2.3.1

2 release files

2.2.1

2 release files

2.2.0

2 release files

2.1.6

2 release files

2.1.5

2 release files

2.1.3

2 release files

2.1.2

2 release files

2.1.1

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page