diff --git a/ota.py b/ota.py index 46fde72..6c8efa9 100644 --- a/ota.py +++ b/ota.py @@ -7,13 +7,17 @@ on the next boot rather than bricking the device. Strategy -------- -1. Fetch ota_manifest.txt from the repo to determine which files to sync. -2. Compare SHA1 hashes with a local manifest (.ota_manifest.json). -3. Download only changed or missing files, writing to .tmp first. -4. On success, rename .tmp files into place and update the manifest. -5. If anything fails mid-update, the manifest is not updated, so the +1. Check if last boot was good (OK flag exists). +2. If good, fetch remote commit SHA and compare with local — if unchanged, + skip file check entirely. +3. If new commit or failed boot, fetch ota_manifest.txt from the repo + to determine which files to sync. +4. Compare SHA1 hashes with a local manifest (.ota_manifest.json). +5. Download only changed or missing files, writing to .tmp first. +6. On success, rename .tmp files into place and update the manifest. +7. If anything fails mid-update, the manifest is not updated, so the next boot will retry. Partially written .tmp files are cleaned up. -6. A "safety" flag file (.ota_ok) is written by main.py on successful +8. A "safety" flag file (.ota_ok) is written by main.py on successful startup. If it is absent on boot, the previous update is suspected bad — the manifest is wiped so all files are re-fetched cleanly. @@ -146,6 +150,26 @@ def _match_pattern(name, pattern): i += 1 return i == n and j == m +def _fetch_commit_sha(): + url = ( + f"{GITEA_BASE}/api/v1/repos/{REPO_OWNER}/{REPO_NAME}" + f"/branches/{REPO_BRANCH}" + ) + info(f"Fetching commit from {url}") + try: + r = urequests.get(url, headers=_headers()) + info(f"Response status: {r.status_code}") + if r.status_code == 200: + data = r.json() + r.close() + sha = data.get("commit", {}).get("sha") + info(f"Got commit: {sha}") + return sha + r.close() + except Exception as e: + log_err(f"Failed to fetch commit: {e}") + return None + def _fetch_manifest(): url = ( f"{GITEA_BASE}/api/v1/repos/{REPO_OWNER}/{REPO_NAME}" @@ -223,9 +247,11 @@ def _load_manifest(): except Exception: return {} -def _save_manifest(manifest): +def _save_manifest(manifest, commit_sha=None): try: with open(MANIFEST_FILE, "w") as f: + if commit_sha: + manifest["_commit"] = commit_sha ujson.dump(manifest, f) except Exception as e: warn(f"Could not save manifest: {e}") @@ -311,7 +337,7 @@ def _fetch_file_list(): fetch_matching(root, manifest_patterns) return files -def _do_update(): +def _do_update(commit_sha=None): """ Fetch file list, download changed files, update manifest. Returns True if all succeeded (or nothing needed updating). @@ -348,11 +374,10 @@ def _do_update(): if failed: log_err(f"Update incomplete — {len(failed)} file(s) failed: {failed}") - # Save partial manifest so successful files aren't re-downloaded - _save_manifest(manifest) + _save_manifest(manifest, commit_sha) return False - _save_manifest(manifest) + _save_manifest(manifest, commit_sha) if updated: info(f"Update complete — {len(updated)} file(s) updated: {updated}") @@ -371,6 +396,8 @@ def update(): - If the OK flag is missing, the previous boot is assumed to have failed — wipes the manifest so everything is re-fetched cleanly. + - If the commit hash hasn't changed and last boot was good, skip + file comparison entirely. - Runs the update. - Clears the OK flag so main.py must re-assert it on successful start. """ @@ -380,17 +407,33 @@ def update(): load_config() - if not _ok_flag_exists(): + ok_flag = _ok_flag_exists() + manifest = _load_manifest() + + if not ok_flag: warn("OK flag missing — last boot may have failed") warn("Wiping manifest to force full re-fetch") _wipe_manifest() + manifest = {} else: info("OK flag present — last boot was good") + commit_sha = _fetch_commit_sha() + + if ok_flag and commit_sha and manifest.get("_commit") == commit_sha: + info(f"Commit unchanged ({commit_sha[:8]}) — skipping file check") + info("-" * 40) + return + + if commit_sha: + info(f"Remote commit: {commit_sha[:8]}") + else: + warn("Could not fetch remote commit — proceeding with file check") + # Clear the flag now; main.py must call ota.mark_ok() to re-set it _clear_ok_flag() - success = _do_update() + success = _do_update(commit_sha) if success: info("OTA check complete — booting application")