From 138d637bbd1918175c3de28f9670880be8e5a528 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Dec 2025 13:34:35 -0500 Subject: [PATCH 1/8] ui: couple Platform Selector refresh in Vehicle panel state updates (#1545) --- selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py index 23b861f8c..1b6c35df2 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py @@ -35,6 +35,7 @@ class VehicleLayout(Widget): def _update_state(self): self._update_brand_settings() + self._platform_selector.refresh() def _render(self, rect): self._scroller.render(rect) From 3c5841ff02966f973b7ff0e339efcb1b6af6d446 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:44:50 -0800 Subject: [PATCH 2/8] ui: vehicle brand settings (#1509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: nayan --- .../layouts/settings/vehicle/__init__.py | 23 ++++++++ .../settings/vehicle/brands/__init__.py | 0 .../layouts/settings/vehicle/brands/base.py | 16 +++++ .../layouts/settings/vehicle/brands/body.py | 15 +++++ .../settings/vehicle/brands/chrysler.py | 15 +++++ .../settings/vehicle/brands/factory.py | 45 ++++++++++++++ .../layouts/settings/vehicle/brands/ford.py | 15 +++++ .../layouts/settings/vehicle/brands/gm.py | 15 +++++ .../layouts/settings/vehicle/brands/honda.py | 15 +++++ .../settings/vehicle/brands/hyundai.py | 59 +++++++++++++++++++ .../layouts/settings/vehicle/brands/mazda.py | 15 +++++ .../layouts/settings/vehicle/brands/nissan.py | 15 +++++ .../layouts/settings/vehicle/brands/psa.py | 15 +++++ .../layouts/settings/vehicle/brands/rivian.py | 15 +++++ .../layouts/settings/vehicle/brands/subaru.py | 54 +++++++++++++++++ .../layouts/settings/vehicle/brands/tesla.py | 43 ++++++++++++++ .../layouts/settings/vehicle/brands/toyota.py | 15 +++++ .../settings/vehicle/brands/volkswagen.py | 15 +++++ .../settings/vehicle/platform_selector.py | 3 - 19 files changed, 405 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/__init__.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/base.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/body.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/chrysler.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/factory.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/ford.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/gm.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/honda.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/mazda.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/nissan.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/psa.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/rivian.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py index 1b6c35df2..86d9e6169 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/__init__.py @@ -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): diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/__init__.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/base.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/base.py new file mode 100644 index 000000000..8d83fdf91 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/base.py @@ -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.""" diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/body.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/body.py new file mode 100644 index 000000000..d1c9ea5d6 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/body.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/chrysler.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/chrysler.py new file mode 100644 index 000000000..ad62dba56 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/chrysler.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/factory.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/factory.py new file mode 100644 index 000000000..678732296 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/factory.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/ford.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/ford.py new file mode 100644 index 000000000..8871087e0 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/ford.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/gm.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/gm.py new file mode 100644 index 000000000..edcd17cdb --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/gm.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/honda.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/honda.py new file mode 100644 index 000000000..fec68795a --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/honda.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py new file mode 100644 index 000000000..f6849eb20 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/hyundai.py @@ -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) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/mazda.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/mazda.py new file mode 100644 index 000000000..d354f0f34 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/mazda.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/nissan.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/nissan.py new file mode 100644 index 000000000..7b3446a1a --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/nissan.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/psa.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/psa.py new file mode 100644 index 000000000..6b767d332 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/psa.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/rivian.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/rivian.py new file mode 100644 index 000000000..876aa2d2e --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/rivian.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py new file mode 100644 index 000000000..66e7ec1d5 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/subaru.py @@ -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"{disabled_msg}

{desc}" if disabled_msg else desc) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py new file mode 100644 index 000000000..46d536c65 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py @@ -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"{coop_steering_warning}

" + + f"{tr('Allows the driver to provide limited steering input while openpilot is engaged.')}
" + + f"{tr(f'Only works above {display_value_coop} {unit}.')}" + ) + + if not ui_state.is_offroad(): + coop_steering_desc = f"{coop_steering_disabled_msg}

{coop_steering_desc}" + + self.coop_steering_toggle.set_description(coop_steering_desc) + self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad()) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py new file mode 100644 index 000000000..e061a8a22 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/toyota.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py new file mode 100644 index 000000000..a6d44c5e4 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/volkswagen.py @@ -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 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py index 60bc335ce..db9059527 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle/platform_selector.py @@ -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) From f08dfa1c14f295209d134db0a8ef9b0de3dce1cb Mon Sep 17 00:00:00 2001 From: Nayan Date: Wed, 3 Dec 2025 00:20:40 -0500 Subject: [PATCH 3/8] ui: sunnylink client-side implementation (#1488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * param to control stock vs sp ui * init styles * SP Toggles * Lint * optimizations * sp raylib preview * fix callback * fix ui preview * sunnylink state * introducing ui_state_sp for py * poll from ui_state_sp * cloudlog & ruff * param to control stock vs sp ui * better * better padding * this * listitem -> listitemsp * add show_description method * remove padding from line separator. like, WHY? 😩😩 * ui: `GuiApplicationExt` * add to readme * use gui_app.sunnypilot_ui() * use gui_app.sunnypilot_ui() * fetch only when connected to network * final --------- Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/ui_state.py | 5 +- sunnypilot/sunnylink/sunnylink_state.py | 205 ++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 sunnypilot/sunnylink/sunnylink_state.py diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 18e35508b..1db5ad613 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -6,6 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ from cereal import messaging, custom from openpilot.common.params import Params +from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState class UIStateSP: @@ -16,8 +17,10 @@ class UIStateSP: "gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP" ] + self.sunnylink_state = SunnylinkState() + def update(self) -> None: - pass + self.sunnylink_state.start() def update_params(self) -> None: CP_SP_bytes = self.params.get("CarParamsSPPersistent") diff --git a/sunnypilot/sunnylink/sunnylink_state.py b/sunnypilot/sunnylink/sunnylink_state.py new file mode 100644 index 000000000..4d0b397e0 --- /dev/null +++ b/sunnypilot/sunnylink/sunnylink_state.py @@ -0,0 +1,205 @@ +""" +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 enum import IntEnum +import threading +import time +import json + +from cereal import messaging +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID, SunnylinkApi + + +class RoleType(IntEnum): + READONLY = 0 + SPONSOR = 1 + ADMIN = 2 + + +class SponsorTier(IntEnum): + FREE = 0 + NOVICE = 1 + SUPPORTER = 2 + CONTRIBUTOR = 3 + BENEFACTOR = 4 + GUARDIAN = 5 + + +class User: + device_id: str + user_id: str + created_at: int + updated_at: int + token_hash: str + + def __init__(self, json_data): + self.device_id = json_data.get("device_id") + self.user_id = json_data.get("user_id") + self.created_at = json_data.get("created_at") + self.updated_at = json_data.get("updated_at") + self.token_hash = json_data.get("token_hash") + + +class Role: + role_type: str + role_tier: str + + def __init__(self, json_data): + self.role_type = json_data.get("role_type") + self.role_tier = json_data.get("role_tier") + + +def _parse_roles(roles: str) -> list[Role]: + lst_roles = [] + try: + roles_list = json.loads(roles) + for r in roles_list: + try: + role = Role(r) + lst_roles.append(role) + except Exception as e: + cloudlog.exception(f"Failed to parse role {r}: {e}") + return lst_roles + except Exception as e: + cloudlog.exception(f"Error parsing roles: {e}") + return [] + + +def _parse_users(users: str) -> list[User]: + lst_users = [] + try: + users_list = json.loads(users) + for u in users_list: + try: + user = User(u) + lst_users.append(user) + except Exception as e: + cloudlog.exception(f"Failed to parse user {u}: {e}") + return lst_users + except Exception as e: + cloudlog.exception(f"Error parsing users: {e}") + return [] + + +class SunnylinkState: + FETCH_INTERVAL = 5.0 # seconds between API calls + API_TIMEOUT = 10.0 # seconds for API requests + SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread + NOT_PAIRED_USERNAMES = ["unregisteredsponsor", "temporarysponsor"] + + def __init__(self): + self._params = Params() + self._lock = threading.Lock() + self._running = False + self._thread = None + self._sm = messaging.SubMaster(['deviceState']) + + self._roles: list[Role] = [] + self._users: list[User] = [] + self.sponsor_tier: SponsorTier = SponsorTier.FREE + self.sunnylink_dongle_id = self._params.get("SunnylinkDongleId") + self._api = SunnylinkApi(self.sunnylink_dongle_id) + + self._load_initial_state() + + def _load_initial_state(self) -> None: + roles_cache = self._params.get("SunnylinkCache_Roles") + users_cache = self._params.get("SunnylinkCache_Users") + if roles_cache is not None: + self._roles = _parse_roles(roles_cache) + self.sponsor_tier = self._get_highest_tier() + if users_cache is not None: + self._users = _parse_users(users_cache) + + def _get_highest_tier(self) -> SponsorTier: + role_tier = SponsorTier.FREE + for role in self._roles: + try: + if RoleType[role.role_type.upper()] == RoleType.SPONSOR: + role_tier = max(role_tier, SponsorTier[role.role_tier.upper()]) + except Exception as e: + cloudlog.exception(f"Error parsing role {role}: {e} for dongle id {self.sunnylink_dongle_id}") + return role_tier + + def _fetch_roles(self) -> None: + if not self.sunnylink_dongle_id or self.sunnylink_dongle_id == UNREGISTERED_SUNNYLINK_DONGLE_ID: + return + + try: + token = self._api.get_token() + response = self._api.api_get(f"device/{self.sunnylink_dongle_id}/roles", method='GET', access_token=token) + if response.status_code == 200: + self._roles = _parse_roles(response.text) + self._params.put("SunnylinkCache_Roles", response.text) + sponsor_tier = self._get_highest_tier() + with self._lock: + if sponsor_tier != self.sponsor_tier: + self.sponsor_tier = sponsor_tier + cloudlog.info(f"Sunnylink sponsor tier updated to {sponsor_tier.name}") + except Exception as e: + cloudlog.exception(f"Failed to fetch sunnylink roles: {e} for dongle id {self.sunnylink_dongle_id}") + + def _fetch_users(self) -> None: + if not self.sunnylink_dongle_id or self.sunnylink_dongle_id == UNREGISTERED_SUNNYLINK_DONGLE_ID: + return + + try: + token = self._api.get_token() + response = self._api.api_get(f"device/{self.sunnylink_dongle_id}/users", method='GET', access_token=token) + if response.status_code == 200: + users = response.text + self._params.put("SunnylinkCache_Users", users) + with self._lock: + _parse_users(users) + except Exception as e: + cloudlog.exception(f"Failed to fetch sunnylink users: {e} for dongle id {self.sunnylink_dongle_id}") + + def _worker_thread(self) -> None: + while self._running: + if self.is_connected(): + self._fetch_roles() + self._fetch_users() + + for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): + if not self._running: + break + time.sleep(self.SLEEP_INTERVAL) + + def start(self) -> None: + if self._thread and self._thread.is_alive(): + return + self._running = True + self._thread = threading.Thread(target=self._worker_thread, daemon=True) + self._thread.start() + + def stop(self) -> None: + self._running = False + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=1.0) + + def get_sponsor_tier(self) -> SponsorTier: + with self._lock: + return self.sponsor_tier + + def is_sponsor(self) -> bool: + with self._lock: + is_sponsor = any(role.role_type.upper() == RoleType.SPONSOR.name and role.role_tier.upper() != SponsorTier.FREE.name + for role in self._roles) + return is_sponsor + + def is_paired(self) -> bool: + with self._lock: + is_paired = any(user.user_id not in self.NOT_PAIRED_USERNAMES for user in self._users) + return is_paired + + def is_connected(self) -> bool: + network_type = self._sm["deviceState"].networkType + return bool(network_type != 0) + + def __del__(self): + self.stop() From a83c64ffbd410e610cbc8be3212fcbd3dad0537f Mon Sep 17 00:00:00 2001 From: David <49467229+TheSecurityDev@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:56:19 -0600 Subject: [PATCH 4/8] ui: fix sidebar scroll in UI screenshots (#1519) * fix: delay between scroll clicks; add larger delay; fix cruise button * scroll more for cruise --------- Co-authored-by: Jason Wen --- .../ui/tests/test_ui/raylib_screenshots.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/selfdrive/ui/tests/test_ui/raylib_screenshots.py b/selfdrive/ui/tests/test_ui/raylib_screenshots.py index e64398d22..164358bfc 100755 --- a/selfdrive/ui/tests/test_ui/raylib_screenshots.py +++ b/selfdrive/ui/tests/test_ui/raylib_screenshots.py @@ -105,7 +105,7 @@ def setup_settings_software_branch_switcher(click, pm: PubMaster, scroll=None): def setup_settings_firehose(click, pm: PubMaster, scroll=None): setup_settings(click, pm) - scroll(-1000, 278, 950) + scroll(-20, 278, 950) click(278, 850) @@ -115,7 +115,7 @@ def setup_settings_developer(click, pm: PubMaster, scroll=None): Params().put("CarParamsPersistent", CP.to_bytes()) setup_settings(click, pm) - scroll(-1000, 278, 950) + scroll(-20, 278, 950) click(278, 950) @@ -171,37 +171,37 @@ def setup_settings_steering(click, pm: PubMaster, scroll=None): def setup_settings_cruise(click, pm: PubMaster, scroll=None): setup_settings(click, pm) - click(278, 1017) - scroll(-140, 278, 950) + scroll(-4, 278, 950) + click(278, 860) def setup_settings_visuals(click, pm: PubMaster, scroll=None): setup_settings(click, pm) - scroll(-1000, 278, 950) + scroll(-20, 278, 950) click(278, 330) def setup_settings_display(click, pm: PubMaster, scroll=None): setup_settings(click, pm) - scroll(-1000, 278, 950) + scroll(-20, 278, 950) click(278, 420) def setup_settings_osm(click, pm: PubMaster, scroll=None): setup_settings(click, pm) - scroll(-1000, 278, 950) + scroll(-20, 278, 950) click(278, 520) def setup_settings_trips(click, pm: PubMaster, scroll=None): setup_settings(click, pm) - scroll(-1000, 278, 950) + scroll(-20, 278, 950) click(278, 630) def setup_settings_vehicle(click, pm: PubMaster, scroll=None): setup_settings(click, pm) - scroll(-1000, 278, 950) + scroll(-20, 278, 950) click(278, 750) @@ -342,9 +342,14 @@ class TestUI: time.sleep(0.01) pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs) - def scroll(self, clicks, x, y, *args, **kwargs): - pyautogui.scroll(clicks, self.ui.left + x, self.ui.top + y, *args, **kwargs) - time.sleep(UI_DELAY) + def scroll(self, clicks: int, x, y, *args, **kwargs): + if clicks == 0: + return + click = -1 if clicks < 0 else 1 # -1 = down, 1 = up + for _ in range(abs(clicks)): + pyautogui.scroll(click, self.ui.left + x, self.ui.top + y, *args, **kwargs) # scroll for individual clicks since we need to delay between clicks + time.sleep(0.01) # small delay between scroll clicks to work properly + time.sleep(2) # wait for scroll to fully settle @with_processes(["ui"]) def test_ui(self, name, setup_case): From c560ac43aa1531dabad415cd0cfecce9c652d616 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:08:38 -0800 Subject: [PATCH 5/8] ui: `NetworkUISP` (#1535) * ui: network panel * refactor adding scan button without overriding upstream layout * trigger immediate network refresh instead of waiting --------- Co-authored-by: Jason Wen --- .../ui/sunnypilot/layouts/settings/network.py | 46 +++++++++++++++++++ .../sunnypilot/layouts/settings/settings.py | 4 +- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/network.py diff --git a/selfdrive/ui/sunnypilot/layouts/settings/network.py b/selfdrive/ui/sunnypilot/layouts/settings/network.py new file mode 100644 index 000000000..14f573c62 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/network.py @@ -0,0 +1,46 @@ +""" +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 threading +import time +import pyray as rl + +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets.button import Button, ButtonStyle +from openpilot.system.ui.widgets.network import NetworkUI, PanelType + + +class NetworkUISP(NetworkUI): + def __init__(self, wifi_manager): + super().__init__(wifi_manager) + + self.scan_button = Button(tr("Scan"), self._scan_clicked, button_style=ButtonStyle.NORMAL, font_size=60, border_radius=30) + self.scan_button.set_rect(rl.Rectangle(0, 0, 400, 100)) + + self._scanning = False + self._wifi_manager.add_callbacks(networks_updated=self._on_networks_updated) + + def _scan_clicked(self): + self._scanning = True + self.scan_button.set_text(tr("Scanning...")) + self.scan_button.set_enabled(False) + + threading.Thread(target=self._wifi_manager._update_networks, daemon=True).start() + self._wifi_manager._request_scan() + self._wifi_manager._last_network_update = time.monotonic() + + def _on_networks_updated(self, networks): + if self._scanning: + self._scanning = False + self.scan_button.set_text(tr("Scan")) + self.scan_button.set_enabled(True) + + def _render(self, rect: rl.Rectangle): + super()._render(rect) + + if self._current_panel == PanelType.WIFI: + self.scan_button.set_position(self._rect.x, self._rect.y + 20) + self.scan_button.render() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/selfdrive/ui/sunnypilot/layouts/settings/settings.py index bf174de90..905338552 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/settings.py @@ -19,10 +19,10 @@ from openpilot.system.ui.lib.multilang import tr_noop from openpilot.system.ui.sunnypilot.lib.styles import style from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.widgets.network import NetworkUI from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.network import NetworkUISP from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.trips import TripsLayout @@ -111,7 +111,7 @@ class SettingsLayoutSP(OP.SettingsLayout): self._panels = { OP.PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_home.png"), - OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager), icon="icons/network.png"), + OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUISP(wifi_manager), icon="icons/network.png"), OP.PanelType.SUNNYLINK: PanelInfo(tr_noop("sunnylink"), SunnylinkLayout(), icon="icons/shell.png"), OP.PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), From b9c54e07fb721bd67bcec3f1ca5c29c70c0198d3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 3 Dec 2025 01:18:00 -0500 Subject: [PATCH 6/8] Revert "ci: disable macos builds" (#1529) Revert "ci: disable macos builds (#1514)" This reverts commit 42b2e1534b06a409efdd01a8284d10236e03bdad. --- .github/workflows/tests.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8d6449cb4..aab16ffbb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -107,7 +107,6 @@ jobs: build_mac: name: build macOS - if: false # temp disable since gcc-arm-embedded install is getting stuck due to checksum mismatch runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} steps: - uses: actions/checkout@v4 From bea05d4624671927b7e1a203e4daa0eb6d5b74a3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 3 Dec 2025 02:29:33 -0500 Subject: [PATCH 7/8] ui: add pressed state and visual feedback for search button in `TreeOptionDialog` (#1547) * ui: add pressed state and visual feedback for search button in `TreeDialog` * less --- system/ui/sunnypilot/lib/styles.py | 3 +++ system/ui/sunnypilot/widgets/tree_dialog.py | 23 +++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/system/ui/sunnypilot/lib/styles.py b/system/ui/sunnypilot/lib/styles.py index 68e68d41a..3597509dc 100644 --- a/system/ui/sunnypilot/lib/styles.py +++ b/system/ui/sunnypilot/lib/styles.py @@ -69,6 +69,9 @@ class DefaultStyleSP(Base): BUTTON_PRIMARY_COLOR = rl.Color(70, 91, 234, 255) # Royal Blue BUTTON_NEUTRAL_GRAY = rl.Color(51, 51, 51, 255) BUTTON_DISABLED_BG_COLOR = rl.Color(30, 30, 30, 255) # Very Dark Grey + TREE_DIALOG_TRANSPARENT = rl.Color(0, 0, 0, 0) + TREE_DIALOG_SEARCH_BUTTON_PRESSED = rl.Color(0x69, 0x68, 0x68, 0xFF) + TREE_DIALOG_SEARCH_BUTTON_BORDER = rl.Color(150, 150, 150, 200) # Vehicle Description Colors GREEN = rl.Color(0, 241, 0, 255) diff --git a/system/ui/sunnypilot/widgets/tree_dialog.py b/system/ui/sunnypilot/widgets/tree_dialog.py index fd2a576d5..cfc0a5caf 100644 --- a/system/ui/sunnypilot/widgets/tree_dialog.py +++ b/system/ui/sunnypilot/widgets/tree_dialog.py @@ -99,6 +99,7 @@ class TreeOptionDialog(MultiOptionDialog): self.search_title = search_title or tr("Enter search query") self.search_subtitle = search_subtitle self.search_dialog = None + self._search_pressed = False self._build_visible_items() @@ -183,9 +184,10 @@ class TreeOptionDialog(MultiOptionDialog): input_rect = rl.Rectangle(self._search_rect.x + inset, self._search_rect.y + inset, self._search_rect.width - inset * 2, self._search_rect.height - inset * 2) - # Transparent fill + border - rl.draw_rectangle_rounded(input_rect, roundness, 10, rl.Color(0, 0, 0, 0)) - rl.draw_rectangle_rounded_lines_ex(input_rect, roundness, 10, 3, rl.Color(150, 150, 150, 200)) + # Transparent fill (unpressed), white fill (pressed), border + fill_color = style.TREE_DIALOG_SEARCH_BUTTON_PRESSED if self._search_pressed else style.TREE_DIALOG_TRANSPARENT + rl.draw_rectangle_rounded(input_rect, roundness, 10, fill_color) + rl.draw_rectangle_rounded_lines_ex(input_rect, roundness, 10, 3, style.TREE_DIALOG_SEARCH_BUTTON_BORDER) # Magnifying glass icon icon_color = rl.Color(180, 180, 180, 240) @@ -233,8 +235,21 @@ class TreeOptionDialog(MultiOptionDialog): return self._result - def _handle_mouse_release(self, mouse_pos): + def _handle_mouse_press(self, mouse_pos): if self._search_rect and rl.check_collision_point_rec(mouse_pos, self._search_rect): + self._search_pressed = True + return True + return super()._handle_mouse_press(mouse_pos) + + def _handle_mouse_release(self, mouse_pos): + clicked_search = False + if self._search_rect and rl.check_collision_point_rec(mouse_pos, self._search_rect): + clicked_search = self._search_pressed + + self._search_pressed = False + + if clicked_search: self._on_search_clicked() return True + return super()._handle_mouse_release(mouse_pos) From 1f967668a545b34e2a3ffc85d5e56afdd837efd7 Mon Sep 17 00:00:00 2001 From: Nayan Date: Thu, 4 Dec 2025 01:51:08 -0500 Subject: [PATCH 8/8] ui: add sunnypilot font (#1552) add sp font --- selfdrive/assets/fonts/Audiowide-Regular.ttf | 3 +++ system/ui/lib/application.py | 1 + 2 files changed, 4 insertions(+) create mode 100644 selfdrive/assets/fonts/Audiowide-Regular.ttf diff --git a/selfdrive/assets/fonts/Audiowide-Regular.ttf b/selfdrive/assets/fonts/Audiowide-Regular.ttf new file mode 100644 index 000000000..1b6913947 --- /dev/null +++ b/selfdrive/assets/fonts/Audiowide-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:434a720871336d359378beff5ebff3f9fd654d958693d272c7c6f2e271c7e41c +size 47676 diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index e4850e9cb..60a110293 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -94,6 +94,7 @@ class FontWeight(StrEnum): BOLD = "Inter-Bold.fnt" SEMI_BOLD = "Inter-SemiBold.fnt" UNIFONT = "unifont.fnt" + AUDIOWIDE = "Audiowide-Regular.fnt" # Small UI fonts DISPLAY_REGULAR = "Inter-Regular.fnt"