mirror of
https://github.com/infiniteCable2/openpilot.git
synced 2026-08-05 08:16:03 +08:00
Merge branch 'master' of https://github.com/sunnypilot/sunnypilot into sync
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:434a720871336d359378beff5ebff3f9fd654d958693d272c7c6f2e271c7e41c
|
||||
size 47676
|
||||
@@ -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()
|
||||
@@ -20,10 +20,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
|
||||
@@ -112,7 +112,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.ICTOGGLES: PanelInfo(tr_noop("infiniteCable"), ICTogglesLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
|
||||
|
||||
@@ -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,34 @@ 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):
|
||||
self._scroller.render(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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user