Hyundai AOL Profiles

This commit is contained in:
firestar5683
2026-10-04 02:37:46 -05:00
parent 1e4c7a3b31
commit 4440104f27
16 changed files with 1003 additions and 48 deletions
@@ -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))
+8 -2
View File
@@ -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)):
@@ -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)
)
@@ -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
@@ -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);
@@ -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;
@@ -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)));
}
@@ -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));
@@ -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)
@@ -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)
@@ -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<uint16_t>(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));
@@ -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()
+33 -4
View File
@@ -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
+7 -2
View File
@@ -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;
}
@@ -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
+4
View File
@@ -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",