diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index 143bb2c262..32d278ee9b 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -4,6 +4,9 @@ from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.list_view import toggle_item from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets import DialogResult # Description constants DESCRIPTIONS = { @@ -80,6 +83,9 @@ class DeveloperLayout(Widget): self._scroller = Scroller(items, line_separator=True, spacing=0) + # Toggles should be not available to change in onroad state + ui_state.add_offroad_transition_callback(self._update_toggles) + def _render(self, rect): self._scroller.render(rect) @@ -87,7 +93,10 @@ class DeveloperLayout(Widget): self._update_toggles() def _update_toggles(self): + ui_state.update_params() + # Hide non-release toggles on release builds + # TODO: we can do an onroad cycle, but alpha long toggle requires a deinit function to re-enable radar and not fault for item in (self._adb_toggle, self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle): item.set_visible(not self._is_release) @@ -100,7 +109,11 @@ class DeveloperLayout(Widget): else: self._alpha_long_toggle.set_visible(True) - self._long_maneuver_toggle.action_item.set_enabled(ui_state.has_longitudinal_control and ui_state.is_offroad) + long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad() + self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled) + if not long_man_enabled: + self._long_maneuver_toggle.action_item.set_state(False) + self._params.put_bool("LongitudinalManeuverMode", False) else: self._long_maneuver_toggle.action_item.set_enabled(False) self._alpha_long_toggle.set_visible(False) @@ -116,12 +129,6 @@ class DeveloperLayout(Widget): ): item.action_item.set_state(self._params.get_bool(key)) - def _update_state(self): - # Disable toggles that require onroad restart - # TODO: we can do an onroad cycle, but alpha long toggle requires a deinit function to re-enable radar and not fault - for item in (self._adb_toggle, self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle): - item.action_item.set_enabled(ui_state.is_offroad) - def _on_enable_adb(self, state: bool): self._params.put_bool("AdbEnabled", state) @@ -139,5 +146,21 @@ class DeveloperLayout(Widget): self._joystick_toggle.action_item.set_state(False) def _on_alpha_long_enabled(self, state: bool): + if state: + def confirm_callback(result: int): + if result == DialogResult.CONFIRM: + self._params.put_bool("AlphaLongitudinalEnabled", True) + self._update_toggles() + else: + self._alpha_long_toggle.action_item.set_state(False) + + # show confirmation dialog + content = (f"

{self._alpha_long_toggle.title}


" + + f"

{self._alpha_long_toggle.description}

") + + dlg = ConfirmDialog(content, "Enable", rich=True) + gui_app.set_modal_overlay(dlg, callback=confirm_callback) + return + self._params.put_bool("AlphaLongitudinalEnabled", state) self._update_toggles() diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index fddcf32fbd..8fdbd6f9a6 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -3,6 +3,9 @@ from openpilot.common.params import Params, UnknownKeyName from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets import DialogResult from openpilot.selfdrive.ui.ui_state import ui_state PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants @@ -131,6 +134,8 @@ class TogglesLayout(Widget): self._update_experimental_mode_icon() self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0) + ui_state.add_engaged_transition_callback(self._update_toggles) + def _update_state(self): if ui_state.sm.updated["selfdriveState"]: personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality] @@ -138,15 +143,12 @@ class TogglesLayout(Widget): self._long_personality_setting.action_item.set_selected_button(personality) ui_state.personality = personality - # these toggles need restart, block while engaged - for toggle_def in self._toggle_defs: - if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles: - self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged) - def show_event(self): self._update_toggles() def _update_toggles(self): + ui_state.update_params() + e2e_description = ( "openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " + "Experimental features are listed below:
" + @@ -174,7 +176,7 @@ class TogglesLayout(Widget): unavailable = "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." long_desc = unavailable + " openpilot longitudinal control may come in a future update." - if ui_state.CP.getAlphaLongitudinalAvailable(): + if ui_state.CP.alphaLongitudinalAvailable: if self._is_release: long_desc = unavailable + " " + ("An alpha version of openpilot longitudinal control can be tested, along with " + "Experimental mode, on non-release branches.") @@ -192,6 +194,11 @@ class TogglesLayout(Widget): for param in self._toggle_defs: self._toggles[param].action_item.set_state(self._params.get_bool(param)) + # these toggles need restart, block while engaged + for toggle_def in self._toggle_defs: + if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles: + self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged) + def _render(self, rect): self._scroller.render(rect) @@ -199,9 +206,30 @@ class TogglesLayout(Widget): icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png" self._toggles["ExperimentalMode"].set_icon(icon) + def _handle_experimental_mode_toggle(self, state: bool): + confirmed = self._params.get_bool("ExperimentalModeConfirmed") + if state and not confirmed: + def confirm_callback(result: int): + if result == DialogResult.CONFIRM: + self._params.put_bool("ExperimentalMode", True) + self._params.put_bool("ExperimentalModeConfirmed", True) + else: + self._toggles["ExperimentalMode"].action_item.set_state(False) + self._update_experimental_mode_icon() + + # show confirmation dialog + content = (f"

