diff --git a/selfdrive/ui/layouts/settings/starpilot/main_panel.py b/selfdrive/ui/layouts/settings/starpilot/main_panel.py index a765b02c1..9d4975658 100644 --- a/selfdrive/ui/layouts/settings/starpilot/main_panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/main_panel.py @@ -17,7 +17,6 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.system_settings import St from openpilot.selfdrive.ui.layouts.settings.starpilot.visuals import StarPilotVisualsLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.themes import StarPilotThemesLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.vehicle import StarPilotVehicleSettingsLayout -from openpilot.selfdrive.ui.layouts.settings.starpilot.wheel import StarPilotWheelLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, RadioTileGroup, SPACING @@ -58,7 +57,7 @@ class StarPilotLayout(Widget): { "title": "Vehicle Settings", "icon": "icon_vehicle.png", - "buttons": [("VEHICLE SETTINGS", "VEHICLE", 0), ("WHEEL CONTROLS", "WHEEL", 0)], + "buttons": [("VEHICLE SETTINGS", "VEHICLE", 0)], "color": "#64748B", }, ] @@ -86,7 +85,6 @@ class StarPilotLayout(Widget): StarPilotPanelType.VISUALS: StarPilotPanelInfo(tr_noop("Appearance"), StarPilotVisualsLayout()), StarPilotPanelType.THEMES: StarPilotPanelInfo(tr_noop("Themes"), StarPilotThemesLayout()), StarPilotPanelType.VEHICLE: StarPilotPanelInfo(tr_noop("Vehicle Settings"), StarPilotVehicleSettingsLayout()), - StarPilotPanelType.WHEEL: StarPilotPanelInfo(tr_noop("Wheel Controls"), StarPilotWheelLayout()), } self._setup_sub_panels( @@ -188,7 +186,6 @@ class StarPilotLayout(Widget): "VISUALS": StarPilotPanelType.VISUALS, "THEMES": StarPilotPanelType.THEMES, "VEHICLE": StarPilotPanelType.VEHICLE, - "WHEEL": StarPilotPanelType.WHEEL, } if self._current_category_idx is None: diff --git a/selfdrive/ui/layouts/settings/starpilot/vehicle.py b/selfdrive/ui/layouts/settings/starpilot/vehicle.py index a2b3d9280..4ea6dbae0 100644 --- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py +++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py @@ -1,13 +1,35 @@ from __future__ import annotations +import pyray as rl + from openpilot.system.hardware import HARDWARE -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import FontWeight, MouseEvent, MousePos, gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 +from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog +from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import AetherSliderDialog, TileGrid, HubTile, ToggleTile, ValueTile +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( + AETHER_COMPACT_ROW_HEIGHT, + AETHER_LIST_METRICS, + AetherListColors, + AetherScrollbar, + AetherSliderDialog, + DEFAULT_PANEL_STYLE, + _point_hits, + draw_list_group_shell, + draw_list_scroll_fades, + draw_metric_strip, + draw_section_header, + draw_selection_list_row, + draw_settings_list_row, + draw_settings_panel_header, + draw_soft_card, + draw_tab_card, + init_list_panel, +) from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state from openpilot.selfdrive.ui.mici.layouts.settings.fingerprint_catalog import ( FingerprintModelOption, @@ -16,6 +38,22 @@ from openpilot.selfdrive.ui.mici.layouts.settings.fingerprint_catalog import ( ) +ACTION_OPTIONS = [ + {"id": 0, "name": "No Action"}, + {"id": 1, "name": "Change Personality", "requires_longitudinal": True}, + {"id": 2, "name": "Force Coast", "requires_longitudinal": True}, + {"id": 3, "name": "Pause Steering"}, + {"id": 4, "name": "Pause Accel/Brake", "requires_longitudinal": True}, + {"id": 5, "name": "Toggle Experimental", "requires_longitudinal": True}, + {"id": 6, "name": "Toggle Traffic", "requires_longitudinal": True}, + {"id": 7, "name": "Toggle Switchback"}, + {"id": 8, "name": "Create Bookmark"}, +] +ACTION_NAMES = [o["name"] for o in ACTION_OPTIONS] +ACTION_IDS = {o["name"]: o["id"] for o in ACTION_OPTIONS} +ACTION_NAME_BY_ID = {o["id"]: o["name"] for o in ACTION_OPTIONS} + + def _lock_doors_timer_labels(): labels: dict[float, str] = {0.0: tr("Never")} for i in range(5, 305, 5): @@ -23,508 +61,630 @@ def _lock_doors_timer_labels(): return labels +class VehicleSettingsManagerView(Widget): + HEADER_SUBTITLE_HEIGHT = 24 + HEADER_SUMMARY_GAP = 12 + HEADER_CARD_HEIGHT = 108 + TAB_HEIGHT = 52 + TAB_GAP = 10 + TAB_BOTTOM_GAP = 18 + SECTION_GAP = AETHER_LIST_METRICS.section_gap + SECTION_HEADER_HEIGHT = AETHER_LIST_METRICS.section_header_height + SECTION_HEADER_GAP = AETHER_LIST_METRICS.section_header_gap + ROW_HEIGHT = AETHER_COMPACT_ROW_HEIGHT + FADE_HEIGHT = AETHER_LIST_METRICS.fade_height + COLUMN_GAP = 22 + TWO_COLUMN_BREAKPOINT = 1180 + ACTION_PILL_WIDTH = 108 + + PANEL_STYLE = DEFAULT_PANEL_STYLE + + def __init__(self, controller: "StarPilotVehicleSettingsLayout"): + super().__init__() + self._controller = controller + self._scroll_panel = GuiScrollPanel2(horizontal=False) + self._scrollbar = AetherScrollbar() + self._content_height = 0.0 + self._scroll_offset = 0.0 + self._interactive_rects: dict[str, rl.Rectangle] = {} + self._pressed_target: str | None = None + self._can_click = True + self._active_tab_key = "identity" + self._shell_rect = rl.Rectangle(0, 0, 0, 0) + self._scroll_rect = rl.Rectangle(0, 0, 0, 0) + + self._tab_defs = [ + {"id": "identity", "title": tr("Identity")}, + {"id": "features", "title": tr("Features")}, + {"id": "controls", "title": tr("Controls")}, + ] + + def _uses_two_columns(self, width: float) -> bool: + return width >= self.TWO_COLUMN_BREAKPOINT + + def _column_width(self, width: float) -> float: + return (width - self.COLUMN_GAP) / 2 if self._uses_two_columns(width) else width + + def _section_height(self, count: int, row_height: float) -> float: + return 0.0 if count <= 0 else count * row_height + + def _section_block_height(self, content_height: float) -> float: + if content_height <= 0: + return 0.0 + return self.SECTION_HEADER_HEIGHT + self.SECTION_HEADER_GAP + content_height + self.SECTION_GAP + + def _stacked_section_height(self, sections: list[float]) -> float: + if not sections: + return 0.0 + return max(0.0, sum(sections) - self.SECTION_GAP) + + def _interactive_state(self, target_id: str, rect: rl.Rectangle, *, pad_y: float = 0) -> tuple[bool, bool]: + self._interactive_rects[target_id] = rect + hovered = _point_hits(gui_app.last_mouse_event.pos, rect, self._scroll_rect, pad_x=6, pad_y=pad_y) + return hovered, self._pressed_target == target_id + + def _clear_state(self): + self._pressed_target = None + self._can_click = True + + def show_event(self): + super().show_event() + self._clear_state() + + def hide_event(self): + super().hide_event() + self._clear_state() + + def _handle_mouse_press(self, mouse_pos: MousePos): + self._pressed_target = self._target_at(mouse_pos) + self._can_click = True + + def _handle_mouse_event(self, mouse_event: MouseEvent): + if not self._scroll_panel.is_touch_valid(): + self._can_click = False + return + if self._pressed_target is not None and self._target_at(mouse_event.pos) != self._pressed_target: + self._pressed_target = None + + def _handle_mouse_release(self, mouse_pos: MousePos): + target = self._target_at(mouse_pos) if self._scroll_panel.is_touch_valid() else None + if self._pressed_target is not None and self._pressed_target == target and self._can_click: + self._activate_target(target) + self._pressed_target = None + self._can_click = True + + def _target_at(self, mouse_pos: MousePos) -> str | None: + for target_id, rect in self._interactive_rects.items(): + if _point_hits(mouse_pos, rect, self._scroll_rect, pad_x=6, pad_y=0): + return target_id + return None + + def _activate_target(self, target_id: str | None): + if not target_id: + return + prefix, _, value = target_id.partition(":") + if prefix == "tab": + self._active_tab_key = value + return + if prefix == "toggle": + self._controller._on_toggle(value) + elif prefix == "select": + self._controller._on_select(value) + + def _tab_subtitle(self, tab_id: str) -> str: + cs = starpilot_state.car_state + if tab_id == "identity": + return tr("Make, model, and fingerprint") + if tab_id == "features": + count = 1 + if cs.isGM: count += 4 + if cs.isGM and cs.isVolt and not cs.hasSNG: count += 1 + if cs.isHKG and cs.isHKGCanFd: count += 1 + if cs.isSubaru: count += 1 + if cs.isToyota: count += 4 + if cs.isToyota and not cs.hasSNG: count += 1 + if cs.isToyota and cs.hasOpenpilotLongitudinal: count += 1 + if cs.isHKGCanFd and cs.hasOpenpilotLongitudinal: count += 1 + return tr("{} settings").format(count) + if tab_id == "controls": + count = 7 + if not cs.isSubaru and not (cs.lkasAllowedForAOL and self._controller._params.get_bool("AlwaysOnLateral") and self._controller._params.get_bool("AlwaysOnLateralLKAS")): + count += 1 + if cs.hasModeStarButtons: count += 6 + return tr("{} buttons").format(count) + return "" + + def _render(self, rect: rl.Rectangle): + self.set_rect(rect) + self._interactive_rects.clear() + + frame, scroll_rect, content_width = init_list_panel(rect, self.PANEL_STYLE) + self._shell_rect = frame.shell + self._scroll_rect = scroll_rect + + self._draw_header(frame.header) + + self._content_height = self._measure_content_height(content_width) + self._scroll_panel.set_enabled(self.is_visible) + self._scroll_offset = self._scroll_panel.update(scroll_rect, max(self._content_height, scroll_rect.height)) + + rl.begin_scissor_mode(int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width), int(scroll_rect.height)) + self._draw_scroll_content(scroll_rect, content_width) + rl.end_scissor_mode() + + if self._content_height > scroll_rect.height: + self._scrollbar.render(scroll_rect, self._content_height, self._scroll_offset) + + draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, AetherListColors.PANEL_BG, fade_height=self.FADE_HEIGHT) + + def _draw_header(self, rect: rl.Rectangle): + draw_settings_panel_header(rect, tr("Vehicle Settings"), + tr("Configure vehicle fingerprint, driving features, and steering controls."), + subtitle_size=22) + + summary_y = rect.y + 48 + self.HEADER_SUBTITLE_HEIGHT + self.HEADER_SUMMARY_GAP + summary_rect = rl.Rectangle(rect.x, summary_y, rect.width, min(self.HEADER_CARD_HEIGHT, rect.y + rect.height - summary_y)) + self._draw_summary_card(summary_rect) + + def _draw_summary_card(self, rect: rl.Rectangle): + draw_soft_card(rect, self.PANEL_STYLE.surface_fill, self.PANEL_STYLE.surface_border) + inset = 18 + left_x = rect.x + inset + left_w = rect.width * 0.40 + + make = self._controller._get_display_make() + model = self._controller._get_display_model() + vehicle_name = f"{make} {model}" if make != tr("None") else tr("No vehicle selected") + + gui_label(rl.Rectangle(left_x, rect.y + 10, left_w, 22), tr("Current Vehicle"), 20, AetherListColors.MUTED, FontWeight.MEDIUM) + gui_label(rl.Rectangle(left_x, rect.y + 34, left_w, 30), vehicle_name, 26, AetherListColors.HEADER, FontWeight.BOLD) + + cs = starpilot_state.car_state + metrics = [] + if cs.hasRadar: + metrics.append((tr("Radar"), tr("Yes"))) + if cs.hasOpenpilotLongitudinal: + metrics.append((tr("Long"), tr("Yes"))) + if cs.hasBSM: + metrics.append((tr("BSM"), tr("Yes"))) + if cs.hasSNG: + metrics.append((tr("SNG"), tr("Yes"))) + + if metrics: + draw_metric_strip( + rl.Rectangle(left_x, rect.y + 72, max(240.0, rect.width * 0.38), 30), + metrics, + style=self.PANEL_STYLE, + label_top_offset=0, + value_top_offset=14, + divider_top_offset=2, + divider_bottom_offset=16, + ) + + right_x = rect.x + rect.width * 0.42 + right_w = rect.width * 0.58 - inset + + hardware_items = [] + if cs.canUsePedal: + hardware_items.append(tr("Pedal")) + if cs.hasSASCM: + hardware_items.append(tr("SASCM")) + if cs.canUseSDSU: + hardware_items.append(tr("SDSU")) + if cs.hasZSS: + hardware_items.append(tr("ZSS")) + + hw_text = ", ".join(hardware_items) if hardware_items else tr("Standard") + gui_label(rl.Rectangle(right_x, rect.y + 10, right_w, 22), tr("Hardware"), 20, AetherListColors.MUTED, FontWeight.MEDIUM) + gui_label(rl.Rectangle(right_x, rect.y + 34, right_w, 26), hw_text, 24, AetherListColors.HEADER, FontWeight.MEDIUM) + + fingerprint_state = tr("Forced") if self._controller._params.get_bool("ForceFingerprint") else tr("Auto") + gui_label(rl.Rectangle(right_x, rect.y + 66, right_w, 20), tr("Fingerprint"), 18, AetherListColors.MUTED, FontWeight.MEDIUM) + gui_label(rl.Rectangle(right_x, rect.y + 84, right_w, 20), fingerprint_state, 18, AetherListColors.HEADER, FontWeight.MEDIUM) + + def _measure_content_height(self, width: float) -> float: + content_height = self._measure_active_tab_height(width) + return self.TAB_HEIGHT + self.TAB_BOTTOM_GAP + content_height + + def _measure_active_tab_height(self, width: float) -> float: + if self._active_tab_key == "identity": + return self._section_block_height(self._section_height(3, self.ROW_HEIGHT)) + if self._active_tab_key == "features": + rows = self._build_driving_rows() + if self._uses_two_columns(width): + max_per_col = (len(rows) + 1) // 2 + return self._section_block_height(self._section_height(max_per_col, self.ROW_HEIGHT)) + return self._section_block_height(self._section_height(len(rows), self.ROW_HEIGHT)) + if self._active_tab_key == "controls": + rows = self._build_steering_rows() + if self._uses_two_columns(width): + max_per_col = (len(rows) + 1) // 2 + return self._section_block_height(self._section_height(max_per_col, self.ROW_HEIGHT)) + return self._section_block_height(self._section_height(len(rows), self.ROW_HEIGHT)) + return 0 + + def _draw_scroll_content(self, rect: rl.Rectangle, width: float): + self._interactive_rects.clear() + y = rect.y + self._scroll_offset + self._draw_tabs(rl.Rectangle(rect.x, y, width, self.TAB_HEIGHT)) + y += self.TAB_HEIGHT + self.TAB_BOTTOM_GAP + + if self._active_tab_key == "identity": + self._draw_identity_tab(y, rect.x, width) + elif self._active_tab_key == "features": + self._draw_features_tab(y, rect.x, width) + else: + self._draw_controls_tab(y, rect.x, width) + + def _draw_tabs(self, rect: rl.Rectangle): + if not self._tab_defs: + return + available_w = max(1.0, rect.width) + tab_w = (available_w - self.TAB_GAP * max(0, len(self._tab_defs) - 1)) / max(1, len(self._tab_defs)) + for index, tab in enumerate(self._tab_defs): + tab_rect = rl.Rectangle(rect.x + index * (tab_w + self.TAB_GAP), rect.y, tab_w, self.TAB_HEIGHT) + target_id = f"tab:{tab['id']}" + hovered, pressed = self._interactive_state(target_id, tab_rect, pad_y=4) + draw_tab_card( + tab_rect, + tab["title"], + self._tab_subtitle(tab["id"]), + current=self._active_tab_key == tab["id"], + hovered=hovered, + pressed=pressed, + title_size=19, + subtitle_size=14, + show_underline=True, + style=self.PANEL_STYLE, + ) + + def _draw_identity_tab(self, y: float, x: float, width: float): + rows = [ + {"target_id": "select:CarMake", "type": "select", "title": tr("Car Make"), + "get_value": self._controller._get_display_make, "pill_width": 160}, + {"target_id": "select:CarModel", "type": "select", "title": tr("Car Model"), + "get_value": self._controller._get_display_model, "pill_width": 160}, + {"target_id": "toggle:ForceFingerprint", "type": "toggle", "title": tr("Disable Fingerprinting"), + "subtitle": tr("Manually select vehicle instead of auto-detecting."), + "get_state": lambda: self._controller._params.get_bool("ForceFingerprint")}, + ] + draw_section_header(rl.Rectangle(x, y, width, self.SECTION_HEADER_HEIGHT), tr("Vehicle Identity"), style=self.PANEL_STYLE) + y += self.SECTION_HEADER_HEIGHT + self.SECTION_HEADER_GAP + container_rect = rl.Rectangle(x, y, width, len(rows) * self.ROW_HEIGHT) + draw_list_group_shell(container_rect) + for index, row in enumerate(rows): + row_rect = rl.Rectangle(x, y + index * self.ROW_HEIGHT, width, self.ROW_HEIGHT) + self._draw_row(row_rect, row, is_last=index == len(rows) - 1) + + def _draw_features_tab(self, y: float, x: float, width: float): + rows = self._build_driving_rows() + if not rows: + return + if self._uses_two_columns(width): + column_w = self._column_width(width) + mid = len(rows) // 2 + self._draw_row_group(y, x, column_w, rows[:mid]) + self._draw_row_group(y, x + column_w + self.COLUMN_GAP, column_w, rows[mid:]) + else: + self._draw_row_group(y, x, width, rows) + + def _draw_controls_tab(self, y: float, x: float, width: float): + rows = self._build_steering_rows() + if not rows: + return + if self._uses_two_columns(width): + column_w = self._column_width(width) + mid = len(rows) // 2 + self._draw_row_group(y, x, column_w, rows[:mid]) + self._draw_row_group(y, x + column_w + self.COLUMN_GAP, column_w, rows[mid:]) + else: + self._draw_row_group(y, x, width, rows) + + def _draw_row_group(self, y: float, x: float, width: float, rows: list[dict]): + if not rows: + return y + container_rect = rl.Rectangle(x, y, width, len(rows) * self.ROW_HEIGHT) + draw_list_group_shell(container_rect) + for index, row in enumerate(rows): + row_rect = rl.Rectangle(x, y + index * self.ROW_HEIGHT, width, self.ROW_HEIGHT) + self._draw_row(row_rect, row, is_last=index == len(rows) - 1) + return y + len(rows) * self.ROW_HEIGHT + self.SECTION_GAP + + def _draw_row(self, rect: rl.Rectangle, row: dict, is_last: bool): + target_id = row["target_id"] + hovered, pressed = self._interactive_state(target_id, rect) + row_type = row.get("type", "toggle") + + if row_type == "toggle": + draw_settings_list_row( + rect, title=row["title"], subtitle=row.get("subtitle", ""), + toggle_value=row["get_state"](), hovered=hovered, pressed=pressed, + is_last=is_last, show_chevron=False, title_size=26, subtitle_size=17, + style=self.PANEL_STYLE, + ) + elif row_type == "select": + draw_selection_list_row( + rect, title=row["title"], subtitle=row.get("subtitle", ""), + action_text=row["get_value"](), hovered=hovered, pressed=pressed, + is_last=is_last, action_width=154, action_pill=True, + action_pill_width=row.get("pill_width", 108), action_pill_height=40, + title_size=26, subtitle_size=17, action_text_size=15, + row_separator=self.PANEL_STYLE.divider_color, + action_fill=self.PANEL_STYLE.current_fill, + action_border=self.PANEL_STYLE.current_border, + action_text_color=AetherListColors.HEADER, + ) + elif row_type == "info": + draw_settings_list_row( + rect, title=row["title"], value=row["get_value"](), + hovered=False, pressed=False, is_last=is_last, + show_chevron=False, title_size=26, subtitle_size=17, + style=self.PANEL_STYLE, + ) + + def _build_driving_rows(self) -> list[dict]: + cs = starpilot_state.car_state + rows = [] + rows.append({"target_id": "toggle:DisableOpenpilotLongitudinal", "type": "toggle", + "title": tr("Disable openpilot Long"), "subtitle": tr("Revert to stock longitudinal control."), + "get_state": lambda: self._controller._params.get_bool("DisableOpenpilotLongitudinal")}) + + if cs.isGM and (cs.hasPedal or cs.canUsePedal): + rows.append({"target_id": "toggle:GMPedalLongitudinal", "type": "toggle", + "title": tr("Pedal for Long"), "get_state": lambda: self._controller._params.get_bool("GMPedalLongitudinal")}) + rows.append({"target_id": "toggle:GMDashSpoofOffsets", "type": "toggle", + "title": tr("Offsets on Dash Spoof"), "get_state": lambda: self._controller._params.get_bool("GMDashSpoofOffsets")}) + if cs.isGM: + rows.append({"target_id": "toggle:LongPitch", "type": "toggle", + "title": tr("Smooth Pedal on Hills"), "get_state": lambda: self._controller._params.get_bool("LongPitch")}) + rows.append({"target_id": "toggle:RemoteStartBootsComma", "type": "toggle", + "title": tr("Remote Start Panda"), "get_state": lambda: self._controller._params.get_bool("RemoteStartBootsComma")}) + if cs.isGM and cs.isVolt and not cs.hasSNG: + rows.append({"target_id": "toggle:VoltSNG", "type": "toggle", + "title": tr("Volt SNG Hack"), "get_state": lambda: self._controller._params.get_bool("VoltSNG")}) + if cs.isHKG and cs.isHKGCanFd: + rows.append({"target_id": "toggle:TacoTuneHacks", "type": "toggle", + "title": tr("Taco Bell Torque Hack"), "get_state": lambda: self._controller._params.get_bool("TacoTuneHacks")}) + if cs.isSubaru: + rows.append({"target_id": "toggle:SubaruSNG", "type": "toggle", + "title": tr("Stop and Go"), "get_state": lambda: self._controller._params.get_bool("SubaruSNG")}) + if cs.isToyota: + rows.append({"target_id": "toggle:LockDoors", "type": "toggle", + "title": tr("Auto Lock Doors"), "get_state": lambda: self._controller._params.get_bool("LockDoors")}) + rows.append({"target_id": "toggle:UnlockDoors", "type": "toggle", + "title": tr("Auto Unlock Doors"), "get_state": lambda: self._controller._params.get_bool("UnlockDoors")}) + rows.append({"target_id": "select:LockDoorsTimer", "type": "select", + "title": tr("Lock Doors Timer"), + "get_value": lambda: _lock_doors_timer_labels().get(float(self._controller._params.get_int("LockDoorsTimer")), f"{self._controller._params.get_int('LockDoorsTimer')}s"), + "pill_width": 100}) + rows.append({"target_id": "select:ClusterOffset", "type": "select", + "title": tr("Dashboard Speed Offset"), + "get_value": lambda: f"{self._controller._params.get_float('ClusterOffset'):.3f}x", + "pill_width": 120}) + if cs.isToyota and not cs.hasSNG: + rows.append({"target_id": "toggle:SNGHack", "type": "toggle", + "title": tr("Stop-and-Go Hack"), "get_state": lambda: self._controller._params.get_bool("SNGHack")}) + if cs.isToyota and cs.hasOpenpilotLongitudinal: + rows.append({"target_id": "toggle:FrogsGoMoosTweak", "type": "toggle", + "title": tr("FrogsGoMoo Tweak"), "get_state": lambda: self._controller._params.get_bool("FrogsGoMoosTweak")}) + + rows.append({"target_id": "toggle:RemapCancelToDistance", "type": "toggle", + "title": tr("Remap Cancel Button"), "subtitle": tr("Remap the Cancel button to act as the Distance button."), + "get_state": lambda: self._controller._params.get_bool("RemapCancelToDistance")}) + if cs.isHKGCanFd and cs.hasOpenpilotLongitudinal: + rows.append({"target_id": "toggle:NostalgiaMode", "type": "toggle", + "title": tr("Nostalgia Mode"), + "subtitle": tr("Use the left paddle to pause openpilot acceleration and braking."), + "get_state": lambda: self._controller._params.get_bool("NostalgiaMode")}) + return rows + + def _build_steering_rows(self) -> list[dict]: + cs = starpilot_state.car_state + rows = [] + for key in ("DistanceButtonControl", "LongDistanceButtonControl", "VeryLongDistanceButtonControl"): + rows.append({"target_id": f"select:{key}", "type": "select", "title": tr(self._controller._action_title(key)), + "get_value": lambda k=key: self._controller._get_action_name(k), "pill_width": 140}) + if not cs.isSubaru and not (cs.lkasAllowedForAOL and self._controller._params.get_bool("AlwaysOnLateral") and self._controller._params.get_bool("AlwaysOnLateralLKAS")): + rows.append({"target_id": "select:LKASButtonControl", "type": "select", "title": tr("LKAS Button"), + "get_value": lambda: self._controller._get_action_name("LKASButtonControl"), "pill_width": 140}) + if cs.hasModeStarButtons: + for key in ("ModeButtonControl", "LongModeButtonControl", "VeryLongModeButtonControl", + "StarButtonControl", "LongStarButtonControl", "VeryLongStarButtonControl"): + rows.append({"target_id": f"select:{key}", "type": "select", "title": tr(self._controller._action_title(key)), + "get_value": lambda k=key: self._controller._get_action_name(k), "pill_width": 140}) + return rows + + class StarPilotVehicleSettingsLayout(StarPilotPanel): def __init__(self): super().__init__() self._make_options, self._models_by_make, self._models_by_value, self._make_by_model = get_fingerprint_catalog() - self._sub_panels = { - "gm": StarPilotGMVehicleLayout(), - "hkg": StarPilotHKGVehicleLayout(), - "subaru": StarPilotSubaruVehicleLayout(), - "toyota": StarPilotToyotaVehicleLayout(), - "info": StarPilotVehicleInfoLayout(), + self._manager_view = VehicleSettingsManagerView(self) + + def _render(self, rect: rl.Rectangle): + self._manager_view.render(rect) + + def show_event(self): + super().show_event() + self._manager_view.show_event() + + def hide_event(self): + super().hide_event() + self._manager_view.hide_event() + + def _action_title(self, key: str) -> str: + titles = { + "DistanceButtonControl": "Distance Button", + "LongDistanceButtonControl": "Distance (Long Press)", + "VeryLongDistanceButtonControl": "Distance (Very Long)", + "LKASButtonControl": "LKAS Button", + "ModeButtonControl": "Mode Button", + "LongModeButtonControl": "Mode (Long Press)", + "VeryLongModeButtonControl": "Mode (Very Long)", + "StarButtonControl": "Star Button", + "LongStarButtonControl": "Star (Long Press)", + "VeryLongStarButtonControl": "Star (Very Long)", } + return titles.get(key, key) - self.CATEGORIES = [ - { - "title": tr_noop("Car Make"), - "type": "value", - "get_value": self._get_display_make, - "on_click": self._on_select_make, - "color": "#64748B", - }, - { - "title": tr_noop("Car Model"), - "type": "value", - "get_value": self._get_display_model, - "on_click": self._on_select_model, - "color": "#64748B", - }, - { - "title": tr_noop("Disable Fingerprinting"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("ForceFingerprint"), - "set_state": lambda s: self._params.put_bool("ForceFingerprint", s), - "color": "#64748B", - }, - { - "title": tr_noop("Disable openpilot Long"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("DisableOpenpilotLongitudinal"), - "set_state": self._on_disable_long, - "color": "#64748B", - }, - {"title": tr_noop("GM Settings"), "panel": "gm", "icon": "toggle_icons/icon_vehicle.png", "color": "#64748B", "key": "gm"}, - {"title": tr_noop("HKG Settings"), "panel": "hkg", "icon": "toggle_icons/icon_vehicle.png", "color": "#64748B", "key": "hkg"}, - {"title": tr_noop("Subaru Settings"), "panel": "subaru", "icon": "toggle_icons/icon_vehicle.png", "color": "#64748B", "key": "subaru"}, - {"title": tr_noop("Toyota Settings"), "panel": "toyota", "icon": "toggle_icons/icon_vehicle.png", "color": "#64748B", "key": "toyota"}, - {"title": tr_noop("Vehicle Info"), "panel": "info", "icon": "toggle_icons/icon_vehicle.png", "color": "#64748B"}, - ] - - for name, panel in self._sub_panels.items(): - if hasattr(panel, 'set_navigate_callback'): - panel.set_navigate_callback(self._navigate_to) - if hasattr(panel, 'set_back_callback'): - panel.set_back_callback(self._go_back) - - self._rebuild_grid() - - def _rebuild_grid(self): - if not self.CATEGORIES: - return - if self._tile_grid is None: - self._tile_grid = TileGrid(columns=None, padding=20) - self._tile_grid.clear() + def _get_action_name(self, key: str) -> str: + if key == "LKASButtonControl" and self._params.get_bool("RemapCancelToDistance"): + if self._params.get_int("LKASButtonControl") != 0: + self._params.put_int("LKASButtonControl", 0) + return ACTION_NAME_BY_ID[0] + idx = self._params.get_int(key) + return ACTION_NAME_BY_ID.get(idx, ACTION_NAMES[0]) + def _get_available_actions(self, key: str | None = None) -> list[str]: + if key == "LKASButtonControl" and self._params.get_bool("RemapCancelToDistance"): + return [ACTION_NAME_BY_ID[0]] cs = starpilot_state.car_state - for cat in self.CATEGORIES: - key = cat.get("key") - visible = True + return [o["name"] for o in ACTION_OPTIONS if cs.hasOpenpilotLongitudinal or not o.get("requires_longitudinal", False)] - if key == "gm": - visible = cs.isGM - elif key == "hkg": - visible = cs.isHKG - elif key == "subaru": - visible = cs.isSubaru - elif key == "toyota": - visible = cs.isToyota - - if not visible: - continue - - tile_type = cat.get("type", "hub") - if tile_type == "hub": - on_click = cat.get("on_click") - if on_click is None: - on_click = lambda c=cat: self._navigate_to(c["panel"]) - tile = HubTile( - title=tr(cat["title"]), - desc=tr(cat.get("desc", "")), - icon_path=cat.get("icon"), - on_click=on_click, - starpilot_icon=cat.get("starpilot_icon", True), - bg_color=cat.get("color"), - ) - elif tile_type == "toggle": - tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color")) - elif tile_type == "value": - tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color")) + def _on_toggle(self, param_key: str): + if param_key == "DisableOpenpilotLongitudinal": + current = self._params.get_bool("DisableOpenpilotLongitudinal") + if not current: + def on_confirm(res): + if res == DialogResult.CONFIRM: + self._params.put_bool("DisableOpenpilotLongitudinal", True) + if starpilot_state.started: + HARDWARE.reboot() + gui_app.push_widget(ConfirmDialog(tr("Disable openpilot longitudinal control?"), tr("Disable"), callback=on_confirm)) else: - continue + self._params.put_bool("DisableOpenpilotLongitudinal", False) + return + if param_key == "RemapCancelToDistance": + new_state = not self._params.get_bool("RemapCancelToDistance") + self._params.put_bool("RemapCancelToDistance", new_state) + if new_state and self._params.get_int("LKASButtonControl") != 0: + self._params.put_int("LKASButtonControl", 0) + return + current = self._params.get_bool(param_key) if self._params.get(param_key, encoding="utf-8") is not None else False + self._params.put_bool(param_key, not current) - self._tile_grid.add_tile(tile) - - def _get_display_make(self): - make = self._params.get("CarMake", encoding='utf-8') or "" - if make: - return make - - model = self._params.get("CarModel", encoding='utf-8') or "" - if model: - return self._make_by_model.get(model, tr("None")) - return tr("None") - - def _get_selected_model_option(self) -> FingerprintModelOption | None: - model = self._params.get("CarModel", encoding='utf-8') or "" - if not model: - return None - - model_name = self._params.get("CarModelName", encoding='utf-8') or "" - make = self._params.get("CarMake", encoding='utf-8') or self._make_by_model.get(model, "") - if make and model_name: - for option in self._models_by_make.get(make, ()): - if option.value == model and option.label == model_name: - return option - - return self._models_by_value.get(model) - - def _get_display_model(self): - selected_option = self._get_selected_model_option() - if selected_option is not None: - return selected_option.button_label - - model = self._params.get("CarModel", encoding='utf-8') or "" - model_name = self._params.get("CarModelName", encoding='utf-8') or "" - make = self._params.get("CarMake", encoding='utf-8') or self._make_by_model.get(model, "") - - if model_name: - return shorten_model_label(make, model_name) if make else model_name - if model and model in self._models_by_value: - return self._models_by_value[model].button_label - return tr("None") + def _on_select(self, key: str): + if key == "CarMake": + self._on_select_make() + elif key == "CarModel": + self._on_select_model() + elif key == "LockDoorsTimer": + self._show_lock_timer_selector() + elif key == "ClusterOffset": + self._show_offset_selector() + else: + self._show_action_picker(key) def _on_select_make(self): makes = list(self._make_options) if not makes: gui_app.push_widget(ConfirmDialog(tr("No fingerprint list available."), tr("OK"))) return - - current_make = self._params.get("CarMake", encoding='utf-8') or "" + current_make = self._params.get("CarMake", encoding="utf-8") or "" default_make = current_make if current_make in makes else makes[0] def on_select(res): if res == DialogResult.CONFIRM and dialog.selection: self._params.put("CarMake", dialog.selection) - current_model = self._params.get("CarModel", encoding='utf-8') or "" - available_models = {option.value for option in self._models_by_make.get(dialog.selection, ())} - if current_model not in available_models: + current_model = self._params.get("CarModel", encoding="utf-8") or "" + available = {o.value for o in self._models_by_make.get(dialog.selection, ())} + if current_model not in available: self._params.remove("CarModel") self._params.remove("CarModelName") - self._rebuild_grid() dialog = MultiOptionDialog(tr("Select Make"), makes, default_make, callback=on_select) gui_app.push_widget(dialog) def _on_select_model(self): - make = self._params.get("CarMake", encoding='utf-8') or "" + make = self._params.get("CarMake", encoding="utf-8") or "" if not make: gui_app.push_widget(ConfirmDialog(tr("Please select a Car Make first!"), tr("OK"))) return - model_options = self._models_by_make.get(make, ()) if not model_options: gui_app.push_widget(ConfirmDialog(tr("No models available for this make."), tr("OK"))) return - - option_labels = [option.option_label for option in model_options] - selected_by_label = {option.option_label: option for option in model_options} - current_model = self._params.get("CarModel", encoding='utf-8') or "" - current_model_name = self._params.get("CarModelName", encoding='utf-8') or "" - default_option = next((option.option_label for option in model_options if option.value == current_model and option.label == current_model_name), None) + option_labels = [o.option_label for o in model_options] + selected_by_label = {o.option_label: o for o in model_options} + current_model = self._params.get("CarModel", encoding="utf-8") or "" + current_model_name = self._params.get("CarModelName", encoding="utf-8") or "" + default_option = next((o.option_label for o in model_options if o.value == current_model and o.label == current_model_name), None) if default_option is None: - default_option = next((option.option_label for option in model_options if option.value == current_model), option_labels[0]) + default_option = next((o.option_label for o in model_options if o.value == current_model), option_labels[0]) def on_select(res): if res == DialogResult.CONFIRM and dialog.selection: - selected_option = selected_by_label[dialog.selection] - self._params.put("CarModel", selected_option.value) - self._params.put("CarModelName", selected_option.label) + opt = selected_by_label[dialog.selection] + self._params.put("CarModel", opt.value) + self._params.put("CarModelName", opt.label) self._params.put("CarMake", make) - self._rebuild_grid() dialog = MultiOptionDialog(tr("Select Model"), option_labels, default_option, callback=on_select) gui_app.push_widget(dialog) - def _on_disable_long(self, state): - if state: - - def on_confirm(res): - if res == DialogResult.CONFIRM: - self._params.put_bool("DisableOpenpilotLongitudinal", True) - from openpilot.selfdrive.ui.ui_state import ui_state - - if ui_state.started: - HARDWARE.reboot() - self._rebuild_grid() - - gui_app.push_widget(ConfirmDialog(tr("Disable openpilot longitudinal control?"), tr("Disable"), callback=on_confirm)) - else: - self._params.put_bool("DisableOpenpilotLongitudinal", False) - self._rebuild_grid() - - -class StarPilotGMVehicleLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Pedal for Long"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("GMPedalLongitudinal"), - "set_state": lambda s: self._params.put_bool("GMPedalLongitudinal", s), - "color": "#64748B", - "key": "GMPedalLongitudinal", - }, - { - "title": tr_noop("Offsets on Dash Spoof"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("GMDashSpoofOffsets"), - "set_state": lambda s: self._params.put_bool("GMDashSpoofOffsets", s), - "color": "#64748B", - "key": "GMDashSpoofOffsets", - }, - { - "title": tr_noop("Smooth Pedal on Hills"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("LongPitch"), - "set_state": lambda s: self._params.put_bool("LongPitch", s), - "color": "#64748B", - "key": "LongPitch", - }, - { - "title": tr_noop("Remote Start Panda"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("RemoteStartBootsComma"), - "set_state": lambda s: self._params.put_bool("RemoteStartBootsComma", s), - "color": "#64748B", - }, - { - "title": tr_noop("Volt SNG Hack"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("VoltSNG"), - "set_state": lambda s: self._params.put_bool("VoltSNG", s), - "color": "#64748B", - "key": "VoltSNG", - }, - ] - self._rebuild_grid() - - def _rebuild_grid(self): - if not self.CATEGORIES: + def _show_action_picker(self, key: str): + if key == "LKASButtonControl" and self._params.get_bool("RemapCancelToDistance"): + if self._params.get_int("LKASButtonControl") != 0: + self._params.put_int("LKASButtonControl", 0) return - if self._tile_grid is None: - self._tile_grid = TileGrid(columns=None, padding=20) - self._tile_grid.clear() + actions = self._get_available_actions(key) + current = self._get_action_name(key) + if current not in actions: + current = actions[0] - cs = starpilot_state.car_state - for cat in self.CATEGORIES: - key = cat.get("key") - visible = True + def on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + self._params.put_int(key, ACTION_IDS.get(dialog.selection, 0)) - if key == "GMPedalLongitudinal": - visible = cs.hasPedal or cs.canUsePedal - elif key == "GMDashSpoofOffsets": - visible = cs.hasPedal or cs.canUsePedal - elif key == "VoltSNG": - visible = cs.isVolt and not cs.hasSNG - - if not visible: - continue - - tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color")) - self._tile_grid.add_tile(tile) - - -class StarPilotHKGVehicleLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Taco Bell Torque Hack"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("TacoTuneHacks"), - "set_state": lambda s: self._params.put_bool("TacoTuneHacks", s), - "color": "#64748B", - "key": "TacoTuneHacks", - }, - ] - self._rebuild_grid() - - def _rebuild_grid(self): - if not self.CATEGORIES: - return - if self._tile_grid is None: - self._tile_grid = TileGrid(columns=None, padding=20) - self._tile_grid.clear() - - cs = starpilot_state.car_state - for cat in self.CATEGORIES: - key = cat.get("key") - visible = True - - if key == "TacoTuneHacks": - visible = cs.isHKGCanFd - - if not visible: - continue - - tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color")) - self._tile_grid.add_tile(tile) - - -class StarPilotSubaruVehicleLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Stop and Go"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("SubaruSNG"), - "set_state": lambda s: self._params.put_bool("SubaruSNG", s), - "color": "#64748B", - }, - ] - self._rebuild_grid() - - -class StarPilotToyotaVehicleLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Auto Lock Doors"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("LockDoors"), - "set_state": lambda s: self._params.put_bool("LockDoors", s), - "color": "#64748B", - }, - { - "title": tr_noop("Auto Unlock Doors"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("UnlockDoors"), - "set_state": lambda s: self._params.put_bool("UnlockDoors", s), - "color": "#64748B", - }, - { - "title": tr_noop("Lock Doors Timer"), - "type": "value", - "get_value": lambda: _lock_doors_timer_labels().get(self._params.get_int('LockDoorsTimer'), f"{self._params.get_int('LockDoorsTimer')}s"), - "on_click": self._show_lock_timer_selector, - "color": "#64748B", - }, - { - "title": tr_noop("Dashboard Speed Offset"), - "type": "value", - "get_value": lambda: f"{self._params.get_float('ClusterOffset'):.3f}x", - "on_click": self._show_offset_selector, - "color": "#64748B", - }, - { - "title": tr_noop("Stop-and-Go Hack"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("SNGHack"), - "set_state": lambda s: self._params.put_bool("SNGHack", s), - "color": "#64748B", - "key": "SNGHack", - }, - { - "title": tr_noop("FrogsGoMoo Tweak"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("FrogsGoMoosTweak"), - "set_state": lambda s: self._params.put_bool("FrogsGoMoosTweak", s), - "color": "#64748B", - "key": "FrogsGoMoosTweak", - }, - ] - self._rebuild_grid() - - def _rebuild_grid(self): - if not self.CATEGORIES: - return - if self._tile_grid is None: - self._tile_grid = TileGrid(columns=None, padding=20) - self._tile_grid.clear() - - cs = starpilot_state.car_state - for cat in self.CATEGORIES: - key = cat.get("key") - visible = True - - if key == "SNGHack": - visible = not cs.hasSNG - elif key == "FrogsGoMoosTweak": - visible = cs.hasOpenpilotLongitudinal - - if not visible: - continue - - tile_type = cat.get("type", "hub") - if tile_type == "toggle": - tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color")) - elif tile_type == "value": - tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color")) - else: - continue - - self._tile_grid.add_tile(tile) + dialog = MultiOptionDialog(tr(key), actions, current, callback=on_select) + gui_app.push_widget(dialog) def _show_lock_timer_selector(self): def on_close(res, val): if res == DialogResult.CONFIRM: self._params.put_int("LockDoorsTimer", int(val)) - self._rebuild_grid() - gui_app.push_widget( - AetherSliderDialog(tr("Lock Doors Timer"), 0, 300, 5, self._params.get_int("LockDoorsTimer"), on_close, labels=_lock_doors_timer_labels(), color="#64748B") - ) + gui_app.push_widget(AetherSliderDialog(tr("Lock Doors Timer"), 0, 300, 5, + self._params.get_int("LockDoorsTimer"), on_close, labels=_lock_doors_timer_labels())) def _show_offset_selector(self): def on_close(res, val): if res == DialogResult.CONFIRM: self._params.put_float("ClusterOffset", float(val)) - self._rebuild_grid() - gui_app.push_widget( - AetherSliderDialog(tr("Dashboard Speed Offset"), 1.000, 1.050, 0.001, self._params.get_float("ClusterOffset"), on_close, unit="x", color="#64748B") - ) + gui_app.push_widget(AetherSliderDialog(tr("Dashboard Speed Offset"), 1.000, 1.050, 0.001, + self._params.get_float("ClusterOffset"), on_close, unit="x")) + def _get_display_make(self) -> str: + make = self._params.get("CarMake", encoding="utf-8") or "" + if make: + return make + model = self._params.get("CarModel", encoding="utf-8") or "" + if model: + return self._make_by_model.get(model, tr("None")) + return tr("None") -class StarPilotVehicleInfoLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Radar Support"), - "type": "value", - "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasRadar else tr("No"), - "on_click": lambda: None, - "color": "#64748B", - }, - { - "title": tr_noop("Longitudinal Support"), - "type": "value", - "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasOpenpilotLongitudinal else tr("No"), - "on_click": lambda: None, - "color": "#64748B", - }, - { - "title": tr_noop("Blind Spot Support"), - "type": "value", - "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasBSM else tr("No"), - "on_click": lambda: None, - "color": "#64748B", - }, - { - "title": tr_noop("Hardware Detected"), - "type": "value", - "get_value": lambda: ( - ", ".join( - filter( - None, - [ - tr("Pedal") if starpilot_state.car_state.canUsePedal else "", - tr("SASCM") if starpilot_state.car_state.hasSASCM else "", - tr("SDSU") if starpilot_state.car_state.canUseSDSU else "", - tr("ZSS") if starpilot_state.car_state.hasZSS else "", - ], - ) - ) - or tr("None") - ), - "on_click": lambda: None, - "color": "#64748B", - }, - { - "title": tr_noop("Pedal Support"), - "type": "value", - "get_value": lambda: tr("Yes") if starpilot_state.car_state.canUsePedal else tr("No"), - "on_click": lambda: None, - "color": "#64748B", - }, - { - "title": tr_noop("SDSU Support"), - "type": "value", - "get_value": lambda: tr("Yes") if starpilot_state.car_state.canUseSDSU else tr("No"), - "on_click": lambda: None, - "color": "#64748B", - }, - { - "title": tr_noop("SNG Support"), - "type": "value", - "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasSNG else tr("No"), - "on_click": lambda: None, - "color": "#64748B", - }, - ] - self._rebuild_grid() + def _get_display_model(self) -> str: + selected = self._get_selected_model_option() + if selected is not None: + return selected.button_label + model = self._params.get("CarModel", encoding="utf-8") or "" + model_name = self._params.get("CarModelName", encoding="utf-8") or "" + make = self._params.get("CarMake", encoding="utf-8") or self._make_by_model.get(model, "") + if model_name: + return shorten_model_label(make, model_name) if make else model_name + if model and model in self._models_by_value: + return self._models_by_value[model].button_label + return tr("None") + + def _get_selected_model_option(self) -> FingerprintModelOption | None: + model = self._params.get("CarModel", encoding="utf-8") or "" + if not model: + return None + model_name = self._params.get("CarModelName", encoding="utf-8") or "" + make = self._params.get("CarMake", encoding="utf-8") or self._make_by_model.get(model, "") + if make and model_name: + for option in self._models_by_make.get(make, ()): + if option.value == model and option.label == model_name: + return option + return self._models_by_value.get(model) diff --git a/selfdrive/ui/layouts/settings/starpilot/wheel.py b/selfdrive/ui/layouts/settings/starpilot/wheel.py deleted file mode 100644 index 30272d513..000000000 --- a/selfdrive/ui/layouts/settings/starpilot/wheel.py +++ /dev/null @@ -1,211 +0,0 @@ -from __future__ import annotations -from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog -from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel - -ACTION_OPTIONS = [ - {"id": 0, "name": "No Action"}, - {"id": 1, "name": "Change Personality", "requires_longitudinal": True}, - {"id": 2, "name": "Force Coast", "requires_longitudinal": True}, - {"id": 3, "name": "Pause Steering"}, - {"id": 4, "name": "Pause Accel/Brake", "requires_longitudinal": True}, - {"id": 5, "name": "Toggle Experimental", "requires_longitudinal": True}, - {"id": 6, "name": "Toggle Traffic", "requires_longitudinal": True}, - {"id": 7, "name": "Toggle Switchback"}, - {"id": 8, "name": "Create Bookmark"}, -] -ACTION_NAMES = [option["name"] for option in ACTION_OPTIONS] -ACTION_IDS = {option["name"]: option["id"] for option in ACTION_OPTIONS} -ACTION_NAME_BY_ID = {option["id"]: option["name"] for option in ACTION_OPTIONS} - - -class StarPilotWheelLayout(StarPilotPanel): - def __init__(self): - super().__init__() - self.CATEGORIES = [ - { - "title": tr_noop("Remap Cancel Button"), - "type": "toggle", - "get_state": lambda: self._params.get_bool("RemapCancelToDistance"), - "set_state": self._set_cancel_remap_state, - "color": "#64748B", - }, - { - "title": tr_noop("Nostalgia Mode"), - "desc": tr_noop("Use the left paddle to pause openpilot acceleration and braking while Always On Lateral stays active on supported Hyundai CAN-FD cars."), - "type": "toggle", - "get_state": lambda: self._params.get_bool("NostalgiaMode"), - "set_state": lambda s: self._params.put_bool("NostalgiaMode", s), - "key": "NostalgiaMode", - "color": "#64748B", - }, - { - "title": tr_noop("Distance Button"), - "type": "value", - "get_value": lambda: self._get_action_name("DistanceButtonControl"), - "on_click": lambda: self._show_action_picker("DistanceButtonControl"), - "color": "#64748B", - }, - { - "title": tr_noop("Distance (Long Press)"), - "type": "value", - "get_value": lambda: self._get_action_name("LongDistanceButtonControl"), - "on_click": lambda: self._show_action_picker("LongDistanceButtonControl"), - "color": "#64748B", - }, - { - "title": tr_noop("Distance (Very Long)"), - "type": "value", - "get_value": lambda: self._get_action_name("VeryLongDistanceButtonControl"), - "on_click": lambda: self._show_action_picker("VeryLongDistanceButtonControl"), - "color": "#64748B", - }, - { - "title": tr_noop("LKAS Button"), - "type": "value", - "get_value": lambda: self._get_action_name("LKASButtonControl"), - "on_click": lambda: self._show_action_picker("LKASButtonControl"), - "is_enabled": lambda: not self._lkas_locked(), - "key": "LKASButtonControl", - "color": "#64748B", - }, - { - "title": tr_noop("Mode Button"), - "type": "value", - "get_value": lambda: self._get_action_name("ModeButtonControl"), - "on_click": lambda: self._show_action_picker("ModeButtonControl"), - "key": "ModeButtonControl", - "color": "#64748B", - }, - { - "title": tr_noop("Mode (Long Press)"), - "type": "value", - "get_value": lambda: self._get_action_name("LongModeButtonControl"), - "on_click": lambda: self._show_action_picker("LongModeButtonControl"), - "key": "LongModeButtonControl", - "color": "#64748B", - }, - { - "title": tr_noop("Mode (Very Long)"), - "type": "value", - "get_value": lambda: self._get_action_name("VeryLongModeButtonControl"), - "on_click": lambda: self._show_action_picker("VeryLongModeButtonControl"), - "key": "VeryLongModeButtonControl", - "color": "#64748B", - }, - { - "title": tr_noop("Star Button"), - "type": "value", - "get_value": lambda: self._get_action_name("StarButtonControl"), - "on_click": lambda: self._show_action_picker("StarButtonControl"), - "key": "StarButtonControl", - "color": "#64748B", - }, - { - "title": tr_noop("Star (Long Press)"), - "type": "value", - "get_value": lambda: self._get_action_name("LongStarButtonControl"), - "on_click": lambda: self._show_action_picker("LongStarButtonControl"), - "key": "LongStarButtonControl", - "color": "#64748B", - }, - { - "title": tr_noop("Star (Very Long)"), - "type": "value", - "get_value": lambda: self._get_action_name("VeryLongStarButtonControl"), - "on_click": lambda: self._show_action_picker("VeryLongStarButtonControl"), - "key": "VeryLongStarButtonControl", - "color": "#64748B", - }, - ] - self._rebuild_grid() - - def _lkas_locked(self): - return self._params.get_bool("RemapCancelToDistance") - - def _force_lkas_no_action(self): - if self._params.get_int("LKASButtonControl") != 0: - self._params.put_int("LKASButtonControl", 0) - self._params_memory.put_bool("StarPilotTogglesUpdated", True) - - def _set_cancel_remap_state(self, state): - self._params.put_bool("RemapCancelToDistance", state) - if state: - self._force_lkas_no_action() - self._rebuild_grid() - - def _get_action_name(self, key): - if key == "LKASButtonControl" and self._lkas_locked(): - self._force_lkas_no_action() - return ACTION_NAME_BY_ID[0] - - idx = self._params.get_int(key) - return ACTION_NAME_BY_ID.get(idx, ACTION_NAMES[0]) - - def _get_available_actions(self, key=None): - if key == "LKASButtonControl" and self._lkas_locked(): - return [ACTION_NAME_BY_ID[0]] - - cs = starpilot_state.car_state - return [ - option["name"] for option in ACTION_OPTIONS - if cs.hasOpenpilotLongitudinal or not option.get("requires_longitudinal", False) - ] - - def _show_action_picker(self, key): - if key == "LKASButtonControl" and self._lkas_locked(): - self._force_lkas_no_action() - self._rebuild_grid() - return - - actions = self._get_available_actions(key) - current = self._get_action_name(key) - if current not in actions: - current = actions[0] - - def on_select(res): - if res == DialogResult.CONFIRM and dialog.selection: - self._params.put_int(key, ACTION_IDS.get(dialog.selection, 0)) - self._params_memory.put_bool("StarPilotTogglesUpdated", True) - self._rebuild_grid() - - dialog = MultiOptionDialog(tr(key), actions, current, callback=on_select) - gui_app.push_widget(dialog) - - def _rebuild_grid(self): - if not self.CATEGORIES: - return - if self._lkas_locked(): - self._force_lkas_no_action() - if self._tile_grid is None: - self._tile_grid = __import__('openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid', fromlist=['TileGrid']).TileGrid(columns=None, padding=20) - self._tile_grid.clear() - cs = starpilot_state.car_state - for cat in self.CATEGORIES: - key = cat.get("key") - visible = True - if key == "LKASButtonControl": - visible &= not cs.isSubaru - visible &= not (cs.lkasAllowedForAOL and self._params.get_bool("AlwaysOnLateral") and self._params.get_bool("AlwaysOnLateralLKAS")) - if key == "NostalgiaMode": - visible &= cs.isHKGCanFd and cs.hasOpenpilotLongitudinal - if key in ("ModeButtonControl", "LongModeButtonControl", "VeryLongModeButtonControl", - "StarButtonControl", "LongStarButtonControl", "VeryLongStarButtonControl"): - visible &= cs.hasModeStarButtons - if not visible: - continue - tile_type = cat.get("type", "hub") - if tile_type == "toggle": - from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ToggleTile - - tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], bg_color=cat.get("color")) - elif tile_type == "value": - from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ValueTile - - tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], bg_color=cat.get("color"), is_enabled=cat.get("is_enabled")) - else: - continue - self._tile_grid.add_tile(tile)