""" Stable recovery boot stub. Install once as /boot.py. It is deliberately excluded from normal OTA updates. If a staged launcher/application update is interrupted or cannot boot, this restores the previous .bak files. """ try: import ujson as json except ImportError: import json try: import uos as os except ImportError: import os PENDING_FILE = "/update_pending.json" def exists(path): try: os.stat(path) return True except OSError: return False def remove(path): try: os.remove(path) except OSError: pass def read_pending(): if not exists(PENDING_FILE): return None try: with open(PENDING_FILE, "r") as f: return json.loads(f.read()) except Exception: return None def rollback(pending): if not pending: return False restored = False for item in reversed(pending.get("files", [])): target = item.get("target") backup = item.get("backup") if not target or not backup: continue try: if exists(backup): remove(target) os.rename(backup, target) restored = True except Exception as exc: print("[boot] rollback failed:", target, exc) remove(PENDING_FILE) return restored pending = read_pending() # Never boot a half-committed file set after a power loss. if pending and not pending.get("commit_complete", False): print("[boot] interrupted OTA commit; rolling back") rollback(pending) try: import machine machine.reset() except Exception: raise SystemExit try: import launcher launcher.run() except Exception as exc: print("[boot] launcher/application startup failed:", exc) pending = read_pending() if pending: print("[boot] reverting pending OTA") rollback(pending) try: import machine machine.reset() except Exception: pass raise