From a7de971334b79fbc0de0386f8be8b70b99e69158 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 1 Mar 2026 02:41:51 -0800 Subject: [PATCH] mici setup: use nav stack (#37507) * pressable * slow * fast and looks great * 0.075 * clean up * fix missing * clean up * mici setup use nav stack! * remove flat state! * todo * clean up * clean up ordering * clean up * reset progress on show, dont mutate nav stack from thread * reset text on show too * rename * clean up --- system/ui/mici_setup.py | 243 +++++++++++++++----------------- system/ui/widgets/nav_widget.py | 10 +- 2 files changed, 123 insertions(+), 130 deletions(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 33e0b8cf6..9656702c1 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -7,7 +7,6 @@ import time import urllib.request import urllib.error from urllib.parse import urlparse -from enum import IntEnum import shutil from collections.abc import Callable @@ -23,6 +22,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton, SmallCircleIconButton, WidishRoundedButton, FullRoundedButton) from openpilot.system.ui.widgets.label import UnifiedLabel @@ -92,16 +92,6 @@ class NetworkConnectivityMonitor: break -class SetupState(IntEnum): - GETTING_STARTED = 0 - NETWORK_SETUP = 1 - NETWORK_SETUP_CUSTOM_SOFTWARE = 2 - SOFTWARE_SELECTION = 3 - DOWNLOADING = 4 - DOWNLOAD_FAILED = 5 - CUSTOM_SOFTWARE_WARNING = 6 - - class StartPage(Widget): def __init__(self): super().__init__() @@ -127,15 +117,24 @@ class StartPage(Widget): self._title.render(rl.Rectangle(rect.x, rect.y + (draw_y - base_draw_y), rect.width, rect.height)) -class SoftwareSelectionPage(Widget): +class SoftwareSelectionPage(NavWidget): def __init__(self, use_openpilot_callback: Callable, use_custom_software_callback: Callable): super().__init__() self._openpilot_slider = LargerSlider("slide to use\nopenpilot", use_openpilot_callback) - self._openpilot_slider.set_enabled(lambda: self.enabled) + self._openpilot_slider.set_enabled(lambda: self.enabled and not self.is_dismissing) self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False) - self._custom_software_slider.set_enabled(lambda: self.enabled) + self._custom_software_slider.set_enabled(lambda: self.enabled and not self.is_dismissing) + + def show_event(self): + super().show_event() + self._nav_bar._alpha = 0.0 + + def _update_state(self): + super()._update_state() + if self.is_dismissing: + self.reset() def reset(self): self._openpilot_slider.reset() @@ -367,11 +366,16 @@ class DownloadingPage(Widget): font_weight=FontWeight.ROMAN, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) self._progress = 0 + def show_event(self): + super().show_event() + self.set_progress(0) + def set_progress(self, progress: int): self._progress = progress self._progress_label.set_text(f"{progress}%") def _render(self, rect: rl.Rectangle): + rl.draw_rectangle_rec(rect, rl.BLACK) self._title_label.render(rl.Rectangle( rect.x + 12, rect.y + 2, @@ -387,9 +391,10 @@ class DownloadingPage(Widget): )) -class FailedPage(Widget): +class FailedPage(NavWidget): def __init__(self, reboot_callback: Callable, retry_callback: Callable, title: str = "download failed"): super().__init__() + self.set_back_callback(retry_callback) self._title_label = UnifiedLabel(title, 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY) @@ -406,6 +411,10 @@ class FailedPage(Widget): def set_reason(self, reason: str): self._reason_label.set_text(reason) + def show_event(self): + super().show_event() + self._reboot_slider.reset() + def _render(self, rect: rl.Rectangle): self._title_label.render(rl.Rectangle( rect.x + 8, @@ -437,10 +446,18 @@ class FailedPage(Widget): )) -class NetworkSetupPage(Widget): - def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Callable): +class NetworkSetupPage(NavWidget): + def __init__(self, network_monitor: NetworkConnectivityMonitor, continue_callback: Callable[[bool], None], + back_callback: Callable[[], None] | None): super().__init__() - self._wifi_ui = WifiUIMici(wifi_manager) + self.set_back_callback(back_callback) + + self._wifi_manager = WifiManager() + self._wifi_manager.set_active(True) + self._network_monitor = network_monitor + self._custom_software = False + self._prev_has_internet = False + self._wifi_ui = WifiUIMici(self._wifi_manager) self._no_wifi_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 58, 50) self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 58, 50) @@ -458,9 +475,27 @@ class NetworkSetupPage(Widget): self._continue_button = WidishRoundedButton("continue") self._continue_button.set_enabled(False) - self._continue_button.set_click_callback(continue_callback) + self._continue_button.set_click_callback(lambda: continue_callback(self._custom_software)) - def set_has_internet(self, has_internet: bool): + gui_app.add_nav_stack_tick(self._nav_stack_tick) + + def show_event(self): + super().show_event() + self._prev_has_internet = False + self._network_monitor.reset() + self._set_has_internet(False) + + def _nav_stack_tick(self): + self._wifi_manager.process_callbacks() + + has_internet = self._network_monitor.network_connected.is_set() + if has_internet != self._prev_has_internet: + self._set_has_internet(has_internet) + if has_internet: + gui_app.pop_widgets_to(self) + self._prev_has_internet = has_internet + + def _set_has_internet(self, has_internet: bool): if has_internet: self._network_header.set_title("connected to internet") self._network_header.set_icon(self._wifi_full_txt) @@ -470,6 +505,9 @@ class NetworkSetupPage(Widget): self._network_header.set_icon(self._no_wifi_txt) self._continue_button.set_enabled(False) + def set_custom_software(self, custom_software: bool): + self._custom_software = custom_software + def _render(self, _): self._network_header.render(rl.Rectangle( self._rect.x + 16, @@ -503,122 +541,55 @@ class NetworkSetupPage(Widget): class Setup(Widget): def __init__(self): super().__init__() - self.state = SetupState.GETTING_STARTED - self.failed_url = "" - self.failed_reason = "" self.download_url = "" self.download_progress = 0 self.download_thread = None - self._wifi_manager = WifiManager() - self._wifi_manager.set_active(True) + self._download_failed_reason: str | None = None + self._network_monitor = NetworkConnectivityMonitor() self._network_monitor.start() - self._prev_has_internet = False - gui_app.add_nav_stack_tick(self._nav_stack_tick) + + def getting_started_button_callback(): + self._software_selection_page.reset() + gui_app.push_widget(self._software_selection_page) self._start_page = StartPage() - self._start_page.set_click_callback(self._getting_started_button_callback) + self._start_page.set_click_callback(getting_started_button_callback) + self._start_page.set_enabled(lambda: self.enabled) # for nav stack - self._network_setup_page = NetworkSetupPage(self._wifi_manager, self._network_setup_continue_button_callback, - self._network_setup_back_button_callback) - # TODO: change these to touch_valid - self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack + self._network_setup_page = NetworkSetupPage(self._network_monitor, self._network_setup_continue_button_callback, + self._pop_to_software_selection) + self._software_selection_page = SoftwareSelectionPage(self._use_openpilot, lambda: gui_app.push_widget(self._custom_software_warning_page)) - self._software_selection_page = SoftwareSelectionPage(self._software_selection_continue_button_callback, - self._software_selection_custom_software_button_callback) - self._software_selection_page.set_enabled(lambda: self.enabled) # for nav stack + self._download_failed_page = FailedPage(HARDWARE.reboot, self._pop_to_software_selection) - self._download_failed_page = FailedPage(HARDWARE.reboot, self._download_failed_startover_button_callback) - self._download_failed_page.set_enabled(lambda: self.enabled) # for nav stack - - self._custom_software_warning_page = CustomSoftwareWarningPage(self._software_selection_custom_software_continue, - self._custom_software_warning_back_button_callback) - self._custom_software_warning_page.set_enabled(lambda: self.enabled) # for nav stack + self._custom_software_warning_page = CustomSoftwareWarningPage(self._software_selection_custom_software_continue, self._pop_to_software_selection) self._downloading_page = DownloadingPage() + gui_app.add_nav_stack_tick(self._nav_stack_tick) + def _nav_stack_tick(self): - has_internet = self._network_monitor.network_connected.is_set() - if has_internet and not self._prev_has_internet: - gui_app.pop_widgets_to(self) - self._prev_has_internet = has_internet + self._downloading_page.set_progress(self.download_progress) - def _update_state(self): - self._wifi_manager.process_callbacks() - - def _set_state(self, state: SetupState): - self.state = state - if self.state == SetupState.SOFTWARE_SELECTION: - self._software_selection_page.reset() - elif self.state == SetupState.CUSTOM_SOFTWARE_WARNING: - self._custom_software_warning_page.reset() - - if self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE): - self._network_setup_page.show_event() - self._network_monitor.reset() - else: - self._network_setup_page.hide_event() + if self._download_failed_reason is not None: + reason = self._download_failed_reason + self._download_failed_reason = None + self._download_failed_page.set_reason(reason) + gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders + gui_app.push_widget(self._download_failed_page) def _render(self, rect: rl.Rectangle): - if self.state == SetupState.GETTING_STARTED: - self._start_page.render(rect) - elif self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE): - self.render_network_setup(rect) - elif self.state == SetupState.SOFTWARE_SELECTION: - self._software_selection_page.render(rect) - elif self.state == SetupState.CUSTOM_SOFTWARE_WARNING: - self._custom_software_warning_page.render(rect) - elif self.state == SetupState.DOWNLOADING: - self.render_downloading(rect) - elif self.state == SetupState.DOWNLOAD_FAILED: - self._download_failed_page.render(rect) - - def _custom_software_warning_back_button_callback(self): - self._set_state(SetupState.SOFTWARE_SELECTION) - - def _getting_started_button_callback(self): - self._set_state(SetupState.SOFTWARE_SELECTION) - - def _software_selection_continue_button_callback(self): - self.use_openpilot() - - def _software_selection_custom_software_button_callback(self): - self._set_state(SetupState.CUSTOM_SOFTWARE_WARNING) - - def _software_selection_custom_software_continue(self): - self._set_state(SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE) - - def _download_failed_startover_button_callback(self): - self._set_state(SetupState.GETTING_STARTED) - - def _network_setup_back_button_callback(self): - self._set_state(SetupState.SOFTWARE_SELECTION) - - def _network_setup_continue_button_callback(self): - if self.state == SetupState.NETWORK_SETUP: - self.download(OPENPILOT_URL) - elif self.state == SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE: - def handle_keyboard_result(text): - url = text.strip() - if url: - self.download(url) - - keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) - gui_app.push_widget(keyboard) + self._start_page.render(rect) def close(self): self._network_monitor.stop() - def render_network_setup(self, rect: rl.Rectangle): - has_internet = self._network_monitor.network_connected.is_set() - self._network_setup_page.set_has_internet(has_internet) - self._network_setup_page.render(rect) + def _pop_to_software_selection(self): + # reset sliders after dismiss completes + gui_app.pop_widgets_to(self._software_selection_page, self._software_selection_page.reset) - def render_downloading(self, rect: rl.Rectangle): - self._downloading_page.set_progress(self.download_progress) - self._downloading_page.render(rect) - - def use_openpilot(self): + def _use_openpilot(self): if os.path.isdir(INSTALL_PATH) and os.path.isfile(VALID_CACHE_PATH): os.remove(VALID_CACHE_PATH) with open(TMP_CONTINUE_PATH, "w") as f: @@ -631,17 +602,40 @@ class Setup(Widget): time.sleep(0.1) gui_app.request_close() else: - self._set_state(SetupState.NETWORK_SETUP) + self._push_network_setup(custom_software=False) - def download(self, url: str): + def _push_network_setup(self, custom_software: bool): + self._network_setup_page.set_custom_software(custom_software) + gui_app.push_widget(self._network_setup_page) + + def _software_selection_custom_software_continue(self): + gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders + self._push_network_setup(custom_software=True) + + def _network_setup_continue_button_callback(self, custom_software): + if not custom_software: + gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders + self._download(OPENPILOT_URL) + else: + def handle_keyboard_result(text): + url = text.strip() + if url: + gui_app.pop_widgets_to(self._software_selection_page, instant=True) # don't reset sliders + self._download(url) + + keyboard = BigInputDialog("custom software URL", "openpilot.comma.ai", confirm_callback=handle_keyboard_result) + gui_app.push_widget(keyboard) + + def _download(self, url: str): # autocomplete incomplete URLs if re.match("^([^/.]+)/([^/]+)$", url): url = f"https://installer.comma.ai/{url}" parsed = urlparse(url, scheme='https') self.download_url = (urlparse(f"https://{url}") if not parsed.netloc else parsed).geturl() + self.download_progress = 0 - self._set_state(SetupState.DOWNLOADING) + gui_app.push_widget(self._downloading_page) self.download_thread = threading.Thread(target=self._download_thread, daemon=True) self.download_thread.start() @@ -672,7 +666,6 @@ class Setup(Widget): if total_size: self.download_progress = int(downloaded * 100 / total_size) - self._downloading_page.set_progress(self.download_progress) is_elf = False with open(tmpfile, 'rb') as f: @@ -680,7 +673,7 @@ class Setup(Widget): is_elf = header == b'\x7fELF' if not is_elf: - self.download_failed(self.download_url, "No custom software found at this URL.") + self._download_failed_reason = "No custom software found at this URL." return # AGNOS might try to execute the installer before this process exits. @@ -697,17 +690,9 @@ class Setup(Widget): except urllib.error.HTTPError as e: if e.code == 409: - error_msg = "Incompatible openpilot version" - self.download_failed(self.download_url, error_msg) + self._download_failed_reason = "Incompatible openpilot version" except Exception: - error_msg = "Invalid URL" - self.download_failed(self.download_url, error_msg) - - def download_failed(self, url: str, reason: str): - self.failed_url = url - self.failed_reason = reason - self._download_failed_page.set_reason(reason) - self._set_state(SetupState.DOWNLOAD_FAILED) + self._download_failed_reason = "Invalid URL" def main(): diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index fe17f12a8..67203d53f 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -63,7 +63,8 @@ class NavWidget(Widget, abc.ABC): self._playing_dismiss_animation = False # released and animating away self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) - self._dismiss_callback: Callable[[], None] | None = None + self._back_callback: Callable[[], None] | None = None # persistent callback for any back navigation + self._dismiss_callback: Callable[[], None] | None = None # transient callback for programmatic dismiss # TODO: move this state into NavBar self._nav_bar = NavBar() @@ -75,6 +76,9 @@ class NavWidget(Widget, abc.ABC): # the top of a vertical scroll panel to prevent erroneous swipes return True + def set_back_callback(self, callback: Callable[[], None]) -> None: + self._back_callback = callback + def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: super()._handle_mouse_event(mouse_event) @@ -145,6 +149,10 @@ class NavWidget(Widget, abc.ABC): if new_y > self._rect.height + DISMISS_PUSH_OFFSET - 10: gui_app.pop_widget() + + if self._back_callback is not None: + self._back_callback() + if self._dismiss_callback is not None: self._dismiss_callback() self._dismiss_callback = None