forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Release Commit @ bec7652
This commit is contained in:
139
iqpilot/system/ui/lib/os_update.py
Normal file
139
iqpilot/system/ui/lib/os_update.py
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IQ.OS compatibility check + in-place AGNOS update for the setup flow.
|
||||
|
||||
A chosen IQ.Pilot channel may target a newer IQ.OS than the device is running
|
||||
(its cloned tree pins the required version in launch_env.sh AGNOS_VERSION). When
|
||||
that differs from the running /VERSION, the setup flow flashes the target IQ.OS
|
||||
via comma's own agnos.py BEFORE writing continue.sh, so the single reboot lands
|
||||
on a compatible OS. The risky flashing is delegated entirely to agnos.py; this
|
||||
module only reads versions, picks the right manifest, and streams coarse
|
||||
progress.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import threading
|
||||
from typing import Callable
|
||||
|
||||
VERSION_PATH = "/VERSION"
|
||||
|
||||
|
||||
def current_os_version() -> str:
|
||||
try:
|
||||
with open(VERSION_PATH) as f:
|
||||
return f.read().strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def required_agnos_version(install_path: str) -> str:
|
||||
"""Read the target OS version the cloned fork pins in launch_env.sh."""
|
||||
path = os.path.join(install_path, "launch_env.sh")
|
||||
try:
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
m = re.search(r'AGNOS_VERSION\s*=\s*"([^"]+)"', line)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _hardware_dir(install_path: str) -> str:
|
||||
nested = os.path.join(install_path, "iqpilot", "system", "hardware", "tici")
|
||||
if os.path.isdir(nested):
|
||||
return nested
|
||||
return os.path.join(install_path, "system", "hardware", "tici")
|
||||
|
||||
|
||||
def agnos_manifest_path(install_path: str, device_type: str) -> str:
|
||||
# comma 3 (tici) uses a different AGNOS manifest than comma 3x (tizi) / comma 4 (mici).
|
||||
fname = "agnos_tici_15_1.json" if device_type == "tici" else "agnos.json"
|
||||
return os.path.join(_hardware_dir(install_path), fname)
|
||||
|
||||
|
||||
def os_update_needed(install_path: str) -> tuple[bool, str, str]:
|
||||
"""Returns (needed, current, required)."""
|
||||
current = current_os_version()
|
||||
required = required_agnos_version(install_path)
|
||||
needed = bool(required and current and required != current)
|
||||
return needed, current, required
|
||||
|
||||
|
||||
ProgressCb = Callable[[int, str], None]
|
||||
|
||||
|
||||
def run_agnos_update(install_path: str, device_type: str, progress_cb: ProgressCb) -> bool:
|
||||
"""Flash + swap to the target IQ.OS. Streams coarse partition-level progress
|
||||
via progress_cb(percent, note). Returns True on success. The device must be
|
||||
rebooted by the caller afterward for the new slot to take effect."""
|
||||
manifest = agnos_manifest_path(install_path, device_type)
|
||||
agnos_py = os.path.join(_hardware_dir(install_path), "agnos.py")
|
||||
if not os.path.isfile(manifest) or not os.path.isfile(agnos_py):
|
||||
progress_cb(0, "manifest_missing")
|
||||
return False
|
||||
|
||||
try:
|
||||
total_partitions = max(1, len(json.load(open(manifest))))
|
||||
except Exception:
|
||||
total_partitions = 1
|
||||
|
||||
progress_cb(1, "starting")
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
["python3", agnos_py, "--swap", manifest],
|
||||
cwd=install_path,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
env={**os.environ, "PYTHONPATH": install_path},
|
||||
)
|
||||
except Exception:
|
||||
progress_cb(0, "launch_failed")
|
||||
return False
|
||||
|
||||
completed = 0
|
||||
swapping = False
|
||||
assert proc.stdout is not None
|
||||
for line in proc.stdout:
|
||||
line = line.strip()
|
||||
if "Downloading and writing" in line or "Already flashed" in line:
|
||||
completed += 1
|
||||
pct = min(94, int((completed / total_partitions) * 90) + 2)
|
||||
progress_cb(pct, "flashing")
|
||||
elif "Swapping to slot" in line or "AGNOS ready" in line:
|
||||
swapping = True
|
||||
progress_cb(96, "swapping")
|
||||
proc.wait()
|
||||
if proc.returncode == 0:
|
||||
progress_cb(100, "done")
|
||||
return True
|
||||
progress_cb(0, "failed" if not swapping else "swap_failed")
|
||||
return False
|
||||
|
||||
|
||||
class OsUpdateCoordinator:
|
||||
"""Bridges the setup UI's install thread and the BLE confirmation from the app.
|
||||
The install thread posts a required-update, waits for the phone's confirm, then
|
||||
runs the flash. On-screen setup can confirm locally too."""
|
||||
|
||||
def __init__(self):
|
||||
self.confirmed = threading.Event()
|
||||
self.needed = False
|
||||
self.current = ""
|
||||
self.required = ""
|
||||
|
||||
def request(self, current: str, required: str) -> None:
|
||||
self.needed = True
|
||||
self.current = current
|
||||
self.required = required
|
||||
self.confirmed.clear()
|
||||
|
||||
def confirm(self) -> None:
|
||||
self.confirmed.set()
|
||||
|
||||
def wait_for_confirm(self, timeout: float) -> bool:
|
||||
return self.confirmed.wait(timeout=timeout)
|
||||
Reference in New Issue
Block a user