updates
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Push a built application image to one Freddy ESP32 on the local Wi-Fi."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG = Path(__file__).resolve().parents[1] / "private" / "ota.json"
|
||||
TARGETS = ("eye-1", "eye-2", "camera")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--host", required=True, help="Device IP address on the home Wi-Fi")
|
||||
parser.add_argument("--target", required=True, choices=TARGETS)
|
||||
parser.add_argument("--bin", required=True, type=Path, dest="image_path")
|
||||
parser.add_argument("--port", type=int, default=3232)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not CONFIG.exists():
|
||||
raise SystemExit("Run firmware/tools/configure_ota.py first")
|
||||
key = json.loads(CONFIG.read_text())["ota_key"].encode()
|
||||
image = args.image_path.read_bytes()
|
||||
marker = f"FREDDY_OTA_TARGET:{args.target}".encode()
|
||||
if marker not in image:
|
||||
raise SystemExit(f"Image does not contain the expected {marker.decode()} marker")
|
||||
|
||||
base = f"http://{args.host}:{args.port}"
|
||||
with urllib.request.urlopen(base + "/ota/info", timeout=10) as response:
|
||||
info = json.load(response)
|
||||
if info.get("device") != args.target:
|
||||
raise SystemExit(f"Device reports {info.get('device')!r}, expected {args.target!r}")
|
||||
if not 0 < len(image) <= info.get("slot_bytes", 0):
|
||||
raise SystemExit(f"Image is {len(image)} bytes; device slot is {info.get('slot_bytes')} bytes")
|
||||
|
||||
digest = hashlib.sha256(image).hexdigest()
|
||||
nonce = info["nonce"]
|
||||
message = f"{args.target}\n{nonce}\n{digest}\n{len(image)}".encode()
|
||||
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
|
||||
request = urllib.request.Request(
|
||||
base + "/ota",
|
||||
data=image,
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"X-Freddy-Nonce": nonce,
|
||||
"X-Freddy-SHA256": digest,
|
||||
"X-Freddy-Signature": signature,
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=180) as response:
|
||||
result = json.load(response)
|
||||
except urllib.error.HTTPError as error:
|
||||
raise SystemExit(f"Update rejected (HTTP {error.code}): {error.read().decode(errors='replace')}") from error
|
||||
if result != {"ok": True}:
|
||||
raise SystemExit(f"Unexpected reply: {result!r}")
|
||||
print(f"{args.target}: image accepted ({len(image)} bytes, SHA-256 {digest}). Waiting for reboot…")
|
||||
old_slot = info["running_slot"]
|
||||
deadline = time.monotonic() + 60
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(2)
|
||||
try:
|
||||
with urllib.request.urlopen(base + "/ota/info", timeout=3) as response:
|
||||
current = json.load(response)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if current.get("device") == args.target and current.get("running_slot") != old_slot:
|
||||
print(f"{args.target}: back online in firmware slot {current['running_slot']}.")
|
||||
return
|
||||
raise SystemExit(
|
||||
"Image was accepted, but the new slot was not confirmed online within 60 seconds. "
|
||||
"Check device power and USB logs before retrying."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user