Files
StarPilot/openpilot/selfdrive/ui/layouts/settings/initial_cruise_speed.py
T
2026-10-20 02:19:24 +00:00

112 lines
4.7 KiB
Python

import time
import pyray as rl
from openpilot.common.constants import CV
from openpilot.selfdrive.car.cruise import V_CRUISE_INITIAL, V_CRUISE_MAX, validated_initial_cruise_speed
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import FontWeight, TextAlignment
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets.list_view import ItemAction, BUTTON_HEIGHT, BUTTON_BORDER_RADIUS
SPEED_STEP = 5
STEP_BUTTON_WIDTH = 160
VALUE_FONT_SIZE = 50
REPEAT_DELAY = 0.5 # seconds held before repeating
REPEAT_INTERVAL = 0.2 # seconds
def _speed_conversion() -> float:
return 1 if ui_state.is_metric else CV.KPH_TO_MPH
def get_speed_options() -> tuple[list[int], int]:
"""Initial cruise speeds in display units, and the index of the nearest to the saved one"""
conversion = _speed_conversion()
options = list(range(round(V_CRUISE_INITIAL * conversion), round(V_CRUISE_MAX * conversion) + 1, SPEED_STEP))
idx = round((validated_initial_cruise_speed(ui_state.initial_cruise_speed) * conversion - options[0]) / SPEED_STEP)
return options, max(0, min(len(options) - 1, idx))
def set_initial_cruise_speed(speed: int) -> None:
"""Save an initial cruise speed in display units"""
ui_state.params.put("InitialCruiseSpeedKph", round(speed / _speed_conversion()), block=True)
ui_state.update_params()
class RepeatButton(Button):
def __init__(self, text, callback, font_size):
super().__init__(text, callback, font_size=font_size, button_style=ButtonStyle.PILL, border_radius=BUTTON_BORDER_RADIUS, text_padding=0)
self._repeat_at: float | None = None
self._repeated = False
def _handle_mouse_press(self, mouse_pos):
super()._handle_mouse_press(mouse_pos)
self._repeat_at = time.monotonic() + REPEAT_DELAY
self._repeated = False
def _handle_mouse_release(self, mouse_pos):
if not self._repeated:
super()._handle_mouse_release(mouse_pos)
def _process_mouse_events(self):
super()._process_mouse_events()
# step repeatedly while held. once it has repeated, the release doesn't step again
if not self.is_pressed or not self._touch_valid():
self._repeat_at = None
elif self._repeat_at is not None and time.monotonic() >= self._repeat_at:
self._repeated = True
self._repeat_at = time.monotonic() + REPEAT_INTERVAL
if self._click_callback:
self._click_callback()
class InitialCruiseSpeedControl(ItemAction):
def __init__(self):
super().__init__(width=580)
self._options, self._selected_idx = get_speed_options()
self._decrease = self._child(RepeatButton("-", lambda: self._adjust_speed(-1), VALUE_FONT_SIZE))
self._increase = self._child(RepeatButton("+", lambda: self._adjust_speed(1), VALUE_FONT_SIZE))
self._decrease.set_enabled(lambda: self.enabled and self._selected_idx > 0)
self._increase.set_enabled(lambda: self.enabled and self._selected_idx < len(self._options) - 1)
self._decrease.set_touch_valid_callback(self._touch_valid)
self._increase.set_touch_valid_callback(self._touch_valid)
def refresh(self):
self._options, self._selected_idx = get_speed_options()
def display_options(self) -> tuple[tuple[str, ...], int]:
self.refresh()
unit = tr("km/h") if ui_state.is_metric else tr("mph")
return tuple(f"{speed} {unit}" for speed in self._options), self._selected_idx
def adjust_speed(self, step: int) -> bool:
self.refresh()
if not self.enabled or step not in (-1, 1):
return False
next_idx = self._selected_idx + step
if not 0 <= next_idx < len(self._options):
return False
return self.select_speed(next_idx)
def select_speed(self, index: int) -> bool:
self.refresh()
if not self.enabled or type(index) is not int or not 0 <= index < len(self._options):
return False
self._selected_idx = index
set_initial_cruise_speed(self._options[index])
return True
def _adjust_speed(self, step: int):
self.adjust_speed(step)
def _render(self, rect):
y = rect.y + (rect.height - BUTTON_HEIGHT) / 2
self._decrease.render(rl.Rectangle(rect.x, y, STEP_BUTTON_WIDTH, BUTTON_HEIGHT))
self._increase.render(rl.Rectangle(rect.x + rect.width - STEP_BUTTON_WIDTH, y, STEP_BUTTON_WIDTH, BUTTON_HEIGHT))
text = f"{self._options[self._selected_idx]} " + (tr("km/h") if ui_state.is_metric else tr("mph"))
gui_label(rl.Rectangle(rect.x + STEP_BUTTON_WIDTH, y, rect.width - 2 * STEP_BUTTON_WIDTH, BUTTON_HEIGHT), text, VALUE_FONT_SIZE,
color=rl.WHITE if self.enabled else rl.GRAY, font_weight=FontWeight.MEDIUM, alignment=TextAlignment.CENTER)