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)