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
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user