From ced757efe2aa7458ebfded882c1ae071d0f9b6f1 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Thu, 5 Nov 2026 12:38:13 -0600 Subject: [PATCH] Distinguish Crosstrek wheel load from steering override --- openpilot/selfdrive/car/car_events.py | 16 ++- .../car/subaru/tests/test_override_events.py | 107 ++++++++++++++++++ .../subaru/tests/test_stock_aol_callers.py | 13 +++ prebuilt | 0 prebuilt.json | 1 - 5 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 openpilot/starpilot/car/subaru/tests/test_override_events.py delete mode 100644 prebuilt delete mode 100644 prebuilt.json diff --git a/openpilot/selfdrive/car/car_events.py b/openpilot/selfdrive/car/car_events.py index 3c844f04d0..f70d170e07 100644 --- a/openpilot/selfdrive/car/car_events.py +++ b/openpilot/selfdrive/car/car_events.py @@ -22,6 +22,7 @@ class CarEvents: from opendbc.car.gm.values import is_volt_cc_profile self.volt_cc_profile = CP.brand == "gm" and is_volt_cc_profile(CP) self.steering_unpressed = 0 + self.subaru_angle_override = False self.low_speed_alert = False self.no_steer_warning = False self.silent_steer_warning = True @@ -146,7 +147,16 @@ class CarEvents: events.add(EventName.accFaulted) if CS.carNotReady: events.add(EventName.carNotReady) - if CS.steeringPressed: + steering_override = CS.steeringPressed + if (self.CP.brand == 'subaru' and self.CP.carFingerprint == 'SUBARU_CROSSTREK_2025' and + self.CP.steerControlType == structs.CarParams.SteerControlType.angle): + torque = abs(CS.steeringTorque) + if torque < 100: + self.subaru_angle_override = False + elif torque > 150: + self.subaru_angle_override = True + steering_override = self.subaru_angle_override + if steering_override: events.add(EventName.steerOverride) if CS.steeringDisengage and not CS_prev.steeringDisengage: events.add(EventName.steerDisengage) @@ -172,9 +182,9 @@ class CarEvents: events.add(EventName.buttonCancel) # Handle permanent and temporary steering faults - self.steering_unpressed = 0 if CS.steeringPressed else self.steering_unpressed + 1 + self.steering_unpressed = 0 if steering_override else self.steering_unpressed + 1 if CS.steerFaultTemporary: - if CS.steeringPressed and (not CS_prev.steerFaultTemporary or self.no_steer_warning): + if steering_override and (not CS_prev.steerFaultTemporary or self.no_steer_warning): self.no_steer_warning = True else: self.no_steer_warning = False diff --git a/openpilot/starpilot/car/subaru/tests/test_override_events.py b/openpilot/starpilot/car/subaru/tests/test_override_events.py new file mode 100644 index 0000000000..3c79513c0a --- /dev/null +++ b/openpilot/starpilot/car/subaru/tests/test_override_events.py @@ -0,0 +1,107 @@ +import pytest +from opendbc.car import structs +from opendbc.car.car_helpers import interfaces +from opendbc.car.subaru.values import CAR +from openpilot.cereal import log +from openpilot.selfdrive.car.car_events import CarEvents + +EventName = log.OnroadEvent.EventName + + +def event_owner(candidate=CAR.SUBARU_CROSSTREK_2025, angle=True): + cp = interfaces[candidate].get_non_essential_params(candidate) + if not angle: + cp.steerControlType = structs.CarParams.SteerControlType.torque + return CarEvents(cp) + + +def sample(owner, torque, **fields): + cs = structs.CarState.new_message() + cs.gearShifter = structs.CarState.GearShifter.drive + cs.cruiseState.available = True + cs.canValid = True + cs.steeringTorque = torque + cs.steeringPressed = abs(torque) > 80 + for key, value in fields.items(): + setattr(cs, key, value) + events = owner.create_common_events(cs, structs.CarState.new_message()) + return cs, events + + +@pytest.mark.parametrize('direction', [-1, 1]) +def test_light_wheel_interaction_is_not_an_override(direction): + owner = event_owner() + for torque in (81, 94, 100, 149, 150): + cs, events = sample(owner, direction * torque) + assert cs.steeringPressed + assert cs.steeringTorque == direction * torque + assert EventName.steerOverride not in events.names + + +@pytest.mark.parametrize('direction', [-1, 1]) +def test_meaningful_override_is_immediate_and_retained_until_release(direction): + owner = event_owner() + for torque, expected in ((150, False), (151, True), (140, True), (101, True), (100, True), + (99, False), (140, False), (180, True), (0, False)): + cs, events = sample(owner, direction * torque) + assert (EventName.steerOverride in events.names) is expected + assert cs.steeringPressed == (torque > 80) + + +@pytest.mark.parametrize('candidate', [CAR.SUBARU_FORESTER_2022, CAR.SUBARU_OUTBACK_2023, + CAR.SUBARU_ASCENT_2023, CAR.SUBARU_IMPREZA]) +def test_neighboring_subaru_profiles_retain_existing_detection(candidate): + owner = event_owner(candidate) + for torque, expected in ((0, False), (80, False), (81, True), (150, True), (-81, True)): + _, events = sample(owner, torque) + assert (EventName.steerOverride in events.names) is expected + + +def test_nonangle_configuration_keeps_existing_detection(): + _, events = sample(event_owner(angle=False), 94) + assert EventName.steerOverride in events.names + + +def test_other_make_keeps_existing_detection(): + from opendbc.car.hyundai.values import CAR as HKG + owner = event_owner(HKG.HYUNDAI_IONIQ_6) + _, events = sample(owner, 94) + assert EventName.steerOverride in events.names + + +@pytest.mark.parametrize('field,event', [('steerFaultPermanent', EventName.steerUnavailable), + ('doorOpen', EventName.doorOpen), + ('seatbeltUnlatched', EventName.seatbeltNotLatched)]) +def test_fault_and_disengagement_events_are_preserved(field, event): + cs, events = sample(event_owner(), 94, **{field: True}) + assert cs.steeringPressed + assert event in events.names + assert EventName.steerOverride not in events.names + + +@pytest.mark.parametrize('direction', [-1, 1]) +def test_existing_controller_yield_and_settled_handoff_are_unchanged(direction): + from types import SimpleNamespace + from opendbc.car.subaru.carcontroller import CarController + owner = event_owner() + controller = CarController.__new__(CarController) + controller.CP = owner.CP + controller.angle_driver_override = False + controller.angle_override_frames = 0 + controller.angle_handoff_active = False + controller.angle_was_active = True + for torque, event_override, active in ((94, False, True), (151, True, True), (151, True, False), + (140, True, False), (100, True, False), (99, False, False), + (99, False, True)): + cs, events = sample(owner, direction * torque) + assert (EventName.steerOverride in events.names) is event_override + assert controller.angle_active(SimpleNamespace(out=cs), True) is active + + +@pytest.mark.parametrize('direction', [-1, 1]) +def test_light_wheel_load_does_not_hide_temporary_eps_warning(direction): + owner = event_owner() + cs, events = sample(owner, direction * 94, steerFaultTemporary=True) + assert cs.steeringPressed + assert EventName.steerOverride not in events.names + assert any(name in events.names for name in (EventName.steerTempUnavailable, EventName.steerTempUnavailableSilent)) diff --git a/openpilot/starpilot/car/subaru/tests/test_stock_aol_callers.py b/openpilot/starpilot/car/subaru/tests/test_stock_aol_callers.py index 917e62c6a9..d7585e0d6f 100644 --- a/openpilot/starpilot/car/subaru/tests/test_stock_aol_callers.py +++ b/openpilot/starpilot/car/subaru/tests/test_stock_aol_callers.py @@ -429,3 +429,16 @@ def test_actual_held_gas_source_outage_withdraws_and_recovers_subaru(candidate, def test_actual_vm_angle_driver_handoff_keeps_native_rate_limit(candidate, direction, subaru_native_pair, tmp_path): test_actual_full_engage_brake_reverse_temporary_recovery_mainoff( candidate, subaru_native_pair, tmp_path, vm_angle_handoff=direction) + + +@pytest.mark.parametrize('aol', [False, True]) +def test_actual_crosstrek_light_load_retains_control_and_temporary_eps_intent(aol, subaru_native_pair, tmp_path, monkeypatch): + frames = CompleteCan.frames + + def wheel_load(self, values, **kwargs): + values = {**values, 'Steering_Torque': {**values['Steering_Torque'], 'Steer_Torque_Sensor': 94}} + return frames(self, values, **kwargs) + + monkeypatch.setattr(CompleteCan, 'frames', wheel_load) + test_actual_full_engage_brake_reverse_temporary_recovery_mainoff( + CAR.SUBARU_CROSSTREK_2025, subaru_native_pair, tmp_path, aol=aol) diff --git a/prebuilt b/prebuilt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/prebuilt.json b/prebuilt.json deleted file mode 100644 index 88f9bf5d5e..0000000000 --- a/prebuilt.json +++ /dev/null @@ -1 +0,0 @@ -{"protected_inventory_sha256": "03e24bc36f837e5456c78ef3a7b9a2739826cbe2ebe67a9f592987be876893e6", "schema_version": 1}