ota deployment script, lots of other fun goodies too

This commit is contained in:
Thaddeus Hughes
2026-04-27 11:14:03 -05:00
parent 3774cde506
commit 9f4362b5fd
261 changed files with 2153 additions and 206003 deletions

109
ota_deploy.py Normal file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""SC-F001 build + OTA deploy.
Runs `idf.py build` and POSTs the resulting firmware binary to the device's
/ota endpoint. Progress is printed as the upload streams.
Usage:
python ota_deploy.py [--ip 192.168.4.1] [--no-build]
The script relies on `idf.py` being on PATH — run it from an ESP-IDF shell.
"""
from __future__ import annotations
import argparse
import http.client
import shutil
import subprocess
import sys
import time
from pathlib import Path
DEFAULT_IP = "192.168.4.1"
DEFAULT_PROJECT = "SC-F001"
DEFAULT_BUILD_DIR = Path(__file__).resolve().parent / "build"
OTA_PATH = "/ota"
CONNECT_TIMEOUT_S = 10.0
UPLOAD_TIMEOUT_S = 120.0
CHUNK_BYTES = 4096
def run_build(project_dir: Path) -> None:
# On Windows `idf.py` is a Python script (no .exe), so CreateProcess can't
# launch it directly. Resolve to an absolute path and run through the shell
# so .py / .bat associations resolve correctly.
resolved = shutil.which("idf.py") or shutil.which("idf.py.bat") or "idf.py"
cmd = f'"{resolved}" build' if " " in resolved else f"{resolved} build"
print(f">> {cmd} (cwd={project_dir})")
rc = subprocess.call(cmd, cwd=str(project_dir), shell=True)
if rc != 0:
sys.exit(f"idf.py build failed (exit {rc})")
def post_ota(ip: str, binary: Path) -> None:
size = binary.stat().st_size
print(f">> uploading {binary.name} ({size} bytes) to http://{ip}{OTA_PATH}")
conn = http.client.HTTPConnection(ip, 80, timeout=CONNECT_TIMEOUT_S)
conn.connect()
# Switch to upload timeout now that we're connected.
conn.sock.settimeout(UPLOAD_TIMEOUT_S)
conn.putrequest("POST", OTA_PATH)
conn.putheader("Content-Type", "application/octet-stream")
conn.putheader("Content-Length", str(size))
conn.endheaders()
start = time.monotonic()
sent = 0
last_pct = -1
with binary.open("rb") as f:
while True:
chunk = f.read(CHUNK_BYTES)
if not chunk:
break
conn.send(chunk)
sent += len(chunk)
pct = (sent * 100) // size
if pct != last_pct:
print(f"\r {pct:3d}% ({sent}/{size})", end="", flush=True)
last_pct = pct
elapsed = time.monotonic() - start
print(f"\n>> upload done in {elapsed:.1f}s, waiting for response...")
resp = conn.getresponse()
body = resp.read().decode("utf-8", errors="replace")
conn.close()
if resp.status // 100 != 2:
sys.exit(f"OTA HTTP {resp.status} {resp.reason}: {body}")
print(f">> device: {body.strip()}")
def main() -> int:
ap = argparse.ArgumentParser(description="Build + OTA deploy SC-F001 firmware")
ap.add_argument("--ip", default=DEFAULT_IP, help=f"device IP (default {DEFAULT_IP})")
ap.add_argument("--project", default=DEFAULT_PROJECT, help="project name (binary = <project>.bin)")
ap.add_argument("--build-dir", default=str(DEFAULT_BUILD_DIR),
help="build dir containing <project>.bin")
ap.add_argument("--no-build", action="store_true", help="skip idf.py build, upload existing binary")
args = ap.parse_args()
project_dir = Path(__file__).resolve().parent
build_dir = Path(args.build_dir).resolve()
binary = build_dir / f"{args.project}.bin"
if not args.no_build:
run_build(project_dir)
if not binary.exists():
sys.exit(f"firmware binary not found: {binary}")
post_ota(args.ip, binary)
print(">> device is rebooting into the new image")
return 0
if __name__ == "__main__":
raise SystemExit(main())