diff --git a/common/params_keys.h b/common/params_keys.h index d34d40bd0..d35c39879 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -151,6 +151,7 @@ inline static std::unordered_map keys = { {"ApiCache_DriveStats", {PERSISTENT, JSON}}, {"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}}, {"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}}, + {"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds {"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h {"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}}, {"Brightness", {PERSISTENT | BACKUP, INT, "0"}}, diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py index 9439c588d..671174ac7 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 000000000..e6846744a --- /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 000000000..c14330d9a --- /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/layouts/settings/steering.py b/selfdrive/ui/sunnypilot/layouts/settings/steering.py index f82c4097c..14d840138 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering.py @@ -72,6 +72,15 @@ class SteeringLayout(Widget): description="", label_callback=lambda speed: f'{speed} {"km/h" if ui_state.is_metric else "mph"}', ) + self._blinker_reengage_delay = option_item_sp( + param="BlinkerLateralReengageDelay", + title=lambda: tr("Post-Blinker Delay"), + min_value=0, + max_value=10, + value_change_step=1, + description=lambda: tr("Delay before lateral control resumes after the turn signal ends."), + label_callback=lambda delay: f'{delay} {"s"}' + ) self._torque_control_toggle = toggle_item_sp( param="EnforceTorqueControl", title=lambda: tr("Enforce Torque Lateral Control"), @@ -96,6 +105,7 @@ class SteeringLayout(Widget): LineSeparatorSP(40), self._blinker_control_toggle, self._blinker_control_options, + self._blinker_reengage_delay, LineSeparatorSP(40), self._torque_control_toggle, self._torque_customization_button, @@ -128,6 +138,7 @@ class SteeringLayout(Widget): 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()) + self._blinker_reengage_delay.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() 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 index d3f38e04d..3542adf28 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py @@ -9,6 +9,7 @@ import pyray as rl from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake 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 @@ -112,7 +113,7 @@ class MadsSettingsLayout(Widget): if self._mads_limited_settings(): ui_state.params.remove("MadsMainCruiseAllowed") ui_state.params.put_bool("MadsUnifiedEngagementMode", True) - ui_state.params.put("MadsSteeringMode", 2) + ui_state.params.put("MadsSteeringMode", MadsSteeringModeOnBrake.DISENGAGE) self._main_cruise_toggle.action_item.set_enabled(False) self._main_cruise_toggle.action_item.set_state(False) @@ -122,9 +123,9 @@ class MadsSettingsLayout(Widget): 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) + self._steering_mode.action_item.set_selected_button(MadsSteeringModeOnBrake.DISENGAGE) + self._steering_mode.action_item.set_enabled_buttons({MadsSteeringModeOnBrake.DISENGAGE}) else: self._main_cruise_toggle.action_item.set_enabled(True) self._main_cruise_toggle.set_description(MADS_MAIN_CRUISE_BASE_DESC) @@ -133,3 +134,4 @@ class MadsSettingsLayout(Widget): self._unified_engagement_toggle.set_description(MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC) self._steering_mode.action_item.set_enabled(True) + self._steering_mode.action_item.set_enabled_buttons(None) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 369909812..4d26e138b 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") @@ -138,15 +142,14 @@ class UIStateSP: self.torque_bar = self.params.get_bool("TorqueBar") self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI") self.turn_signals = self.params.get_bool("ShowTurnSignals") + self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True) class DeviceSP: - def __init__(self): - self._params = Params() - - def _set_awake(self, on: bool): - if on and self._params.get("DeviceBootMode", return_default=True) == 1: - self._params.put_bool("OffroadMode", True) + @staticmethod + def _set_awake(on: bool, _ui_state): + if _ui_state.boot_offroad_mode == 1 and not on: + _ui_state.params.put_bool("OffroadMode", True) @staticmethod def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float: diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index bbc26d96b..f30789a7e 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -321,7 +321,7 @@ class Device(DeviceSP): def _set_awake(self, on: bool): if on != self._awake: - DeviceSP._set_awake(self, on) + DeviceSP._set_awake(on, ui_state) self._awake = on cloudlog.debug(f"setting display power {int(on)}") HARDWARE.set_display_power(on) diff --git a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py index 2f27e7e42..98757cd53 100644 --- a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -17,13 +17,16 @@ class BlinkerPauseLateral: self.enabled = self.params.get_bool("BlinkerPauseLateralControl") self.is_metric = self.params.get_bool("IsMetric") self.min_speed = 0 + self.reengage_delay = 0 + self.blinker_off_timer = 0.0 def get_params(self) -> None: self.enabled = self.params.get_bool("BlinkerPauseLateralControl") self.is_metric = self.params.get_bool("IsMetric") - self.min_speed = self.params.get("BlinkerMinLateralControlSpeed") + self.min_speed = self.params.get("BlinkerMinLateralControlSpeed", return_default=True) + self.reengage_delay = self.params.get("BlinkerLateralReengageDelay", return_default=True) - def update(self, CS: car.CarState) -> bool: + def update(self, CS: car.CarState, DT_CTRL: float = 0.01) -> bool: if not self.enabled: return False @@ -31,4 +34,11 @@ class BlinkerPauseLateral: speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS min_speed_ms = self.min_speed * speed_factor - return bool(one_blinker and CS.vEgo < min_speed_ms) + below_speed = CS.vEgo < min_speed_ms + + if one_blinker and below_speed: + self.blinker_off_timer = self.reengage_delay + elif self.blinker_off_timer > 0: + self.blinker_off_timer -= DT_CTRL + + return bool((one_blinker and below_speed) or self.blinker_off_timer > 0) diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py index ca8f16fe7..b547ea96b 100644 --- a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -20,6 +20,8 @@ class TestBlinkerPauseLateral: self.blinker_pause_lateral.enabled = True self.blinker_pause_lateral.is_metric = False self.blinker_pause_lateral.min_speed = 20 # MPH + self.blinker_pause_lateral.reengage_delay = 0 + self.blinker_pause_lateral.blinker_off_timer = 0.0 self.CS = car.CarState.new_message() self.CS.vEgo = 0 @@ -46,6 +48,18 @@ class TestBlinkerPauseLateral: } self._test_should_blinker_pause_lateral(expected_results) + def test_reengage_delay(self): + self.blinker_pause_lateral.reengage_delay = 2 # seconds + self.CS.vEgo = 4.5 # ~10 MPH + + expected_results = { + (False, False): True, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + def test_above_min_speed_blinker(self): self.CS.vEgo = 13.4 # ~30 MPH diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index c0c84eac0..ad94ad35f 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -93,6 +93,10 @@ "title": "[TIZI/TICI only] Blind Spot Detection", "description": "Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported." }, + "BlinkerLateralReengageDelay": { + "title": "Post-Blinker Delay", + "description": "Delay before lateral control resumes after the turn signal ends." + }, "BlinkerMinLateralControlSpeed": { "title": "Blinker Min Lateral Control Speed", "description": "" diff --git a/system/ui/sunnypilot/widgets/__init__.py b/system/ui/sunnypilot/widgets/__init__.py index e69de29bb..96865e604 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) diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 39e88169d..1dd69c24c 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -134,6 +134,10 @@ class MultipleButtonActionSP(MultipleButtonAction): if self.param_key: self.selected_button = int(self.params.get(self.param_key, return_default=True)) self._anim_x: float | None = None + self.enabled_buttons: set[int] | None = None + + def set_enabled_buttons(self, indices: set[int] | None): + self.enabled_buttons = indices def _render(self, rect: rl.Rectangle): @@ -171,10 +175,31 @@ class MultipleButtonActionSP(MultipleButtonAction): text_x = button_x + (self.button_width - text_size.x) / 2 text_y = button_y + (style.BUTTON_HEIGHT - text_size.y) / 2 - rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color) + # Check individual button enabled state + is_button_enabled = self.enabled and (self.enabled_buttons is None or i in self.enabled_buttons) + current_text_color = text_color if is_button_enabled else style.MBC_DISABLED + + rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, current_text_color) def _handle_mouse_release(self, mouse_pos: MousePos): - MultipleButtonAction._handle_mouse_release(self, mouse_pos) + # Override parent method to check individual button enabled state + if not self.enabled: + return + + button_y = self._rect.y + (self._rect.height - style.BUTTON_HEIGHT) / 2 + for i, _ in enumerate(self.buttons): + button_x = self._rect.x + i * self.button_width + button_rect = rl.Rectangle(button_x, button_y, self.button_width, style.BUTTON_HEIGHT) + + if rl.check_collision_point_rec(mouse_pos, button_rect): + # Check if this specific button is enabled + if self.enabled_buttons is not None and i not in self.enabled_buttons: + return + + self.selected_button = i + if self.callback: + self.callback(i) + if self.param_key: self.params.put(self.param_key, self.selected_button) @@ -212,6 +237,12 @@ class ListItemSP(ListItem): content_width = int(self._rect.width - style.ITEM_PADDING * 2) self._rect.height = self.get_item_height(self._font, content_width) + def set_parent_rect(self, parent_rect: rl.Rectangle) -> None: + super().set_parent_rect(parent_rect) + if self.description_visible: + content_width = int(self._rect.width - style.ITEM_PADDING * 2) + self._rect.height = self.get_item_height(self._font, content_width) + def get_item_height(self, font: rl.Font, max_width: int) -> float: height = super().get_item_height(font, max_width)