ui: vehicle brand settings (#1509)

* commaai/openpilot:d05cb31e2e916fba41ba8167030945f427fd811b

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* sunnypilot: remove Qt

* cabana: revert to stock Qt

* commaai/openpilot:5198b1b079c37742c1050f02ce0aa6dd42b038b9

* commaai/openpilot:954b567b9ba0f3d1ae57d6aa7797fa86dd92ec6e

* commaai/openpilot:7534b2a160faa683412c04c1254440e338931c5e

* sum more

* bump opendbc

* not yet

* should've been symlink'ed

* raylib says wut

* quiet mode back

* more fixes

* no more

* too extra red diff on the side

* need to bring this back

* too extra

* let's update docs here

* Revert "let's update docs here"

This reverts commit 51fe03cd5121e6fdf14657b2c33852c34922b851.

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* multi-button

* Lint

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* Panels. With Icons. And Scroller.

* patience, grasshopper

* more patience, grasshopper

* sp raylib preview

* fix callback

* fix ui preview

* add ui previews

* dialog txt

* compare vs what used to be done before InputDialog

* introducing ui_state_sp for py

* param to control stock vs sp ui

* better

* add ui_update callback

* better padding

* this

* support for next line multi-button

* uhh

* disabled colors

* listitem -> listitemsp

* listitem -> listitemsp

* Revert "add ui_update callback"

This reverts commit 4da32cc0097434aab0aa6a3c35465eabb23c8958.

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* scroller -> scroller_tici

* scroller -> scroller_tici

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* lint

* no fancy toggles :(

* match them

* mici scroller - no touchy

* no

* more

* size adjustments

* fix scroller. yay

* ui vehicle panel: platform selector

* some brands

* platform selector

* bruh

* ui_state_sp

* o

* is_offroad() and format

* huh

* use in toggles panel

* ugh. no

* better & animated

* lint

* cleanup

* lint. LINT

* slight

* rebase

* no more

* rebae

* vic

* v

* # Conflicts:
#	system/ui/sunnypilot/lib/styles.py
#	system/ui/sunnypilot/widgets/helpers/fuzzy_search.py

* loathing loathing, unadulterated loathing, i loathe it all

* more changes!

* Update styles.py

* set visibility

* Update hyundai.py

* add padding

* use symlink on sp side

* use make from json and show all actual makes

* all done!

* Revert "all done!"

This reverts commit 595c45f057d97bd317e4a0518755d393aa289d0c.

* reimpl onroad/offroad confirmation

* use global offroad directly

* ui: highlight on pressed, and less indent

* inherit MultiOptionDialog main buttons

* align top level folders to the edge properly

* lint

* lint

* handle folder presses too

* ui: overridable title and subtitle for `TreeOptionDialog`

* override TreeOptionDialog title and subtitle

* lint

* more

* ui: magnifying glass icon and new search bar style in `TreeDialog`

* cleanup

* ui: recreate search dialog with the latest query

* make model year but display as platform

* move into settings directory

* move into dir

* sync

* equality

* use singleton directly

* also use singleton directly

* inherit from base class

* include all brands

* added refresh

* always assume it's subaru

* slight

* split get brand

* hyundai changes

* tesla changes

* do not allow while offroad

* fix

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: nayan <nayan8teen@gmail.com>
This commit is contained in:
James Vecellio-Grant
2025-12-02 11:44:50 -08:00
committed by GitHub
parent 138d637bbd
commit 3c5841ff02
19 changed files with 405 additions and 3 deletions
@@ -9,6 +9,7 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.list_view import ButtonAction
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.factory import BrandSettingsFactory
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.platform_selector import PlatformSelector, LegendWidget
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
@@ -17,6 +18,9 @@ from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
class VehicleLayout(Widget):
def __init__(self):
super().__init__()
self._brand_settings = None
self._brand_items = []
self._current_brand = None
self._platform_selector = PlatformSelector(self._update_brand_settings)
self._vehicle_item = ListItemSP(title=self._platform_selector.text, action_item=ButtonAction(text=tr("Select")),
@@ -27,14 +31,33 @@ class VehicleLayout(Widget):
self.items = [self._vehicle_item, self._legend_widget]
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
@staticmethod
def get_brand():
if bundle := ui_state.params.get("CarPlatformBundle"):
return bundle.get("brand", "")
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
return ui_state.CP.brand
return ""
def _update_brand_settings(self):
self._vehicle_item._title = self._platform_selector.text
self._vehicle_item.title_color = self._platform_selector.color
vehicle_text = tr("Remove") if ui_state.params.get("CarPlatformBundle") else tr("Select")
self._vehicle_item.action_item.set_text(vehicle_text)
brand = self.get_brand()
if brand != self._current_brand:
self._current_brand = brand
self._brand_settings = BrandSettingsFactory.create_brand_settings(brand)
self._brand_items = self._brand_settings.items if self._brand_settings else []
self.items = [self._vehicle_item, self._legend_widget] + self._brand_items
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
def _update_state(self):
self._update_brand_settings()
if self._brand_settings:
self._brand_settings.update_settings()
self._platform_selector.refresh()
def _render(self, rect):
@@ -0,0 +1,16 @@
"""
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.
"""
import abc
class BrandSettings(abc.ABC):
def __init__(self):
self.items = []
@abc.abstractmethod
def update_settings(self) -> None:
"""Update the settings based on the current vehicle brand."""
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class BodySettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class ChryslerSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,45 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.body import BodySettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.chrysler import ChryslerSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.ford import FordSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.gm import GMSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.honda import HondaSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.hyundai import HyundaiSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.mazda import MazdaSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.nissan import NissanSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.psa import PSASettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.rivian import RivianSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.subaru import SubaruSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.tesla import TeslaSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.toyota import ToyotaSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.volkswagen import VolkswagenSettings
class BrandSettingsFactory:
_BRAND_MAP: dict[str, type[BrandSettings]] = {
"body": BodySettings,
"chrysler": ChryslerSettings,
"ford": FordSettings,
"gm": GMSettings,
"honda": HondaSettings,
"hyundai": HyundaiSettings,
"mazda": MazdaSettings,
"nissan": NissanSettings,
"psa": PSASettings,
"rivian": RivianSettings,
"subaru": SubaruSettings,
"tesla": TeslaSettings,
"toyota": ToyotaSettings,
"volkswagen": VolkswagenSettings,
}
@staticmethod
def create_brand_settings(brand: str) -> BrandSettings | None:
cls = BrandSettingsFactory._BRAND_MAP.get(brand)
return cls() if cls is not None else None
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class FordSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class GMSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class HondaSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,59 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
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 opendbc.car.hyundai.values import CAR, CANFD_UNSUPPORTED_LONGITUDINAL_CAR, UNSUPPORTED_LONGITUDINAL_CAR
class HyundaiSettings(BrandSettings):
def __init__(self):
super().__init__()
self.alpha_long_available = False
tuning_texts = [tr("Off"), tr("Dynamic"), tr("Predictive")]
self.longitudinal_tuning_item = multiple_button_item_sp(tr("Custom Longitudinal Tuning"), "", tuning_texts,
button_width=300, callback=self._on_tuning_selected,
param="HyundaiLongitudinalTuning", inline=False)
self.items = [self.longitudinal_tuning_item]
@staticmethod
def _on_tuning_selected(index):
ui_state.params.put("HyundaiLongitudinalTuning", index)
def update_settings(self):
self.alpha_long_available = False
bundle = ui_state.params.get("CarPlatformBundle")
if bundle:
platform = bundle.get("platform")
self.alpha_long_available = CAR[platform] not in (UNSUPPORTED_LONGITUDINAL_CAR | CANFD_UNSUPPORTED_LONGITUDINAL_CAR)
elif ui_state.CP:
self.alpha_long_available = ui_state.CP.alphaLongitudinalAvailable
tuning_param = int(ui_state.params.get("HyundaiLongitudinalTuning") or "0")
long_enabled = ui_state.has_longitudinal_control
long_tuning_descs = [
tr("Your vehicle will use the Default longitudinal tuning."),
tr("Your vehicle will use the Dynamic longitudinal tuning."),
tr("Your vehicle will use the Predictive longitudinal tuning."),
]
long_tuning_desc = long_tuning_descs[tuning_param] if tuning_param < len(long_tuning_descs) else long_tuning_descs[0]
longitudinal_tuning_disabled = not ui_state.is_offroad() or not long_enabled
if longitudinal_tuning_disabled:
if not ui_state.is_offroad():
long_tuning_desc = tr("This feature is unavailable while the car is onroad.")
elif not long_enabled:
long_tuning_desc = tr("This feature is unavailable because sunnypilot Longitudinal Control (Alpha) is not enabled.")
self.longitudinal_tuning_item.action_item.set_enabled(not longitudinal_tuning_disabled)
self.longitudinal_tuning_item.set_description(long_tuning_desc)
self.longitudinal_tuning_item.show_description(True)
self.longitudinal_tuning_item.action_item.set_selected_button(tuning_param)
self.longitudinal_tuning_item.set_visible(self.alpha_long_available)
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class MazdaSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class NissanSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class PSASettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class RivianSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,54 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
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 toggle_item_sp
from opendbc.car.subaru.values import CAR, SubaruFlags
class SubaruSettings(BrandSettings):
def __init__(self):
super().__init__()
self.has_stop_and_go = False
self.stop_and_go_toggle = toggle_item_sp(tr("Stop and Go (Beta)"), "", param="SubaruStopAndGo", callback=self._on_toggle_changed)
self.stop_and_go_manual_parking_brake_toggle = toggle_item_sp(tr("Stop and Go for Manual Parking Brake (Beta)"), "",
param="SubaruStopAndGoManualParkingBrake", callback=self._on_toggle_changed)
self.items = [self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle]
def _on_toggle_changed(self, _):
self.update_settings()
def stop_and_go_disabled_msg(self):
if not self.has_stop_and_go:
return tr("This feature is currently not available on this platform.")
elif not ui_state.is_offroad():
return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.")
return ""
def update_settings(self):
bundle = ui_state.params.get("CarPlatformBundle")
if bundle:
platform = bundle.get("platform")
config = CAR[platform].config
self.has_stop_and_go = not (config.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
elif ui_state.CP:
self.has_stop_and_go = not (ui_state.CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
disabled_msg = self.stop_and_go_disabled_msg()
descriptions = [
tr("Experimental feature to enable auto-resume during stop-and-go for certain supported Subaru platforms."),
tr("Experimental feature to enable stop and go for Subaru Global models with manual handbrake. " +
"Models with electric parking brake should keep this disabled. Thanks to martinl for this implementation!")
]
for toggle, desc in zip([self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle], descriptions, strict=True):
toggle.action_item.set_enabled(self.has_stop_and_go and ui_state.is_offroad())
toggle.set_description(f"<b>{disabled_msg}</b><br><br>{desc}" if disabled_msg else desc)
@@ -0,0 +1,43 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
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 toggle_item_sp
COOP_STEERING_MIN_KMH = 23
OEM_STEERING_MIN_KMH = 48
KM_TO_MILE = 0.621371
class TeslaSettings(BrandSettings):
def __init__(self):
super().__init__()
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
self.items = [self.coop_steering_toggle]
def update_settings(self):
is_metric = ui_state.is_metric
unit = "km/h" if is_metric else "mph"
display_value_coop = COOP_STEERING_MIN_KMH if is_metric else round(COOP_STEERING_MIN_KMH * KM_TO_MILE)
display_value_oem = OEM_STEERING_MIN_KMH if is_metric else round(OEM_STEERING_MIN_KMH * KM_TO_MILE)
coop_steering_disabled_msg = tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.")
coop_steering_warning = tr(f"Warning: May experience steering oscillations below {display_value_oem} {unit} during turns, " +
"recommend disabling this feature if you experience these.")
coop_steering_desc = (
f"<b>{coop_steering_warning}</b><br><br>" +
f"{tr('Allows the driver to provide limited steering input while openpilot is engaged.')}<br>" +
f"{tr(f'Only works above {display_value_coop} {unit}.')}"
)
if not ui_state.is_offroad():
coop_steering_desc = f"<b>{coop_steering_disabled_msg}</b><br><br>{coop_steering_desc}"
self.coop_steering_toggle.set_description(coop_steering_desc)
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class ToyotaSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class VolkswagenSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass
@@ -123,19 +123,16 @@ class PlatformSelector(Button):
gui_app.set_modal_overlay(dialog, callback=callback)
def refresh(self):
self.brand = ""
self.color = style.YELLOW
self._platform = tr("Unrecognized Vehicle")
self.set_text(tr("No vehicle selected"))
if bundle := ui_state.params.get("CarPlatformBundle"):
self._platform = bundle.get("name", "")
self.brand = bundle.get("brand", "")
self.set_text(self._platform)
self.color = style.BLUE
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
self._platform = ui_state.CP.carFingerprint
self.brand = ui_state.CP.brand
self.set_text(self._platform)
self.color = style.GREEN
self.set_enabled(True)