diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py index 9439c588d3..671174ac7a 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py @@ -4,27 +4,190 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from openpilot.common.params import Params -from openpilot.system.ui.widgets.scroller_tici import Scroller +from enum import IntEnum + +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_settings import SpeedLimitSettingsLayout +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr, tr_noop +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, simple_button_item_sp from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.scroller_tici import Scroller + + +class PanelType(IntEnum): + CRUISE = 0 + SLA = 1 + + +ICBM_DESC = tr_noop("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons " + + "by emulating button presses for limited longitudinal control.") +ICMB_UNAVAILABLE = tr_noop("Intelligent Cruise Button Management is currently unavailable on this platform.") +ICMB_UNAVAILABLE_LONG_AVAILABLE = tr_noop("Disable the sunnypilot Longitudinal Control (alpha) toggle to allow Intelligent Cruise Button Management.") +ICMB_UNAVAILABLE_LONG_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control is the default longitudinal control for this platform.") + +ACC_ENABLED_DESCRIPTION = tr_noop("Enable custom Short & Long press increments for cruise speed increase/decrease.") +ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot longitudinal control enabled.") +ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.") +ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.") class CruiseLayout(Widget): def __init__(self): super().__init__() + self._current_panel = PanelType.CRUISE + self._speed_limit_layout = SpeedLimitSettingsLayout(lambda: self._set_current_panel(PanelType.CRUISE)) - self._params = Params() items = self._initialize_items() self._scroller = Scroller(items, line_separator=True, spacing=0) def _initialize_items(self): + self.icbm_toggle = toggle_item_sp( + title=tr("Intelligent Cruise Button Management (ICBM) (Alpha)"), + description="", + param="IntelligentCruiseButtonManagement") + + self.scc_v_toggle = toggle_item_sp( + title=tr("Smart Cruise Control - Vision"), + description=tr("Use vision path predictions to estimate the appropriate speed to drive through turns ahead."), + param="SmartCruiseControlVision") + + self.scc_m_toggle = toggle_item_sp( + title=tr("Smart Cruise Control - Map"), + description=tr("Use map data to estimate the appropriate speed to drive through turns ahead."), + param="SmartCruiseControlMap") + + self.custom_acc_toggle = toggle_item_sp( + title=tr("Custom ACC Speed Increments"), + description="", + param="CustomAccIncrementsEnabled", + callback=self._on_custom_acc_toggle) + + self.custom_acc_short_increment = option_item_sp( + title=tr("Short Press Increment"), + param="CustomAccShortPressIncrement", + min_value=1, max_value=10, value_change_step=1, + inline=True) + + self.custom_acc_long_increment = option_item_sp( + title=tr("Long Press Increment"), + param="CustomAccLongPressIncrement", + value_map={1: 1, 2: 5, 3: 10}, + min_value=1, max_value=3, value_change_step=1, + inline=True) + + self.sla_settings_button = simple_button_item_sp( + button_text=lambda: tr("Speed Limit"), + button_width=800, + callback=lambda: self._set_current_panel(PanelType.SLA) + ) + + self.dec_toggle = toggle_item_sp( + title=tr("Enable Dynamic Experimental Control"), + description=tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."), + param="DynamicExperimentalControl") + items = [ + self.icbm_toggle, + self.dec_toggle, + self.scc_v_toggle, + self.scc_m_toggle, + self.custom_acc_toggle, + self.custom_acc_short_increment, + self.custom_acc_long_increment, + self.sla_settings_button, ] return items def _render(self, rect): - self._scroller.render(rect) + if self._current_panel == PanelType.SLA: + self._speed_limit_layout.render(rect) + else: + self._scroller.render(rect) def show_event(self): + self._set_current_panel(PanelType.CRUISE) self._scroller.show_event() + self.icbm_toggle.show_description(True) + self.custom_acc_toggle.show_description(True) + + def _set_current_panel(self, panel: PanelType): + self._current_panel = panel + if panel == PanelType.SLA: + self._speed_limit_layout.show_event() + + def _update_state(self): + super()._update_state() + + if ui_state.CP is not None and ui_state.CP_SP is not None: + has_icbm = ui_state.has_icbm + has_long = ui_state.has_longitudinal_control + + if ui_state.CP_SP.intelligentCruiseButtonManagementAvailable and not has_long: + self.icbm_toggle.action_item.set_enabled(ui_state.is_offroad()) + self.icbm_toggle.set_description(tr(ICBM_DESC)) + else: + ui_state.params.remove("IntelligentCruiseButtonManagement") + self.icbm_toggle.action_item.set_enabled(False) + + long_desc = ICMB_UNAVAILABLE + if has_long: + if ui_state.CP.alphaLongitudinalAvailable: + long_desc += " " + ICMB_UNAVAILABLE_LONG_AVAILABLE + else: + long_desc += " " + ICMB_UNAVAILABLE_LONG_UNAVAILABLE + + new_desc = "" + tr(long_desc) + "\n\n" + tr(ICBM_DESC) + if self.icbm_toggle.description != new_desc: + self.icbm_toggle.set_description(new_desc) + self.icbm_toggle.show_description(True) + + if has_long or has_icbm: + self.custom_acc_toggle.action_item.set_enabled(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad()) + self.dec_toggle.action_item.set_enabled(has_long) + self.scc_v_toggle.action_item.set_enabled(True) + self.scc_m_toggle.action_item.set_enabled(True) + else: + ui_state.params.remove("CustomAccIncrementsEnabled") + ui_state.params.remove("DynamicExperimentalControl") + ui_state.params.remove("SmartCruiseControlVision") + ui_state.params.remove("SmartCruiseControlMap") + self.custom_acc_toggle.action_item.set_enabled(False) + self.dec_toggle.action_item.set_enabled(False) + self.scc_v_toggle.action_item.set_enabled(False) + self.scc_m_toggle.action_item.set_enabled(False) + + else: + has_icbm = has_long = False + self.icbm_toggle.action_item.set_enabled(False) + self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION)) + + show_custom_acc_desc = False + + if ui_state.is_offroad(): + new_custom_acc_desc = tr(ONROAD_ONLY_DESCRIPTION) + show_custom_acc_desc = True + else: + if has_long or has_icbm: + if has_long and ui_state.CP.pcmCruise: + new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION) + show_custom_acc_desc = True + else: + new_custom_acc_desc = tr(ACC_ENABLED_DESCRIPTION) + else: + new_custom_acc_desc = tr(ACC_NOLONG_DESCRIPTION) + show_custom_acc_desc = True + self.custom_acc_toggle.action_item.set_state(False) + + if self.custom_acc_toggle.description != new_custom_acc_desc: + self.custom_acc_toggle.set_description(new_custom_acc_desc) + if show_custom_acc_desc: + self.custom_acc_toggle.show_description(True) + + self._on_custom_acc_toggle(self.custom_acc_toggle.action_item.get_state()) + + def _on_custom_acc_toggle(self, state): + self.custom_acc_short_increment.set_visible(state) + self.custom_acc_long_increment.set_visible(state) + self.custom_acc_short_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled) + self.custom_acc_long_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py new file mode 100644 index 0000000000..e6846744a2 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py @@ -0,0 +1,65 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from collections.abc import Callable + +import pyray as rl +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp +from openpilot.system.ui.widgets.network import NavButton +from openpilot.system.ui.widgets.scroller_tici import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description + +SPEED_LIMIT_POLICY_BUTTONS = [tr("Car Only"), tr("Map Only"), tr("Car First"), tr("Map First"), tr("Combined")] + +SPEED_LIMIT_POLICY_DESCRIPTIONS = [ + tr("Car Only: Use Speed Limit data only from Car"), + tr("Map Only: Use Speed Limit data only from OpenStreetMaps"), + tr("Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps"), + tr("Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car"), + tr("Combined: Use combined Speed Limit data from Car & OpenStreetMaps") +] + + +class SpeedLimitPolicyLayout(Widget): + def __init__(self, back_btn_callback: Callable): + super().__init__() + self._back_button = NavButton(tr("Back")) + self._back_button.set_click_callback(back_btn_callback) + + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=False, spacing=0) + + def _initialize_items(self): + self._speed_limit_policy = multiple_button_item_sp( + title=lambda: tr("Speed Limit Source"), + description=self._get_policy_description, + buttons=SPEED_LIMIT_POLICY_BUTTONS, + param="SpeedLimitPolicy", + button_width=250, + ) + + items = [ + self._speed_limit_policy + ] + return items + + @staticmethod + def _get_policy_description(): + return get_highlighted_description(ui_state.params, "SpeedLimitPolicy", SPEED_LIMIT_POLICY_DESCRIPTIONS) + + def _render(self, rect): + self._back_button.set_position(self._rect.x, self._rect.y + 20) + self._back_button.render() + + content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40) + self._scroller.render(content_rect) + + def show_event(self): + self._scroller.show_event() + self._speed_limit_policy.show_description(True) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py new file mode 100644 index 0000000000..c14330d9ac --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py @@ -0,0 +1,178 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from collections.abc import Callable +from enum import IntEnum + +import pyray as rl +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_policy import SpeedLimitPolicyLayout +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import OffsetType as SpeedLimitOffsetType +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description +from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, option_item_sp, simple_button_item_sp, LineSeparatorSP +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.network import NavButton +from openpilot.system.ui.widgets.scroller_tici import Scroller + +SPEED_LIMIT_MODE_BUTTONS = [tr("Off"), tr("Info"), tr("Warning"), tr("Assist")] +SPEED_LIMIT_OFFSET_TYPE_BUTTONS = [tr("None"), tr("Fixed"), tr("%")] + +SPEED_LIMIT_MODE_DESCRIPTIONS = [ + tr("Off: Disables the Speed Limit functions."), + tr("Information: Displays the current road's speed limit."), + tr("Warning: Provides a warning when exceeding the current road's speed limit."), + tr("Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons."), +] + +SPEED_LIMIT_OFFSET_DESCRIPTIONS = [ + tr("None: No Offset"), + tr("Fixed: Adds a fixed offset [Speed Limit + Offset]"), + tr("Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]"), +] + + +class PanelType(IntEnum): + SETTINGS = 0 + POLICY = 1 + + +class SpeedLimitSettingsLayout(Widget): + def __init__(self, back_btn_callback: Callable): + super().__init__() + self._current_panel = PanelType.SETTINGS + + self._back_button = NavButton(tr("Back")) + self._back_button.set_click_callback(back_btn_callback) + + self._policy_layout = SpeedLimitPolicyLayout(lambda: self._set_current_panel(PanelType.SETTINGS)) + + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=False, spacing=0) + + def _initialize_items(self): + self._speed_limit_mode = multiple_button_item_sp( + title=lambda: tr("Speed Limit"), + description=self._get_mode_description, + buttons=SPEED_LIMIT_MODE_BUTTONS, + param="SpeedLimitMode", + button_width=380, + ) + + self._source_button = simple_button_item_sp( + button_text=lambda: tr("Customize Source"), + button_width=720, + callback=lambda: self._set_current_panel(PanelType.POLICY) + ) + + self._speed_limit_offset_type = multiple_button_item_sp( + title=lambda: tr("Speed Limit Offset"), + description="", + buttons=SPEED_LIMIT_OFFSET_TYPE_BUTTONS, + param="SpeedLimitOffsetType", + button_width=450, + ) + + self._speed_limit_value_offset = option_item_sp( + title="", + param="SpeedLimitValueOffset", + min_value=-30, + max_value=30, + description=self._get_offset_description, + label_callback=self._get_offset_label, + ) + + items = [ + self._speed_limit_mode, + LineSeparatorSP(40), + self._source_button, + LineSeparatorSP(40), + self._speed_limit_offset_type, + self._speed_limit_value_offset + ] + return items + + def _set_current_panel(self, panel: PanelType): + self._current_panel = panel + if panel == PanelType.POLICY: + self._policy_layout.show_event() + + @staticmethod + def _get_mode_description(): + return get_highlighted_description(ui_state.params, "SpeedLimitMode", SPEED_LIMIT_MODE_DESCRIPTIONS) + + @staticmethod + def _get_offset_description(): + return get_highlighted_description(ui_state.params, "SpeedLimitOffsetType", SPEED_LIMIT_OFFSET_DESCRIPTIONS) + + @staticmethod + def _get_offset_label(value): + offset_type = int(ui_state.params.get("SpeedLimitOffsetType", return_default=True)) + unit = tr("km/h") if ui_state.is_metric else tr("mph") + + if offset_type == int(SpeedLimitOffsetType.percentage): + return f"{value}%" + elif offset_type == int(SpeedLimitOffsetType.fixed): + return f"{value} {unit}" + return str(value) + + def _update_state(self): + super()._update_state() + + speed_limit_mode_param = ui_state.params.get("SpeedLimitMode", return_default=True) + if ui_state.CP is not None and ui_state.CP_SP is not None: + brand = ui_state.CP.brand + has_long = ui_state.has_longitudinal_control + has_icbm = ui_state.has_icbm + + """ + Speed Limit Assist is available when: + - has_long or has_icbm, and + - is not a release branch or not a disallowed brand, and + - is not always disallwed + """ + sla_disallow_in_release = brand == "tesla" and ui_state.is_sp_release + sla_always_disallow = brand == "rivian" + sla_available = (has_long or has_icbm) and not sla_disallow_in_release and not sla_always_disallow + + if not sla_available and speed_limit_mode_param == int(SpeedLimitMode.assist): + ui_state.params.put("SpeedLimitMode", int(SpeedLimitMode.warning)) + + else: + sla_available = False + + if not sla_available: + self._speed_limit_mode.action_item.set_enabled_buttons({ + int(SpeedLimitMode.off), + int(SpeedLimitMode.information), + int(SpeedLimitMode.warning), + }) + else: + self._speed_limit_mode.action_item.set_enabled_buttons(None) + + offset_type = ui_state.params.get("SpeedLimitOffsetType", return_default=True) + self._speed_limit_value_offset.set_visible(offset_type != int(SpeedLimitOffsetType.off)) + + def _render(self, rect): + if self._current_panel == PanelType.POLICY: + self._policy_layout.render(rect) + return + + self._back_button.set_position(self._rect.x, self._rect.y + 20) + self._back_button.render() + + content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40) + self._scroller.render(content_rect) + + def show_event(self): + self._current_panel = PanelType.SETTINGS + self._scroller.show_event() + self._speed_limit_mode.show_description(True) + + def hide_event(self): + self._current_panel = PanelType.SETTINGS + self._scroller.hide_event() diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 3699098121..5a2f722fec 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -39,6 +39,9 @@ class UIStateSP: self.onroad_brightness_timer: int = 0 self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True) self.reset_onroad_sleep_timer() + self.CP_SP: custom.CarParamsSP | None = None + self.has_icbm: bool = False + self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch") def update(self) -> None: if self.sunnylink_enabled: @@ -121,6 +124,7 @@ class UIStateSP: CP_SP_bytes = self.params.get("CarParamsSPPersistent") if CP_SP_bytes is not None: self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP) + self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement") self.active_bundle = self.params.get("ModelManager_ActiveBundle") self.blindspot = self.params.get_bool("BlindSpot") self.chevron_metrics = self.params.get("ChevronInfo") diff --git a/system/ui/sunnypilot/widgets/__init__.py b/system/ui/sunnypilot/widgets/__init__.py index e69de29bb2..96865e6042 100644 --- a/system/ui/sunnypilot/widgets/__init__.py +++ b/system/ui/sunnypilot/widgets/__init__.py @@ -0,0 +1,18 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + + +def get_highlighted_description(params, param_name: str, descriptions: list[str]) -> str: + index = int(params.get(param_name, return_default=True)) + lines = [] + for i, desc in enumerate(descriptions): + if i == index: + lines.append(f"{desc}") + else: + lines.append(f"{desc}") + + return "
".join(lines)