diff --git a/common/spinner.py b/common/spinner.py deleted file mode 100644 index 12a816eaf8..0000000000 --- a/common/spinner.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import subprocess -from openpilot.common.basedir import BASEDIR - - -class Spinner: - def __init__(self): - try: - self.spinner_proc = subprocess.Popen(["./spinner.py"], - stdin=subprocess.PIPE, - cwd=os.path.join(BASEDIR, "system", "ui"), - close_fds=True) - except OSError: - self.spinner_proc = None - - def __enter__(self): - return self - - def update(self, spinner_text: str): - if self.spinner_proc is not None: - self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n") - try: - self.spinner_proc.stdin.flush() - except BrokenPipeError: - pass - - def update_progress(self, cur: float, total: float): - self.update(str(round(100 * cur / total))) - - def close(self): - if self.spinner_proc is not None: - self.spinner_proc.kill() - try: - self.spinner_proc.communicate(timeout=2.) - except subprocess.TimeoutExpired: - print("WARNING: failed to kill spinner") - self.spinner_proc = None - - def __del__(self): - self.close() - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - -if __name__ == "__main__": - import time - with Spinner() as s: - s.update("Spinner text") - time.sleep(5.0) - print("gone") - time.sleep(5.0) diff --git a/common/text_window.py b/common/text_window.py deleted file mode 100755 index 358243d1f1..0000000000 --- a/common/text_window.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -import os -import time -import subprocess -from openpilot.common.basedir import BASEDIR - - -class TextWindow: - def __init__(self, text): - try: - self.text_proc = subprocess.Popen(["./text.py", text], - stdin=subprocess.PIPE, - cwd=os.path.join(BASEDIR, "system", "ui"), - close_fds=True) - except OSError: - self.text_proc = None - - def get_status(self): - if self.text_proc is not None: - self.text_proc.poll() - return self.text_proc.returncode - return None - - def __enter__(self): - return self - - def close(self): - if self.text_proc is not None: - self.text_proc.terminate() - self.text_proc = None - - def wait_for_exit(self): - if self.text_proc is not None: - while True: - if self.get_status() == 1: - return - time.sleep(0.1) - - def __del__(self): - self.close() - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - -if __name__ == "__main__": - text = """Traceback (most recent call last): - File "./controlsd.py", line 608, in - main() - File "./controlsd.py", line 604, in main - controlsd_thread(sm, pm, logcan) - File "./controlsd.py", line 455, in controlsd_thread - 1/0 -ZeroDivisionError: division by zero""" - print(text) - - with TextWindow(text) as s: - for _ in range(100): - if s.get_status() == 1: - print("Got exit button") - break - time.sleep(0.1) - print("gone") diff --git a/system/athena/registration.py b/system/athena/registration.py index 964fbff51e..717050fd8a 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -2,15 +2,18 @@ import time import json import jwt +import pyray as rl from pathlib import Path +from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta, UTC from openpilot.common.api import api_get from openpilot.common.params import Params -from openpilot.common.spinner import Spinner from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.system.hardware import HARDWARE, PC from openpilot.system.hardware.hw import Paths +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.spinner import Spinner from openpilot.common.swaglog import cloudlog @@ -20,20 +23,8 @@ def is_registered_device() -> bool: dongle = Params().get("DongleId", encoding='utf-8') return dongle not in (None, UNREGISTERED_DONGLE_ID) - -def register(show_spinner=False) -> str | None: - """ - All devices built since March 2024 come with all - info stored in /persist/. This is kept around - only for devices built before then. - - With a backend update to take serial number instead - of dongle ID to some endpoints, this can be removed - entirely. - """ - params = Params() - - dongle_id: str | None = params.get("DongleId", encoding='utf8') +def _get_dongle_id() -> str | None: + dongle_id: str | None = Params().get("DongleId", encoding='utf8') if dongle_id is None and Path(Paths.persist_root()+"/comma/dongle_id").is_file(): # not all devices will have this; added early in comma 3X production (2/28/24) with open(Paths.persist_root()+"/comma/dongle_id") as f: @@ -43,11 +34,19 @@ def register(show_spinner=False) -> str | None: if not pubkey.is_file(): dongle_id = UNREGISTERED_DONGLE_ID cloudlog.warning(f"missing public key: {pubkey}") - elif dongle_id is None: - if show_spinner: - spinner = Spinner() - spinner.update("registering device") + return dongle_id + +def do_register(spinner = None, done_event=None) -> str | None: + """ + All devices built since March 2024 come with all + info stored in /persist/. This is kept around + only for devices built before then. + + With a backend update to take serial number instead + of dongle ID to some endpoints, this can be removed + entirely. + """ # Create registration token, in the future, this key will make JWTs directly with open(Paths.persist_root()+"/comma/id_rsa.pub") as f1, open(Paths.persist_root()+"/comma/id_rsa") as f2: public_key = f1.read() @@ -65,9 +64,10 @@ def register(show_spinner=False) -> str | None: cloudlog.exception("Error getting imei, trying again...") time.sleep(1) - if time.monotonic() - start_time > 60 and show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + if time.monotonic() - start_time > 60 and spinner: + spinner.set_text(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + dongle_id = None backoff = 0 start_time = time.monotonic() while True: @@ -89,14 +89,32 @@ def register(show_spinner=False) -> str | None: backoff = min(backoff + 1, 15) time.sleep(backoff) - if time.monotonic() - start_time > 60 and show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + if time.monotonic() - start_time > 60 and spinner: + spinner.set_text(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + return dongle_id + +def register(show_spinner=False) -> str | None: + dongle_id = _get_dongle_id() + if not dongle_id: if show_spinner: - spinner.close() + with ThreadPoolExecutor(max_workers=1) as executor: + gui_app.init_window("Register") + spinner = Spinner() + spinner.set_text("registering device") + future = executor.submit(do_register, spinner) + while not future.done(): + rl.begin_drawing() + rl.clear_background(rl.BLACK) + spinner.render() + rl.end_drawing() + gui_app.close() + dongle_id = future.result() + else: + dongle_id = do_register() if dongle_id: - params.put("DongleId", dongle_id) + Params().put("DongleId", dongle_id) set_offroad_alert("Offroad_UnofficialHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC) return dongle_id diff --git a/system/manager/build.py b/system/manager/build.py index 93c0546c85..25e70eb96f 100755 --- a/system/manager/build.py +++ b/system/manager/build.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 import os import subprocess +import select from pathlib import Path +import pyray as rl # NOTE: Do NOT import anything here that needs be built (e.g. params) from openpilot.common.basedir import BASEDIR -from openpilot.common.spinner import Spinner -from openpilot.common.text_window import TextWindow from openpilot.system.hardware import HARDWARE, AGNOS from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.version import get_build_metadata +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.spinner import Spinner +from openpilot.system.ui.text import TextWindow MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9 CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache") @@ -17,39 +20,45 @@ CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache") TOTAL_SCONS_NODES = 3130 MAX_BUILD_PROGRESS = 100 -def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: +def build(dirty: bool = False, minimal: bool = False) -> None: env = os.environ.copy() env['SCONS_PROGRESS'] = "1" - nproc = os.cpu_count() - if nproc is None: - nproc = 2 + nproc = os.cpu_count() or 2 extra_args = ["--minimal"] if minimal else [] - if AGNOS: HARDWARE.set_power_save(False) os.sched_setaffinity(0, range(8)) # ensure we can use the isolcpus cores # building with all cores can result in using too # much memory, so retry with less parallelism + gui_app.init_window("Spinner") + spinner = Spinner() compile_output: list[bytes] = [] for n in (nproc, nproc/2, 1): compile_output.clear() scons: subprocess.Popen = subprocess.Popen(["scons", f"-j{int(n)}", "--cache-populate", *extra_args], cwd=BASEDIR, env=env, stderr=subprocess.PIPE) assert scons.stderr is not None + os.set_blocking(scons.stderr.fileno(), False) # Non-blocking reads # Read progress from stderr and update spinner + spinner.set_text("0") while scons.poll() is None: try: - line = scons.stderr.readline() - if line is None: - continue + rl.begin_drawing() + rl.clear_background(rl.BLACK) + spinner.render() + rl.end_drawing() + + if scons.stderr in select.select([scons.stderr], [], [], 0.02)[0]: + line = scons.stderr.readline() + if not line: + continue line = line.rstrip() - prefix = b'progress: ' if line.startswith(prefix): i = int(line[len(prefix):]) - spinner.update_progress(MAX_BUILD_PROGRESS * min(1., i / TOTAL_SCONS_NODES), 100.) + spinner.set_text(str(int(MAX_BUILD_PROGRESS * min(1., i / TOTAL_SCONS_NODES)))) elif len(line): compile_output.append(line) print(line.decode('utf8', 'replace')) @@ -62,7 +71,7 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: if scons.returncode != 0: # Read remaining output if scons.stderr is not None: - compile_output += scons.stderr.read().split(b'\n') + compile_output += [line for line in scons.stderr.read().split(b'\n') if not line.startswith(b'progress')] # Build failed log errors error_s = b"\n".join(compile_output).decode('utf8', 'replace') @@ -70,10 +79,13 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: cloudlog.error("scons build failed\n" + error_s) # Show TextWindow - spinner.close() if not os.getenv("CI"): - with TextWindow("openpilot failed to build\n \n" + error_s) as t: - t.wait_for_exit() + text_window = TextWindow("openpilot failed to build\n \n" + error_s) + while True: + rl.begin_drawing() + rl.clear_background(rl.BLACK) + text_window.render() + rl.end_drawing() exit(1) # enforce max cache size @@ -88,7 +100,5 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: if __name__ == "__main__": - spinner = Spinner() - spinner.update_progress(0, 100) build_metadata = get_build_metadata() - build(spinner, build_metadata.openpilot.is_dirty, minimal = AGNOS) + build(build_metadata.openpilot.is_dirty, minimal = AGNOS) diff --git a/system/manager/manager.py b/system/manager/manager.py index 89e5a472f2..2e0791b886 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -9,7 +9,6 @@ from cereal import log import cereal.messaging as messaging import openpilot.system.sentry as sentry from openpilot.common.params import Params, ParamKeyType -from openpilot.common.text_window import TextWindow from openpilot.system.hardware import HARDWARE from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog from openpilot.system.manager.process import ensure_running @@ -18,6 +17,7 @@ from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_I from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.version import get_build_metadata, terms_version, training_version from openpilot.system.hardware.hw import Paths +from openpilot.system.ui.text import show_text_in_window def manager_init() -> None: @@ -221,8 +221,7 @@ if __name__ == "__main__": # Show last 3 lines of traceback error = traceback.format_exc(-3) error = "Manager failed to start\n\n" + error - with TextWindow(error) as t: - t.wait_for_exit() + show_text_in_window(error) raise