Repository navigation
Releases
Releases · simonl65/otampy
OTAmpy v4.9.3
OTAmpy 4.9.3 — 2026-09-14
Major (breaking changes)
- None.
Minor (functionality added in a backward compatible manner)
- None.
Patch (backward compatible bug fixes)
- Fixed
footprint_boot.py,footprint_update.py, andfootprint_stress.py(device RAM-footprint probes undersrc/otampy/device/tools/) importingOTA/boot/managerfrom a non-existent on-device path (src.otampy...) instead of the real deployed package (otampy) — these would have failed immediately if actually run on a device. - Fixed the project's Pyright configuration replacing the entire standard-library typeshed with the MicroPython stub set instead of overlaying it, which silently broke type-checking for host-only code (
dataclasses,pathlib,os.environ, etc.) and had caused everyDeployArgs(...)construction to misreport as invalid. - Fixed
cli.py'sinitcommand reusing one variable (path) across two incompatible types (the rawstr | Noneargument and the resolvedPath), which the previous# type: ignorecomments were silently papering over.
OTAmpy v4.9.2
OTAmpy 4.9.2 — 2026-09-13
Major (breaking changes)
- None.
Minor (functionality added in a backward compatible manner)
- None.
Patch (backward compatible bug fixes)
- Fixed
mux._write()allocating two extra bytes objects (~256 B) on every channel-1 send by building the channel-prefixed frame in a pre-sizedbytearrayand writing the COBS-encoded frame and delimiter in a single call.
OTAmpy v4.9.1
OTAmpy 4.9.1 — 2026-09-13
Major (breaking changes)
- None.
Minor (functionality added in a backward compatible manner)
- None.
Patch (backward compatible bug fixes)
- Fixed
--minifyshipping an f-string conversion (!r/!s/!a) that MicroPython's parser rejects andmpy-crossrefuses to compile, sootampy upd/cp/deploy --minifyno longer ships a device library file that fails on the next boot.
OTAmpy v4.9.0
OTAmpy 4.9.0 — 2026-09-13
Major (breaking changes)
- None.
Minor (functionality added in a backward compatible manner)
- Added
OTA.mark_application_alive(), which clears the boot marker without polling the transport. An application that only callspoll()whenframe_ready()is true (becausepoll()can block for up toACK_TIMEOUT_MSon an idle link) never cleared the marker in a boot where no OTA command arrived. The next boot then paid the wideOTA_BOOT_RECOVERY_LISTEN_MSwindow, which does not answerPING, andotampy rollbackcould miss itspost-commit-ready-timeout. Call it once, immediately before the main loop. It never touches the transport, never raises, and a second call does nothing. Applications that poll unconditionally, including both shipped scaffolds, need no change.
Patch (backward compatible bug fixes)
docs/protocol.md§2.4 anddocs/architecture.mdno longer say that reachingpoll()is the only proof the application is alive. Both now describemark_application_alive()and theframe_ready()case that needs it.- The
examples/shared-uart/main.pyscaffold has a new comment pointing real-time loops atframe_ready()plusmark_application_alive(). The scaffold code is unchanged. - Updated the README with trial-boot updates, rollback and recovery details.
CHANGELOG.mdnow has separate, dated 4.7.0 and 4.8.0 sections instead of anUnreleasedblock. It also adds the missing 4.8.0 entries forotampy cp device:<path>(device-to-host copy) andotampy upd --all-filesskipping__pycache__and*.example.*.
OTAmpy v4.8.0
OTAmpy 4.8.0 — 2026-09-11
Major (breaking changes)
- None.
Minor (functionality added in a backward compatible manner)
- Shared-UART (channel-mux) mode, end to end. A project whose own code needs the same physical UART as OTAmpy can wrap it in
otampy.mux.SerialMuxon the device and have the host CLI speak the matching framing, so one radio link carries OTAmpy traffic alongside the application's own stream. New wire contract below URST (docs/protocol.md§1.3,PROTOCOL_VERSIONunchanged), a hostotampy.channel.ChannelCodec/ChannelSerialcodec, a--mux/--no-muxCLI flag andmuxconfig key, and anotampy init --muxscaffold. Opt-in; existing direct-mode devices and CLIs are unaffected. - Boot-time recovery window — recover a device stranded before
ota.poll(), over the radio, no USB.boot.pynow listens briefly on every boot forUPDATE_REQUESTorROLLBACK, widening to an ~8 s window after a boot that never reachedpoll()or still carries an unconfirmed candidate. New device settingsOTA_BOOT_RECOVERY_LISTEN_MS/OTA_BOOT_MARK_FILE; new host retry loops viaotampy upd --recoverandotampy rollback --recoverwith a configurablerecovery-wait. HonorsOTA_REQUIRE_AUTHwhen set. OptionalOTA(...).boot(heartbeat=wdt.feed)lets a customboot.pyfeed a watchdog across both blocking stretches.PROTOCOL_VERSIONunchanged. - A committed OTA update is now on trial until it is confirmed.
boot.pytracks unconfirmed candidates in a retain-previous journal and auto-restores the previous generation if the device reboots withinOTA_TRIAL_BOOTS(default3) boots without being confirmed. New channel-0 commandsCONFIRM/UPDATE_STATE; new CLI commandsotampy confirmandotampy state;otampy updnow auto-confirms after a healthyPING.PROTOCOL_VERSIONunchanged. otampy rollbackreverts a confirmed (or trialling) update over the radio, restoring the one retained previous generation via a newROLLBACKchannel-0 command. Refuses safely when nothing is retained or a commit is mid-flight.- Boot-time
UPDATE_COMMITis now all-or-nothing and retains the previous generation, via a new commit journal (OTA_JOURNAL_FILE) andrestore.repair()run on every boot.UPDATE_STARTdiscards the previously retained generation, keeping at most one prior generation on device. otampy cpcan now copy device-to-host. Adevice:<path>[:<host target>]argument downloads a single file from the device, reusing the existingCATcommand rather than adding new device-side protocol.
Patch (backward compatible bug fixes)
otampy upd --all-filesno longer uploads__pycache__artifacts or*.example.*template files, which were never meant to be flashed to the device.- Session-scoped settings (
ports/log-level/config/device-dir/mux) no longer silently revert under automation that forks a fresh subprocess per command. A newOTAMPY_SESSION_IDenv var lets such callers opt into a fixed session identifier instead of relying on process ancestry, which changed on every invocation with no persistent parent shell. - Every successful
otampy updno longer reports failure due to the post-commit health check racing the new recovery window. Post-commit androllbackhealth checks now use their ownpost-commit-ready-timeout(default30) instead of sharing the shorterupdate-ready-timeout. - The shipped
boot.py/main.pyexample scaffold is direct mode again, fixing a regression where it wiredSerialMuxunconditionally and silently broke plain (non-mux) host CLI commands.
OTAmpy v4.7.0
OTAmpy 4.7.0 — 2026-09-11
Major (breaking changes)
- None.
Minor (functionality added in a backward compatible manner)
- Added optional channel-mux mode for sharing one serial port between OTAmpy and other host/device traffic: outer frame protocol,
ChannelCodec/ChannelSerial,otampy muxcommand,
--muxflag,OTAMPY_MUXsetting, and anotampy init --muxscaffold. - Added fail-safe, all-or-nothing update commits with a retain-previous journal, automatic sweep of orphaned
.bckfiles, andrestore.repair()run on every boot. - Added a trial-boot lifecycle: boots are counted with automatic rollback on repeated failure, plus
otampy confirm/otampy statecommands and a device-sideota.confirm()facade. - Added
otampy rollbackfor a user-initiated revert to the previous firmware generation. - Added a boot-time recovery window so a stranded device can be reached over the radio after a failed or unpolled boot, via
otampy upd --recoverandotampy rollback --recover. - Replaced the old watchdog guidance with a heartbeat threaded through
boot.run()andOTA.boot().
Patch (backward compatible bug fixes)
- Fixed
main.pyto runrecover()so a lostboot.pyself-heals (F-06). - Fixed the orphan sweep so it no longer deletes retained
.bckfiles (F-04). - Fixed the one-shot RTC helper so it is no longer retained or journaled (F-05).
- Fixed post-reboot waits to use their own timeout instead of inheriting an unrelated one (F-11).
- Fixed the boot-time recovery window so it is actually reachable in practice, including a two-tier window and boot marker (F-10, F-14).
- Fixed the host-side recovery handshake to hold one port open and retry at stock timings (F-15).
- Fixed
--recoverto skip the bytecode-shadow preflight, which doesn't apply during recovery (F-16). - Fixed
otampy rollback --recoverto refuse to run against an already-healthy device (F-17). - Fixed
ChannelSerialso it no longer blocks on an empty serial port. - Fixed the CLI to ignore a malformed
OTAMPY_MUXvalue instead of aborting every command.
OTAmpy v4.6.0
OTAmpy 4.6.0 — 2026-09-03
Major (breaking changes)
- None. Every new setting below is opt-in: an existing device with an unchanged
configota.py, driven by a CLI with noOTAMPY_COMMAND_AUTH_KEY, behaves exactly as it did in 4.5.0.
Minor (functionality added in a backward compatible manner)
- Optional authentication for the runtime command surface. Until now, any station that could reach the device's UART could read, overwrite, delete or reboot it with no credential. On a wired link that is the same trust boundary as physical access; on a radio link — where OTAmpy is most useful — it means anyone in range.
- Setting
OTA_REQUIRE_AUTH = Trueon the device makes it require every command (RB,SR,UPDATE_REQUEST,RM,CAT,LS,MEM,RTC*,CP_*) to arrive asAUTH:<counter>:<hex-mac>:<command>. The host signs whenOTAMPY_COMMAND_AUTH_KEYis set. - The MAC is HMAC-SHA256 truncated to 8 bytes over a domain-separated, counter-prefixed copy of the command. The inner command is dispatched through the existing parser unchanged, so no command grammar moves.
- Counters are strictly increasing and seeded from the wall clock, so a command captured off the air cannot be replayed — including after a reboot, since an accepted
RB/SR/UPDATE_REQUESTsaves the counter as a boot floor before resetting. - The MAC sits above URST, so URST's retransmission is unchanged and unaware of it: a legitimate retry is never mistaken for a replay.
- New device settings:
OTA_REQUIRE_AUTH,COMMAND_AUTH_KEY,OTA_REPLAY_FLOOR_FILE. New host environment variables:OTAMPY_COMMAND_AUTH_KEY,OTAMPY_COUNTER_FILE. - Documented in
docs/protocol.mdsection 1.2, with all five settings shown commented out inconfigota.example.py.
- Setting
OTA_ALLOWED_PATH_PREFIXESoptionally restricts which pathsCP_START,CATandRMmay target. Unset, any non-traversing path is still allowed.
Patch (backward compatible bug fixes)
- Fixed an arbitrary file write in
CP_START(path traversal / persistent remote code execution).CP_START:<target>:<size>:<sha256>took its target straight from the command with no directory restriction and no..rejection — and since the SHA-256 field is attacker-supplied too, the checksum gate constrained nothing. A craftedCP_START:main.py:…(orboot.py, or anylib/path) overwrote device code, which then ran on the next reboot...is now always refused forCP_START,CATandRM, whether or not an allowlist is configured. There is no legitimate traversal on a filesystem rooted at/.- Prefix matching respects path-segment boundaries, so an allowed prefix of
datadoes not also admitdatabase.db. - The refusal happens before any directory or staging file is created, so a rejected copy leaves nothing behind.
OTAmpy v4.5.0
OTAmpy 4.5.0 — 2026-08-20
Major (breaking changes)
- None.
Minor (functionality added in a backward compatible manner)
OTA.poll()/manager.poll()accept a new optionalheartbeatcallback, called once per fragment during a largeCAT/LSresponse's transfer. A large response streams entirely
inside onepoll()call, one fragment at a time, each its own reliable send-and-wait-for-ACK — on a slow or lossy link this can legitimately take far longer than a singlepoll()call
normally would. A caller feeding a hardware watchdog only once perpoll()call would otherwise see no progress for the whole transfer and could mistake it for a hang. Distinct from
the existingcallbackparameter, which remains a one-shot "about to reset" hook used only byRB/UPDATE_REQUEST. Exceptions raised fromheartbeatare swallowed — a caller's
watchdog feed must never be able to abort an in-progress transfer. Defaults toNone(no-op) at every layer, so existing callers are unaffected.
Patch (backward compatible bug fixes)
- None.
OTAmpy v4.4.0
OTAmpy 4.4.0 — 2026-08-18
Major (breaking changes)
- None.
Minor (functionality added in a backward compatible manner)
updnow reports transfer progress within each file, not only at its start. An update is dominated by its largest file: at the default 128-byte chunk size a ~34 kBmain.pyis ~268 chunks, each an ack round-trip over the link, so the longest stretch of an update previously produced no output at all and was indistinguishable from a hang.- On a terminal, each file gets a live progress bar showing its position in the manifest, percentage, transfer rate and estimated time remaining.
- When output is not a terminal (piped to a dashboard, a log, or CI), progress is emitted as plain lines at 25% steps — for example
main.py: 50% (17152/34188 bytes). A redrawn bar is meaningless once captured and its control codes are litter in whatever reads it. The rendering is selected from the real stream, so redirecting is enough; there is nothing to configure. - The per-file announcement now carries its position in the manifest:
[2/13] main.py (34188 bytes). - Progress advances only once the device has acknowledged a chunk, so it reflects bytes the device accepted rather than bytes written to the wire.
- New
upd --no-progressflag restores exactly the previous output — one plainTransferring <path> (<n> bytes)...line per file, with no counter and no percentages — for scripts and captured runs where the extra lines are noise.
Patch (backward compatible bug fixes)
- None.
OTAmpy v4.3.1
OTAmpy 4.3.1 — 2026-08-17
Major (breaking changes)
- None.
Minor (functionality added in a backward compatible manner)
- None.
Patch (backward compatible bug fixes)
- Fixed
otampy catprinting its"Showing content of specified file: ..."status line to stdout instead of stderr, which always prepended itself to any piped/captured output. - Fixed
otampy catrunning raw device file content through Rich console markup, which silently swallowed any literal[...]in the file (list literals, dict/subscript syntax, bracketed comments) as an unrecognized style tag. - Fixed
otampy catappending an extra trailing newline even when the file content already ended in one.