From 362ddfc0c718a2c24e7228583caccf2ec82e5eb0 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Fri, 25 Apr 2025 14:17:47 +0100 Subject: [PATCH] ui: replace qt text window with raylib (#35064) * remove qt text window * use wrapper, render text window in thread * add wait_for_exit method * update imports --- .gitattributes | 1 - common/text_window.py | 63 ----------------------------------- selfdrive/ui/.gitignore | 2 -- selfdrive/ui/SConscript | 3 -- selfdrive/ui/qt/text.cc | 64 ------------------------------------ selfdrive/ui/qt/text_larch64 | 3 -- selfdrive/ui/text | 7 ---- system/manager/build.py | 2 +- system/manager/manager.py | 2 +- system/ui/text.py | 60 ++++++++++++++++++++++++++++----- 10 files changed, 54 insertions(+), 153 deletions(-) delete mode 100755 common/text_window.py delete mode 100644 selfdrive/ui/qt/text.cc delete mode 100755 selfdrive/ui/qt/text_larch64 delete mode 100755 selfdrive/ui/text diff --git a/.gitattributes b/.gitattributes index 1384edb626..cc1605a132 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,7 +11,6 @@ selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text system/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text -selfdrive/ui/qt/text_larch64 filter=lfs diff=lfs merge=lfs -text third_party/**/*.a filter=lfs diff=lfs merge=lfs -text third_party/**/*.so filter=lfs diff=lfs merge=lfs -text third_party/**/*.so.* filter=lfs diff=lfs merge=lfs -text diff --git a/common/text_window.py b/common/text_window.py deleted file mode 100755 index d2762ebf7d..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", text], - stdin=subprocess.PIPE, - cwd=os.path.join(BASEDIR, "selfdrive", "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/selfdrive/ui/.gitignore b/selfdrive/ui/.gitignore index d9176a3fd4..7e9eaf932f 100644 --- a/selfdrive/ui/.gitignore +++ b/selfdrive/ui/.gitignore @@ -3,8 +3,6 @@ moc_* translations/main_test_en.* -_text - ui mui watch3 diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 233addaa6a..e63359da05 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -66,9 +66,6 @@ if GetOption('extras'): qt_env.SharedLibrary("qt/python_helpers", ["qt/qt_window.cc"], LIBS=qt_libs) - # text window - qt_env.Program("_text", ["qt/text.cc"], LIBS=qt_libs) - # setup and factory resetter qt_env.Program("qt/setup/reset", ["qt/setup/reset.cc"], LIBS=qt_libs) qt_env.Program("qt/setup/setup", ["qt/setup/setup.cc", asset_obj], diff --git a/selfdrive/ui/qt/text.cc b/selfdrive/ui/qt/text.cc deleted file mode 100644 index 21ec5eedcf..0000000000 --- a/selfdrive/ui/qt/text.cc +++ /dev/null @@ -1,64 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "system/hardware/hw.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/widgets/scrollview.h" - -int main(int argc, char *argv[]) { - initApp(argc, argv); - QApplication a(argc, argv); - QWidget window; - setMainWindow(&window); - - QGridLayout *main_layout = new QGridLayout(&window); - main_layout->setMargin(50); - - QLabel *label = new QLabel(argv[1]); - label->setWordWrap(true); - label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); - ScrollView *scroll = new ScrollView(label); - scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - main_layout->addWidget(scroll, 0, 0, Qt::AlignTop); - - // Scroll to the bottom - QObject::connect(scroll->verticalScrollBar(), &QAbstractSlider::rangeChanged, [=]() { - scroll->verticalScrollBar()->setValue(scroll->verticalScrollBar()->maximum()); - }); - - QPushButton *btn = new QPushButton(); -#ifdef __aarch64__ - btn->setText(QObject::tr("Reboot")); - QObject::connect(btn, &QPushButton::clicked, [=]() { - Hardware::reboot(); - }); -#else - btn->setText(QObject::tr("Exit")); - QObject::connect(btn, &QPushButton::clicked, &a, &QApplication::quit); -#endif - main_layout->addWidget(btn, 0, 0, Qt::AlignRight | Qt::AlignBottom); - - window.setStyleSheet(R"( - * { - outline: none; - color: white; - background-color: black; - font-size: 60px; - } - QPushButton { - padding: 50px; - padding-right: 100px; - padding-left: 100px; - border: 2px solid white; - border-radius: 20px; - margin-right: 40px; - } - )"); - - return a.exec(); -} diff --git a/selfdrive/ui/qt/text_larch64 b/selfdrive/ui/qt/text_larch64 deleted file mode 100755 index 7d74f4d31c..0000000000 --- a/selfdrive/ui/qt/text_larch64 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:61f539845ebfc9568c8d28867f1e5642e882f52ead8862c9b2224b7139f4a552 -size 3787480 diff --git a/selfdrive/ui/text b/selfdrive/ui/text deleted file mode 100755 index b12235f4e6..0000000000 --- a/selfdrive/ui/text +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -if [ -f /TICI ] && [ ! -f _text ]; then - cp qt/text_larch64 _text -fi - -exec ./_text "$1" diff --git a/system/manager/build.py b/system/manager/build.py index 21eff2fecc..771024794f 100755 --- a/system/manager/build.py +++ b/system/manager/build.py @@ -5,10 +5,10 @@ from pathlib import Path # NOTE: Do NOT import anything here that needs be built (e.g. params) from openpilot.common.basedir import BASEDIR -from openpilot.common.text_window import TextWindow from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.hardware import HARDWARE, AGNOS from openpilot.system.ui.spinner import Spinner +from openpilot.system.ui.text import TextWindow from openpilot.system.version import get_build_metadata MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9 diff --git a/system/manager/manager.py b/system/manager/manager.py index 89e5a472f2..d8c56cbc73 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 TextWindow def manager_init() -> None: diff --git a/system/ui/text.py b/system/ui/text.py index 531ed1919c..e57ae45d9a 100755 --- a/system/ui/text.py +++ b/system/ui/text.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 +import os import re +import threading +import time import pyray as rl from openpilot.system.hardware import HARDWARE, PC from openpilot.system.ui.lib.button import gui_button, ButtonStyle @@ -42,7 +45,8 @@ def wrap_text(text, font_size, max_width): return lines -class TextWindow: + +class TextWindowRenderer: def __init__(self, text: str): self._textarea_rect = rl.Rectangle(MARGIN, MARGIN, gui_app.width - MARGIN * 2, gui_app.height - MARGIN * 2) self._wrapped_lines = wrap_text(text, FONT_SIZE, self._textarea_rect.width - 20) @@ -70,13 +74,53 @@ class TextWindow: return ret -def show_text_in_window(text: str): - gui_app.init_window("Text") - text_window = TextWindow(text) - for _ in gui_app.render(): - text_window.render() - gui_app.close() +class TextWindow: + def __init__(self, text: str): + self._text = text + + self._renderer: TextWindowRenderer | None = None + self._stop_event = threading.Event() + self._thread = threading.Thread(target=self._run) + self._thread.start() + + # wait for the renderer to be initialized + while self._renderer is None and self._thread.is_alive(): + time.sleep(0.01) + + def wait_for_exit(self): + while self._thread.is_alive(): + time.sleep(0.01) + + def _run(self): + if os.getenv("CI") is not None: + return + gui_app.init_window("Text") + self._renderer = renderer = TextWindowRenderer(self._text) + try: + for _ in gui_app.render(): + if self._stop_event.is_set(): + break + renderer.render() + finally: + gui_app.close() + + def __enter__(self): + return self + + def close(self): + if self._thread.is_alive(): + self._stop_event.set() + self._thread.join(timeout=2.0) + if self._thread.is_alive(): + print("WARNING: failed to join text window thread") + + def __del__(self): + self.close() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() if __name__ == "__main__": - show_text_in_window(DEMO_TEXT) + with TextWindow(DEMO_TEXT): + time.sleep(5)