diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml deleted file mode 100644 index aeb0f11d8..000000000 --- a/.github/workflows/prebuilt.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: prebuilt -on: - schedule: - - cron: '0 * * * *' - workflow_dispatch: - -env: - DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - BUILD: release/ci/docker_build_sp.sh - -jobs: - build_prebuilt: - name: build prebuilt - runs-on: ubuntu-latest - if: github.repository == 'sunnypilot/sunnypilot' - env: - PUSH_IMAGE: true - permissions: - checks: read - contents: read - packages: write - steps: - - name: Wait for green check mark - if: ${{ github.event_name != 'workflow_dispatch' }} - uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc - with: - ref: master - wait-interval: 30 - running-workflow-name: 'build prebuilt' - repo-token: ${{ secrets.GITHUB_TOKEN }} - check-regexp: ^((?!.*(build master-ci|create badges).*).)*$ - - uses: actions/checkout@v6 - with: - submodules: true - - run: git lfs pull - - name: Build and Push docker image - run: | - $DOCKER_LOGIN - eval "$BUILD" diff --git a/opendbc_repo b/opendbc_repo index 077429f40..1c35c8131 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 077429f40ed5ddf33035d8b4e151e73d005a4157 +Subproject commit 1c35c813166ea3211b9f39ad0c51d2285e013d7f diff --git a/openpilot/cereal/custom.capnp b/openpilot/cereal/custom.capnp index d2fc9dcd8..a856df6fc 100644 --- a/openpilot/cereal/custom.capnp +++ b/openpilot/cereal/custom.capnp @@ -69,6 +69,8 @@ struct LeadData { struct SelfdriveStateSP @0x81c2f05a394cf4af { mads @0 :ModularAssistiveDrivingSystem; intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement; + buttonsPressed @2 :UInt16; + buttonsReleaseToggle @3 :UInt16; enum AudibleAlert { none @0; diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 5aad60fa6..58cadfdec 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -244,6 +244,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/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py index d2454103c..6c87c63e0 100644 --- a/openpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -170,7 +170,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def publish(self, sm, pm): plan_send = messaging.new_message('longitudinalPlan') - plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState']) + plan_send.valid = sm.all_checks() longitudinalPlan = plan_send.longitudinalPlan longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2'] diff --git a/openpilot/selfdrive/controls/plannerd.py b/openpilot/selfdrive/controls/plannerd.py index 99a61c711..45d724cc2 100755 --- a/openpilot/selfdrive/controls/plannerd.py +++ b/openpilot/selfdrive/controls/plannerd.py @@ -29,19 +29,19 @@ def main(): longitudinal_planner = LongitudinalPlanner(CP, CP_SP) pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP', 'longitudinalPlanIC']) sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState', - 'liveMapDataSP', 'carStateSP', gps_location_service], - poll='carState', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services) + 'liveMapDataSP', 'carStateSP', 'selfdriveStateSP', gps_location_service], + poll='modelV2', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services) while True: sm.update() - longitudinal_planner.sla.update_car_state(sm['carState']) + longitudinal_planner.sla.update_buttons(sm['selfdriveStateSP'].buttonsReleaseToggle) if sm.updated['modelV2']: longitudinal_planner.update(sm) longitudinal_planner.publish(sm, pm) ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl']) msg = messaging.new_message('driverAssistance') - msg.valid = sm.all_checks(['carState', 'carControl', 'modelV2', 'liveParameters']) + msg.valid = sm.all_checks() msg.driverAssistance.leftLaneDeparture = ldw.left msg.driverAssistance.rightLaneDeparture = ldw.right pm.send('driverAssistance', msg) diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index aff7d5e2f..7734db581 100644 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -30,6 +30,7 @@ from openpilot.sunnypilot import get_sanitize_int_param from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement +from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP REPLAY = "REPLAY" in os.environ @@ -191,6 +192,7 @@ class SelfdriveD(CruiseHelper): self.car_events_sp = CarSpecificEventsSP(self.CP, self.CP_SP) CruiseHelper.__init__(self, self.CP) + self.button_state_tracker = ButtonStateTracker() def update_events(self, CS): """Compute onroadEvents from carState""" @@ -615,6 +617,8 @@ class SelfdriveD(CruiseHelper): icbm.sendButton = self.icbm.cruise_button icbm.vTarget = self.icbm.v_target + self.button_state_tracker.publish(ss_sp) + self.pm.send('selfdriveStateSP', ss_sp_msg) # onroadEventsSP - logged every second or on change @@ -634,6 +638,7 @@ class SelfdriveD(CruiseHelper): self.mads.update(CS) self.update_alerts(CS) + self.button_state_tracker.update(CS) self.publish_selfdriveState(CS) self.CS_prev = CS 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/selfdrive/controls/lib/speed_limit/speed_limit_assist.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_assist.py index 1ff541167..40c8c6fdf 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_assist.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/speed_limit_assist.py @@ -91,7 +91,7 @@ class SpeedLimitAssist: self._plus_hold = 0. self._minus_hold = 0. - self._last_carstate_ts = 0. + self._release_toggle_prev = 0 # TODO-SP: SLA's own output_a_target for planner # Solution functions mapped to respective states @@ -146,16 +146,16 @@ class SpeedLimitAssist: set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params) self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist - def update_car_state(self, CS: car.CarState) -> None: + def update_buttons(self, release_toggle: int) -> None: + released = self._release_toggle_prev ^ release_toggle + self._release_toggle_prev = release_toggle + if not released: + return now = time.monotonic() - self._last_carstate_ts = now - - for b in CS.buttonEvents: - if not b.pressed: - if b.type in CRUISE_BUTTONS_PLUS: - self._plus_hold = max(self._plus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD) - elif b.type in CRUISE_BUTTONS_MINUS: - self._minus_hold = max(self._minus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD) + if any((released >> b) & 1 for b in CRUISE_BUTTONS_PLUS): + self._plus_hold = max(self._plus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD) + if any((released >> b) & 1 for b in CRUISE_BUTTONS_MINUS): + self._minus_hold = max(self._minus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD) def _get_button_release(self, req_plus: bool, req_minus: bool) -> bool: now = time.monotonic() diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py index 1f02ff6b3..4608fab69 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py @@ -5,11 +5,14 @@ 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 time + import pytest from openpilot.cereal import custom from opendbc.car.car_helpers import interfaces from opendbc.car.rivian.values import CAR as RIVIAN +from opendbc.car.structs import car from opendbc.car.tesla.values import CAR as TESLA from opendbc.car.toyota.values import CAR as TOYOTA from openpilot.common.constants import CV @@ -21,9 +24,13 @@ from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfac from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \ - PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES + PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES, CRUISE_BUTTON_CONFIRM_HOLD +from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +ButtonEvent = car.CarState.ButtonEvent +ButtonType = car.CarState.ButtonEvent.Type + SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values()) @@ -277,3 +284,86 @@ class TestSpeedLimitAssist: assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active] elif initial_state in ACTIVE_STATES: assert self.sla.state in ACTIVE_STATES + + +class TestButtonStateTrackerSLAIntegration: + + def setup_method(self, method): + self.tracker = ButtonStateTracker() + self.params = Params() + self.params.put("IsReleaseSpBranch", True, block=True) + self.params.put("SpeedLimitMode", int(Mode.assist), block=True) + self.params.put_bool("IsMetric", False, block=True) + self.params.put("SpeedLimitOffsetType", 0, block=True) + self.params.put("SpeedLimitValueOffset", 0, block=True) + + CarInterface = interfaces[DEFAULT_CAR] + CP = CarInterface.get_non_essential_params(DEFAULT_CAR) + CP.openpilotLongitudinalControl = True + CP_SP = CarInterface.get_non_essential_params_sp(CP, DEFAULT_CAR) + self.sla = SpeedLimitAssist(CP, CP_SP) + + def _make_cs(self, events=None) -> car.CarState: + CS = car.CarState() + CS.buttonEvents = events or [] + return CS + + def _run_ctrl_frames(self, frames: list[car.CarState]) -> None: + for cs in frames: + self.tracker.update(cs) + + def test_button_confirm_via_tracker(self) -> None: + self._run_ctrl_frames([ + self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]), + self._make_cs(), + self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]), + self._make_cs(), + self._make_cs(), + ]) + self.sla.update_buttons(self.tracker.release_toggle) + assert self.sla._get_button_release(req_plus=True, req_minus=False) + + def test_rapid_press_release_between_polls(self) -> None: + self.sla.update_buttons(self.tracker.release_toggle) + + self._run_ctrl_frames([ + self._make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=True)]), + self._make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=False)]), + self._make_cs(), + self._make_cs(), + self._make_cs(), + ]) + self.sla.update_buttons(self.tracker.release_toggle) + assert self.sla._get_button_release(req_plus=False, req_minus=True) + + def test_multiple_releases_between_polls(self) -> None: + self.sla.update_buttons(self.tracker.release_toggle) + + self._run_ctrl_frames([ + self._make_cs([ + ButtonEvent(type=ButtonType.accelCruise, pressed=True), + ButtonEvent(type=ButtonType.decelCruise, pressed=True), + ]), + self._make_cs([ + ButtonEvent(type=ButtonType.accelCruise, pressed=False), + ButtonEvent(type=ButtonType.decelCruise, pressed=False), + ]), + ]) + self.sla.update_buttons(self.tracker.release_toggle) + assert self.sla._get_button_release(req_plus=True, req_minus=False) + assert self.sla._get_button_release(req_plus=False, req_minus=True) + + def test_no_false_positive_same_toggle(self) -> None: + self.sla.update_buttons(self.tracker.release_toggle) + self.sla.update_buttons(self.tracker.release_toggle) + assert not self.sla._get_button_release(req_plus=True, req_minus=False) + assert not self.sla._get_button_release(req_plus=False, req_minus=True) + + def test_button_confirm_expires(self) -> None: + self._run_ctrl_frames([ + self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]), + self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]), + ]) + self.sla.update_buttons(self.tracker.release_toggle) + time.sleep(CRUISE_BUTTON_CONFIRM_HOLD + 0.1) + assert not self.sla._get_button_release(req_plus=True, req_minus=False) diff --git a/openpilot/sunnypilot/selfdrive/selfdrived/button_state_tracker.py b/openpilot/sunnypilot/selfdrive/selfdrived/button_state_tracker.py new file mode 100644 index 000000000..ffbff7b44 --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/selfdrived/button_state_tracker.py @@ -0,0 +1,26 @@ +""" +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.car import structs + + +class ButtonStateTracker: + def __init__(self) -> None: + self.pressed: int = 0 + self.release_toggle: int = 0 + + def update(self, CS: structs.CarState) -> None: + for b in CS.buttonEvents: + bit = 1 << b.type.raw + if b.pressed: + self.pressed |= bit + else: + self.pressed &= ~bit + self.release_toggle ^= bit + + def publish(self, ss_sp) -> None: + ss_sp.buttonsPressed = self.pressed + ss_sp.buttonsReleaseToggle = self.release_toggle diff --git a/openpilot/sunnypilot/selfdrive/selfdrived/tests/__init__.py b/openpilot/sunnypilot/selfdrive/selfdrived/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openpilot/sunnypilot/selfdrive/selfdrived/tests/test_button_state_tracker.py b/openpilot/sunnypilot/selfdrive/selfdrived/tests/test_button_state_tracker.py new file mode 100644 index 000000000..798fab650 --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/selfdrived/tests/test_button_state_tracker.py @@ -0,0 +1,67 @@ +""" +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.car.structs import car +from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker + +ButtonEvent = car.CarState.ButtonEvent +ButtonType = car.CarState.ButtonEvent.Type + + +class TestButtonStateTracker: + def setup_method(self) -> None: + self.tracker = ButtonStateTracker() + + def make_cs(self, events: list) -> car.CarState: + CS = car.CarState() + CS.buttonEvents = events + return CS + + def test_initial_state(self) -> None: + assert self.tracker.pressed == 0 + assert self.tracker.release_toggle == 0 + + def test_press_sets_bit(self) -> None: + self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)])) + assert self.tracker.pressed == (1 << ButtonType.accelCruise) + assert self.tracker.release_toggle == 0 + + def test_release_clears_and_toggles(self) -> None: + self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)])) + self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)])) + assert self.tracker.pressed == 0 + assert self.tracker.release_toggle == (1 << ButtonType.accelCruise) + + def test_multiple_buttons(self) -> None: + self.tracker.update(self.make_cs([ + ButtonEvent(type=ButtonType.accelCruise, pressed=True), + ButtonEvent(type=ButtonType.decelCruise, pressed=True), + ])) + assert self.tracker.pressed == (1 << ButtonType.accelCruise) | (1 << ButtonType.decelCruise) + + self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)])) + assert self.tracker.pressed == (1 << ButtonType.decelCruise) + assert self.tracker.release_toggle == (1 << ButtonType.accelCruise) + + def test_release_toggle_flips(self) -> None: + for _ in range(2): + self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=True)])) + self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=False)])) + assert self.tracker.release_toggle == 0 + + def test_publish(self) -> None: + self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)])) + self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=True)])) + self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)])) + + class MockSP: + buttonsPressed = 0 + buttonsReleaseToggle = 0 + + sp = MockSP() + self.tracker.publish(sp) + assert sp.buttonsPressed == self.tracker.pressed + assert sp.buttonsReleaseToggle == self.tracker.release_toggle 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) diff --git a/release/ci/docker_build_sp.sh b/release/ci/docker_build_sp.sh deleted file mode 100755 index 369daf523..000000000 --- a/release/ci/docker_build_sp.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -e - -SCRIPT_DIR=$(dirname "$0") -OPENPILOT_DIR=$SCRIPT_DIR/../../ - -DOCKER_IMAGE=sunnypilot -DOCKER_FILE=Dockerfile.openpilot -DOCKER_REGISTRY=ghcr.io/sunnypilot -COMMIT_SHA=$(git rev-parse HEAD) - -if [ -n "$TARGET_ARCHITECTURE" ]; then - PLATFORM="linux/$TARGET_ARCHITECTURE" - TAG_SUFFIX="-$TARGET_ARCHITECTURE" -else - PLATFORM="linux/$(uname -m)" - TAG_SUFFIX="" -fi - -LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX -REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG -REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA - -DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR - -if [ -n "$PUSH_IMAGE" ]; then - docker push $REMOTE_TAG - docker tag $REMOTE_TAG $REMOTE_SHA_TAG - docker push $REMOTE_SHA_TAG -fi