diff --git a/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py b/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py index 505fa79b6c..7e94dbd309 100644 --- a/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py +++ b/opendbc_repo/opendbc/car/hyundai/canfd_stock_aol.py @@ -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) diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index aa092c18ff..886c81484c 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -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, diff --git a/opendbc_repo/opendbc/car/hyundai/carstate.py b/opendbc_repo/opendbc/car/hyundai/carstate.py index 719cd771f9..0078d9eb88 100644 --- a/opendbc_repo/opendbc/car/hyundai/carstate.py +++ b/opendbc_repo/opendbc/car/hyundai/carstate.py @@ -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), diff --git a/opendbc_repo/opendbc/car/hyundai/generic_hda2_startup.py b/opendbc_repo/opendbc/car/hyundai/generic_hda2_startup.py index 73e55b7a15..b60be0affb 100644 --- a/opendbc_repo/opendbc/car/hyundai/generic_hda2_startup.py +++ b/opendbc_repo/opendbc/car/hyundai/generic_hda2_startup.py @@ -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) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_carnival_long_aol.py b/opendbc_repo/opendbc/car/hyundai/tests/test_carnival_long_aol.py new file mode 100644 index 0000000000..9109a40747 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_carnival_long_aol.py @@ -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() diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_carnival_long_profile.py b/opendbc_repo/opendbc/car/hyundai/tests/test_carnival_long_profile.py new file mode 100644 index 0000000000..8f9af37d8d --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_carnival_long_profile.py @@ -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 diff --git a/opendbc_repo/opendbc/car/hyundai/torque_ev_scc.py b/opendbc_repo/opendbc/car/hyundai/torque_ev_scc.py index 78ae9b9a41..54b189cce8 100644 --- a/opendbc_repo/opendbc/car/hyundai/torque_ev_scc.py +++ b/opendbc_repo/opendbc/car/hyundai/torque_ev_scc.py @@ -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): diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h b/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h index 6ec8744bd4..ed9f422a68 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_canfd.h @@ -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); } diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h index cd7284c69c..7c0f16df2e 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_canfd_aol.h @@ -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 diff --git a/opendbc_repo/opendbc/safety/tests/test_hyundai_carnival_long_aol.py b/opendbc_repo/opendbc/safety/tests/test_hyundai_carnival_long_aol.py new file mode 100644 index 0000000000..8b99ac2ff4 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_hyundai_carnival_long_aol.py @@ -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 diff --git a/openpilot/selfdrive/car/card.py b/openpilot/selfdrive/car/card.py index c3a225e1c7..5e577b02de 100644 --- a/openpilot/selfdrive/car/card.py +++ b/openpilot/selfdrive/car/card.py @@ -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) diff --git a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc index 8b35231360..522b66adb7 100644 --- a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc +++ b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc @@ -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; } diff --git a/prebuilt b/prebuilt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/prebuilt.json b/prebuilt.json deleted file mode 100644 index d30e1b89cc..0000000000 --- a/prebuilt.json +++ /dev/null @@ -1 +0,0 @@ -{"protected_inventory_sha256": "3ec588956664c5943c0dab67dcd0817f33c89ba30d19ca6841a86a57c1edf6c8", "schema_version": 1} diff --git a/tools/ci/run_vehicle_tests.py b/tools/ci/run_vehicle_tests.py index e238681c5e..b18795df2b 100644 --- a/tools/ci/run_vehicle_tests.py +++ b/tools/ci/run_vehicle_tests.py @@ -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",