Restore Carnival ICE independent lateral and longitudinal startup

This commit is contained in:
firestar5683
2026-11-03 11:01:08 -06:00
parent 55d6fb1778
commit d42ddc3ac5
15 changed files with 451 additions and 16 deletions
@@ -96,9 +96,12 @@ def qualified(cp, *, marked_only=False):
SPORTAGE_LONG_AOL_WORDS = frozenset(0x0804 | fuel | topology for fuel in (0, 2) for topology in (0, 8, 16, 144, 32, 40))
LONG_AOL_WORDS = frozenset((0x080D, 0x0815, 0x0895, 0x2815, 0x2895)) | SPORTAGE_LONG_AOL_WORDS
CARNIVAL_LONG_AOL_WORD = 0x28B4
LONG_AOL_WORDS = frozenset((CARNIVAL_LONG_AOL_WORD, 0x080D, 0x0815, 0x0895, 0x2815, 0x2895)) | SPORTAGE_LONG_AOL_WORDS
LONG_EV_CARS = frozenset((CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_KONA_EV_2ND_GEN))
INDEPENDENT_LONG_BUTTON_CARS = LONG_EV_CARS | {CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN}
INDEPENDENT_LONG_BUTTON_CARS = LONG_EV_CARS | {
CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN, CAR.KIA_CARNIVAL_2025,
}
def qualified_long(cp, *, marked_only=False):
@@ -118,6 +121,9 @@ def qualified_long(cp, *, marked_only=False):
elif cp.carFingerprint == CAR.KIA_SPORTAGE_5TH_GEN:
if word is None or word | 0x804 not in SPORTAGE_LONG_AOL_WORDS:
return False
elif cp.carFingerprint == CAR.KIA_CARNIVAL_2025:
if word != 0x20B0:
return False
elif word not in (0x11, 0x91):
return False
raw = int(cp.safetyConfigs[config_index(cp)].safetyParam)
@@ -512,8 +512,9 @@ class CarController(CarControllerBase):
)
else:
steering_status_active = CC.enabled
if self.ev6_aol_status or (self.ioniq6_longitudinal is not None and
self.CP.safetyConfigs[-1].safetyParam in (0x8815, 0x8895)):
if ((self.CP.carFingerprint == CAR.KIA_CARNIVAL_2025 and self.CP.safetyConfigs[-1].safetyParam == 0x28B4) or
self.ev6_aol_status or (self.ioniq6_longitudinal is not None and
self.CP.safetyConfigs[-1].safetyParam in (0x8815, 0x8895))):
# AOL may steer while ordinary cruise engagement remains off.
steering_status_active |= CC.latActive
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN,
@@ -669,6 +669,12 @@ class CarState(CarStateBase):
msgs.append(("CRUISE_BUTTONS_ALT", math.nan))
if not (ev9_long_qualified(CP) or pe_long_qualified(CP)):
(cam_msgs if CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else msgs).append(("SCC_CONTROL", 50))
elif CP.carFingerprint == CAR.KIA_CARNIVAL_2025 and CP.openpilotLongitudinalControl and CP.safetyConfigs[-1].safetyParam in (0x20B4, 0x28B4):
msgs = [(name, frequency) for name, frequency in msgs if name != "CRUISE_BUTTONS_ALT"]
msgs.extend((("CRUISE_BUTTONS_ALT", 50), ("ACCELERATOR_BRAKE_ALT", 100), ("TCS", 50),
("WHEEL_SPEEDS", 100), ("MDPS", 100), (self.gear_msg_canfd, 100),
("STEERING_SENSORS", 100), ("DOORS_SEATBELTS", 10), ("BLINKERS", 10), ("SCC_CONTROL", math.nan)))
cam_msgs.append(("CAM_0x362", 20))
elif torque_ev_scc_eligible(CP):
msgs.extend(((self.accelerator_msg_canfd, 100), ("TCS", 50), ("WHEEL_SPEEDS", 100), ("MDPS", 100)))
msgs.extend((("CRUISE_BUTTONS_ALT", math.nan), ("DOORS_SEATBELTS", math.nan),
@@ -49,6 +49,11 @@ EXISTING_OWNERS_OR_DISTINCT_RECEIVERS = frozenset((
CAR.GENESIS_GV80,
))
def carnival_long(cp):
return (cp.carFingerprint == CAR.KIA_CARNIVAL_2025 and qualified_long(cp) and
cp.safetyConfigs[0].safetyParam in (0x20B4, 0x28B4))
STOCK_WORDS = (0x10, 0x11, 0x12, 0x90, 0x91, 0x92)
@@ -65,6 +70,8 @@ def qualified(cp):
HyundaiFlags.CANFD_ALT_GEARS | HyundaiFlags.CANFD_ALT_GEARS_2)
if not required(cp) or cp.carFingerprint not in CARS:
return False
if carnival_long(cp):
return cp.alphaLongitudinalAvailable and cp.alternativeExperience == 0
factory = CAR(cp.carFingerprint)
stock = stock_copy(cp)
return (cp.alphaLongitudinalAvailable and cp.alternativeExperience == 0 and not cp.pcmCruise and
@@ -91,13 +98,13 @@ class GenericHDA2Startup(RadarHDA2Startup):
raise RuntimeError('Ordinary HDA-II finalization requires configured startup')
if self.prepared_for(ci.CP):
return
long_aol = self.outcome is Outcome.SENT_UNCONFIRMED and qualified_long(ci.CP, marked_only=True)
long_aol = self.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and qualified_long(ci.CP, marked_only=True)
if not long_aol and (self.outcome not in (Outcome.STOCK_UNTOUCHED, Outcome.STOCK_RESTORED) or
not stock_qualified(ci.CP, marked_only=True)):
raise RuntimeError('Ordinary HDA-II startup does not admit this AOL configuration')
expected: Any = deepcopy(self.prepared_cp)
configs = expected.get('safetyConfigs', [])
allowed_words = tuple(word | 4 for word in STOCK_WORDS) if long_aol else STOCK_WORDS
allowed_words = (*tuple(word | 4 for word in STOCK_WORDS), 0x20B4) if long_aol else STOCK_WORDS
if (len(configs) != 1 or configs[0]['safetyModel'] != 'hyundaiCanfd' or
configs[0]['safetyParam'] not in allowed_words or expected['alternativeExperience'] != 0):
raise RuntimeError('Ordinary HDA-II prepared stock profile cannot be finalized')
@@ -107,3 +114,50 @@ class GenericHDA2Startup(RadarHDA2Startup):
if ci.CP.to_dict() != expected:
raise RuntimeError('Ordinary HDA-II finalization changed unrelated CarParams')
self.prepared_cp = expected
def prepare_when_controls_ready(self, *, admission):
if not carnival_long(self.cp):
return self.prepare(admission=admission)
if self.outcome is not None or self.closed:
raise RuntimeError('Carnival startup preparation is single-use')
if admission() is not True:
return self.prepare(admission=lambda: False)
self.admission = admission
self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
self.outcome = Outcome.PENDING
self.prepared_cp = deepcopy(self.cp.to_dict())
return self.cp
def commit_pending(self, ci, *, sources_current, control_current):
if (not carnival_long(ci.CP) or self.outcome is not Outcome.PENDING or self.closed or not self.published or
ci is not self.ci or not self.prepared_for(ci.CP) or not sources_current or not control_current or
self.admission is None or self.admission() is not True):
raise RuntimeError('Carnival takeover requires current diagnostic, source and inactive control admission')
try:
success = self._disable(self.callbacks[0], self._send, bus=self.bus, addr=self.address,
com_cont_req=self.communication_control_request())
if not success or not self.disable_attempted:
raise RuntimeError('Carnival pending ECU disable was not sent successfully')
self.outcome = Outcome.SENT_UNCONFIRMED
self.floor_ns = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
from opendbc.car import make_tester_present_msg
self.keeper = self.keeper_factory(
lambda: self.callbacks[1]([make_tester_present_msg(self.address, self.bus, suppress_response=True)]), clock=self.clock)
self.keeper.start()
except BaseException:
if self.keeper is not None:
self.keeper.stop()
try:
restored = not (self.session_attempted or self.disable_attempted) or self._restore()
except Exception:
restored = False
self.outcome = Outcome.ABORT_UNCERTAIN
if not restored:
raise RuntimeError('Carnival pending takeover failed; stock restoration unverified') from None
raise
def before_control(self, **context):
if self.outcome is Outcome.PENDING:
self.check()
return False
return super().before_control(**context)
@@ -0,0 +1,182 @@
"""Keep factory SCC until an inactive producer is current; never bypass native RX."""
import os
from unittest.mock import patch
import pytest
from opendbc.car import Bus, structs
from opendbc.car.hyundai.ecu_startup import Outcome
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.radar_interface import RadarInterface
from opendbc.car.hyundai.tests.test_torque_ev_startup import Keeper
from opendbc.car.hyundai.tests.test_generic_hda2_startup_lifecycle import physical_frames as source_frames
from opendbc.car.hyundai.tests.test_ccnc_six import params
from opendbc.car.hyundai.values import CAR
from opendbc.car.hyundai.canfd_stock_aol import qualified_long
from opendbc.car.hyundai.torque_ev_scc import eligible as ev_scc_eligible
from opendbc.safety.tests.libsafety import libsafety_py
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.starpilot.vehicle_startup import VehicleStartupOwner
@pytest.mark.parametrize("scenario", (
"healthy_suppressed", "healthy_positive", "stale_control", "active_control", "missing_source",
"native_mode", "cruise_fault", "stock_engaged", "disable_negative", "disable_malformed", "restore_failure", "publisher_failure"))
def test_actual_card_defers_carnival_until_initialized_and_then_native_rx(scenario):
stock = params(CAR.KIA_CARNIVAL_2025, hda2=True, alt_buttons=True, alpha=True, adas=True, alt_lka=True)
assert stock.safetyConfigs[0].safetyParam == 0x20B4 and qualified_long(stock) and not ev_scc_eligible(stock)
native = libsafety_py.libsafety
assert native.set_safety_hooks(int(structs.CarParams.SafetyModel.elm327), 1) == 0
native.init_tests()
now = [100_000_000_000]
requests, emitted = [], []
muted = [False]
def send(frames):
for address, data, bus in frames:
assert native.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data))
emitted.extend(frames)
if scenario == "publisher_failure" and any(data[:4] == b"\x03\x28\x83\x01" for _, data, _ in frames):
muted[0] = True
raise OSError("diagnostic publisher failed after possible ECU delivery")
def query(sender, receiver, bus, addresses, commands, responses):
command = commands[0]
requests.append((now[0], command))
class Query:
def get_data(self, timeout):
sender([(0x730, bytes([len(command)]) + command + bytes(7 - len(command)), bus)])
if command == b"\x28\x83\x01":
muted[0] = True
if scenario in ("disable_negative", "restore_failure"):
return {(0x730, None): b"\x7f\x28\x22"}
if scenario == "disable_malformed":
return {(0x730, None): b"\x68"}
return {(0x730, None): b"\x68\x03"} if scenario == "healthy_positive" else {}
if command == b"\x28\x00\x01":
if scenario == "restore_failure":
return {}
muted[0] = False
return {(0x730, None): b""}
return Query()
def sleep(seconds):
now[0] += round(seconds * 1e9)
def update_sm(host, command, stale=False):
message = messaging.new_message("carControl", valid=True, logMonoTime=now[0] - (151_000_000 if stale else 0))
message.carControl = command
panda = messaging.new_message("pandaStates", 1, valid=True, logMonoTime=now[0])
state = panda.pandaStates[0]
state.safetyModel, state.safetyParam = native.get_current_safety_mode(), native.get_current_safety_param()
state.alternativeExperience = host.CP.alternativeExperience
state.ignitionLine = True
state.safetyRxChecksInvalid = native.safety_test_toyota_rx_summary_invalid()
host.sm.update_msgs(now[0] / 1e9, [message.as_reader(), panda.as_reader()])
with OpenpilotPrefix(), patch.dict(os.environ, {"SIMULATION": "0", "AOL_REPLAY_RUNTIME": "1"}), \
patch("time.clock_gettime_ns", side_effect=lambda clock: now[0]), \
patch("time.monotonic_ns", side_effect=lambda: now[0]), \
patch("time.monotonic", side_effect=lambda: now[0] / 1e9), \
patch("time.sleep", side_effect=sleep):
saved = Params()
for key in ("OpenpilotEnabledToggle", "AlwaysOnLateral", "AlphaLongitudinalEnabled"):
saved.put_bool(key, True, block=True)
holder = VehicleStartupOwner()
prepared = holder.prepare(stock, CarInterface, (list, send), requested=True,
admission=lambda: True,
defer_until_controls=True)
startup = holder.owner
assert startup is not None
startup.clock, startup.keeper_factory, startup._query = lambda: now[0] / 1e9, Keeper, query
try:
assert startup.outcome is Outcome.PENDING and not requests and not emitted
assert not startup.before_control(configured=True, sources_current=True, control_current=True)
card = Car(CI=CarInterface(prepared), RI=RadarInterface(prepared), startup_owner=startup)
startup.admission = card.startup_diagnostic_admission
card.params.put_bool("IsOffroad", False, block=True)
original_cp = card.CP.to_dict()
assert card.CP.safetyConfigs[0].safetyParam == 0x28B4 and card.CP.alternativeExperience == 32
assert not ev_scc_eligible(card.CP)
# Explicit synthetic delayed producer: stock has not been told to stop.
for tick in range(1, 701):
now[0] += 10_000_000
state = card.CI.update([(now[0], [f for f in source_frames(card.CI, tick) if f[0] not in (0x1A0, 0x1CF, 0x160)])])
assert state.canValid and not state.canTimeout and not state.accFaulted
assert not requests and not muted[0] and startup.keeper is None
command = structs.CarControl(enabled=scenario == "active_control", latActive=scenario == "active_control").as_reader()
if scenario == "missing_source":
parser = card.CI.can_parsers[Bus.pt]
parser.message_states[parser.dbc.name_to_msg["TCS"].address].timestamps.clear()
if scenario == "cruise_fault":
state.accFaulted = True
if scenario == "stock_engaged":
state.cruiseState.enabled = True
config = card.CP.safetyConfigs[-1]
if scenario == "native_mode":
native.set_alternative_experience(card.CP.alternativeExperience)
assert native.set_safety_hooks(int(config.safetyModel.raw), int(config.safetyParam)) == 0
update_sm(card, command, stale=scenario == "stale_control")
card.controls_update(state, command, initialize_only=True)
assert not requests and not muted[0] and not card.ci_initialized
if scenario in ("disable_negative", "disable_malformed", "restore_failure", "publisher_failure"):
with pytest.raises(RuntimeError, match="pending"):
card.controls_update(state, command)
assert startup.outcome is Outcome.ABORT_UNCERTAIN and not card.ci_initialized
assert card.CP.to_dict() == original_cp and not startup.handed_off
assert muted[0] == (scenario == "restore_failure")
assert startup.restored == (scenario != "restore_failure")
return
card.controls_update(state, command)
if not scenario.startswith("healthy"):
assert not requests and not muted[0] and not card.ci_initialized
assert startup.outcome is Outcome.PENDING and card.CP.to_dict() == original_cp
return
assert [command for _, command in requests] == [b"\x10\x03", b"\x28\x83\x01"]
assert muted[0] and card.ci_initialized and startup.outcome is Outcome.SENT_UNCONFIRMED
assert startup.keeper.started and not startup.handed_off
# No SCC while native is diagnostic, mismatched, or awaiting RX health.
emitted.clear()
card.controls_update(state, command)
assert not emitted and not startup.handed_off
native.set_alternative_experience(card.CP.alternativeExperience)
assert native.set_safety_hooks(int(config.safetyModel.raw), int(config.safetyParam)) == 0
native.init_tests()
update_sm(card, command)
card.controls_update(state, command)
assert not emitted and not startup.handed_off
for tick in range(701, 811):
now[0] += 10_000_000
frames = [f for f in source_frames(card.CI, tick) if f[0] not in (0x1A0, 0x1CF, 0x160)]
state = card.CI.update([(now[0], frames)])
native.set_timer(tick * 10_000)
for address, data, bus in frames:
native.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
if tick % 100 == 0:
native.safety_tick()
assert native.safety_config_valid()
update_sm(card, command)
captured = []
def publish(frames, valid=True):
if valid:
send(frames)
card.vehicle_startup.sent(frames, valid=valid)
captured.extend(frames)
with patch.object(card, "publish_sendcan", side_effect=publish):
card.controls_update(state, command)
assert any(address == 0x1A0 and bus == 1 for address, _, bus in captured)
decoder = card.CI.can_parsers[Bus.pt]
decoder.update((now[0], captured))
scc = decoder.vl['SCC_CONTROL']
assert scc['ACCMode'] == 0 and scc['aReqRaw'] == 0 and scc['aReqValue'] == 0
assert startup.handed_off and not startup.keeper.started
assert not native.get_controls_allowed() and native.aol_get_permission_mask() == 0
assert card.CP.to_dict() == original_cp
finally:
holder.close()
@@ -0,0 +1,51 @@
import pytest
from opendbc.car import Bus
from opendbc.car.hyundai.tests.test_ccnc_six import params
from opendbc.car.hyundai.values import CAR, HyundaiFlags
from opendbc.car.hyundai.canfd_stock_aol import qualified_long
from opendbc.car.hyundai.generic_hda2_startup import qualified
from opendbc.car.hyundai.carstate import CarState
def carnival():
return params(CAR.KIA_CARNIVAL_2025, hda2=True, alt_buttons=True, alpha=True, adas=True, alt_lka=True)
@pytest.mark.parametrize('word,experience,expected', [
(0x20B4,0,True), (0x28B4,32,True), (0x28B4,0,False), (0x20B4,32,False),
(0x28B6,32,False), (0x28B0,32,False), (0x8B4,32,False), (0x28B5,32,False),
])
def test_exact_profile(word, experience, expected):
cp = carnival()
cp.safetyConfigs[0].safetyParam, cp.alternativeExperience = word, experience
assert qualified_long(cp) == expected
@pytest.mark.parametrize('mutation', ['hybrid','other_car','passive','dashcam','wrong_model','angle','other_bus'])
def test_reject_other_configuration(mutation):
cp = carnival()
if mutation == 'hybrid':
cp.flags = int(cp.flags | HyundaiFlags.HYBRID)
elif mutation == 'other_car':
cp.carFingerprint = CAR.KIA_CARNIVAL_HEV_4TH_GEN
elif mutation == 'passive':
cp.passive = True
elif mutation == 'dashcam':
cp.dashcamOnly = True
elif mutation == 'angle':
cp.flags = int(cp.flags | HyundaiFlags.CANFD_ANGLE_STEERING)
elif mutation == 'other_bus':
cp.safetyConfigs = [dict(safetyModel='noOutput'),dict(safetyModel='hyundaiCanfd',safetyParam=0x20B4)]
else:
cp.safetyConfigs[0].safetyModel = 'noOutput'
assert not qualified(cp)
def test_takeover_health_binds_physical_sources_not_removed_scc():
cp = carnival()
parser = CarState(cp).get_can_parsers(cp)[Bus.pt]
assert parser.message_states[0x1AA].frequency == 50
assert 0x1CF not in parser.addresses
assert not parser.message_states[0x1A0].frequency > 0
for address in (0x100,0x175,0xA0,0xEA):
assert parser.message_states[address].frequency > 0
@@ -8,7 +8,7 @@ from opendbc.car.hyundai.canfd_stock_aol import qualified_long
def eligible(cp):
from opendbc.car.hyundai.values import CAR
# Genesis keeps its existing longitudinal controller.
return cp.carFingerprint not in (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN) and qualified_long(cp)
return cp.carFingerprint not in (CAR.GENESIS_GV70_ELECTRIFIED_1ST_GEN, CAR.GENESIS_GV60_EV_1ST_GEN, CAR.KIA_CARNIVAL_2025) and qualified_long(cp)
def options(available, control_state, lead):
@@ -1359,13 +1359,15 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
// under torque LKA + LONG. Other uses of the bit retain their original meaning.
const uint16_t damping_profile_mask = HYUNDAI_PARAM_CANFD_ANGLE | 16U | HYUNDAI_PARAM_LONGITUDINAL | 8192U;
hyundai_canfd_lkas_damping_100 = (param & damping_profile_mask) == (16U | HYUNDAI_PARAM_LONGITUDINAL | 8192U);
const bool carnival_long_requested = (param == 0x20B4U) || (param == 0x28B4U);
if (hyundai_canfd_lkas_damping_100) {
param &= (uint16_t)~8192U;
}
const bool angle_requested = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ANGLE);
const bool ioniq6_stock_aol_requested = hyundai_canfd_stock_torque_aol_param(param);
#ifdef ALLOW_DEBUG
const bool torque_long_aol_requested = hyundai_canfd_torque_long_aol_param(param) && ((unsigned int)alternative_experience == 32U);
const bool torque_long_aol_requested = ((carnival_long_requested && ((param & 0x0800U) != 0U)) || hyundai_canfd_torque_long_aol_param(param)) && ((unsigned int)alternative_experience == 32U);
#else
const bool torque_long_aol_requested = false;
#endif
@@ -1559,7 +1561,7 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
hyundai_canfd_angle_model = 0U;
#ifdef ALLOW_DEBUG
hyundai_canfd_ioniq6_long = ioniq6_long_requested;
hyundai_canfd_torque_ev_long = (param == 0x0015U) || (param == 0x0095U) || (torque_long_aol_requested && GET_FLAG(param, 16U)) || gv60_long_requested;
hyundai_canfd_torque_ev_long = (param == 0x0015U) || (param == 0x0095U) || (torque_long_aol_requested && GET_FLAG(param, 16U) && !carnival_long_requested) || gv60_long_requested;
#endif
hyundai_torque_aol_cancel_preserves_lateral = ioniq6_stock_aol_requested || torque_long_aol_requested ||
(param == 0x8815U) || (param == 0x8895U);
@@ -1813,7 +1815,20 @@ static safety_config hyundai_canfd_init_validated(uint16_t supplied_param) {
HYUNDAI_CANFD_STD_BUTTONS_RX_CHECKS(1)
};
static const RxCheck hyundai_canfd_lka_alt_buttons_long_rx_checks[] = {
HYUNDAI_CANFD_ALT_BUTTONS_RX_CHECKS(1)
};
static const RxCheck carnival_long_rx_checks[] = {
HYUNDAI_CANFD_BANKED_ANGLE_ALT_RX_CHECKS(1, 0x100)
};
ret = BUILD_SAFETY_CFG(hyundai_canfd_lka_steer_msg_long_rx_checks, HYUNDAI_CANFD_LKA_STEER_MSG_LONG_TX_MSGS);
if (carnival_long_requested) {
SET_RX_CHECKS(carnival_long_rx_checks, ret);
} else if (hyundai_canfd_alt_buttons) {
SET_RX_CHECKS(hyundai_canfd_lka_alt_buttons_long_rx_checks, ret);
} else {
// Standard-button profiles retain their existing physical source.
}
if (hyundai_canfd_lka_steer_msg_alt) {
SET_TX_MSGS(HYUNDAI_CANFD_LKA_STEER_MSG_ALT_LONG_TX_MSGS, ret);
}
@@ -25,6 +25,6 @@ static inline bool hyundai_canfd_torque_long_aol_param(uint16_t param) {
((base & 4U) != 0U) && ((fuel == 0U) || (fuel == 2U)) &&
((topology == 0U) || (topology == 8U) || (topology == 16U) ||
(topology == 144U) || (topology == 32U) || (topology == 40U));
return sportage || (param == 0x080DU) || (param == 0x0815U) || (param == 0x0895U) || (param == 0x2815U) || (param == 0x2895U);
return (param == 0x28B4U) || sportage || (param == 0x080DU) || (param == 0x0815U) || (param == 0x0895U) || (param == 0x2815U) || (param == 0x2895U);
}
#endif
@@ -0,0 +1,115 @@
import unittest
from types import SimpleNamespace
from opendbc.can import CANPacker
from opendbc.car import structs
from opendbc.car.hyundai.hyundaicanfd import create_steering_messages
from opendbc.car.hyundai.values import CAR, HyundaiFlags
from opendbc.car.hyundai.canfd_stock_aol import CARNIVAL_LONG_AOL_WORD
SPORTAGE_LONG_AOL_WORDS = (CARNIVAL_LONG_AOL_WORD,)
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests import test_hyundai_shared_canfd_stock_aol as fixtures
class TestCarnivalLongAol(unittest.TestCase):
packet = staticmethod(fixtures.TestHyundaiSharedCanfdStockAol.packet)
healthy = fixtures.TestHyundaiSharedCanfdStockAol.healthy
gesture = fixtures.TestHyundaiSharedCanfdStockAol.gesture
def setUp(self):
self.safety = libsafety_py.libsafety
self.packer = CANPacker('hyundai_canfd_generated')
self.release = self.safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) != 0
def tearDown(self):
self.safety.set_alternative_experience(0)
self.safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0)
def mode(self, word, experience=32):
self.safety.set_alternative_experience(experience)
assert self.safety.set_safety_hooks(structs.CarParams.SafetyModel.hyundaiCanfd, word) == 0
self.safety.init_tests()
self.safety.set_timer(1_000_000)
def test_physical_arm_cancel_pedals_and_ungranted_longitudinal(self):
for word in SPORTAGE_LONG_AOL_WORDS:
with self.subTest(word=hex(word)):
self.mode(word)
if self.release:
assert not self.safety.safety_config_valid()
continue
self.healthy(word)
self.safety.set_aol_test_heartbeat(True)
self.safety.aol_set_host_request(3)
assert self.safety.aol_get_permission_mask() == 0
self.gesture(word)
assert self.safety.aol_get_permission_mask() == 1
bus = 1 if word & 16 else 0
flags = HyundaiFlags.CANFD
if word & 16:
flags |= HyundaiFlags.CANFD_LKA_STEER_MSG
if word & 128:
flags |= HyundaiFlags.CANFD_LKA_STEER_MSG_ALT
cp = SimpleNamespace(carFingerprint=CAR.KIA_CARNIVAL_2025, flags=int(flags), openpilotLongitudinalControl=True)
frames = create_steering_messages(self.packer, cp, SimpleNamespace(ECAN=bus, ACAN=1-bus), True, True, 1)
for frame in frames:
assert self.safety.safety_tx_hook(self.packet(frame))
excessive = create_steering_messages(self.packer, cp, SimpleNamespace(ECAN=bus, ACAN=1-bus), True, True, 410)
assert not self.safety.safety_tx_hook(self.packet(excessive[0]))
name = 'CRUISE_BUTTONS_ALT' if word & 32 else 'CRUISE_BUTTONS'
frame = self.packer.make_can_msg(name, bus, {'COUNTER': 9, 'CRUISE_BUTTONS': 4})
assert self.safety.safety_rx_hook(self.packet(frame))
assert self.safety.aol_get_permission_mask() == 1
for counter, brake in ((7, True), (8, False)):
frame = self.packer.make_can_msg('TCS', bus, {'COUNTER': counter, 'DriverBraking': brake})
assert self.safety.safety_rx_hook(self.packet(frame))
assert self.safety.aol_get_permission_mask() == 1
accel = self.packet(self.packer.make_can_msg('SCC_CONTROL', bus, {'ACCMode': 1, 'aReqRaw': .1, 'aReqValue': .1}))
assert not self.safety.safety_tx_hook(accel)
self.safety.set_timer(1_500_001)
assert self.safety.aol_get_permission_mask() == 0
def test_wrong_experience_missing_source_and_wrong_bus_deny(self):
for word in SPORTAGE_LONG_AOL_WORDS:
for experience in (0, 1, 33):
self.mode(word, experience)
assert not self.safety.safety_config_valid()
if self.release:
continue
fuel = 'ACCELERATOR_ALT' if word & 2 else 'ACCELERATOR_BRAKE_ALT'
buttons = 'CRUISE_BUTTONS_ALT' if word & 32 else 'CRUISE_BUTTONS'
for omitted in (fuel, buttons, 'TCS', 'WHEEL_SPEEDS', 'MDPS'):
self.mode(word)
self.healthy(word, omit=omitted)
if omitted != buttons:
self.gesture(word)
assert self.safety.aol_get_permission_mask() == 0
def test_fresh_set_release_arms_lateral_through_cancel_and_brake(self):
for word in SPORTAGE_LONG_AOL_WORDS:
self.mode(word)
if self.release:
assert not self.safety.safety_config_valid()
continue
name = 'CRUISE_BUTTONS_ALT' if word & 32 else 'CRUISE_BUTTONS'
maximum = 255 if word & 32 else 14
bus = 1 if word & 16 else 0
self.healthy(word, omit=name)
self.safety.set_aol_test_heartbeat(True)
self.safety.aol_set_host_request(3)
for tick, counter, button in ((0, maximum - 1, 0), (1, maximum, 2), (2, 0, 0)):
self.safety.set_timer(1_000_000 + tick * 20_000)
frame = self.packer.make_can_msg(name, bus, {'COUNTER': counter, 'CRUISE_BUTTONS': button})
assert self.safety.safety_rx_hook(self.packet(frame))
self.safety.aol_set_host_request(3)
assert self.safety.get_controls_allowed()
assert self.safety.aol_get_permission_mask() == 3
self.safety.set_timer(1_060_000)
frame = self.packer.make_can_msg(name, bus, {'COUNTER': 1, 'CRUISE_BUTTONS': 4})
assert self.safety.safety_rx_hook(self.packet(frame))
assert not self.safety.get_controls_allowed()
assert self.safety.aol_get_permission_mask() == 1, hex(word)
frame = self.packer.make_can_msg('TCS', bus, {'COUNTER': 7, 'DriverBraking': True})
assert self.safety.safety_rx_hook(self.packet(frame))
assert self.safety.aol_get_permission_mask() == 1
+9 -4
View File
@@ -1557,9 +1557,10 @@ class Car:
from opendbc.car.hyundai.torque_ev_startup import TorqueEVStartup, required
from opendbc.car.hyundai.ecu_startup import Outcome
owner = getattr(getattr(self, 'vehicle_startup', None), 'owner', None)
return (isinstance(owner, TorqueEVStartup) and owner.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and
from opendbc.car.hyundai.generic_hda2_startup import GenericHDA2Startup, carnival_long
return (isinstance(owner, (TorqueEVStartup, GenericHDA2Startup)) and owner.outcome in (Outcome.PENDING, Outcome.SENT_UNCONFIRMED) and
owner.ready and owner.published and owner.ci is self.CI and not owner.closed and
required(self.CP))
(required(self.CP) or (isinstance(owner, GenericHDA2Startup) and carnival_long(self.CP))))
def mixed_startup_keepalive(self):
from opendbc.car.hyundai.blended_longitudinal import BlendedStartup
@@ -1622,7 +1623,11 @@ class Car:
if getattr(self.CI.CC, "bolt_cc_profile", False):
self.CI.CC.bolt_cc_metric = self.is_metric
if (initialize_only and self.mixed_startup_keepalive() and self.vehicle_startup.owner.pending_target):
from opendbc.car.hyundai.generic_hda2_startup import GenericHDA2Startup
from opendbc.car.hyundai.ecu_startup import Outcome
pending_carnival = (isinstance(self.vehicle_startup.owner, GenericHDA2Startup) and
self.vehicle_startup.owner.outcome is Outcome.PENDING)
if initialize_only and (pending_carnival or (self.mixed_startup_keepalive() and self.vehicle_startup.owner.pending_target)):
# Fresh disabled controls alone are insufficient: finish the host's
# selfdriveInitializing phase before suppressing factory ADAS output.
return
@@ -1639,7 +1644,7 @@ class Car:
from opendbc.car.hyundai.ev6_startup import EV6Startup
from opendbc.car.hyundai.ecu_startup import Outcome
classic = self.vehicle_startup.owner
if isinstance(classic, (G90Startup, TorqueEVStartup, EV6Startup)) and classic.outcome is Outcome.PENDING:
if isinstance(classic, (G90Startup, TorqueEVStartup, EV6Startup, GenericHDA2Startup)) and classic.outcome is Outcome.PENDING:
now = time.monotonic_ns()
source = int(self.sm.logMonoTime['carControl'])
receipt = int(self.sm.recv_time['carControl'] * 1e9)
@@ -505,7 +505,7 @@ int main() {
constexpr uint16_t sportage_long_words[] = {0x0804U, 0x0806U, 0x080CU, 0x080EU, 0x0814U, 0x0816U,
0x0894U, 0x0896U, 0x0824U, 0x0826U, 0x082CU, 0x082EU};
for (uint32_t word = 0U; word <= 0xFFFFU; ++word) {
bool expected = word == 0x080DU || word == 0x2815U || word == 0x2895U || word == 0x0815U || word == 0x0895U || word == 0x8815U || word == 0x8895U || word == 0x5491U || word == 0x5C91U || word == 0x5C95U || word == 0x5495U;
bool expected = word == 0x28B4U || word == 0x080DU || word == 0x2815U || word == 0x2895U || word == 0x0815U || word == 0x0895U || word == 0x8815U || word == 0x8895U || word == 0x5491U || word == 0x5C91U || word == 0x5C95U || word == 0x5495U;
for (uint16_t stock_word : stock_canfd_words) {
expected = expected || word == stock_word;
}
View File
-1
View File
@@ -1 +0,0 @@
{"protected_inventory_sha256": "3ec588956664c5943c0dab67dcd0817f33c89ba30d19ca6841a86a57c1edf6c8", "schema_version": 1}
+1
View File
@@ -530,6 +530,7 @@ RELEASE_TARGETS = (
"opendbc_repo/opendbc/safety/tests/test_subaru_ascent_angle.py",
"opendbc_repo/opendbc/safety/tests/test_ford_three.py",
"opendbc_repo/opendbc/safety/tests/test_hyundai_canfd_carnival.py",
"opendbc_repo/opendbc/safety/tests/test_hyundai_carnival_long_aol.py",
"opendbc_repo/opendbc/safety/tests/test_subaru_stock_aol.py",
"opendbc_repo/opendbc/safety/tests/test_subaru_software_cruise.py",
"opendbc_repo/opendbc/safety/tests/test_subaru_sng.py",