diff --git a/opendbc_repo/opendbc/car/gm/carstate.py b/opendbc_repo/opendbc/car/gm/carstate.py index eac50c25bd..7e8a2c092a 100644 --- a/opendbc_repo/opendbc/car/gm/carstate.py +++ b/opendbc_repo/opendbc/car/gm/carstate.py @@ -70,6 +70,8 @@ class CarState(CarStateBase): self.buttons_counter = 0 self.distance_button = 0 + self.flashed_volt_profile = is_volt_flashed_profile(CP) + self.flashed_lkas_button = None self.pedal_sensor_healthy = False self.pedal_sensor_ts_nanos = 0 self.bolt_pedal_gear_ts_nanos = 0 @@ -535,7 +537,7 @@ class CarState(CarStateBase): # A missing camera can leave the ECU faulted; that is not engaged cruise. ret.cruiseState.enabled &= pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.FAULTED ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL - if is_volt_flashed_profile(self.CP): + if self.flashed_volt_profile: # There is no stock cruise/ACC controller. Preserve the actual EBCM fault. ret.accFaulted = friction_unavailable ret.brakePressed |= bool(pt_cp.vl["ECMEngineStatus"]["BrakePressed"]) @@ -684,6 +686,15 @@ class CarState(CarStateBase): ret.buttonEvents = [event for old, new in edges for event in create_button_events(new, old, BUTTONS_DICT, unpressed_btn=CruiseButtons.UNPRESS)] + create_button_events( self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise}) + if self.flashed_volt_profile: + lkas_events = [] + for pressed in pt_cp.vl_all["ASCMSteeringButton"]["LKAButton"]: + if self.flashed_lkas_button is not None: + lkas_events.extend(create_button_events(pressed, self.flashed_lkas_button, {1: ButtonType.lkas})) + self.flashed_lkas_button = pressed + if lkas_events: + ret.buttonEvents = [event.to_dict() for event in ret.buttonEvents] + lkas_events + if ret.vEgo < self.CP.minSteerSpeed: ret.lowSpeedAlert = True diff --git a/openpilot/starpilot/car/gm/aol.py b/openpilot/starpilot/car/gm/aol.py index 09e35fb125..607b8eda94 100644 --- a/openpilot/starpilot/car/gm/aol.py +++ b/openpilot/starpilot/car/gm/aol.py @@ -4,17 +4,17 @@ import math from opendbc.car.gm.cancel_remap import aol_enabled, enabled as cancel_remap_enabled, supported as cancel_remap_supported from opendbc.car.structs import CarParams -from opendbc.car.gm.values import CAR, GMFlags, control_flags, camera_acc_pedal_profile, is_volt_ascm_longitudinal +from opendbc.car.gm.values import CAR, GMFlags, control_flags, camera_acc_pedal_profile, is_volt_ascm_longitudinal, is_volt_flashed_profile from opendbc.car.gm.aol import GM_AOL_ALTERNATIVE_EXPERIENCE, GM_AOL_WORDS, qualified_gm from openpilot.starpilot.aol.policy import AolVehiclePolicy -from openpilot.starpilot.aol.intent import AOL_TOGGLE, AolCardIntent +from openpilot.starpilot.aol.intent import AOL_TOGGLE, PAUSE_LATERAL, AolCardIntent def policy_for(cp) -> AolVehiclePolicy: qualified = qualified_gm(cp) return AolVehiclePolicy(intent_supported=qualified, settings_supported=qualified, runtime_supported=qualified, normal_runtime_supported=qualified, - lkas_button_supported=False, + lkas_button_supported=qualified and is_volt_flashed_profile(cp), full_axis_runtime_required=qualified and aol_enabled(cp), alternative_experience_addition=GM_AOL_ALTERNATIVE_EXPERIENCE if qualified else 0) @@ -205,7 +205,7 @@ class GmAolCardIntent(AolCardIntent): """Follow physical MAIN; a current fault inhibits intent until healthy again.""" def __init__(self, settings, cp=None): - super().__init__(settings, lkas_button_supported=False) + super().__init__(settings, lkas_button_supported=is_volt_flashed_profile(cp)) self.cp = cp self.requires_fault_observation = True self._main_cycle_required = False @@ -222,6 +222,12 @@ class GmAolCardIntent(AolCardIntent): semantic.buttonEvents = [event for event in CS.buttonEvents if str(event.type) != 'cancel'] super().update_auxiliary(semantic, media=media, media_eligible=media_eligible, fault_active=fault_active) + @property + def lkas_action(self): + # MAIN still owns automatic arming; a mapped LKAS toggle pauses steering. + action = super().lkas_action + return PAUSE_LATERAL if action == AOL_TOGGLE else action + @property def main_action(self): # GM MAIN reports a persistent physical level, not an independent AOL toggle. diff --git a/openpilot/starpilot/car/gm/tests/test_flashed_lkas.py b/openpilot/starpilot/car/gm/tests/test_flashed_lkas.py new file mode 100644 index 0000000000..447762d16c --- /dev/null +++ b/openpilot/starpilot/car/gm/tests/test_flashed_lkas.py @@ -0,0 +1,106 @@ +from types import SimpleNamespace + +import pytest +from opendbc.can import CANPacker +from opendbc.car import Bus, structs +from opendbc.car.gm.interface import CarInterface +from opendbc.car.gm.tests.test_volt_flashed_obd import params as flashed_params, tick +from opendbc.car.gm.values import DBC +from openpilot.common.params import Params +from openpilot.starpilot.aol.intent import AolSettings +from openpilot.starpilot.aol.tests.test_gm_startup_main import cases +from openpilot.starpilot.aol.vehicle import create_intent, policy_for +from openpilot.starpilot.controllers.wheel_actions import button_reported +from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner +from openpilot.starpilot.ui.feature_settings_state import row_request + + +def params(): + cp = flashed_params() + cp.alternativeExperience = 32 + return cp + + +def parser(): + cp = params() + return CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt]) + + +def feed_lkas(ci, packer, levels, index, cruise=1): + now = 1_000_000_000 + index * 10_000_000 + _, frames = tick(SimpleNamespace(update=lambda _: None), packer, now, index) + frames = [m for m in frames if m[0] != 0x1E1] + for level in levels: + frames.append(packer.make_can_msg('ASCMSteeringButton', 0, {'ACCButtons': cruise, 'ACCAlwaysOne': 1, + 'LKAButton': level, 'RollingCounter': index % 4})) + return ci.update([(now, frames)]) + + +def test_parser_emits_each_lkas_edge_without_cruise_enable(): + ci, packer = parser() + for i in range(40): + cs = feed_lkas(ci, packer, [0], i) + assert cs.canValid + cs = feed_lkas(ci, packer, [1, 0, 1, 1, 0], 40) + assert [(str(b.type), b.pressed) for b in cs.buttonEvents] == [('lkas', True), ('lkas', False), ('lkas', True), ('lkas', False)] + assert not cs.buttonEnable and not cs.cruiseState.enabled + assert not feed_lkas(ci, packer, [], 41).buttonEvents + + +def test_first_held_sample_is_not_a_new_press(): + ci, packer = parser() + assert not feed_lkas(ci, packer, [1], 0).buttonEvents + assert not feed_lkas(ci, packer, [1], 1).buttonEvents + assert [(str(b.type), b.pressed) for b in feed_lkas(ci, packer, [0], 2).buttonEvents] == [('lkas', False)] + + +@pytest.mark.parametrize('mapping', [0, 3, 4, 9]) +def test_main_arms_at_startup_and_lkas_only_performs_its_mapping(mapping): + cp = params() + intent = create_intent(cp, AolSettings(True, 0., mapping, 0, (0, 0, 0), (0, 0, 0)), policy_for(cp)) + cs = structs.CarState(canValid=True, gearShifter='drive', vEgo=10.) + cs.cruiseState.available = True + intent.update(cs, now_ns=1, fault_active=False) + assert intent.allowed_latch and not intent.pause_lateral and not intent.pause_longitudinal + for pressed, now in [(True, 2), (False, 3)]: + cs.buttonEvents = [structs.CarState.ButtonEvent(type='lkas', pressed=pressed)] + intent.update(cs, now_ns=now, fault_active=False) + assert intent.allowed_latch and intent.pause_lateral == (mapping in (3, 9)) + assert intent.pause_longitudinal == (mapping == 4) + cs.buttonEvents = [] + intent.update(cs, now_ns=4, fault_active=False) + assert intent.pause_lateral == (mapping in (3, 9)) + cs.buttonEvents = [structs.CarState.ButtonEvent(type='lkas', pressed=True)] + intent.update(cs, now_ns=5, fault_active=False) + assert not intent.pause_lateral and not intent.pause_longitudinal and intent.allowed_latch + cs.cruiseState.available = False + cs.buttonEvents = [] + intent.update(cs, now_ns=6, fault_active=False) + assert not intent.allowed_latch + + +def test_lkas_row_is_visible_and_assignment_uses_actual_settings_owner(tmp_path): + cp = params() + saved = Params(str(tmp_path)) + owner = FeatureSettingsOwner(saved, lambda _: True, vehicle_fingerprint=lambda: str(cp.carFingerprint), vehicle_params=lambda: cp) + page = owner.snapshot('wheel', parked=True, system_long=True, lateral_context=True, metric=False) + row = next(r for r in page.rows if r.key == 'wheel:LKASButtonControl') + assert row.value == 'Off' and row.available and 'Pause steering' in row.choices and 'Toggle AOL' in row.choices + assert owner.apply(row_request(row, 'Pause steering')) + assert saved.get('LKASButtonControl') == 3 + + +def test_other_gm_profiles_keep_the_existing_lkas_contract(): + for cp in cases(): + assert not policy_for(cp).lkas_button_supported + assert not button_reported(cp, 'LKASButtonControl') + + +def test_lkas_does_not_erase_a_simultaneous_set_release(): + ci, packer = parser() + for i in range(40): + feed_lkas(ci, packer, [0], i) + feed_lkas(ci, packer, [0], 40, cruise=3) + cs = feed_lkas(ci, packer, [1], 41) + assert cs.buttonEnable + assert [(str(b.type), b.pressed) for b in cs.buttonEvents] == [('decelCruise', False), ('lkas', True)] diff --git a/openpilot/starpilot/controllers/wheel_actions.py b/openpilot/starpilot/controllers/wheel_actions.py index 6396cf48e7..c7600dc025 100644 --- a/openpilot/starpilot/controllers/wheel_actions.py +++ b/openpilot/starpilot/controllers/wheel_actions.py @@ -68,7 +68,10 @@ def button_reported(cp, key): cp.flags & TeslaFlags.AOL_SCREEN_BUTTON) return bool(key in KEYS[5:8] and cp.carFingerprint == CAR.TESLA_MODEL_S_PREAP) if key == 'LKASButtonControl': - if brand in ('chrysler', 'gm', 'mazda', 'nissan', 'volkswagen', 'volvo'): + if brand == 'gm': + from opendbc.car.gm.values import is_volt_flashed_profile + return is_volt_flashed_profile(cp) + if brand in ('chrysler', 'mazda', 'nissan', 'volkswagen', 'volvo'): return False if brand == 'hyundai': from opendbc.car.hyundai.values import CAR, HyundaiFlags, is_blended diff --git a/prebuilt.json b/prebuilt.json index 8fd8b01fb8..e07cc86c61 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "dde91e1ce9c780f132c91438b2fb0559f51132e10dea387a6c1f64a03f2da779", "schema_version": 1} +{"protected_inventory_sha256": "576253242aefccbd2c9bb70dccf54b8ac10c06832c285e686e0105bacce2fd6c", "schema_version": 1}