From 4440104f27e2d4d2baab9f8acfef07fa35a16e07 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:37:46 -0500 Subject: [PATCH] Hyundai AOL Profiles --- .../opendbc/car/hyundai/canfd_stock_aol.py | 106 ++++++++--- opendbc_repo/opendbc/car/hyundai/carstate.py | 10 +- .../opendbc/car/hyundai/classic_long_aol.py | 62 ++++++ .../tests/test_shared_canfd_stock_aol.py | 180 ++++++++++++++++++ opendbc_repo/opendbc/safety/modes/hyundai.h | 4 + .../opendbc/safety/modes/hyundai_canfd.h | 114 ++++++++++- .../opendbc/safety/modes/hyundai_canfd_aol.h | 17 ++ .../modes/hyundai_classic_non_scc_aol.h | 54 +++++- .../tests/test_hyundai_classic_long_aol.py | 125 ++++++++++++ .../test_hyundai_shared_canfd_stock_aol.py | 95 +++++++++ .../pandad/tests/test_aol_protocol.cc | 27 ++- .../aol/tests/test_classic_long_profiles.py | 164 ++++++++++++++++ openpilot/starpilot/car/hyundai/aol.py | 37 +++- openpilot/starpilot/car/hyundai/aol_policy.h | 9 +- .../car/hyundai/classic_long_intent.py | 43 +++++ tools/ci/run_vehicle_tests.py | 4 + 16 files changed, 1003 insertions(+), 48 deletions(-) create mode 100644 opendbc_repo/opendbc/car/hyundai/classic_long_aol.py create mode 100644 opendbc_repo/opendbc/car/hyundai/tests/test_shared_canfd_stock_aol.py create mode 100644 opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h create mode 100644 opendbc_repo/opendbc/safety/tests/test_hyundai_classic_long_aol.py create mode 100644 opendbc_repo/opendbc/safety/tests/test_hyundai_shared_canfd_stock_aol.py create mode 100644 openpilot/starpilot/aol/tests/test_classic_long_profiles.py create mode 100644 openpilot/starpilot/car/hyundai/classic_long_intent.py diff --git a/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py b/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py index cd5642f969..ee65e4b809 100644 --- a/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py +++ b/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py @@ -1,32 +1,92 @@ -"""Stock-cruise AOL for the shared EV CAN FD torque/button layout.""" +"""AOL admission for the ordinary CAN FD torque and physical-button layouts.""" + from opendbc.car.structs import CarParams from opendbc.car.hyundai.values import CAR, HyundaiFlags from opendbc.car.hyundai.hyundaicanfd import CanBus STOCK_AOL_MARKER = 0x0800 -STOCK_AOL_WORDS = frozenset((0x0811, 0x0891)) -# Ioniq 6 retains its existing stock/longitudinal owner and settings. -STOCK_EV_CARS = frozenset((CAR.HYUNDAI_KONA_EV_2ND_GEN, CAR.HYUNDAI_IONIQ_5, - CAR.HYUNDAI_IONIQ_5_N, CAR.KIA_NIRO_EV_2ND_GEN, CAR.KIA_EV6, - CAR.GENESIS_GV60_EV_1ST_GEN, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN)) +STOCK_EV_CARS = frozenset( + ( + CAR.HYUNDAI_KONA_EV_2ND_GEN, + CAR.HYUNDAI_IONIQ_5, + CAR.HYUNDAI_IONIQ_5_N, + CAR.KIA_NIRO_EV_2ND_GEN, + CAR.KIA_EV6, + CAR.GENESIS_GV60_EV_1ST_GEN, + CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, + ) +) +CARNIVAL_CARS = frozenset((CAR.KIA_CARNIVAL_2025, CAR.KIA_CARNIVAL_HEV_4TH_GEN)) +STOCK_AOL_WORDS = frozenset( + 0x0800 | gas | topology | extra + for gas in (0, 1, 2) + for topology in (0, 8, 16, 144, 32, 40, 48, 176) + for extra in (0, 0x0400, 0x2000, 0x2400) + if (topology not in (48, 176) or extra & 0x2000) + and (not extra & 0x0400 or topology in (8, 40)) + and (not extra & 0x2000 or (topology in (32, 40, 48, 176) and gas != 1)) +) + + +def _base_word(cp): + allowed = ( + HyundaiFlags.CANFD + | HyundaiFlags.EV + | HyundaiFlags.HYBRID + | HyundaiFlags.CANFD_LKA_STEER_MSG + | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT + | HyundaiFlags.CANFD_ALT_BUTTONS + | HyundaiFlags.CANFD_CAMERA_SCC + | HyundaiFlags.CANFD_ALT_GEARS + | HyundaiFlags.CANFD_ALT_GEARS_2 + | HyundaiFlags.CANFD_RADAR_SCC + | HyundaiFlags.CANFD_NO_RADAR_DISABLE + | HyundaiFlags.CCNC + ) + try: + factory = CAR(cp.carFingerprint) + except ValueError: + return None + if ( + cp.brand != 'hyundai' + or factory == CAR.HYUNDAI_IONIQ_6 + or not factory.config.flags & HyundaiFlags.CANFD + or factory.config.flags & (HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.NON_SCC) + or cp.passive + or cp.dashcamOnly + or cp.notCar + or cp.alternativeExperience != 0 + or cp.steerControlType != CarParams.SteerControlType.torque + or not cp.flags & HyundaiFlags.CANFD + or int(cp.flags) & ~int(allowed) + or len(cp.safetyConfigs) != 1 + or cp.safetyConfigs[0].safetyModel != CarParams.SafetyModel.hyundaiCanfd + ): + return None + # EV identity is static; hybrid fuel is selected by the actual 0xFA fingerprint. + if bool(cp.flags & HyundaiFlags.EV) != bool(factory.config.flags & HyundaiFlags.EV): + return None + ev, hybrid = bool(cp.flags & HyundaiFlags.EV), bool(cp.flags & HyundaiFlags.HYBRID) + lka = bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG) + alt_lka = bool(cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT) + alt_buttons = bool(cp.flags & HyundaiFlags.CANFD_ALT_BUTTONS) + camera = bool(cp.flags & HyundaiFlags.CANFD_CAMERA_SCC) + if (ev and hybrid) or (alt_lka and not lka) or (lka and camera): + return None + bus = CanBus(cp) + if (bus.ACAN, bus.ECAN, bus.CAM) != ((0, 1, 2) if lka else (1, 0, 2)): + return None + word = (1 if ev else 2 if hybrid else 0) | (16 if lka else 0) | (128 if alt_lka else 0) + word |= (32 if alt_buttons else 0) | (8 if camera else 0) + if cp.flags & HyundaiFlags.CCNC and camera: + word |= 0x0400 + if factory in CARNIVAL_CARS and alt_buttons: + word |= 0x2000 + return word if word | STOCK_AOL_MARKER in STOCK_AOL_WORDS else None def qualified(cp, *, marked_only=False): - required = HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEER_MSG - allowed = (required | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT | HyundaiFlags.CANFD_ALT_GEARS | - HyundaiFlags.CANFD_ALT_GEARS_2 | HyundaiFlags.CANFD_NO_RADAR_DISABLE | HyundaiFlags.CCNC) - if (cp.brand != 'hyundai' or cp.carFingerprint not in STOCK_EV_CARS or - cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience != 0 or - cp.openpilotLongitudinalControl or not cp.pcmCruise or - cp.steerControlType != CarParams.SteerControlType.torque or - int(cp.flags) & int(required) != int(required) or int(cp.flags) & ~int(allowed) or - len(cp.safetyConfigs) != 1): + word = _base_word(cp) + if word is None or cp.openpilotLongitudinalControl or not cp.pcmCruise: return False - bus = CanBus(cp) - if (bus.ACAN, bus.ECAN, bus.CAM) != (0, 1, 2): - return False - safety = cp.safetyConfigs[0] - base = 0x91 if cp.flags & HyundaiFlags.CANFD_LKA_STEER_MSG_ALT else 0x11 - return (safety.safetyModel == CarParams.SafetyModel.hyundaiCanfd and - (safety.safetyParam == (base | STOCK_AOL_MARKER) or - (not marked_only and safety.safetyParam == base))) + return cp.safetyConfigs[0].safetyParam in ((word | STOCK_AOL_MARKER,) if marked_only else (word, word | STOCK_AOL_MARKER)) diff --git a/opendbc_repo/opendbc/car/hyundai/carstate.py b/opendbc_repo/opendbc/car/hyundai/carstate.py index 30fba665ed..6a6b0f2fa3 100644 --- a/opendbc_repo/opendbc/car/hyundai/carstate.py +++ b/opendbc_repo/opendbc/car/hyundai/carstate.py @@ -1,3 +1,4 @@ +from opendbc.car.hyundai.classic_long_aol import qualified as qualified_classic_long from opendbc.car.hyundai.classic_scc_aol import qualified as qualified_classic_scc, ClassicSccLkasSources from opendbc.car.hyundai.ev9_camera_lead import EV9CameraLead from opendbc.car.hyundai.ev9_longitudinal import qualified as ev9_long_qualified @@ -54,7 +55,7 @@ class CarState(CarStateBase): def __init__(self, CP): super().__init__(CP) self.forte_lkas_sources = NonSccLkasSources() if qualified_non_scc(CP) and CP.flags & HyundaiFlags.HAS_LDA_BUTTON else None - if qualified_classic_scc(CP) and CP.flags & HyundaiFlags.HAS_LDA_BUTTON: + if (qualified_classic_scc(CP) or qualified_classic_long(CP)) and CP.flags & HyundaiFlags.HAS_LDA_BUTTON: self.forte_lkas_sources = ClassicSccLkasSources(CP.carFingerprint) self.ev9_long = ev9_long_qualified(CP) self.ev9_camera_lead = EV9CameraLead(CP) if self.ev9_long else None @@ -538,6 +539,10 @@ class CarState(CarStateBase): ] elif not CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CARNIVAL_ALT_RESUME: msgs.append(("CRUISE_BUTTONS_ALT", math.nan)) + from opendbc.car.hyundai.canfd_stock_aol import qualified as stock_aol_qualified + if stock_aol_qualified(CP): + msgs = [(name, frequency) for name, frequency in msgs if name != "CRUISE_BUTTONS_ALT"] + msgs.append(("CRUISE_BUTTONS_ALT", 50 if CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS else math.nan)) if CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: # These are actual state and safety inputs, not DBC defaults. Keep this # subscription expansion scoped to the new angle profile. @@ -575,7 +580,8 @@ class CarState(CarStateBase): from opendbc.car.hyundai.gv70_startup import eligible as gv70_eligible if ev6_eligible(CP) or gv70_eligible(CP): # Standard 1CF supplies buttons; alternate 1AA only holds optional units. - msgs.append(("CRUISE_BUTTONS_ALT", math.nan)) + if not any(name == "CRUISE_BUTTONS_ALT" for name, _ in msgs): + msgs.append(("CRUISE_BUTTONS_ALT", math.nan)) existing = {name for name, _ in msgs} for name, frequency in (("ACCELERATOR", 100), ("TCS", 50), ("WHEEL_SPEEDS", 100), ("MDPS", 100), (self.gear_msg_canfd, 100)): diff --git a/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py b/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py new file mode 100644 index 0000000000..0f9cc834f6 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/classic_long_aol.py @@ -0,0 +1,62 @@ +from opendbc.car import structs +from opendbc.car.hyundai.values import CAR, HyundaiFlags, HyundaiSafetyFlags, is_blended +from opendbc.car.hyundai.classic_scc_aol import CLASSIC_SCC_IDS, stock_word + +AOL_MARKER = 0x0400 +AOL_EXPERIENCE = 32 +_EXCLUDED = int(HyundaiFlags.LEGACY | HyundaiFlags.UNSUPPORTED_LONGITUDINAL | HyundaiFlags.ALT_LIMITS_2 | HyundaiFlags.CAMERA_SCC) +CLASSIC_LONG_IDS = frozenset(identity for identity in CLASSIC_SCC_IDS if not int(identity.config.flags) & _EXCLUDED and identity != CAR.GENESIS_G90) +LONG_AOL_WORDS = frozenset(AOL_MARKER | 4 | gas | limits | lda for gas in (0, 1, 2) for limits in (0, 64) for lda in (0, 2048)) + + +def ordinary_word(cp): + return stock_word(cp) | int(HyundaiSafetyFlags.LONG) + + +def aol_word(cp): + return ordinary_word(cp) | AOL_MARKER | (int(HyundaiSafetyFlags.HAS_LDA_BUTTON) if int(cp.flags) & int(HyundaiFlags.HAS_LDA_BUTTON) else 0) + + +def qualified(cp, *, marked_only=False): + if cp.brand != 'hyundai' or cp.carFingerprint not in CLASSIC_LONG_IDS: + return False + declared = int(CAR[cp.carFingerprint].config.flags) + allowed = declared | int(HyundaiFlags.HAS_LDA_BUTTON | HyundaiFlags.USE_FCA | HyundaiFlags.SEND_LFA) + identity_mask = int( + HyundaiFlags.CANFD + | HyundaiFlags.NON_SCC + | HyundaiFlags.FCEV + | HyundaiFlags.LEGACY + | HyundaiFlags.EV + | HyundaiFlags.HYBRID + | HyundaiFlags.ALT_LIMITS + | HyundaiFlags.ALT_LIMITS_2 + | HyundaiFlags.CAMERA_SCC + ) + if ( + int(cp.flags) & identity_mask != declared & identity_mask + or int(cp.flags) & ~allowed + or is_blended(cp) + or cp.passive + or cp.notCar + or cp.dashcamOnly + or not cp.openpilotLongitudinalControl + or cp.pcmCruise + or cp.steerControlType != structs.CarParams.SteerControlType.torque + or not cp.alphaLongitudinalAvailable + or cp.alternativeExperience not in (0, AOL_EXPERIENCE) + or len(cp.safetyConfigs) != 1 + ): + return False + safety = cp.safetyConfigs[0] + accepted = (aol_word(cp),) if marked_only else (ordinary_word(cp), aol_word(cp)) + return safety.safetyModel == structs.CarParams.SafetyModel.hyundai and int(safety.safetyParam) in accepted + + +def native_accepts(cp, model, param): + return ( + qualified(cp, marked_only=True) + and cp.alternativeExperience == AOL_EXPERIENCE + and model == int(structs.CarParams.SafetyModel.hyundai) + and param == aol_word(cp) + ) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_shared_canfd_stock_aol.py b/opendbc_repo/opendbc/car/hyundai/tests/test_shared_canfd_stock_aol.py new file mode 100644 index 0000000000..d94924dd9f --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_shared_canfd_stock_aol.py @@ -0,0 +1,180 @@ +"""Typed CAN FD topology and startup ownership contracts for shared AOL.""" + +from unittest.mock import patch + +import pytest +from opendbc.car import gen_empty_fingerprint, structs +from opendbc.car.hyundai.canfd_stock_aol import qualified, STOCK_AOL_WORDS +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.values import CAR, HyundaiFlags +from openpilot.starpilot.car.hyundai.aol import policy_for, native_accepts_cp + + +@pytest.mark.parametrize('identity', [CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN]) +@pytest.mark.parametrize('topology', ['hda1_standard', 'hda1_alternate', 'hda2_standard', 'hda2_alternate']) +def test_actual_typed_factory_binds_stock_word(identity, topology): + fp = gen_empty_fingerprint() + hda2 = topology.startswith('hda2') + if hda2: + fp[2].update({0x110: 32, 0x362: 32} if topology == 'hda2_alternate' else {0x50: 16, 0x2A4: 24}) + if topology != 'hda1_alternate': + fp[1 if hda2 else 0][0x1CF] = 8 + else: + fp[0][0x1AA] = 16 + cp = CarInterface.get_params(identity, fp, [], False, False, False) + assert qualified(cp) + policy = policy_for(cp) + assert policy.explicit_latch and policy.safety_param_addition == 0x800 + cp.safetyConfigs[0].safetyParam |= 0x800 + assert int(cp.safetyConfigs[0].safetyParam) in STOCK_AOL_WORDS + assert native_accepts_cp(cp, cp.safetyConfigs[0].safetyModel.raw, int(cp.safetyConfigs[0].safetyParam)) + + +@pytest.mark.parametrize('identity', [CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN]) +def test_fuel_identity_mismatch_is_not_a_different_native_profile(identity): + fp = gen_empty_fingerprint() + fp[0][0x1CF] = 8 + cp = CarInterface.get_params(identity, fp, [], False, False, False) + assert qualified(cp) + cp.flags = int(cp.flags) & ~int(HyundaiFlags.EV) + cp.safetyConfigs[0].safetyParam = int(cp.safetyConfigs[0].safetyParam) & ~1 + assert not qualified(cp) + + +def test_ioniq6_keeps_its_separate_namespace_and_policy(): + fp = gen_empty_fingerprint() + fp[2][0x50] = 16 + fp[1][0x1CF] = 8 + fp[0][0x3A5] = 24 + cp = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fp, [], False, False, False) + assert not qualified(cp) + assert policy_for(cp).safety_param_addition == 0x800 + + +@pytest.mark.parametrize('identity,fuel', [(CAR.HYUNDAI_SONATA_2024, 0), (CAR.KIA_SPORTAGE_5TH_GEN, 2), (CAR.HYUNDAI_IONIQ_5, 1)]) +@pytest.mark.parametrize('alternate', [False, True]) +def test_stock_hda1_factory_fuel_and_button_source(identity, fuel, alternate): + fp = gen_empty_fingerprint() + fp[0][0x1AA if alternate else 0x1CF] = 16 if alternate else 8 + if fuel == 2: + fp[0][0xFA] = 8 + cp = CarInterface.get_params(identity, fp, [], False, False, False) + assert qualified(cp) + raw = int(cp.safetyConfigs[0].safetyParam) + assert raw & 3 == fuel + assert bool(cp.flags & HyundaiFlags.CANFD_ALT_BUTTONS) == alternate + cp.safetyConfigs[0].safetyParam = raw | 0x800 + assert qualified(cp, marked_only=True) + assert native_accepts_cp(cp, cp.safetyConfigs[0].safetyModel.raw, raw | 0x800) + + +def test_static_angle_identity_cannot_alias_torque_admission(): + fp = gen_empty_fingerprint() + fp[2].update({0x110: 32, 0x362: 32}) + fp[1][0x1CF] = 8 + cp = CarInterface.get_params(CAR.HYUNDAI_IONIQ_5_PE, fp, [], False, False, False) + cp.steerControlType = structs.CarParams.SteerControlType.torque + cp.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_LKA_STEER_MSG | HyundaiFlags.CANFD_LKA_STEER_MSG_ALT) + cp.safetyConfigs[0].safetyParam = 0x891 + assert not qualified(cp) + assert not policy_for(cp).explicit_latch + + +FACTORY_CASES = tuple( + (identity, topology, 1) + for identity in (CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN) + for topology in ('hda1_standard', 'hda1_alternate', 'hda2_standard', 'hda2_alternate') +) + tuple( + (identity, 'hda1_alternate' if alternate else 'hda1_standard', fuel) + for identity, fuel in ((CAR.HYUNDAI_SONATA_2024, 0), (CAR.KIA_SPORTAGE_5TH_GEN, 2), (CAR.HYUNDAI_IONIQ_5, 1)) + for alternate in (False, True) +) + + +@pytest.mark.parametrize('identity,topology,fuel', FACTORY_CASES) +@pytest.mark.parametrize('enabled', [False, True]) +def test_actual_stock_card_publication_and_controls(identity, topology, fuel, enabled): + import gc + import os + from openpilot.common.params import Params + from openpilot.common.prefix import OpenpilotPrefix + from openpilot.selfdrive.car.card import Car + from openpilot.selfdrive.controls.controlsd import Controls + from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque + from opendbc.car.hyundai.radar_interface import RadarInterface + + fp = gen_empty_fingerprint() + hda2 = topology.startswith('hda2') + if hda2: + fp[2].update({0x110: 32, 0x362: 32} if topology == 'hda2_alternate' else {0x50: 16, 0x2A4: 24}) + fp[1 if hda2 else 0][0x1AA if topology == 'hda1_alternate' else 0x1CF] = 16 if topology == 'hda1_alternate' else 8 + if fuel == 2: + fp[0][0xFA] = 8 + with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '1'}): + saved = Params() + saved.put_bool('OpenpilotEnabledToggle', True, block=True) + saved.put_bool('AlwaysOnLateral', enabled, block=True) + saved.put('MainCruiseButtonControl', 9, block=True) + saved.put('LKASButtonControl', 9, block=True) + cp = CarInterface.get_params(identity, fp, [], False, False, False) + assert qualified(cp) + original_word = int(cp.safetyConfigs[0].safetyParam) + original_flags = cp.flags + ci = CarInterface(cp) + card = Car(CI=ci, RI=RadarInterface(cp)) + with structs.CarParams.from_bytes(saved.get('CarParams')) as published: + assert published.pcmCruise and not published.openpilotLongitudinalControl and not published.passive + assert published.safetyConfigs[0].safetyParam == original_word | (0x800 if enabled else 0) + assert published.alternativeExperience == 0 + assert published.flags == original_flags + assert qualified(published, marked_only=enabled) + assert published.carFingerprint == identity + controls = Controls() + assert controls.CP.to_dict() == card.CP.to_dict() + assert isinstance(controls.LaC, LatControlTorque) + assert card.aol_qualified == enabled + del controls, card, ci + gc.collect() + + +@pytest.mark.parametrize('identity', [CAR.KIA_EV6, CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN]) +def test_existing_stock_startup_profiles_update_without_duplicate_units_source(identity): + from opendbc.can import CANPacker + + fp = gen_empty_fingerprint() + fp[2].update({0x50: 16, 0x2A4: 24}) + fp[1][0x1CF] = 8 + cp = CarInterface.get_params(identity, fp, [], False, False, False) + assert qualified(cp) + cp.safetyConfigs[0].safetyParam |= 0x800 + ci = CarInterface(cp) + packer = CANPacker('hyundai_canfd_generated') + drive = next(code for code, label in ci.CS.shifter_values.items() if label == 'D') + inventory = [ + ('ACCELERATOR', 1), + ('TCS', 1), + ('WHEEL_SPEEDS', 1), + ('MDPS', 1), + ('STEERING_SENSORS', 1), + ('DOORS_SEATBELTS', 1), + ('BLINKERS', 1), + ('GEAR_ALT_2', 1), + ('CRUISE_BUTTONS', 1), + ('SCC_CONTROL', 1), + ('CAM_0x2a4', 2), + ('MANUAL_SPEED_LIMIT_ASSIST', 1), + ] + assert all(name != 'CRUISE_BUTTONS_ALT' for name, _ in inventory) + for tick in range(40): + frames = [] + for name, bus in inventory: + values = {'COUNTER': tick % (16 if name == 'CRUISE_BUTTONS' else 256)} + if name == 'GEAR_ALT_2': + values['GEAR'] = drive + elif name == 'DOORS_SEATBELTS': + values['DRIVER_SEATBELT'] = 1 + elif name == 'WHEEL_SPEEDS': + values.update(WHL_SpdFLVal=72, WHL_SpdFRVal=72, WHL_SpdRLVal=72, WHL_SpdRRVal=72) + frames.append(packer.make_can_msg(name, bus, values)) + state = ci.update([(1_000_000_000 + tick * 10_000_000, frames)]) + assert state.canValid and not state.canTimeout diff --git a/opendbc_repo/opendbc/safety/modes/hyundai.h b/opendbc_repo/opendbc/safety/modes/hyundai.h index 0bd77466d0..0acb81babd 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai.h @@ -584,6 +584,10 @@ static safety_config hyundai_init(uint16_t param) { hyundai_common_init(param); classic_non_scc_aol_configure(param, false); + if (classic_long_aol_param(param) && !classic_long_aol_enabled) { + const safety_config denied = {0}; + return denied; + } hyundai_blended_alpha = false; #ifdef ALLOW_DEBUG hyundai_blended_alpha = (param == 0x2004U) || (param == 0x2014U); diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h b/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h index 1cdd0b0a4e..7505e28ef4 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h @@ -2,6 +2,7 @@ #include "opendbc/safety/declarations.h" #include "opendbc/safety/modes/hyundai_common.h" +#include "opendbc/safety/modes/hyundai_canfd_aol.h" #define HYUNDAI_CANFD_CRUISE_BUTTON_TX_MSGS(bus) \ {0x1CF, bus, 8, .check_relay = false}, /* CRUISE_BUTTON */ \ @@ -420,8 +421,8 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *msg) { if (hyundai_canfd_ev9_long && !previous_controls_allowed && controls_allowed) { hyundai_ev9_inactive_accel_count = 0U; } - if (aol_ioniq6_long && (msg_matches(msg, 0x1cfU, pt_bus))) { - const bool gesture = main_button || GET_BIT(msg, 23U); // LDA/LKAS button + if (aol_ioniq6_long && (msg_matches(msg, hyundai_canfd_alt_buttons ? 0x1aaU : 0x1cfU, pt_bus))) { + const bool gesture = main_button || (hyundai_canfd_alt_buttons ? GET_BIT(msg, 39U) : GET_BIT(msg, 23U)); // LDA/LKAS button if (cruise_button == HYUNDAI_BTN_CANCEL) { aol_ioniq6_lateral_latch = false; aol_ioniq6_lateral_token_claimed = false; @@ -949,7 +950,7 @@ static safety_config hyundai_canfd_init_validated(uint16_t param) { const uint16_t HYUNDAI_PARAM_ANGLE_MODEL_MASK = 2048U | 4096U | HYUNDAI_PARAM_ANGLE_MODEL_BANK; const uint16_t HYUNDAI_PARAM_ANGLE_TOPOLOGY_MASK = 8U | 16U | 32U | 128U; const bool angle_requested = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ANGLE); - const bool ioniq6_stock_aol_requested = (param == 0x0811U) || (param == 0x0891U); + const bool ioniq6_stock_aol_requested = hyundai_canfd_stock_torque_aol_param(param); const bool pe_requested = (param == HYUNDAI_IONIQ5_PE_STOCK_PARAM) && ((unsigned int)alternative_experience == 0U); const bool ev9_requested = (param == HYUNDAI_EV9_STOCK_PARAM) && ((unsigned int)alternative_experience == 0U); const bool ordinary_angle_requested = pe_requested || ev9_requested; @@ -1462,6 +1463,108 @@ static safety_config hyundai_canfd_init_validated(uint16_t param) { } } + if (ioniq6_stock_aol_requested && (param != 0x0811U) && (param != 0x0891U)) { + static RxCheck torque_hda2_ev_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck torque_hda2_ev_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck torque_hda2_ice_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck torque_hda2_ice_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck torque_hda2_hybrid_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(1, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck torque_hda2_hybrid_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(1) + }; + static RxCheck torque_radar_ev_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + static RxCheck torque_radar_ev_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + static RxCheck torque_radar_ice_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + static RxCheck torque_radar_ice_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + static RxCheck torque_radar_hybrid_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + static RxCheck torque_radar_hybrid_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(0) + }; + static RxCheck torque_camera_ev_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck torque_camera_ev_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x35) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck torque_camera_ice_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck torque_camera_ice_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x100) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck torque_camera_hybrid_std[] = { + HYUNDAI_CANFD_BANKED_ANGLE_STD_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + static RxCheck torque_camera_hybrid_alt[] = { + HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(0, 0x105) + HYUNDAI_CANFD_SCC_ADDR_CHECK(2) + }; + RxCheck *selected = NULL; + if (hyundai_canfd_lka_steer_msg) { + if (hyundai_ev_gas_signal) { + selected = hyundai_canfd_alt_buttons ? torque_hda2_ev_alt : torque_hda2_ev_std; + } else if (hyundai_hybrid_gas_signal) { + selected = hyundai_canfd_alt_buttons ? torque_hda2_hybrid_alt : torque_hda2_hybrid_std; + } else { + selected = hyundai_canfd_alt_buttons ? torque_hda2_ice_alt : torque_hda2_ice_std; + } + } else if (hyundai_camera_scc) { + if (hyundai_ev_gas_signal) { + selected = hyundai_canfd_alt_buttons ? torque_camera_ev_alt : torque_camera_ev_std; + } else if (hyundai_hybrid_gas_signal) { + selected = hyundai_canfd_alt_buttons ? torque_camera_hybrid_alt : torque_camera_hybrid_std; + } else { + selected = hyundai_canfd_alt_buttons ? torque_camera_ice_alt : torque_camera_ice_std; + } + } else { + if (hyundai_ev_gas_signal) { + selected = hyundai_canfd_alt_buttons ? torque_radar_ev_alt : torque_radar_ev_std; + } else if (hyundai_hybrid_gas_signal) { + selected = hyundai_canfd_alt_buttons ? torque_radar_hybrid_alt : torque_radar_hybrid_std; + } else { + selected = hyundai_canfd_alt_buttons ? torque_radar_ice_alt : torque_radar_ice_std; + } + } + ret.rx_checks = selected; + ret.rx_checks_len = 6; + } return ret; } @@ -1475,7 +1578,10 @@ static safety_config hyundai_canfd_init(uint16_t param) { const bool valid = !GET_FLAG(param, 32768U); #endif safety_config ret = {0}; - if (valid) { + const bool marker_valid = GET_FLAG(param, 16384U) || !GET_FLAG(param, 2048U) || + hyundai_canfd_stock_torque_aol_param(param) || + (param == 0x8815U) || (param == 0x8895U); + if (valid && marker_valid) { ret = hyundai_canfd_init_validated(param); } return ret; diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h new file mode 100644 index 0000000000..201faef3dd --- /dev/null +++ b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h @@ -0,0 +1,17 @@ +#pragma once + +// Bit 11 selects the shared axis lease only for these ordinary torque layouts. +// Angle model selectors and the separate Ioniq 6 actuator profile stay separate. +static inline bool hyundai_canfd_stock_torque_aol_param(uint16_t param) { + const uint16_t base = param & (uint16_t)~0x0800U; + const uint16_t gas = base & 3U; + const uint16_t topology = base & 0x00B8U; + const bool carnival = (base & 0x2000U) != 0U; + const bool ccnc = (base & 0x0400U) != 0U; + const bool standard = (topology == 0U) || (topology == 8U) || (topology == 16U) || (topology == 144U); + const bool alternate = (topology == 32U) || (topology == 40U) || + (carnival && ((topology == 48U) || (topology == 176U))); + return ((param & 0x0800U) != 0U) && ((base & (uint16_t)~0x24BBU) == 0U) && (gas != 3U) && + (standard || alternate) && (!ccnc || ((topology == 8U) || (topology == 40U))) && + (!carnival || (alternate && (gas != 1U))); +} diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h b/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h index 20635b2fcf..ceb4eb8994 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h @@ -15,6 +15,13 @@ static bool classic_scc_aol_enabled = false; static bool classic_scc_aol_camera = false; +static bool classic_long_aol_enabled = false; +static bool classic_long_main_neutral = false; +static bool classic_long_main_pressed = false; + +static bool classic_long_aol_param(uint16_t param) { + return ((param & 0x0404U) == 0x0404U) && ((param & 0xF3B8U) == 0U) && ((param & 3U) != 3U); +} static bool classic_scc_aol_param(uint16_t param) { // Stock SCC gas, torque limits and camera routing only. No LONG or NON_SCC. @@ -56,6 +63,8 @@ static void classic_non_scc_aol_clear_authorization(void) { classic_non_scc_aol_source_neutral[i] = false; classic_non_scc_aol_source_ts[i] = 0U; } + classic_long_main_neutral = false; + classic_long_main_pressed = false; aol_host_axis_mask = 0U; } @@ -64,6 +73,7 @@ static void classic_non_scc_aol_reset(void) { classic_non_scc_aol_enabled = false; classic_scc_aol_enabled = false; classic_scc_aol_camera = false; + classic_long_aol_enabled = false; classic_non_scc_aol_ev = false; classic_non_scc_aol_hev = false; classic_non_scc_aol_lda = false; @@ -94,8 +104,9 @@ static uint8_t classic_non_scc_aol_request_mask(void) { classic_non_scc_aol_clear_authorization(); } else if (heartbeat_engaged) { classic_non_scc_aol_session = true; - if (classic_scc_aol_enabled && classic_non_scc_aol_lda && controls_allowed && - classic_non_scc_aol_main_seen && aol_rx_healthy() && ((aol_host_axis_mask & 1U) != 0U)) { + if (((classic_scc_aol_enabled && classic_non_scc_aol_lda) || classic_long_aol_enabled) && controls_allowed && + classic_non_scc_aol_main_seen && (!classic_long_aol_enabled || classic_non_scc_aol_main) && + aol_rx_healthy() && ((aol_host_axis_mask & 1U) != 0U)) { // Actual stock ACC activation can seed the host's LKAS-on-engage latch. classic_non_scc_aol_token = true; classic_non_scc_aol_token_ts = now; @@ -103,7 +114,7 @@ static uint8_t classic_non_scc_aol_request_mask(void) { if (((aol_host_axis_mask & 1U) != 0U) && classic_non_scc_aol_token) { classic_non_scc_aol_claimed = true; } - result = aol_host_axis_mask & 1U; // NON-SCC never grants an independent longitudinal axis. + result = aol_host_axis_mask & (classic_long_aol_enabled ? 3U : 1U); } else { // Physical evidence may precede heartbeat but grants no permission yet. } @@ -116,10 +127,14 @@ static uint8_t classic_non_scc_aol_permission_mask(void) { classic_non_scc_aol_clear_authorization(); } else { const uint8_t request = classic_non_scc_aol_request_mask(); - const bool physical_authorized = (classic_scc_aol_enabled && classic_non_scc_aol_lda) ? + const bool physical_authorized = (classic_long_aol_enabled || (classic_scc_aol_enabled && classic_non_scc_aol_lda)) ? (classic_non_scc_aol_token || controls_allowed) : (classic_non_scc_aol_main || classic_non_scc_aol_token || controls_allowed); - result = (((request & 1U) != 0U) && classic_non_scc_aol_main_seen && physical_authorized) ? 1U : 0U; + result = (((request & 1U) != 0U) && classic_non_scc_aol_main_seen && physical_authorized && + (!classic_long_aol_enabled || classic_non_scc_aol_main)) ? 1U : 0U; + if (classic_long_aol_enabled && classic_non_scc_aol_main && controls_allowed && ((request & 2U) != 0U)) { + result |= 2U; + } } return result; } @@ -137,11 +152,31 @@ static void classic_non_scc_aol_rx(const CANPacket_t *msg) { if (classic_non_scc_aol_enabled && ((msg->bus == 0U) || (classic_scc_aol_enabled && classic_scc_aol_camera && (msg->bus == 2U)))) { const uint32_t now = microsecond_timer_get(); const uint8_t scc_bus = classic_scc_aol_camera ? 2U : 0U; + if (classic_long_aol_enabled && (msg->bus == 0U) && (msg->addr == 0x394U) && (GET_LEN(msg) == 8U)) { + classic_non_scc_aol_main = ((msg->data[5] >> 3U) & 3U) == 0U; + classic_non_scc_aol_main_seen = true; + if (!classic_non_scc_aol_main) { + classic_non_scc_aol_token = false; + classic_non_scc_aol_claimed = false; + } + } + if (classic_long_aol_enabled && (msg->bus == 0U) && (msg->addr == 0x4F1U) && (GET_LEN(msg) == 4U)) { + const bool main_pressed = GET_BIT(msg, 3U); + if (!main_pressed) { + classic_long_main_neutral = true; + } else if (classic_long_main_neutral && !classic_long_main_pressed && (aol_host_axis_mask == 0U)) { + classic_non_scc_aol_token = true; + classic_non_scc_aol_claimed = false; + classic_non_scc_aol_token_ts = now; + } else { + } + classic_long_main_pressed = main_pressed; + } if (classic_scc_aol_enabled && (msg->bus == scc_bus) && (msg->addr == 0x420U) && (GET_LEN(msg) == 8U)) { classic_non_scc_aol_main = GET_BIT(msg, 0U); classic_non_scc_aol_main_seen = true; } - if (!classic_scc_aol_enabled && (GET_LEN(msg) == 8U) && + if (!classic_scc_aol_enabled && !classic_long_aol_enabled && (GET_LEN(msg) == 8U) && ((!classic_non_scc_aol_ev && !classic_non_scc_aol_hev && (msg->addr == 0x260U)) || (classic_non_scc_aol_ev && (msg->addr == 0x592U)) || (classic_non_scc_aol_hev && (msg->addr == 0x595U)))) { @@ -192,13 +227,16 @@ static void classic_non_scc_aol_configure(uint16_t param, bool legacy) { classic_non_scc_aol_reset(); classic_scc_aol_enabled = classic_scc_aol_param(param) && (!legacy || ((param & 8U) == 0U)) && ((unsigned int)alternative_experience == 32U); +#ifdef ALLOW_DEBUG + classic_long_aol_enabled = !legacy && classic_long_aol_param(param) && ((unsigned int)alternative_experience == 32U); +#endif classic_scc_aol_camera = classic_scc_aol_enabled && ((param & 8U) != 0U); classic_non_scc_aol_enabled = ((!legacy && ((param == HYUNDAI_FORTE_AOL_MAIN) || (param == HYUNDAI_FORTE_AOL_LDA) || (param == HYUNDAI_KONA_AOL_MAIN) || (param == HYUNDAI_KONA_AOL_LDA) || (param == HYUNDAI_NON_SCC_HEV_AOL_MAIN) || (param == HYUNDAI_NON_SCC_HEV_AOL_LDA) || - (param == HYUNDAI_NON_SCC_EV_ALT_AOL_MAIN) || (param == HYUNDAI_NON_SCC_EV_ALT_AOL_LDA))) || classic_scc_aol_enabled) && + (param == HYUNDAI_NON_SCC_EV_ALT_AOL_MAIN) || (param == HYUNDAI_NON_SCC_EV_ALT_AOL_LDA))) || classic_scc_aol_enabled || classic_long_aol_enabled) && ((unsigned int)alternative_experience == 32U); - classic_non_scc_aol_lda = classic_non_scc_aol_enabled && ((classic_scc_aol_enabled && ((param & 0x0800U) != 0U)) || (param == HYUNDAI_FORTE_AOL_LDA) || (param == HYUNDAI_KONA_AOL_LDA) || + classic_non_scc_aol_lda = classic_non_scc_aol_enabled && (((classic_scc_aol_enabled || classic_long_aol_enabled) && ((param & 0x0800U) != 0U)) || (param == HYUNDAI_FORTE_AOL_LDA) || (param == HYUNDAI_KONA_AOL_LDA) || (param == HYUNDAI_NON_SCC_HEV_AOL_LDA) || (param == HYUNDAI_NON_SCC_EV_ALT_AOL_LDA)); classic_non_scc_aol_ev = classic_non_scc_aol_enabled && ((param == HYUNDAI_NON_SCC_EV_ALT_AOL_MAIN) || (param == HYUNDAI_NON_SCC_EV_ALT_AOL_LDA)); diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_long_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_long_aol.py new file mode 100644 index 0000000000..c9c7c69121 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_classic_long_aol.py @@ -0,0 +1,125 @@ +from opendbc.car.structs import CarParams +import unittest +from opendbc.safety.tests import test_hyundai_kona_lda_aol as fixture + + +class TestHyundaiClassicLongAol(unittest.TestCase): + def setUp(self): + fixture.TestHyundaiKonaLdaAol.setUp(self) + self.release = self.safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) != 0 + + tearDown = fixture.TestHyundaiKonaLdaAol.tearDown + reset = fixture.TestHyundaiKonaLdaAol.reset + rx = fixture.TestHyundaiKonaLdaAol.rx + request = fixture.TestHyundaiKonaLdaAol.request + + def feed(self, word=0x404, button=0, main=False, available=True, tcs=True, clu=True, wrong_bus=None, corrupt_tcs=False): + self.safety.set_timer(self.now) + c = self.counter + self.counter += 1 + if word & 3: + self.rx('E_EMS11', {'Accel_Pedal_Pos': 0, 'CR_Vcu_AccPedDep_Pos': 0}) + else: + self.rx('EMS16', {'AliveCounter': c % 4}, integrity=True) + self.rx('WHL_SPD11', {'WHL_SPD_AliveCounter_LSB': c % 4, 'WHL_SPD_AliveCounter_MSB': (c // 4) % 4}, integrity=True) + if tcs: + self.rx('TCS13', {'AliveCounterTCS': c % 8, 'ACCEnable': 0 if available else 1}, integrity=not corrupt_tcs, bus=0 if wrong_bus is None else wrong_bus) + self.rx('MDPS12', {'CR_Mdps_StrColTq': 0}) + if clu: + self.rx('CLU11', {'CF_Clu_AliveCnt1': c % 16, 'CF_Clu_CruiseSwState': button, 'CF_Clu_CruiseSwMain': int(main)}) + self.now += 10000 + + def test_availability_and_held_main_cannot_authorize(self): + self.reset(0x404) + for _ in range(6): + self.feed(main=True) + self.assertEqual(self.request(3), 0) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_physical_main_grants_lateral_only_without_stock_scc(self): + self.reset(0x404) + for _ in range(6): + self.feed() + self.feed(main=True) + expected = 0 if self.release else 1 + self.assertEqual(self.request(3), expected) + self.assertFalse(self.safety.get_controls_allowed()) + + def test_set_resume_capture_and_cancel_keep_longitudinal_physical(self): + if self.release: + self.skipTest('Current classic OP-long is DEBUG-only') + for button in (1, 2): + self.reset(0xC04) + for _ in range(6): + self.feed(0xC04) + self.feed(0xC04, button=button) + self.feed(0xC04) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertEqual(self.request(3), 3) + self.feed(0xC04, button=4) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertEqual(self.request(3), 1) + self.safety.set_aol_test_heartbeat(False) + self.assertEqual(self.request(3), 0) + + def test_tcs_fault_and_expiry_revoke_without_disabled_scc_requirement(self): + if self.release: + self.skipTest('Current classic OP-long is DEBUG-only') + for cause in ('fault', 'expiry'): + self.reset(0x404) + for _ in range(6): + self.feed() + self.feed(main=True) + self.assertEqual(self.request(1), 1) + if cause == 'fault': + self.feed(available=False) + else: + for _ in range(120): + self.feed(tcs=False) + self.request(1) + self.assertEqual(self.request(3), 0) + + def test_release_marked_long_has_zero_tx_and_existing_unmarked_is_unchanged(self): + if not self.release: + self.skipTest('Explicit release marked-owner boundary') + for word in (0x404, 0xC04, 0x405, 0xC05, 0x406, 0xC06, 0x444, 0xC44, 0x445, 0xC45, 0x446, 0xC46): + self.reset(word) + self.assertEqual(self.request(3), 0) + for name, values in (('LKAS11', {'CR_Lkas_StrToqReq': 0, 'CF_Lkas_ActToi': 0}), ('SCC12', {'ACCMode': 0, 'aReqRaw': 0, 'aReqValue': 0})): + self.assertFalse(self.safety.safety_tx_hook(self.packer.make_can_msg_safety(name, 0, values))) + self.reset(4) + self.assertTrue(self.safety.safety_tx_hook(self.packer.make_can_msg_safety('LKAS11', 0, {'CR_Lkas_StrToqReq': 0, 'CF_Lkas_ActToi': 0}))) + self.assertFalse(self.safety.safety_tx_hook(self.packer.make_can_msg_safety('SCC12', 0, {'ACCMode': 1, 'aReqRaw': 1, 'aReqValue': 1}))) + + def test_required_clu_expiry_clears_prior_main_and_requires_fresh_gesture(self): + if self.release: + self.skipTest('Current classic OP-long is DEBUG-only') + self.reset(0x404) + for _ in range(6): + self.feed() + self.feed(main=True) + self.assertEqual(self.request(1), 1) + for _ in range(120): + self.feed(clu=False) + self.request(1) + self.assertEqual(self.request(3), 0) + for _ in range(6): + self.feed(main=True) + self.assertEqual(self.request(1), 0) + self.assertEqual(self.request(0), 0) + self.feed() + self.feed(main=True) + self.assertEqual(self.request(1), 1) + + def test_wrong_bus_tcs_does_not_renew_ready_health(self): + if self.release: + self.skipTest('Current classic OP-long is DEBUG-only') + self.reset(0x404) + for _ in range(6): + self.feed() + self.feed(main=True) + self.assertEqual(self.request(1), 1) + for _ in range(120): + self.feed(wrong_bus=1) + self.request(1) + self.assertEqual(self.request(3), 0) diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_shared_canfd_stock_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_shared_canfd_stock_aol.py new file mode 100644 index 0000000000..f99a4d1cce --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_shared_canfd_stock_aol.py @@ -0,0 +1,95 @@ +import unittest + +from opendbc.can import CANPacker +from opendbc.car import structs +from opendbc.car.hyundai.canfd_stock_aol import STOCK_AOL_WORDS +from opendbc.safety.tests.libsafety import libsafety_py + + +class TestHyundaiSharedCanfdStockAol(unittest.TestCase): + def setUp(self): + self.safety = libsafety_py.libsafety + self.packer = CANPacker('hyundai_canfd_generated') + + def mode(self, word): + self.safety.set_alternative_experience(0) + self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, word), 0) + self.safety.init_tests() + self.safety.set_timer(1_000_000) + + @staticmethod + def packet(frame): + return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1]) + + def healthy(self, word, *, omit=None, wrong_bus=None): + pt = 1 if word & 0x10 else 0 + fuel = {0: 'ACCELERATOR_BRAKE_ALT', 1: 'ACCELERATOR', 2: 'ACCELERATOR_ALT'}[word & 3] + buttons = 'CRUISE_BUTTONS_ALT' if word & 0x20 else 'CRUISE_BUTTONS' + for counter in range(1, 7): + for name in (fuel, 'TCS', 'WHEEL_SPEEDS', 'MDPS', buttons): + if name == omit: + continue + bus = 1 - pt if name == wrong_bus else pt + values = {'COUNTER': counter % (16 if name == 'CRUISE_BUTTONS' else 256)} + if name == 'MDPS': + values['MDPS_StrTqSnsrVal'] = 0 + frame = self.packer.make_can_msg(name, bus, values) + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame)), name) + if not word & 4 and omit != 'SCC_CONTROL': + frame = self.packer.make_can_msg('SCC_CONTROL', 2 if word & 8 else pt, {'COUNTER': counter}) + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame))) + + def gesture(self, word): + pt = 1 if word & 0x10 else 0 + name = 'CRUISE_BUTTONS_ALT' if word & 0x20 else 'CRUISE_BUTTONS' + for count, pressed in ((7, 1), (8, 0)): + frame = self.packer.make_can_msg(name, pt, {'COUNTER': count, 'LDA_BTN': pressed}) + self.assertTrue(self.safety.safety_rx_hook(self.packet(frame))) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + + def test_selected_fuel_button_and_scc_sources_are_required(self): + for word in (0x800 | fuel | topology for fuel in (0, 1, 2) for topology in (0, 8, 16, 32, 40, 144)): + fuel = {0: 'ACCELERATOR_BRAKE_ALT', 1: 'ACCELERATOR', 2: 'ACCELERATOR_ALT'}[word & 3] + buttons = 'CRUISE_BUTTONS_ALT' if word & 0x20 else 'CRUISE_BUTTONS' + for omitted in (fuel, buttons, 'SCC_CONTROL'): + self.mode(word) + self.healthy(word, omit=omitted) + if omitted != buttons: + self.gesture(word) + else: + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.mode(word) + self.healthy(word) + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + + def test_full_stock_word_inventory_and_unknown_marker_rejection(self): + for word in STOCK_AOL_WORDS: + with self.subTest(word=hex(word)): + self.mode(word) + self.healthy(word) + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + pt = 1 if word & 0x10 else 0 + self.assertFalse(self.safety.safety_tx_hook(self.packet(self.packer.make_can_msg('SCC_CONTROL', pt, {})))) + for word in (0x803, 0x804, 0x815, 0x895, 0x830, 0x880, 0xC00): + self.mode(word) + self.safety.set_aol_test_heartbeat(True) + self.safety.aol_set_host_request(3) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + + def test_source_expiry_and_wrong_bus_withdraw_permission(self): + for word in (0x800 | fuel | topology for fuel in (0, 1, 2) for topology in (0, 8, 16, 32, 40, 144)): + self.mode(word) + self.healthy(word) + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 1) + self.safety.set_timer(2_000_000) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) + self.mode(word) + self.healthy(word, wrong_bus='WHEEL_SPEEDS') + self.gesture(word) + self.assertEqual(self.safety.aol_get_permission_mask(), 0) diff --git a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc index 783ae13bc7..b7f31a5515 100644 --- a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc +++ b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc @@ -103,7 +103,15 @@ int main() { legacy.safety_mode = HYUNDAI_LEGACY_AOL_PROFILE.mode; assert(aol_capable(legacy, HYUNDAI_LEGACY_AOL_PROFILE.mode, VEHICLE_REGISTRY) == (scc_expected && ((word & 8U) == 0U))); - const bool expected = scc_expected || word == 0x1400U || word == 0x1C00U || word == 0x1440U || word == 0x1C40U || + bool long_expected = false; + for (const uint16_t gas : {0U, 1U, 2U}) { + for (const uint16_t limits : {0U, 64U}) { + for (const uint16_t lda : {0U, 2048U}) { + long_expected |= word == (0x0404U | gas | limits | lda); + } + } + } + const bool expected = long_expected || scc_expected || word == 0x1400U || word == 0x1C00U || word == 0x1440U || word == 0x1C40U || word == 0x1402U || word == 0x1C02U || word == 0x1441U || word == 0x1C41U; assert(aol_capable(forte, HYUNDAI_CLASSIC_AOL_PROFILE.mode, VEHICLE_REGISTRY) == expected); assert(aol_runtime_enabled(false, forte, VEHICLE_REGISTRY) == expected); @@ -164,10 +172,19 @@ int main() { assert(!aol_capable(ev9, HYUNDAI_AOL_PROFILE.mode, VEHICLE_REGISTRY)); } - // Transport namespace is exactly the retained siblings plus PE and EV9. No other - // model2/3 or LONG word gets access through this mode28 registry predicate. + constexpr uint16_t stock_canfd_words[] = { + 0x0800U, 0x0801U, 0x0802U, 0x0808U, 0x0809U, 0x080AU, + 0x0810U, 0x0811U, 0x0812U, 0x0820U, 0x0821U, 0x0822U, + 0x0828U, 0x0829U, 0x082AU, 0x0890U, 0x0891U, 0x0892U, + 0x0C08U, 0x0C09U, 0x0C0AU, 0x0C28U, 0x0C29U, 0x0C2AU, + 0x2820U, 0x2822U, 0x2828U, 0x282AU, 0x2830U, 0x2832U, + 0x28B0U, 0x28B2U, 0x2C28U, 0x2C2AU, + }; for (uint32_t word = 0U; word <= 0xFFFFU; ++word) { - const bool expected = word == 0x0811U || word == 0x0891U || word == 0x8815U || word == 0x8895U || word == 0x5491U || word == 0x5C91U; + bool expected = word == 0x8815U || word == 0x8895U || word == 0x5491U || word == 0x5C91U; + for (uint16_t stock_word : stock_canfd_words) { + expected = expected || word == stock_word; + } assert(hyundai_aol_param(static_cast(word)) == expected); } @@ -242,7 +259,7 @@ int main() { HYUNDAI_AOL_PROFILE.mode); assert(stock_transport.last_write == 0U && stock_result.outcome.compatible); } - for (uint16_t raw : {0x0011U, 0x0091U, 0x0810U, 0x0890U, 0x0815U, 0x0895U, + for (uint16_t raw : {0x0011U, 0x0091U, 0x0815U, 0x0895U, 0x0813U, 0x0831U, 0x0911U, 0x8015U, 0x8095U, 0x8814U, 0x8894U, 0x8817U}) { ioniq.safety_param = raw; assert(!aol_capable(ioniq, HYUNDAI_AOL_PROFILE.mode, VEHICLE_REGISTRY)); diff --git a/openpilot/starpilot/aol/tests/test_classic_long_profiles.py b/openpilot/starpilot/aol/tests/test_classic_long_profiles.py new file mode 100644 index 0000000000..1c617ef898 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_classic_long_profiles.py @@ -0,0 +1,164 @@ +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.GENESIS_G90, 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.radar_interface import RadarInterface + 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.selfdrive.car.card import Car + 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 not CarInterface.startup_required(cp) + 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 = Car(CI=ci, RI=RadarInterface(cp)) + 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 None + 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: + del controls, card, ci, subscriber + gc.collect() diff --git a/openpilot/starpilot/car/hyundai/aol.py b/openpilot/starpilot/car/hyundai/aol.py index 8ac64cf951..a4b175caaa 100644 --- a/openpilot/starpilot/car/hyundai/aol.py +++ b/openpilot/starpilot/car/hyundai/aol.py @@ -1,10 +1,16 @@ +from opendbc.car.hyundai.classic_long_aol import ( + qualified as qualified_classic_long, + aol_word as classic_long_word, + native_accepts as classic_long_accepts, + LONG_AOL_WORDS, +) from opendbc.car.hyundai.classic_scc_aol import ( qualified as qualified_classic_scc, aol_word as classic_aol_word, native_accepts as classic_native_accepts, native_profile_supported as classic_native_profile_supported, ) -from opendbc.car.hyundai.canfd_stock_aol import qualified as qualified_canfd_stock, STOCK_AOL_MARKER +from opendbc.car.hyundai.canfd_stock_aol import qualified as qualified_canfd_stock, STOCK_AOL_MARKER, STOCK_AOL_WORDS from opendbc.car.hyundai.non_scc_aol import qualified as qualified_non_scc, aol_word, AOL_MARKER, AOL_EXPERIENCE, AOL_WORDS from opendbc.car.hyundai.kona_aol import allow_lateral_onset as kona_lateral_onset from opendbc.car.structs import car @@ -62,6 +68,16 @@ def ioniq6_settings_capable(CP) -> bool: def policy_for(CP) -> AolVehiclePolicy: + if qualified_classic_long(CP): + return AolVehiclePolicy( + intent_supported=True, + settings_supported=True, + runtime_supported=True, + normal_runtime_supported=True, + explicit_latch=True, + safety_param_addition=classic_long_word(CP) ^ int(CP.safetyConfigs[0].safetyParam), + alternative_experience_addition=AOL_EXPERIENCE, + ) if qualified_classic_scc(CP): return AolVehiclePolicy( intent_supported=True, @@ -113,13 +129,16 @@ def policy_for(CP) -> AolVehiclePolicy: def native_profile_supported(model: int, param: int) -> bool: return ( - classic_native_profile_supported(model, param) - or (model == int(car.CarParams.SafetyModel.hyundaiCanfd) and param in IONIQ6_AOL_SAFETY_PARAMS | set(STOCK_SAFETY_PARAMS.values())) + (model == int(car.CarParams.SafetyModel.hyundai) and param in LONG_AOL_WORDS) + or classic_native_profile_supported(model, param) + or (model == int(car.CarParams.SafetyModel.hyundaiCanfd) and param in IONIQ6_AOL_SAFETY_PARAMS | STOCK_AOL_WORDS | set(STOCK_SAFETY_PARAMS.values())) or (model == int(car.CarParams.SafetyModel.hyundai) and param in AOL_WORDS) ) def native_accepts_cp(CP, model: int, param: int) -> bool: + if qualified_classic_long(CP, marked_only=True): + return classic_long_accepts(CP, model, param) if qualified_classic_scc(CP, marked_only=True): return classic_native_accepts(CP, model, param) if qualified_canfd_stock(CP, marked_only=True): @@ -135,13 +154,23 @@ def native_latch_rejected(CP, native) -> bool: # In this exact native policy a requested lateral axis remains permitted # through pedal override; denial means the physical authorization was lost. return ( - (qualified_ioniq6(CP) or qualified_non_scc(CP) or qualified_canfd_stock(CP, marked_only=True) or qualified_classic_scc(CP, marked_only=True)) + ( + qualified_classic_long(CP, marked_only=True) + or qualified_ioniq6(CP) + or qualified_non_scc(CP) + or qualified_canfd_stock(CP, marked_only=True) + or qualified_classic_scc(CP, marked_only=True) + ) and native.requestedLateral and not native.lateralAllowed ) def create_intent(CP, settings): + if qualified_classic_long(CP): + from openpilot.starpilot.car.hyundai.classic_long_intent import ClassicLongCardIntent + + return ClassicLongCardIntent(CP, settings) if qualified_classic_scc(CP): from openpilot.starpilot.car.hyundai.classic_scc_intent import ClassicSccCardIntent diff --git a/openpilot/starpilot/car/hyundai/aol_policy.h b/openpilot/starpilot/car/hyundai/aol_policy.h index bd390b9a3b..e00819f90e 100644 --- a/openpilot/starpilot/car/hyundai/aol_policy.h +++ b/openpilot/starpilot/car/hyundai/aol_policy.h @@ -1,10 +1,11 @@ #pragma once #include "selfdrive/pandad/aol_protocol.h" +#include "opendbc/safety/modes/hyundai_canfd_aol.h" inline bool hyundai_aol_param(uint16_t param) { // Existing mode28 entry also transports exact ordinary PE/EV9 requests. - return param == 0x0811U || param == 0x0891U || param == 0x8815U || param == 0x8895U || param == 0x5491U || param == 0x5C91U; + return hyundai_canfd_stock_torque_aol_param(param) || param == 0x8815U || param == 0x8895U || param == 0x5491U || param == 0x5C91U; } inline constexpr AolSafetyProfile HYUNDAI_AOL_PROFILE{28U, hyundai_aol_param, true}; @@ -21,8 +22,12 @@ inline bool hyundai_legacy_aol_param(uint16_t param) { inline constexpr AolSafetyProfile HYUNDAI_LEGACY_AOL_PROFILE{23U, hyundai_legacy_aol_param, true}; +inline bool hyundai_classic_long_aol_param(uint16_t param) { + return ((param & 0x0404U) == 0x0404U) && ((param & 0xF3B8U) == 0U) && ((param & 3U) != 3U); +} + inline bool hyundai_classic_aol_param(uint16_t param) { - return hyundai_classic_scc_aol_param(param) || param == 0x1400U || param == 0x1C00U || param == 0x1440U || param == 0x1C40U || + return hyundai_classic_long_aol_param(param) || hyundai_classic_scc_aol_param(param) || param == 0x1400U || param == 0x1C00U || param == 0x1440U || param == 0x1C40U || param == 0x1402U || param == 0x1C02U || param == 0x1441U || param == 0x1C41U; } diff --git a/openpilot/starpilot/car/hyundai/classic_long_intent.py b/openpilot/starpilot/car/hyundai/classic_long_intent.py new file mode 100644 index 0000000000..20123735d1 --- /dev/null +++ b/openpilot/starpilot/car/hyundai/classic_long_intent.py @@ -0,0 +1,43 @@ +from opendbc.car.hyundai.values import HyundaiFlags +from openpilot.starpilot.aol.intent import AolCardIntent, AOL_TOGGLE + + +class ClassicLongCardIntent(AolCardIntent): + def __init__(self, cp, settings): + super().__init__(settings, explicit_latch=True) + self.requires_fault_observation = True + self.observe_stock_engagement = True + self.has_lkas = bool(cp.flags & HyundaiFlags.HAS_LDA_BUTTON) + self._normal_previous = False + + def update(self, CS, *, consumed_buttons=frozenset(), fault_active=None, now_ns=0, native_rejection_ns=0, standard_enabled=False): + rejected = self._native_rejection_ns < native_rejection_ns <= now_ns + super().update( + CS, + consumed_buttons=consumed_buttons, + fault_active=fault_active, + now_ns=now_ns, + native_rejection_ns=native_rejection_ns, + standard_enabled=standard_enabled, + ) + normal = bool(standard_enabled and CS.canValid and not CS.canTimeout and CS.cruiseState.available) + deliberate = any(event.pressed for event in CS.buttonEvents) + if ( + normal + and not self._normal_previous + and self.has_lkas + and self.settings.enabled + and self.settings.lkas_action == AOL_TOGGLE + and not deliberate + and not rejected + and not fault_active + and not CS.steerFaultPermanent + and not CS.steerFaultTemporary + and not CS.gasPressed + and not CS.brakePressed + and not CS.accFaulted + ): + self.allowed_latch = True + self.pause_lateral = False + self._last_latch_edge_ns = now_ns + self._normal_previous = normal diff --git a/tools/ci/run_vehicle_tests.py b/tools/ci/run_vehicle_tests.py index 41450f3aec..5b5b248aa2 100644 --- a/tools/ci/run_vehicle_tests.py +++ b/tools/ci/run_vehicle_tests.py @@ -21,6 +21,10 @@ sys.path.insert(0, str(ROOT)) from tools import test_runner RELEASE_TARGETS = ( + "opendbc_repo/opendbc/safety/tests/test_hyundai_classic_long_aol.py", + "openpilot/starpilot/aol/tests/test_classic_long_profiles.py", + "opendbc_repo/opendbc/safety/tests/test_hyundai_shared_canfd_stock_aol.py", + "opendbc_repo/opendbc/car/hyundai/tests/test_shared_canfd_stock_aol.py", "opendbc_repo/opendbc/safety/tests/test_hyundai_classic_scc_aol.py", "openpilot/starpilot/aol/tests/test_classic_scc_profiles.py", "opendbc_repo/opendbc/safety/tests/test_hyundai_blended.py",