diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering.py b/selfdrive/ui/sunnypilot/layouts/settings/steering.py index ac67eb916..f82c4097c 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering.py @@ -4,27 +4,147 @@ 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 cereal import car +from enum import IntEnum + +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 toggle_item_sp, simple_button_item_sp, option_item_sp, LineSeparatorSP from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering_sub_layouts.lane_change_settings import LaneChangeSettingsLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering_sub_layouts.mads_settings import MadsSettingsLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering_sub_layouts.torque_settings import TorqueSettingsLayout + + +class PanelType(IntEnum): + STEERING = 0 + MADS = 1 + LANE_CHANGE = 2 + TORQUE_CONTROL = 3 class SteeringLayout(Widget): def __init__(self): super().__init__() - self._params = Params() + self._current_panel = PanelType.STEERING + self._lane_change_settings_layout = LaneChangeSettingsLayout(lambda: self._set_current_panel(PanelType.STEERING)) + self._mads_settings_layout = MadsSettingsLayout(lambda: self._set_current_panel(PanelType.STEERING)) + self._torque_control_layout = TorqueSettingsLayout(lambda: self._set_current_panel(PanelType.STEERING)) + items = self._initialize_items() - self._scroller = Scroller(items, line_separator=True, spacing=0) + self._scroller = Scroller(items, line_separator=False, spacing=0) def _initialize_items(self): - items = [ + self._mads_base_desc = tr("Enable the beloved MADS feature. " + + "Disable toggle to revert back to stock sunnypilot engagement/disengagement.") + self._mads_limited_desc = tr("This platform supports limited MADS settings.") + self._mads_full_desc = tr("This platform supports all MADS settings.") + self._mads_check_compat_desc = tr("Start the vehicle to check vehicle compatibility.") + self._mads_toggle = toggle_item_sp( + param="Mads", + title=lambda: tr("Modular Assistive Driving System (MADS)"), + description=self._mads_base_desc, + ) + self._mads_settings_button = simple_button_item_sp( + button_text=lambda: tr("Customize MADS"), + button_width=800, + callback=lambda: self._set_current_panel(PanelType.MADS) + ) + self._lane_change_settings_button = simple_button_item_sp( + button_text=lambda: tr("Customize Lane Change"), + button_width=800, + callback=lambda: self._set_current_panel(PanelType.LANE_CHANGE) + ) + self._blinker_control_toggle = toggle_item_sp( + param="BlinkerPauseLateralControl", + description=lambda: tr("Pause lateral control with blinker when traveling below the desired speed selected."), + title=lambda: tr("Pause Lateral Control with Blinker"), + ) + self._blinker_control_options = option_item_sp( + param="BlinkerMinLateralControlSpeed", + title=lambda: tr("Minimum Speed to Pause Lateral Control"), + min_value=0, + max_value=255, + value_change_step=5, + description="", + label_callback=lambda speed: f'{speed} {"km/h" if ui_state.is_metric else "mph"}', + ) + self._torque_control_toggle = toggle_item_sp( + param="EnforceTorqueControl", + title=lambda: tr("Enforce Torque Lateral Control"), + description=lambda: tr("Enable this to enforce sunnypilot to steer with Torque lateral control."), + ) + self._torque_customization_button = simple_button_item_sp( + button_text=lambda: tr("Customize Torque Params"), + button_width=850, + callback=lambda: self._set_current_panel(PanelType.TORQUE_CONTROL) + ) + self._nnlc_toggle = toggle_item_sp( + param="NeuralNetworkLateralControl", + title=lambda: tr("Neural Network Lateral Control (NNLC)"), + description="" + ) + + items = [ + self._mads_toggle, + self._mads_settings_button, + LineSeparatorSP(40), + self._lane_change_settings_button, + LineSeparatorSP(40), + self._blinker_control_toggle, + self._blinker_control_options, + LineSeparatorSP(40), + self._torque_control_toggle, + self._torque_customization_button, + LineSeparatorSP(40), + self._nnlc_toggle, ] return items + def _set_current_panel(self, panel: PanelType): + self._current_panel = panel + + def _update_state(self): + super()._update_state() + + torque_allowed = True + if ui_state.CP is not None: + mads_main_desc = self._mads_limited_desc if self._mads_settings_layout._mads_limited_settings() else self._mads_full_desc + self._mads_toggle.set_description(f"{mads_main_desc}

{self._mads_base_desc}") + + if ui_state.CP.steerControlType == car.CarParams.SteerControlType.angle: + ui_state.params.remove("EnforceTorqueControl") + ui_state.params.remove("NeuralNetworkLateralControl") + torque_allowed = False + else: + self._mads_toggle.set_description(f"{self._mads_check_compat_desc}

{self._mads_base_desc}") + ui_state.params.remove("EnforceTorqueControl") + ui_state.params.remove("NeuralNetworkLateralControl") + torque_allowed = False + + self._mads_toggle.action_item.set_enabled(ui_state.is_offroad()) + self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state()) + self._blinker_control_options.set_visible(self._blinker_control_toggle.action_item.get_state()) + + enforce_torque_enabled = self._torque_control_toggle.action_item.get_state() + nnlc_enabled = self._nnlc_toggle.action_item.get_state() + self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled) + self._torque_control_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not nnlc_enabled) + self._torque_customization_button.action_item.set_enabled(self._torque_control_toggle.action_item.get_state()) + def _render(self, rect): - self._scroller.render(rect) + if self._current_panel == PanelType.LANE_CHANGE: + self._lane_change_settings_layout.render(rect) + elif self._current_panel == PanelType.MADS: + self._mads_settings_layout.render(rect) + elif self._current_panel == PanelType.TORQUE_CONTROL: + self._torque_control_layout.render(rect) + else: + self._scroller.render(rect) def show_event(self): + self._set_current_panel(PanelType.STEERING) self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py new file mode 100644 index 000000000..9a5d5202a --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py @@ -0,0 +1,81 @@ +""" +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 toggle_item_sp, option_item_sp, LineSeparatorSP +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.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode + + +class LaneChangeSettingsLayout(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._lane_change_timer = option_item_sp( + title=lambda: tr("Auto Lane Change by Blinker"), + param="AutoLaneChangeTimer", + description=lambda: tr("Set a timer to delay the auto lane change operation when the blinker is used. " + + "No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge.
" + + "Please use caution when using this feature. Only use the blinker when traffic and road conditions permit."), + min_value=-1, + max_value=5, + value_change_step=1, + label_callback=(lambda x: + tr("Off") if x == -1 else + tr("Nudge") if x == 0 else + tr("Nudgeless") if x == 1 else + f"0.5 {tr('s')}" if x == 2 else + f"1 {tr('s')}" if x == 3 else + f"2 {tr('s')}" if x == 4 else + f"3 {tr('s')}") + ) + self._bsm_delay = toggle_item_sp( + param="AutoLaneChangeBsmDelay", + title=lambda: tr("Auto Lane Change: Delay with Blind Spot"), + description=lambda: tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring " + + "(BSM) detects a obstructing vehicle, ensuring safe maneuvering."), + ) + + items = [ + self._lane_change_timer, + LineSeparatorSP(40), + self._bsm_delay, + ] + + return items + + def _update_state(self): + super()._update_state() + self._update_toggles() + + def _render(self, rect): + self._back_button.set_position(self._rect.x, self._rect.y + 20) + self._back_button.render() + # subtract button + 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() + + def _update_toggles(self): + enable_bsm = ui_state.CP and ui_state.CP.enableBsm + if not enable_bsm and ui_state.params.get_bool("AutoLaneChangeBsmDelay"): + ui_state.params.remove("AutoLaneChangeBsmDelay") + self._bsm_delay.action_item.set_enabled(enable_bsm and ui_state.params.get("AutoLaneChangeTimer", return_default=True) > AutoLaneChangeMode.NUDGE) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py new file mode 100644 index 000000000..d3f38e04d --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py @@ -0,0 +1,135 @@ +""" +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 opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr, tr_noop +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.network import NavButton +from openpilot.system.ui.widgets.scroller_tici import Scroller +from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, toggle_item_sp + +MADS_STEERING_MODE_OPTIONS = [ + (tr("Remain Active"), tr_noop("Remain Active: ALC will remain active when the brake pedal is pressed.")), + (tr("Pause"), tr_noop("Pause: ALC will pause when the brake pedal is pressed.")), + (tr("Disengage"), tr_noop("Disengage: ALC will disengage when the brake pedal is pressed.")), +] + +MADS_MAIN_CRUISE_BASE_DESC = tr("Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement.") +MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC = "{engage}

{note}

".format( + engage=tr("Engage lateral and longitudinal control with cruise control engagement."), + note=tr("Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off."), +) + +STATUS_CHECK_COMPATIBILITY = tr("Start the vehicle to check vehicle compatibility.") +DEFAULT_TO_OFF = tr("This feature defaults to OFF, and does not allow selection due to vehicle limitations.") +DEFAULT_TO_ON = tr("This feature defaults to ON, and does not allow selection due to vehicle limitations.") +STATUS_DISENGAGE_ONLY = tr("This platform only supports Disengage mode due to vehicle limitations.") + + +class MadsSettingsLayout(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) + self._initialize_items() + self._scroller = Scroller(self.items, line_separator=True, spacing=0) + + def _initialize_items(self): + self._main_cruise_toggle = toggle_item_sp( + title=lambda: tr("Toggle with Main Cruise"), + description=MADS_MAIN_CRUISE_BASE_DESC, + param="MadsMainCruiseAllowed", + ) + self._unified_engagement_toggle = toggle_item_sp( + title=lambda: tr("Unified Engagement Mode (UEM)"), + description=MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC, + param="MadsUnifiedEngagementMode" + ) + self._steering_mode = multiple_button_item_sp( + param="MadsSteeringMode", + title=lambda: tr("Steering Mode on Brake Pedal"), + description="", + buttons=[opt[0] for opt in MADS_STEERING_MODE_OPTIONS], + inline=False, + button_width=350, + callback=self._update_steering_mode_description, + ) + + self.items = [ + self._main_cruise_toggle, + self._unified_engagement_toggle, + self._steering_mode, + ] + + def _update_state(self): + super()._update_state() + self._update_toggles() + + def _render(self, rect): + self._back_button.set_position(self._rect.x, self._rect.y + 20) + self._back_button.render() + # subtract button + 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() + + @staticmethod + def _mads_limited_settings() -> bool: + brand = "" + if ui_state.is_offroad(): + bundle = ui_state.params.get("CarPlatformBundle") + if bundle: + brand = bundle.get("brand", "") + if not brand: + brand = ui_state.CP.brand if ui_state.CP else "" + + if brand == "rivian": + return True + elif brand == "tesla": + return not (ui_state.CP_SP and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS) + return False + + def _update_steering_mode_description(self, button_index: int): + base_desc = tr("Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.") + result = base_desc + "

" + for opt in MADS_STEERING_MODE_OPTIONS: + desc = "" + opt[1] + "" if button_index == MADS_STEERING_MODE_OPTIONS.index(opt) else opt[1] + result += desc + "
" + self._steering_mode.set_description(result) + self._steering_mode.show_description(True) + + def _update_toggles(self): + self._update_steering_mode_description(self._steering_mode.action_item.get_selected_button()) + if self._mads_limited_settings(): + ui_state.params.remove("MadsMainCruiseAllowed") + ui_state.params.put_bool("MadsUnifiedEngagementMode", True) + ui_state.params.put("MadsSteeringMode", 2) + + self._main_cruise_toggle.action_item.set_enabled(False) + self._main_cruise_toggle.action_item.set_state(False) + self._main_cruise_toggle.set_description("" + DEFAULT_TO_OFF + "
" + MADS_MAIN_CRUISE_BASE_DESC) + + self._unified_engagement_toggle.action_item.set_enabled(False) + self._unified_engagement_toggle.action_item.set_state(True) + self._unified_engagement_toggle.set_description("" + DEFAULT_TO_ON + "
" + MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC) + + self._steering_mode.action_item.set_enabled(False) + self._steering_mode.set_description(STATUS_DISENGAGE_ONLY) + self._steering_mode.action_item.set_selected_button(2) + else: + self._main_cruise_toggle.action_item.set_enabled(True) + self._main_cruise_toggle.set_description(MADS_MAIN_CRUISE_BASE_DESC) + + self._unified_engagement_toggle.action_item.set_enabled(True) + self._unified_engagement_toggle.set_description(MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC) + + self._steering_mode.action_item.set_enabled(True) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py new file mode 100644 index 000000000..dceee8b51 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py @@ -0,0 +1,115 @@ +""" +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 toggle_item_sp, option_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 + + +class TorqueSettingsLayout(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=True, spacing=0) + + def _initialize_items(self): + self._self_tune_toggle = toggle_item_sp( + param="LiveTorqueParamsToggle", + title=lambda: tr("Self-Tune"), + description=lambda: tr("Enables self-tune for Torque lateral control for platforms that do not use " + + "Torque lateral control by default."), + ) + self._relaxed_tune_toggle = toggle_item_sp( + param="LiveTorqueParamsRelaxedToggle", + title=lambda: tr("Less Restrict Settings for Self-Tune (Beta)"), + description=lambda: tr("Less strict settings when using Self-Tune. This allows torqued to be more " + + "forgiving when learning values."), + ) + self._custom_tune_toggle = toggle_item_sp( + param="CustomTorqueParams", + title=lambda: tr("Enable Custom Tuning"), + description=lambda: tr("Enables custom tuning for Torque lateral control. " + + "Modifying Lateral Acceleration Factor and Friction below will override the offline values " + + "indicated in the YAML files within \"opendbc/car/torque_data\". " + + "The values will also be used live when \"Manual Real-Time Tuning\" toggle is enabled."), + ) + self._torque_prams_override_toggle = toggle_item_sp( + param="TorqueParamsOverrideEnabled", + title=lambda: tr("Manual Real-Time Tuning"), + description=lambda: tr("Enforces the torque lateral controller to use the fixed values instead of the learned " + + "values from Self-Tune. Enabling this toggle overrides Self-Tune values."), + ) + self._torque_lat_accel_factor = option_item_sp( + title=lambda: tr("Lateral Acceleration Factor"), + param="TorqueParamsOverrideLatAccelFactor", + description="", + min_value=1, + max_value=500, + value_change_step=1, + label_callback=(lambda x: f"{x/100} m/s^2"), + use_float_scaling=True + ) + + self._torque_friction = option_item_sp( + title=lambda: tr("Friction"), + param="TorqueParamsOverrideFriction", + description="", + min_value=1, + max_value=100, + value_change_step=1, + label_callback=(lambda x: f"{x/100}"), + use_float_scaling=True + ) + + items = [ + self._self_tune_toggle, + self._relaxed_tune_toggle, + self._custom_tune_toggle, + self._torque_prams_override_toggle, + self._torque_lat_accel_factor, + self._torque_friction, + ] + return items + + def _update_state(self): + super()._update_state() + if not ui_state.params.get_bool("LiveTorqueParamsToggle"): + ui_state.params.remove("LiveTorqueParamsRelaxedToggle") + self._relaxed_tune_toggle.action_item.set_state(False) + self._self_tune_toggle.action_item.set_enabled(ui_state.is_offroad()) + self._relaxed_tune_toggle.action_item.set_enabled(ui_state.is_offroad() and self._self_tune_toggle.action_item.get_state()) + self._custom_tune_toggle.action_item.set_enabled(ui_state.is_offroad()) + custom_tune_enabled = self._custom_tune_toggle.action_item.get_state() + self._torque_prams_override_toggle.set_visible(custom_tune_enabled) + self._torque_lat_accel_factor.set_visible(custom_tune_enabled) + self._torque_friction.set_visible(custom_tune_enabled) + + self._torque_prams_override_toggle.action_item.set_enabled(ui_state.is_offroad()) + sliders_enabled = self._torque_prams_override_toggle.action_item.get_state() or ui_state.is_offroad() + self._torque_lat_accel_factor.action_item.set_enabled(sliders_enabled) + self._torque_friction.action_item.set_enabled(sliders_enabled) + + title_text = tr("Real-Time & Offline") if ui_state.params.get("TorqueParamsOverrideEnabled") else tr("Offline Only") + self._torque_lat_accel_factor.set_title(lambda: tr("Lateral Acceleration Factor") + " (" + title_text + ")") + self._torque_friction.set_title(lambda: tr("Friction") + " (" + title_text + ")") + + def _render(self, rect): + self._back_button.set_position(self._rect.x, self._rect.y + 20) + self._back_button.render() + # subtract button + 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()