Preserve independent GM steering across speed and input transitions

This commit is contained in:
firestar5683
2026-11-05 19:50:48 -06:00
parent 3160182986
commit dc73f431a4
5 changed files with 173 additions and 4 deletions
+2 -1
View File
@@ -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):
@@ -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
@@ -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
+3 -2
View File
@@ -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)
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "68e37513f82da5dfd36a359db864fdc92a984ed68ade1edc3a924fad108cf493", "schema_version": 1}
{"protected_inventory_sha256": "b02cde93f4bbbd868768eee6f18c3f2ed5275dc4d3ee1972f6ed728204777f03", "schema_version": 1}