import gc import itertools import time from types import SimpleNamespace import pytest from openpilot.selfdrive.car.card import alpha_long_requested from opendbc.car import gen_empty_fingerprint, structs from opendbc.car.hyundai.interface import CarInterface from opendbc.car.hyundai.classic_long_aol import CLASSIC_LONG_IDS, qualified, aol_word, native_accepts from opendbc.car.hyundai.values import CAR, HyundaiFlags from openpilot.starpilot.car.hyundai.aol import policy_for, create_intent from openpilot.starpilot.aol.intent import AolSettings, AOL_TOGGLE def params(identity=CAR.HYUNDAI_SONATA, *, release=False, lda=False): fingerprint = gen_empty_fingerprint() if lda: fingerprint[0][0x391] = 8 request = alpha_long_requested(SimpleNamespace(get_bool=lambda key: True), is_release=release) return CarInterface.get_params(identity, fingerprint, [], request, release, False) @pytest.mark.parametrize('identity', sorted(CLASSIC_LONG_IDS, key=str)) @pytest.mark.parametrize('lda', (False, True)) def test_actual_final_factory_state_and_exact_marked_configuration(identity, lda): cp = params(identity, lda=lda) if cp.dashcamOnly or cp.lateralTuning.which() != 'torque' or cp.steerControlType != structs.CarParams.SteerControlType.torque: assert not qualified(cp) return assert qualified(cp) before = cp.to_dict() policy = policy_for(cp) cp.safetyConfigs[0].safetyParam |= policy.safety_param_addition cp.alternativeExperience |= policy.alternative_experience_addition expected = dict(before) expected['safetyConfigs'] = [{**before['safetyConfigs'][0], 'safetyParam': aol_word(cp)}] expected['alternativeExperience'] = 32 assert cp.to_dict() == expected assert native_accepts(cp, int(cp.safetyConfigs[0].safetyModel.raw), aol_word(cp)) release = params(identity, release=True, lda=lda) assert not release.openpilotLongitudinalControl assert release.pcmCruise assert not qualified(release) @pytest.mark.parametrize('identity', (CAR.GENESIS_G80, CAR.KIA_XCEED_PHEV, CAR.HYUNDAI_ELANTRA_2024, CAR.HYUNDAI_ELANTRA_HEV_2024)) def test_unqualified_legacy_prepared_owner_and_camera_topologies_excluded(identity): assert not qualified(params(identity)) def test_malformed_final_contract_never_marks_aol(): cp = params() for field, value in ( ('flags', int(cp.flags) | int(HyundaiFlags.EV)), ('pcmCruise', True), ('openpilotLongitudinalControl', False), ('passive', True), ('notCar', True), ('dashcamOnly', True), ('alternativeExperience', 1), ('alphaLongitudinalAvailable', False), ): bad = cp.as_reader().as_builder() setattr(bad, field, value) assert not qualified(bad) for word in (0x404 | 8, 0x404 | 256, 0x404 | 512, 0x404 | 4096, 0x404 | 32768, 0x407): bad = cp.as_reader().as_builder() bad.safetyConfigs[0].safetyParam = word assert not qualified(bad) bad = cp.as_reader().as_builder() bad.safetyConfigs[0].safetyModel = structs.CarParams.SafetyModel.hyundaiLegacy assert not qualified(bad) def test_tcs_availability_without_physical_or_standard_engagement_is_not_intent(): cp = params() intent = create_intent(cp, AolSettings(True, 0.0, AOL_TOGGLE, AOL_TOGGLE, (0, 0, 0), (0, 0, 0))) cs = structs.CarState.new_message(canValid=True, canTimeout=False, gearShifter='drive') cs.cruiseState.available = True for tick in range(20): intent.update(cs, fault_active=False, now_ns=1_000_000_000 + tick * 10_000_000, standard_enabled=False) assert not intent.allowed_latch CARD_IDENTITIES = (CAR.HYUNDAI_SONATA, CAR.HYUNDAI_IONIQ, CAR.HYUNDAI_KONA_EV) CARD_CASES = [ (identity, alpha, disable, aol, False) for identity, alpha, disable, aol in itertools.product(CARD_IDENTITIES, (False, True), (False, True), (False, True)) ] CARD_CASES += [(identity, True, False, True, True) for identity in CARD_IDENTITIES] @pytest.mark.parametrize('identity,alpha,disable,aol,release', CARD_CASES) def test_actual_card_published_cp_controls_and_disabled_loop(identity, alpha, disable, aol, release, monkeypatch): from opendbc.car.hyundai.classic_scc_aol import qualified as stock_qualified, aol_word as stock_word from opendbc.car.hyundai.values import HyundaiSafetyFlags from openpilot.cereal import messaging from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix from openpilot.starpilot.aol.tests.classic_startup_test_helpers import prepared_classic_card from openpilot.selfdrive.controls.controlsd import Controls from openpilot.starpilot.lateral.tests.test_lane_runtime import feed monkeypatch.setenv('SIMULATION', '1') monkeypatch.setenv('REPLAY', '1') monkeypatch.setenv('AOL_REPLAY_RUNTIME', '0') with OpenpilotPrefix(): saved = Params() for key, value in ( ('OpenpilotEnabledToggle', True), ('AlphaLongitudinalEnabled', alpha), ('IsReleaseBranch', release), ('DisableOpenpilotLongitudinal', disable), ('AlwaysOnLateral', aol), ): saved.put_bool(key, value, block=True) saved.put('LKASButtonControl', AOL_TOGGLE, block=True) saved.put('MainCruiseButtonControl', AOL_TOGGLE, block=True) fingerprint = gen_empty_fingerprint() fingerprint[0][0x391] = 8 requested = alpha_long_requested(saved, is_release=saved.get_bool('IsReleaseBranch')) cp = CarInterface.get_params(identity, fingerprint, [], requested, release, False) assert cp.openpilotLongitudinalControl == requested assert cp.pcmCruise != requested assert CarInterface.startup_required(cp) == requested assert qualified(cp) if requested else stock_qualified(cp) before = cp.to_dict() subscriber = messaging.sub_sock('carParams', timeout=100, conflate=True) ci = card = controls = None try: ci = CarInterface(cp) card = prepared_classic_card(cp) ci = card.CI expected = dict(before) if aol: expected['safetyConfigs'] = [{**before['safetyConfigs'][0], 'safetyParam': aol_word(card.CP) if requested else stock_word(card.CP)}] expected['alternativeExperience'] = 32 assert card.CP.to_dict() == expected assert bool(card.CP.safetyConfigs[0].safetyParam & HyundaiSafetyFlags.LONG) == requested assert card.aol_qualified == aol assert (card.vehicle_startup.owner is not None) == requested with structs.CarParams.from_bytes(saved.get('CarParams')) as published: assert published.to_dict() == card.CP.to_dict() parsed = ci.update([]) event = None for _ in range(10): card.car_params_published = False card.state_publish(parsed, None) event = messaging.recv_one(subscriber) if event is not None: break time.sleep(0.01) assert event is not None and event.which() == 'carParams' and event.valid assert event.carParams.to_dict() == card.CP.to_dict() controls = Controls() assert controls.CP.to_dict() == card.CP.to_dict() feed(controls, 1_000_000_000, 0, active=False, enabled=False, can_valid=False, can_timeout=True) command, lateral_log = controls.state_control() assert not command.enabled and not command.latActive and not command.longActive controls.publish(command, lateral_log) finally: if card is not None: card.vehicle_startup.close() del controls, card, ci, subscriber gc.collect() @pytest.mark.parametrize('identity', (CAR.GENESIS_G80, CAR.KIA_XCEED_PHEV)) @pytest.mark.parametrize('lda', (False, True)) def test_prepared_legacy_candidate_exact_axis_contract(identity, lda): from opendbc.car.hyundai.legacy_long_aol import candidate, ordinary_word cp = candidate(params(identity, lda=lda), requested=True) assert cp is not None and qualified(cp) before = cp.to_dict() policy = policy_for(cp) cp.safetyConfigs[0].safetyParam |= policy.safety_param_addition cp.alternativeExperience = policy.alternative_experience_addition assert qualified(cp, marked_only=True) assert native_accepts(cp, int(cp.safetyConfigs[0].safetyModel.raw), aol_word(cp)) assert cp.safetyConfigs[0].safetyParam == ordinary_word(cp) + 2 assert cp.longitudinalTuning.to_dict() == before['longitudinalTuning'] assert cp.steerActuatorDelay == before['steerActuatorDelay'] for word in (4, 6, 0xE904, 0xE914): bad = cp.as_reader().as_builder() bad.safetyConfigs[0].safetyParam = word assert not qualified(bad) for field in ('passive', 'dashcamOnly', 'notCar', 'pcmCruise'): bad = cp.as_reader().as_builder() setattr(bad, field, True) assert not qualified(bad) @pytest.mark.parametrize('identity', (CAR.GENESIS_G80, CAR.KIA_XCEED_PHEV)) @pytest.mark.parametrize('radar_available', (False, True)) def test_legacy_candidate_radar_state_and_consumed_stopping_rate(identity, radar_available): from opendbc.car.hyundai.legacy_long_aol import candidate, aol_word from openpilot.selfdrive.controls.lib.longcontrol import LongControl stock = params(identity) stock.radarUnavailable = not radar_available active = candidate(stock, requested=True) assert active is not None assert active.radarUnavailable == (True if identity == CAR.GENESIS_G80 else not radar_available) assert stock.radarUnavailable == (not radar_available) assert active.longitudinalTuning.to_dict() == stock.longitudinalTuning.to_dict() for marked in (False, True): cp = active.as_reader().as_builder() if marked: cp.safetyConfigs[0].safetyParam = aol_word(cp) cp.alternativeExperience = 32 control = LongControl(cp) assert control.extension is not None assert control.stopping_decel_rate == 0.8 assert LongControl(stock).stopping_decel_rate == 1.0 malformed = active.as_reader().as_builder() malformed.safetyConfigs[0].safetyParam = 4 assert LongControl(malformed).stopping_decel_rate == 1.0 @pytest.mark.parametrize('family', ('legacy', 'classic_long', 'mixed', 'mixed_alpha', 'classic', 'non_scc', 'canfd_stock', 'canfd_long', 'ioniq6', 'angle')) @pytest.mark.parametrize('marked', (False, True)) def test_retained_marked_cp_saved_off_restarts_axis_transport(family, marked, monkeypatch): from opendbc.car.hyundai.legacy_long_aol import candidate from opendbc.car.hyundai.blended_longitudinal import candidate_from_stock from opendbc.car.hyundai.tests.test_blended_stock_aol import params as mixed_params from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix from openpilot.selfdrive.controls.controlsd import Controls from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD from openpilot.starpilot.feature_runtime import enabled monkeypatch.setenv('SIMULATION', '1') monkeypatch.setenv('REPLAY', '1') monkeypatch.setenv('AOL_REPLAY_RUNTIME', '0') from openpilot.starpilot.aol.tests.test_classic_scc_profiles import params as classic_params from openpilot.starpilot.aol.tests.test_non_scc_profiles import params as non_scc_params from openpilot.starpilot.aol.tests.test_canfd_stock_profiles import params as canfd_params from opendbc.car.hyundai.torque_ev_startup import candidate as canfd_candidate from opendbc.car.hyundai.tests.test_stock_angle_aol import source_profile from opendbc.car.hyundai.tests.test_ioniq6_stock_parser import params as ioniq6_params factories = { 'legacy': lambda: candidate(params(CAR.GENESIS_G80), requested=True), 'classic_long': params, 'mixed': mixed_params, 'mixed_alpha': lambda: candidate_from_stock(mixed_params(), alpha_requested=True, native_qualified=True), 'classic': lambda: classic_params(CAR.HYUNDAI_SONATA), 'non_scc': lambda: non_scc_params(CAR.KIA_FORTE), 'canfd_stock': lambda: canfd_params(CAR.KIA_EV6), 'canfd_long': lambda: canfd_candidate(canfd_params(CAR.HYUNDAI_IONIQ_5), requested=True, is_release=False), 'ioniq6': lambda: ioniq6_params(False)[0], 'angle': lambda: source_profile(CAR.KIA_EV9), } cp = factories[family]() assert cp is not None if marked: policy = policy_for(cp) cp.safetyConfigs[0].safetyParam |= policy.safety_param_addition cp.alternativeExperience = policy.alternative_experience_addition with OpenpilotPrefix(): saved = Params() saved.put_bool('AlwaysOnLateral', False, block=True) saved.put('CarParams', cp.to_bytes(), block=True) policy = policy_for(cp) assert policy.full_axis_runtime_required == marked assert policy.ordinary_axis_ack_required == (marked or family == 'angle') assert enabled(saved, cp, 'aol', {'AOL_REPLAY_RUNTIME': '0'}) == marked if marked: negatives = [('passive', True)] if family != 'ioniq6': negatives.append(('alternativeExperience', 1)) for field, value in negatives: bad = cp.as_reader().as_builder() setattr(bad, field, value) assert not policy_for(bad).full_axis_runtime_required bad = cp.as_reader().as_builder() bad.safetyConfigs[0].safetyParam = 0xFFFF assert not policy_for(bad).full_axis_runtime_required controls = drive = None try: controls = Controls() drive = SelfdriveD(CP=cp) assert controls.aol_replay == marked assert drive.aol_replay == marked assert drive.axis_transport_required == (marked or family == 'angle') assert not saved.get_bool('AlwaysOnLateral') assert controls.CP.to_dict() == cp.to_dict() finally: del controls, drive gc.collect()