diff --git a/opendbc_repo b/opendbc_repo index d6b9c1ada..4c64e8a95 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit d6b9c1adaafe2b752c9ee9a4af0024d22c2c12e6 +Subproject commit 4c64e8a95b4eadca6a9e631d06a854f52bb9ebfa diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 3c39fc5f5..279d36abf 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -222,6 +222,7 @@ inline static std::unordered_map keys = { {"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}}, {"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}}, {"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}}, {"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}}, diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py index 098fcf8ce..4cde48995 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. from collections.abc import Callable import pyray as rl -from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP +from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake from openpilot.system.ui.lib.multilang import tr, tr_noop @@ -96,7 +96,10 @@ class MadsSettingsLayout(Widget): if brand == "rivian": return True elif brand == "tesla": - return not (ui_state.CP_SP is not None and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS) + if ui_state.CP_SP is None or not ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS: + return True + screen_button = int(ui_state.params.get("TeslaMadsScreenButton", return_default=True)) + return screen_button == MadsScreenButtonType.OFF return False def _update_steering_mode_description(self, button_index: int): diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py index 46d536c65..10fb3e1b4 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/vehicle/brands/tesla.py @@ -4,10 +4,11 @@ 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 opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP 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 openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, toggle_item_sp COOP_STEERING_MIN_KMH = 23 OEM_STEERING_MIN_KMH = 48 @@ -18,7 +19,14 @@ 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] + self.mads_screen_button = multiple_button_item_sp( + title=lambda: tr("MADS Screen Activation"), + description="", + buttons=[lambda: tr("Off"), lambda: tr("3-Finger"), lambda: tr("4-Finger"), lambda: tr("5-Finger")], + param="TeslaMadsScreenButton", + inline=False, + ) + self.items = [self.coop_steering_toggle, self.mads_screen_button] def update_settings(self): is_metric = ui_state.is_metric @@ -41,3 +49,18 @@ class TeslaSettings(BrandSettings): self.coop_steering_toggle.set_description(coop_steering_desc) self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad()) + + has_vehicle_bus = ui_state.CP_SP is not None and bool(ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS) + self.mads_screen_button.set_visible(has_vehicle_bus) + + mads_screen_button_desc = ( + f"{tr('Use a multi-finger press on the infotainment screen to toggle MADS.')} " + + f"{tr('This allows the use of full MADS functionality when enabled.')}

" + + f"{tr('Selecting a higher finger count may reduce accidental activations.')}

" + + f"{tr('Note: Setting this to Off will reset your MADS settings to default.')}" + ) + if not ui_state.is_offroad(): + mads_screen_button_disabled_msg = tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to change.") + mads_screen_button_desc = f"{mads_screen_button_disabled_msg}

