diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 3c8e45c646..b3a0eade3f 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -1463,7 +1463,8 @@ class SelfdriveD: if (toyota_qualified(self.CP, marked_only=True) and CS.standstill or angle_qualified(self.CP, marked_only=True) or self.CP.brand == 'hyundai' and len(self.CP.safetyConfigs) == 1 and - stock_canfd_qualified(self.CP, marked_only=True)): + stock_canfd_qualified(self.CP, marked_only=True) or + self.CP.brand == 'gm' and qualified_gm(self.CP) and self.CP.pcmCruise and not self.CP.openpilotLongitudinalControl): native = current_native(self.sm, self.CP, now_ns=now_ns, axis_session_id=self.aol_session_id) if stock_lateral_role(self, CS, native, intent, allow_independent_cancel=True, allow_independent_brake=True, allow_independent_gas=True, now_ns=now_ns): diff --git a/openpilot/starpilot/aol/tests/test_gm_ordinary_cc_lateral_minimum.py b/openpilot/starpilot/aol/tests/test_gm_ordinary_cc_lateral_minimum.py new file mode 100644 index 0000000000..8075d0453d --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_gm_ordinary_cc_lateral_minimum.py @@ -0,0 +1,105 @@ +"""Conventional CC's longitudinal speed minimum cannot inhibit independent LAT.""" +from dataclasses import replace +from contextlib import ExitStack +from unittest.mock import patch + +import pytest +from opendbc.can import CANPacker +from opendbc.car import Bus +from opendbc.car.gm import gmcan +from opendbc.car.gm.tests.test_ascm_intercept import params +from opendbc.car.gm.tests.test_bolt_cc import native +from opendbc.car.gm.tests.test_ordinary_cc import qualified_frames +from opendbc.car.gm.tests.test_sierra_cc_layout import params as sierra_params +from opendbc.car.gm.values import CAR, DBC, ORDINARY_CC_CAR, CruiseButtons, GMFlags +from opendbc.safety.tests.libsafety import libsafety_py +from openpilot.cereal import log +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.starpilot.aol.intent import read_settings +from openpilot.starpilot.aol.tests import test_gm_parked_startup as parked, test_ioniq_availability as fixture +from openpilot.starpilot.aol.tests.test_gm_cc_lateral_minimum import setup +from openpilot.starpilot.car.gm.aol import cc_independent_lateral_below_engage + + +def cc_params(candidate): + cp = sierra_params() if candidate == CAR.CHEVROLET_SILVERADO_CC else params(candidate) + cp.alternativeExperience = 32 + return cp + + +@pytest.mark.parametrize('candidate', sorted(ORDINARY_CC_CAR | {CAR.CHEVROLET_SILVERADO_CC})) +def test_all_exact_ordinary_cc_owners_keep_independent_steering_minimum(candidate): + cp = cc_params(candidate) + _, intent = setup() + assert cc_independent_lateral_below_engage(cp, intent, enabled=False, active=False) + for enabled, active in ((True, True), (True, False), (False, True)): + assert not cc_independent_lateral_below_engage(cp, intent, enabled=enabled, active=active) + for key in ('settingsQualified', 'allowedLatch', 'lateralArmed'): + assert not cc_independent_lateral_below_engage(cp, replace(intent, **{key: False}), enabled=False, active=False) + assert not cc_independent_lateral_below_engage(cp, replace(intent, pauseLateral=True), enabled=False, active=False) + for flag in (GMFlags.PEDAL_LONG, GMFlags.NO_CAMERA, 1 << 30): + denied = cp.as_reader().as_builder() + denied.flags |= int(flag) + assert not cc_independent_lateral_below_engage(denied, intent, enabled=False, active=False) + + +@pytest.mark.parametrize('main_action', [0, 9]) +def test_sierra_parser_card_selfdrived_controls_and_native_steer_below_cruise_minimum(main_action): + cp = cc_params(CAR.CHEVROLET_SILVERADO_CC) + original = parked.parsed_packets + + def packets(*args): + packer = args[1] + return original(*args) + [packer.make_can_msg('BCMBlindSpotMonitor', 0, {})] + + with patch.object(parked, 'parsed_packets', side_effect=packets): + parked._exercise_parked_main(3, main_action, below_engage=True, cp=cp) + + +@pytest.mark.parametrize('candidate', [CAR.CHEVROLET_SILVERADO_CC, CAR.CHEVROLET_MALIBU_CC]) +@pytest.mark.parametrize('speed_mph,allowed', [(20., False), (30., True)]) +def test_native_longitudinal_direction_buttons_keep_existing_24mph_floor(candidate, speed_mph, allowed): + cp = cc_params(candidate) + assert cp.minEnableSpeed == pytest.approx(24 * .44704) + safety = libsafety_py.libsafety + safety.init_tests() + safety.set_alternative_experience(cp.alternativeExperience) + assert safety.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam) == 0 + packer = CANPacker(DBC[candidate][Bus.pt]) + for tick in range(5): + now = 1_000_000_000 + tick * 10_000_000 + frames = [f for f in qualified_frames(packer, tick % 4) if f[0] not in (0x34A, 0x3D1)] + frames += [packer.make_can_msg('EBCMWheelSpdRear', 0, {'RLWheelSpd': speed_mph * 1.609344, + 'RRWheelSpd': speed_mph * 1.609344, 'RLWheelDir': 1, 'RRWheelDir': 1}), + packer.make_can_msg('ECMCruiseControl', 0, {'CruiseActive': 1, 'CruiseSetSpeed': 50})] + for frame in frames: + assert native('rx', frame, now // 1000) + safety.safety_tick() + assert safety.safety_config_valid() and safety.get_controls_allowed() + safety.set_aol_test_heartbeat(True) + safety.aol_set_host_request(3) + if allowed: + assert safety.aol_get_permission_mask() & 2 + command = gmcan.create_buttons(packer, 0, (tick + 1) % 4, CruiseButtons.RES_ACCEL) + assert bool(native('tx', command, now // 1000)) == allowed + + +@pytest.mark.parametrize('braking', [False, True]) +def test_sierra_actual_lateral_callers_keep_main_intent_when_crossing_24mph(braking): + cp = cc_params(CAR.CHEVROLET_SILVERADO_CC) + case = fixture.IoniqAvailabilityTests(methodName='runTest') + with OpenpilotPrefix(), ExitStack() as cleanup: + cleanup.callback(case.doCleanups) + sd, _, step = case._authority_callers(False, cp=cp, genuine_events=True) + sd.aol_settings = read_settings(sd.params) + for tick, mph in enumerate((30., 25., 23.85, 20., 10., 7.5, 6.5, 10.)): + cs = fixture.car_state(brake=braking) + cs.vEgo = cs.vEgoRaw = mph * .44704 + axis, state, command = step(tick, observed_state=cs, native_request_long=False, panda_controls_allowed=False) + assert cs.cruiseState.available + assert not state.enabled and not command.enabled + assert not axis.longitudinalActive and not command.longActive + assert axis.desiredLateral and axis.lateralActive, (mph, axis.to_dict(), sd.events.names) + assert bool(command.latActive) == (mph > 7.) + assert (log.OnroadEvent.EventName.belowEngageSpeed in sd.events.names) == (mph < 24.) + assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names diff --git a/openpilot/starpilot/aol/tests/test_gm_stock_fresh_continuity.py b/openpilot/starpilot/aol/tests/test_gm_stock_fresh_continuity.py new file mode 100644 index 0000000000..6f52337385 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_gm_stock_fresh_continuity.py @@ -0,0 +1,62 @@ +"""A fresh stock-GM LAT source does not inherit an expired output's PCM-off veto.""" +from contextlib import ExitStack +from unittest.mock import patch + +import pytest +from openpilot.cereal import messaging +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.selfdrive.selfdrived.events import EventName +from openpilot.starpilot.aol.intent import read_settings +from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture +from openpilot.starpilot.aol.tests.test_gm_stock_gas_source_gap import params +from openpilot.starpilot.aol.tests.test_input_deadline_continuity import intent + +BASE = 10_000_000_000 + + +@pytest.mark.parametrize('name', ['ascm', 'sdgm', 'bolt_acc']) +@pytest.mark.parametrize('fault', ['none', 'native', 'can', 'eps', 'cancel', 'main']) +def test_fresh_stock_gas_source_keeps_lateral_after_previous_output_expires(name, fault): + with OpenpilotPrefix(), ExitStack() as cleanup: + case = fixture.IoniqAvailabilityTests(methodName='runTest') + cleanup.callback(case.doCleanups) + sd, controls, step = case._authority_callers(False, cp=params(name), genuine_events=True) + sd.aol_settings = read_settings(sd.params) + state = fixture.car_state() + state.vEgo = state.vEgoRaw = 12.1 + state.cruiseState.enabled = False + for tick in (0, 1): + axis, host, command = step(tick, observed_state=state, gas=True, + native_request_long=False, panda_controls_allowed=False) + assert axis.lateralActive and command.latActive and not command.longActive and not host.enabled + prior_deadline = axis.validUntilMonoTime + now = BASE + 54_063_646 + with patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=now): + expired, _ = controls.state_control() + assert now > prior_deadline and not expired.latActive and not expired.longActive + if fault == 'can': + state.canValid = False + elif fault == 'eps': + state.steerFaultPermanent = True + elif fault == 'main': + state.cruiseState.available = False + elif fault == 'cancel': + state.buttonEvents = [fixture.car.CarState.ButtonEvent(type=fixture.car.CarState.ButtonEvent.Type.cancel, pressed=True)] + source = BASE + 44_038_177 + raw = messaging.new_message('carState', valid=True, logMonoTime=source) + raw.carState = state + authority = messaging.new_message('starpilotCarState', valid=True, logMonoTime=source) + authority.starpilotCarState.sourceCarStateMonoTime = source + if sd.steering_companion is not None: + sd.steering_companion._observe(source, True, authority.starpilotCarState) + axis, host, command = step(5, offset_ns=4_063_646, observed_state=state, gas=True, + car_state_message=raw, authority_message=authority, intent_message=intent(source, 6), native_request_long=False, + panda_controls_allowed=False, native_valid=fault != 'native') + assert not command.longActive and not axis.longitudinalActive and not host.enabled + if fault != 'none': + assert not command.latActive and not axis.lateralActive + return + assert axis.sourceCarStateMonoTime == source and axis.validUntilMonoTime > prior_deadline + assert axis.lateralActive and command.latActive and axis.nativeAcknowledged + assert not sd.aol_authority_lost and EventName.controlsMismatch not in sd.events.names + assert EventName.pcmDisable in sd.events.names diff --git a/openpilot/starpilot/car/gm/aol.py b/openpilot/starpilot/car/gm/aol.py index 1ab2b5cf71..1b1fa06681 100644 --- a/openpilot/starpilot/car/gm/aol.py +++ b/openpilot/starpilot/car/gm/aol.py @@ -30,9 +30,10 @@ def native_accepts_cp(cp, model: int, param: int) -> bool: def cc_independent_lateral_below_engage(cp, intent, *, enabled, active): """An armed conventional-cruise LAT request uses its steering speed gate.""" - from opendbc.car.gm.values import is_bolt_cc_profile, is_volt_cc_profile + from opendbc.car.gm.values import is_bolt_cc_profile, is_volt_cc_profile, is_ordinary_cc_profile return bool(not enabled and not active and qualified_gm(cp) and aol_enabled(cp) and - (is_bolt_cc_profile(cp) or is_volt_cc_profile(cp)) and intent is not None and intent.settingsQualified and + (is_bolt_cc_profile(cp) or is_volt_cc_profile(cp) or is_ordinary_cc_profile(cp)) and + intent is not None and intent.settingsQualified and intent.allowedLatch and intent.lateralArmed and not intent.pauseLateral) diff --git a/prebuilt.json b/prebuilt.json index 862e821f40..d36bb79c92 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "68e37513f82da5dfd36a359db864fdc92a984ed68ade1edc3a924fad108cf493", "schema_version": 1} +{"protected_inventory_sha256": "b02cde93f4bbbd868768eee6f18c3f2ed5275dc4d3ee1972f6ed728204777f03", "schema_version": 1}