diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py
index 1e0e7bfd53..01f663d4b9 100644
--- a/selfdrive/ui/layouts/settings/toggles.py
+++ b/selfdrive/ui/layouts/settings/toggles.py
@@ -1,7 +1,11 @@
-from openpilot.common.params import Params
+from cereal import log
+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.selfdrive.ui.ui_state import ui_state
+
+PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
# Description constants
DESCRIPTIONS = {
@@ -30,66 +34,179 @@ class TogglesLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
- items = [
- toggle_item(
+
+ # param, title, desc, icon, needs_restart
+ self._toggle_defs = {
+ "OpenpilotEnabledToggle": (
"Enable openpilot",
DESCRIPTIONS["OpenpilotEnabledToggle"],
- self._params.get_bool("OpenpilotEnabledToggle"),
- icon="chffr_wheel.png",
+ "chffr_wheel.png",
+ True,
),
- toggle_item(
+ "ExperimentalMode": (
"Experimental Mode",
- initial_state=self._params.get_bool("ExperimentalMode"),
- icon="experimental_white.png",
+ "",
+ "experimental_white.png",
+ False,
),
- toggle_item(
+ "DisengageOnAccelerator": (
"Disengage on Accelerator Pedal",
DESCRIPTIONS["DisengageOnAccelerator"],
- self._params.get_bool("DisengageOnAccelerator"),
- icon="disengage_on_accelerator.png",
+ "disengage_on_accelerator.png",
+ False,
),
- multiple_button_item(
- "Driving Personality",
- DESCRIPTIONS["LongitudinalPersonality"],
- buttons=["Aggressive", "Standard", "Relaxed"],
- button_width=255,
- callback=self._set_longitudinal_personality,
- selected_index=self._params.get("LongitudinalPersonality", return_default=True),
- icon="speed_limit.png"
- ),
- toggle_item(
+ "IsLdwEnabled": (
"Enable Lane Departure Warnings",
DESCRIPTIONS["IsLdwEnabled"],
- self._params.get_bool("IsLdwEnabled"),
- icon="warning.png",
+ "warning.png",
+ False,
),
- toggle_item(
+ "AlwaysOnDM": (
"Always-On Driver Monitoring",
DESCRIPTIONS["AlwaysOnDM"],
- self._params.get_bool("AlwaysOnDM"),
- icon="monitoring.png",
+ "monitoring.png",
+ False,
),
- toggle_item(
+ "RecordFront": (
"Record and Upload Driver Camera",
DESCRIPTIONS["RecordFront"],
- self._params.get_bool("RecordFront"),
- icon="monitoring.png",
+ "monitoring.png",
+ True,
),
- toggle_item(
+ "RecordAudio": (
"Record and Upload Microphone Audio",
DESCRIPTIONS["RecordAudio"],
- self._params.get_bool("RecordAudio"),
- icon="microphone.png",
+ "microphone.png",
+ True,
),
- toggle_item(
- "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png"
+ "IsMetric": (
+ "Use Metric System",
+ DESCRIPTIONS["IsMetric"],
+ "metric.png",
+ False,
),
- ]
+ }
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ self._long_personality_setting = multiple_button_item(
+ "Driving Personality",
+ DESCRIPTIONS["LongitudinalPersonality"],
+ buttons=["Aggressive", "Standard", "Relaxed"],
+ button_width=255,
+ callback=self._set_longitudinal_personality,
+ selected_index=self._params.get("LongitudinalPersonality", return_default=True),
+ icon="speed_limit.png"
+ )
+
+ self._toggles = {}
+ self._locked_toggles = set()
+ for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
+ toggle = toggle_item(
+ title,
+ desc,
+ self._params.get_bool(param),
+ callback=lambda state, p=param: self._toggle_callback(state, p),
+ icon=icon,
+ )
+
+ try:
+ locked = self._params.get_bool(param + "Lock")
+ except UnknownKeyName:
+ locked = False
+ toggle.action_item.set_enabled(not locked)
+
+ if needs_restart and not locked:
+ toggle.set_description(toggle.description + " Changing this setting will restart openpilot if the car is powered on.")
+
+ # track for engaged state updates
+ if locked:
+ self._locked_toggles.add(param)
+
+ self._toggles[param] = toggle
+
+ # insert longitudinal personality after NDOG toggle
+ if param == "DisengageOnAccelerator":
+ self._toggles["LongitudinalPersonality"] = self._long_personality_setting
+
+ self._update_experimental_mode_icon()
+ self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
+
+ def _update_state(self):
+ if ui_state.sm.updated["selfdriveState"]:
+ personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality]
+ if personality != ui_state.personality and ui_state.started:
+ 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):
+ 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:
" +
+ "
End-to-End Longitudinal Control
" +
+ "Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " +
+ "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " +
+ "mistakes should be expected.
" +
+ "New Driving Visualization
" +
+ "The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. " +
+ "The Experimental mode logo will also be shown in the top right corner."
+ )
+
+ is_release = self._params.get_bool("IsReleaseBranch")
+
+ if ui_state.CP is not None:
+ if ui_state.has_longitudinal_control:
+ self._toggles["ExperimentalMode"].action_item.set_enabled(True)
+ self._toggles["ExperimentalMode"].set_description(e2e_description)
+ self._long_personality_setting.action_item.set_enabled(True)
+ else:
+ # no long for now
+ self._toggles["ExperimentalMode"].action_item.set_enabled(False)
+ self._toggles["ExperimentalMode"].action_item.set_state(False)
+ self._long_personality_setting.action_item.set_enabled(False)
+ self._params.remove("ExperimentalMode")
+
+ 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 is_release:
+ long_desc = unavailable + " " + ("An alpha version of openpilot longitudinal control can be tested, along with " +
+ "Experimental mode, on non-release branches.")
+ else:
+ long_desc = "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode."
+
+ self._toggles["ExperimentalMode"].set_description("" + long_desc + "
" + e2e_description)
+ else:
+ self._toggles["ExperimentalMode"].set_description(e2e_description)
+
+ self._update_experimental_mode_icon()
+
+ # TODO: make a param control list item so we don't need to manage internal state as much here
+ # refresh toggles from params to mirror external changes
+ for param in self._toggle_defs:
+ self._toggles[param].action_item.set_state(self._params.get_bool(param))
def _render(self, rect):
self._scroller.render(rect)
+ def _update_experimental_mode_icon(self):
+ icon = "experimental.png" if self._toggles["ExperimentalMode"].action_item.get_state() else "experimental_white.png"
+ self._toggles["ExperimentalMode"].set_icon(icon)
+
+ def _toggle_callback(self, state: bool, param: str):
+ if param == "ExperimentalMode":
+ self._update_experimental_mode_icon()
+
+ self._params.put_bool(param, state)
+ if self._toggle_defs[param][3]:
+ self._params.put_bool("OnroadCycleRequested", True)
+
def _set_longitudinal_personality(self, button_index: int):
self._params.put("LongitudinalPersonality", button_index)
diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py
index 9c45519b4b..be83517980 100644
--- a/selfdrive/ui/ui_state.py
+++ b/selfdrive/ui/ui_state.py
@@ -4,9 +4,9 @@ import time
import threading
from collections.abc import Callable
from enum import Enum
-from cereal import messaging, log
+from cereal import messaging, car, log
from openpilot.common.filter_simple import FirstOrderFilter
-from openpilot.common.params import Params, UnknownKeyName
+from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.system.ui.lib.application import gui_app
@@ -69,7 +69,10 @@ class UIState:
self.ignition: bool = False
self.panda_type: log.PandaState.PandaType = log.PandaState.PandaType.unknown
self.personality: log.LongitudinalPersonality = log.LongitudinalPersonality.standard
+ self.has_longitudinal_control: bool = False
+ self.CP: car.CarParams | None = None
self.light_sensor: float = -1.0
+ self._param_update_time: float = 0.0
self._update_params()
@@ -87,6 +90,9 @@ class UIState:
self.sm.update(0)
self._update_state()
self._update_status()
+ if time.monotonic() - self._param_update_time > 5.0:
+ self._update_params()
+ self._param_update_time = time.monotonic()
device.update()
def _update_state(self) -> None:
@@ -137,10 +143,16 @@ class UIState:
self._started_prev = self.started
def _update_params(self) -> None:
- try:
- self.is_metric = self.params.get_bool("IsMetric")
- except UnknownKeyName:
- self.is_metric = False
+ self.is_metric = self.params.get_bool("IsMetric")
+
+ # Update longitudinal control state
+ CP_bytes = self.params.get("CarParams")
+ 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
class Device:
diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py
index 509c49be35..bbf66c6555 100644
--- a/system/ui/widgets/list_view.py
+++ b/system/ui/widgets/list_view.py
@@ -56,10 +56,10 @@ class ItemAction(Widget, ABC):
class ToggleAction(ItemAction):
- def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True):
+ def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True,
+ callback: Callable[[bool], None] | None = None):
super().__init__(width, enabled)
- self.toggle = Toggle(initial_state=initial_state)
- self.state = initial_state
+ self.toggle = Toggle(initial_state=initial_state, callback=callback)
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(touch_callback)
@@ -68,15 +68,13 @@ class ToggleAction(ItemAction):
def _render(self, rect: rl.Rectangle) -> bool:
self.toggle.set_enabled(self.enabled)
clicked = self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self._rect.width, TOGGLE_HEIGHT))
- self.state = self.toggle.get_state()
return bool(clicked)
def set_state(self, state: bool):
- self.state = state
self.toggle.set_state(state)
def get_state(self) -> bool:
- return self.state
+ return self.toggle.get_state()
class ButtonAction(ItemAction):
@@ -207,6 +205,13 @@ class MultipleButtonAction(ItemAction):
self.callback = callback
self._font = gui_app.font(FontWeight.MEDIUM)
+ def set_selected_button(self, index: int):
+ if 0 <= index < len(self.buttons):
+ self.selected_button = index
+
+ def get_selected_button(self) -> int:
+ return self.selected_button
+
def _render(self, rect: rl.Rectangle):
spacing = RIGHT_ITEM_PADDING
button_y = rect.y + (rect.height - BUTTON_HEIGHT) / 2
@@ -259,7 +264,7 @@ class ListItem(Widget):
action_item: ItemAction | None = None):
super().__init__()
self.title = title
- self.icon = icon
+ self.set_icon(icon)
self._description = description
self.description_visible = description_visible
self.callback = callback
@@ -267,7 +272,6 @@ class ListItem(Widget):
self.set_rect(rl.Rectangle(0, 0, ITEM_BASE_WIDTH, ITEM_BASE_HEIGHT))
self._font = gui_app.font(FontWeight.NORMAL)
- self._icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE) if self.icon else None
self._html_renderer = HtmlRenderer(text="", text_size={ElementType.P: ITEM_DESC_FONT_SIZE},
text_color=ITEM_DESC_TEXT_COLOR)
@@ -352,6 +356,10 @@ class ListItem(Widget):
if self.callback:
self.callback()
+ def set_icon(self, icon: str | None):
+ self.icon = icon
+ self._icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE) if self.icon else None
+
def set_description(self, description: str | Callable[[], str] | None):
self._description = description
new_desc = self.description
@@ -398,8 +406,8 @@ def simple_item(title: str, callback: Callable | None = None) -> ListItem:
def toggle_item(title: str, description: str | Callable[[], str] | None = None, initial_state: bool = False,
callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True) -> ListItem:
- action = ToggleAction(initial_state=initial_state, enabled=enabled)
- return ListItem(title=title, description=description, action_item=action, icon=icon, callback=callback)
+ action = ToggleAction(initial_state=initial_state, enabled=enabled, callback=callback)
+ return ListItem(title=title, description=description, action_item=action, icon=icon)
def button_item(title: str, button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,
diff --git a/system/ui/widgets/toggle.py b/system/ui/widgets/toggle.py
index 968afda9c8..0fbf3c844a 100644
--- a/system/ui/widgets/toggle.py
+++ b/system/ui/widgets/toggle.py
@@ -1,4 +1,5 @@
import pyray as rl
+from collections.abc import Callable
from openpilot.system.ui.lib.application import MousePos
from openpilot.system.ui.widgets import Widget
@@ -14,9 +15,10 @@ ANIMATION_SPEED = 8.0
class Toggle(Widget):
- def __init__(self, initial_state=False):
+ def __init__(self, initial_state: bool = False, callback: Callable[[bool], None] | None = None):
super().__init__()
self._state = initial_state
+ self._callback = callback
self._enabled = True
self._progress = 1.0 if initial_state else 0.0
self._target = self._progress
@@ -32,8 +34,10 @@ class Toggle(Widget):
self._clicked = True
self._state = not self._state
self._target = 1.0 if self._state else 0.0
+ if self._callback:
+ self._callback(self._state)
- def get_state(self):
+ def get_state(self) -> bool:
return self._state
def set_state(self, state: bool):