{mads_screen_button_desc}" + self.mads_screen_button.set_description(mads_screen_button_desc) + self.mads_screen_button.action_item.set_enabled(ui_state.is_offroad()) diff --git a/openpilot/sunnypilot/mads/helpers.py b/openpilot/sunnypilot/mads/helpers.py index b9efd620f..655d7bc9a 100644 --- a/openpilot/sunnypilot/mads/helpers.py +++ b/openpilot/sunnypilot/mads/helpers.py @@ -9,7 +9,7 @@ from openpilot.common.params import Params from opendbc.car import structs from opendbc.safety import ALTERNATIVE_EXPERIENCE from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP -from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP +from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP MADS_NO_ACC_MAIN_BUTTON = ("rivian", "tesla") @@ -21,17 +21,20 @@ class MadsSteeringModeOnBrake: DISENGAGE = 2 -def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP) -> bool: +def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params) -> bool: if CP.brand == 'rivian': return True if CP.brand == 'tesla': - return not CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS + if not CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS: + return True + screen_button = int(params.get("TeslaMadsScreenButton", return_default=True)) + return screen_button == MadsScreenButtonType.OFF return False def read_steering_mode_param(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params): - if get_mads_limited_brands(CP, CP_SP): + if get_mads_limited_brands(CP, CP_SP, params): return MadsSteeringModeOnBrake.DISENGAGE return params.get("MadsSteeringMode", return_default=True) @@ -63,7 +66,7 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p # MADS is currently partially supported for these platforms due to lack of consistent states to engage controls # Only MadsSteeringModeOnBrake.DISENGAGE is supported for these platforms # TODO-SP: To enable MADS full support for Rivian and most Tesla, identify consistent signals for MADS toggling - mads_partial_support = get_mads_limited_brands(CP, CP_SP) + mads_partial_support = get_mads_limited_brands(CP, CP_SP, params) if mads_partial_support: params.put("MadsSteeringMode", 2, block=True) params.put_bool("MadsUnifiedEngagementMode", True, block=True) diff --git a/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py b/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py index 3b7e27a86..2bc6130ec 100644 --- a/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py +++ b/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py @@ -13,7 +13,7 @@ from openpilot.selfdrive.selfdrived.events import Events from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem -from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP +from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventName = log.OnroadEvent.EventName @@ -38,6 +38,12 @@ def make_panda_state(mocker, controls_allowed_lateral=True): return ps +def make_params_mock(mocker, values): + params = mocker.MagicMock() + params.get = mocker.MagicMock(side_effect=lambda k, **kwargs: values[k]) + return params + + def make_mads(mocker, steering_mode): sd = mocker.MagicMock() sd.CP = structs.CarParams() @@ -223,15 +229,27 @@ class TestBrandSteeringModeRestrictions: params = mocker.MagicMock() assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE - def test_tesla_with_vehicle_bus_uses_param(self, mocker): + @pytest.mark.parametrize("screen_button", [MadsScreenButtonType.THREE_FINGER, + MadsScreenButtonType.FOUR_FINGER, + MadsScreenButtonType.FIVE_FINGER]) + def test_tesla_with_vehicle_bus_uses_param(self, mocker, screen_button): CP = structs.CarParams() CP.brand = "tesla" CP_SP = structs.CarParamsSP() CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS - params = mocker.MagicMock() - params.get = mocker.MagicMock(return_value=MadsSteeringModeOnBrake.REMAIN_ACTIVE) + params = make_params_mock(mocker, {"TeslaMadsScreenButton": screen_button, + "MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE}) assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.REMAIN_ACTIVE + def test_tesla_with_vehicle_bus_screen_button_off_forced_to_disengage(self, mocker): + CP = structs.CarParams() + CP.brand = "tesla" + CP_SP = structs.CarParamsSP() + CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS + params = make_params_mock(mocker, {"TeslaMadsScreenButton": MadsScreenButtonType.OFF, + "MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE}) + assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE + @pytest.mark.parametrize("brand", ["hyundai", "toyota", "honda", "gm"]) def test_other_brands_use_param(self, mocker, brand): CP = structs.CarParams() diff --git a/openpilot/sunnypilot/selfdrive/car/interfaces.py b/openpilot/sunnypilot/selfdrive/car/interfaces.py index 5be227c26..ed5b71d4b 100644 --- a/openpilot/sunnypilot/selfdrive/car/interfaces.py +++ b/openpilot/sunnypilot/selfdrive/car/interfaces.py @@ -123,6 +123,7 @@ def initialize_params(params) -> list[dict[str, Any]]: # tesla keys.extend([ "TeslaCoopSteering", + "TeslaMadsScreenButton", ]) # toyota diff --git a/openpilot/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json index 1745235d9..cd5f0b118 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui.json +++ b/openpilot/sunnypilot/sunnylink/settings_ui.json @@ -2161,6 +2161,42 @@ "type": "offroad_only" } ] + }, + { + "key": "TeslaMadsScreenButton", + "widget": "multiple_button", + "title": "MADS Screen Activation", + "description": "Use a multi-finger press on the infotainment screen to toggle MADS. This allows the use of full MADS functionality when enabled. Selecting a higher finger count may reduce accidental activations. Note: Setting this to Off will reset your MADS settings to default.", + "options": [ + { + "value": 0, + "label": "Off" + }, + { + "value": 1, + "label": "3-Finger" + }, + { + "value": 2, + "label": "4-Finger" + }, + { + "value": 3, + "label": "5-Finger" + } + ], + "visibility": [ + { + "type": "capability", + "field": "tesla_has_vehicle_bus", + "equals": true + } + ], + "enablement": [ + { + "type": "offroad_only" + } + ] } ] }, diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml index 2f6f20730..7dd23256b 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml +++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml @@ -56,6 +56,28 @@ sections: title: Cooperative Steering (Beta) enablement: - $ref: '#/macros/offroad' + - key: TeslaMadsScreenButton + widget: multiple_button + title: MADS Screen Activation + description: 'Use a multi-finger press on the infotainment screen to toggle MADS. + This allows the use of full MADS functionality when enabled. Selecting a higher + finger count may reduce accidental activations. Note: Setting this to Off will + reset your MADS settings to default.' + options: + - value: 0 + label: 'Off' + - value: 1 + label: 3-Finger + - value: 2 + label: 4-Finger + - value: 3 + label: 5-Finger + visibility: + - type: capability + field: tesla_has_vehicle_bus + equals: true + enablement: + - $ref: '#/macros/offroad' - id: toyota title: Toyota / Lexus Settings description: '' diff --git a/openpilot/sunnypilot/system/params_migration.py b/openpilot/sunnypilot/system/params_migration.py index f9d7d1bc4..130fd6431 100644 --- a/openpilot/sunnypilot/system/params_migration.py +++ b/openpilot/sunnypilot/system/params_migration.py @@ -17,6 +17,26 @@ ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 3, 1: 5, 2: 7, 3: 10, 4: 15, 5: 30, **{i: ( VALID_TIMER_VALUES = set(ONROAD_BRIGHTNESS_TIMER_VALUES.values()) +def _resolve_brand(_params) -> str: + bundle = _params.get("CarPlatformBundle") + if isinstance(bundle, dict) and bundle.get("brand"): + return str(bundle["brand"]) + + # Auto-fingerprinted cars have no bundle, fall back to the last known CarParams. + CP_bytes = _params.get("CarParamsPersistent") + if CP_bytes is None: + return "" + + # Never raises: callers rely on "" to mean "brand unknown, skip the migration". + try: + from openpilot.cereal import messaging # lazy: avoids heavy import at module level + from opendbc.car.structs import car + return str(messaging.log_from_bytes(CP_bytes, car.CarParams).brand) + except Exception as e: + cloudlog.exception(f"params_migration: failed to resolve brand from CarParamsPersistent: {e}") + return "" + + def _migrate_car_platform_bundle(_params): bundle = _params.get("CarPlatformBundle") if bundle is None: @@ -47,6 +67,23 @@ def _migrate_car_platform_bundle(_params): cloudlog.info(f"params_migration: CarPlatformBundle migrated {old_platform!r} -> {new_platform!r}") +def _migrate_tesla_mads_screen_button(_params): + # TeslaMadsScreenButton defaults to Off for fresh installs, but the screen button was previously always + # active on Teslas with a vehicle bus. Seed existing Tesla installs with 3-finger to preserve that. + try: + if _params.get("TeslaMadsScreenButton") is not None: + return + + if _resolve_brand(_params) != "tesla": + return + + from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType # lazy: avoids heavy import at module level + _params.put("TeslaMadsScreenButton", MadsScreenButtonType.THREE_FINGER, block=True) + cloudlog.info("params_migration: seeded TeslaMadsScreenButton with 3-finger to preserve existing behavior") + except Exception as e: + cloudlog.exception(f"Error migrating TeslaMadsScreenButton: {e}") + + def run_migration(_params): # migrate OnroadScreenOffBrightness if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION: @@ -80,3 +117,6 @@ def run_migration(_params): cloudlog.exception(f"Error migrating OnroadScreenOffTimer: {e}") _migrate_car_platform_bundle(_params) + + # seed TeslaMadsScreenButton for existing Tesla installs + _migrate_tesla_mads_screen_button(_params)