{self._toggles['ExperimentalMode'].title}


" + + f"

{self._toggles['ExperimentalMode'].description}

") + dlg = ConfirmDialog(content, "Enable", rich=True) + gui_app.set_modal_overlay(dlg, callback=confirm_callback) + else: + self._update_experimental_mode_icon() + self._params.put_bool("ExperimentalMode", state) + def _toggle_callback(self, state: bool, param: str): if param == "ExperimentalMode": - self._update_experimental_mode_icon() + self._handle_experimental_mode_toggle(state) + return self._params.put_bool(param, state) if self._toggle_defs[param][3]: diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index be83517980..f4b9e1a9be 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -74,7 +74,17 @@ class UIState: self.light_sensor: float = -1.0 self._param_update_time: float = 0.0 - self._update_params() + # Callbacks + self._offroad_transition_callbacks: list[Callable[[], None]] = [] + self._engaged_transition_callbacks: list[Callable[[], None]] = [] + + self.update_params() + + def add_offroad_transition_callback(self, callback: Callable[[], None]): + self._offroad_transition_callbacks.append(callback) + + def add_engaged_transition_callback(self, callback: Callable[[], None]): + self._engaged_transition_callbacks.append(callback) @property def engaged(self) -> bool: @@ -91,8 +101,7 @@ class UIState: self._update_state() self._update_status() if time.monotonic() - self._param_update_time > 5.0: - self._update_params() - self._param_update_time = time.monotonic() + self.update_params() device.update() def _update_state(self) -> None: @@ -131,6 +140,8 @@ class UIState: # Check for engagement state changes if self.engaged != self._engaged_prev: + for callback in self._engaged_transition_callbacks: + callback() self._engaged_prev = self.engaged # Handle onroad/offroad transition @@ -140,19 +151,23 @@ class UIState: self.started_frame = self.sm.frame self.started_time = time.monotonic() + for callback in self._offroad_transition_callbacks: + callback() + self._started_prev = self.started - def _update_params(self) -> None: + def update_params(self) -> None: self.is_metric = self.params.get_bool("IsMetric") # Update longitudinal control state - CP_bytes = self.params.get("CarParams") + CP_bytes = self.params.get("CarParamsPersistent") if CP_bytes is not None: self.CP = messaging.log_from_bytes(CP_bytes, car.CarParams) if self.CP.alphaLongitudinalAvailable: self.has_longitudinal_control = self.params.get_bool("AlphaLongitudinalEnabled") else: self.has_longitudinal_control = self.CP.openpilotLongitudinalControl + self._param_update_time = time.monotonic() class Device: diff --git a/system/ui/widgets/confirm_dialog.py b/system/ui/widgets/confirm_dialog.py index e9d0cb53bd..789ae53f3d 100644 --- a/system/ui/widgets/confirm_dialog.py +++ b/system/ui/widgets/confirm_dialog.py @@ -3,7 +3,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.label import Label -from openpilot.system.ui.widgets.html_render import HtmlRenderer +from openpilot.system.ui.widgets.html_render import HtmlRenderer, ElementType from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.scroller import Scroller @@ -19,7 +19,7 @@ class ConfirmDialog(Widget): def __init__(self, text: str, confirm_text: str, cancel_text: str = "Cancel", rich: bool = False): super().__init__() self._label = Label(text, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255)) - self._html_renderer = HtmlRenderer(text=text) + self._html_renderer = HtmlRenderer(text=text, text_size={ElementType.P: 50}) self._cancel_button = Button(cancel_text, self._cancel_button_callback) self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY) self._rich = rich @@ -64,7 +64,9 @@ class ConfirmDialog(Widget): if not self._rich: self._label.render(text_rect) else: - self._html_renderer.set_rect(text_rect) + html_rect = rl.Rectangle(text_rect.x, text_rect.y, text_rect.width, + self._html_renderer.get_total_height(int(text_rect.width))) + self._html_renderer.set_rect(html_rect) self._scroller.render(text_rect) if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER): diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py index 8d713ee7ea..938867f748 100644 --- a/system/ui/widgets/html_render.py +++ b/system/ui/widgets/html_render.py @@ -121,6 +121,8 @@ class HtmlRenderer(Widget): is_start_tag, is_end_tag, tag = is_tag(token) if tag is not None: if tag == ElementType.BR: + # Close current tag and add a line break + close_tag() self._add_element(ElementType.BR, "") elif is_start_tag or is_end_tag: