From 4dd24beabca53759f8f45f212b685e2ddb8c0af5 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:19:20 -0500 Subject: [PATCH] Separate Volt CC steering minimum from cruise engagement --- openpilot/selfdrive/car/car_events.py | 10 +++- .../aol/tests/test_gm_parked_startup.py | 23 +++++++-- .../aol/tests/test_volt_cc_engagement.py | 50 +++++++++++++++++++ openpilot/starpilot/car/gm/aol.py | 6 +-- prebuilt.json | 2 +- 5 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 openpilot/starpilot/aol/tests/test_volt_cc_engagement.py diff --git a/openpilot/selfdrive/car/car_events.py b/openpilot/selfdrive/car/car_events.py index 2d2b91fec8..3c844f04d0 100644 --- a/openpilot/selfdrive/car/car_events.py +++ b/openpilot/selfdrive/car/car_events.py @@ -19,6 +19,8 @@ class CarEvents: self.CP = CP self.event_rules = event_rules(CP) + 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.low_speed_alert = False self.no_steer_warning = False @@ -74,6 +76,12 @@ class CarEvents: events.add(EventName.manualRestart) elif self.CP.brand == 'gm': + if self.volt_cc_profile: + # Conventional LONG requires the car's cruise acknowledgment. MAIN alone + # still arms independent steering and cannot create a LONG engagement. + if (CS.canValid and not CS.canTimeout and CS.cruiseState.available and CS.cruiseState.enabled and + (CS.buttonEnable or not CS_prev.cruiseState.enabled)): + events.add(EventName.buttonEnable) # Enabling at a standstill with brake is allowed # TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs if CS.vEgo < self.CP.minEnableSpeed and not (CS.standstill and CS.brakePressed and @@ -152,7 +160,7 @@ class CarEvents: events.add(EventName.invalidLkasSetting) if CS.lowSpeedAlert: events.add(EventName.belowSteerSpeed) - if CS.buttonEnable: + if CS.buttonEnable and not self.volt_cc_profile: events.add(EventName.buttonEnable) # Handle cancel button presses diff --git a/openpilot/starpilot/aol/tests/test_gm_parked_startup.py b/openpilot/starpilot/aol/tests/test_gm_parked_startup.py index 1095af5a3d..0c060ea258 100644 --- a/openpilot/starpilot/aol/tests/test_gm_parked_startup.py +++ b/openpilot/starpilot/aol/tests/test_gm_parked_startup.py @@ -26,8 +26,8 @@ def test_cold_park_main_waits_without_losing_intent(index, main_action): _exercise_parked_main(index, main_action) -def _exercise_parked_main(index, main_action, *, below_engage=False): - cp = list(cases())[index] +def _exercise_parked_main(index, main_action, *, below_engage=False, cp=None): + cp = list(cases())[index] if cp is None else cp case = fixture.IoniqAvailabilityTests(methodName='runTest') with OpenpilotPrefix(), ExitStack() as cleanup: cleanup.callback(case.doCleanups) @@ -44,6 +44,9 @@ def _exercise_parked_main(index, main_action, *, below_engage=False): cleanup.callback(card.vehicle_startup.close) card.ci_initialized = True card.car_params_published = True + if getattr(card, 'volt_cc_selected', False): + card.volt_cc_boot_offset_ns = 0 + card.volt_cc_source_floor_ns = 9_000_000_000 card.sm.frame = 1 assert card.CP.to_dict() == sd.CP.to_dict() == controls.CP.to_dict() original_pm = card.pm @@ -159,6 +162,11 @@ def _exercise_parked_main(index, main_action, *, below_engage=False): def receive_card(now): nonlocal last_control_stamp rows = list(wire_batch(now)) + if getattr(card, 'volt_cc_selected', False): + device = messaging.new_message('deviceState', valid=True, logMonoTime=now) + device.deviceState.started = True + device.deviceState.startedMonoTime = 9_000_000_000 + rows.append(device.as_reader()) rows += [sd.pm.messages[k] for k in ('aolAxisState', 'selfdriveState', 'onroadEvents') if k in sd.pm.messages] published = controls.pm.messages['carControl'] if published.logMonoTime != last_control_stamp: @@ -178,10 +186,19 @@ def _exercise_parked_main(index, main_action, *, below_engage=False): def physical(tick, stamp, driving): speed = max(4.0, cp.minSteerSpeed + 2.0) if below_engage: + speed = cp.minSteerSpeed + 0.05 assert speed < cp.minEnableSpeed else: speed = max(speed, cp.minEnableSpeed + 2.0) packets = parsed_packets(cp, packer, stamp, tick, speed if driving else 0.0, True) + from opendbc.car.gm.values import CAR, is_volt_cc_f1_profile + if is_volt_cc_f1_profile(cp): + packets = [f for f in packets if f[0] != 0xBE] + packets.append(packer.make_can_msg('EBCMBrakePedalPosition', 0, {})) + if cp.carFingerprint == CAR.CHEVROLET_VOLT_CC and tick in (320, 321): + button = packer.make_can_msg('ASCMSteeringButton', 0, + {'ACCButtons': 3 if tick == 320 else 1, 'RollingCounter': tick % 4}) + packets = [f for f in packets if f[0] != button[0]] + [button] packets = [f for f in packets if f[0] not in (0x1F5, 0x12A, 0x184, 0x17D)] if cp.pcmCruise and cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera: address = packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {})[0] @@ -210,7 +227,7 @@ def _exercise_parked_main(index, main_action, *, below_engage=False): assert safety.safety_config_valid() and safety.aol_get_permission_mask() == 0 tx = 0 active = 0 - for tick in range(1, 381): + for tick in range(1, 581 if below_engage and cp.carFingerprint == "CHEVROLET_VOLT_CC" else 381): now = 10_020_000_000 + (tick - 1) * 10_000_000 driving = tick > 300 safety.set_timer(now // 1000) diff --git a/openpilot/starpilot/aol/tests/test_volt_cc_engagement.py b/openpilot/starpilot/aol/tests/test_volt_cc_engagement.py new file mode 100644 index 0000000000..6ba1fd8096 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_volt_cc_engagement.py @@ -0,0 +1,50 @@ +from dataclasses import replace + +import pytest +from opendbc.car import structs +from opendbc.car.common.conversions import Conversions as CV +from opendbc.car.gm.tests.test_volt_cc_f1 import params as f1_params +from opendbc.car.gm.tests.test_volt_cc_gas import fixture as be_fixture +from openpilot.cereal import log +from openpilot.selfdrive.car.car_events import CarEvents +from openpilot.starpilot.aol.tests.test_gm_cc_lateral_minimum import setup +from openpilot.starpilot.aol.tests.test_gm_parked_startup import _exercise_parked_main +from openpilot.starpilot.car.gm.aol import cc_independent_lateral_below_engage + + +def params(f1): + cp = f1_params() if f1 else be_fixture()[0] + cp.alternativeExperience = 32 + return cp + + +@pytest.mark.parametrize('f1', [False, True]) +def test_volt_cc_steering_minimum_keeps_cruise_minimum(f1): + cp = params(f1) + _, intent = setup() + assert cp.minSteerSpeed == pytest.approx(7 * CV.MPH_TO_MS) + assert cp.minEnableSpeed == pytest.approx(24 * CV.MPH_TO_MS) + assert cc_independent_lateral_below_engage(cp, intent, enabled=False, active=False) + assert not cc_independent_lateral_below_engage(cp, replace(intent, allowedLatch=False), enabled=False, active=False) + assert not cc_independent_lateral_below_engage(cp, intent, enabled=True, active=True) + + +@pytest.mark.parametrize('f1', [False, True]) +@pytest.mark.parametrize('active,previous,button,valid,expected', [ + (False, False, True, True, False), (True, False, False, True, True), + (True, True, True, True, True), (True, True, False, True, False), + (True, False, False, False, False)]) +def test_volt_cc_long_waits_for_real_cruise_ack(f1, active, previous, button, valid, expected): + cp = params(f1) + cs = structs.CarState(canValid=valid, buttonEnable=button, vEgo=12, gearShifter='drive') + cs.cruiseState.available = True + cs.cruiseState.enabled = active + prior = structs.CarState() + prior.cruiseState.enabled = previous + events = CarEvents(cp).update(cs, prior, structs.CarControl()) + assert (log.OnroadEvent.EventName.buttonEnable in events.names) == expected + + +@pytest.mark.parametrize('f1', [False, True]) +def test_volt_cc_main_already_on_arms_steering_at_seven_mph(f1): + _exercise_parked_main(0, 0, below_engage=True, cp=params(f1)) diff --git a/openpilot/starpilot/car/gm/aol.py b/openpilot/starpilot/car/gm/aol.py index a5773dfda2..09e35fb125 100644 --- a/openpilot/starpilot/car/gm/aol.py +++ b/openpilot/starpilot/car/gm/aol.py @@ -29,10 +29,10 @@ def native_accepts_cp(cp, model: int, param: int) -> bool: def cc_independent_lateral_below_engage(cp, intent, *, enabled, active): - """An armed independent Bolt LAT request uses its steering speed gate.""" - from opendbc.car.gm.values import is_bolt_cc_profile + """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 return bool(not enabled and not active and qualified_gm(cp) and aol_enabled(cp) and - is_bolt_cc_profile(cp) and intent is not None and intent.settingsQualified and + (is_bolt_cc_profile(cp) or is_volt_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 f98c93bd2c..a6b398f4de 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "df360061636f084000aa4afe04c561fc519e8cebcb1c84a483a8eb716452db98", "schema_version": 1} +{"protected_inventory_sha256": "505172af4fab7bd15c27b90d3c52205e2a64db67875aa1447008cc0ed932c5dc", "schema_version": 1}