From 42eef3f21ccb06b0ac0a742d47d96b162d45b9a3 Mon Sep 17 00:00:00 2001 From: LowkeyNEXT Date: Sun, 19 Jul 2026 17:54:54 -0500 Subject: [PATCH] feat(hyundai): add EV9 alpha longitudinal support --- .../opendbc/car/hyundai/carcontroller.py | 216 +++++++++++++++-- opendbc_repo/opendbc/car/hyundai/carstate.py | 32 ++- .../opendbc/car/hyundai/hyundaicanfd.py | 217 ++++++++++++++++- opendbc_repo/opendbc/car/hyundai/interface.py | 12 +- .../opendbc/car/hyundai/tests/test_hyundai.py | 224 +++++++++++++++++- opendbc_repo/opendbc/car/hyundai/values.py | 13 +- 6 files changed, 681 insertions(+), 33 deletions(-) diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index bd0d632cc..123064a7f 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -8,8 +8,8 @@ from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_steer_an from opendbc.car.common.conversions import Conversions as CV from opendbc.car.hyundai import hyundaicanfd, hyundaican from opendbc.car.hyundai.hyundaicanfd import CanBus -from opendbc.car.hyundai.values import HyundaiFlags, Buttons, CarControllerParams, CAR, CANFD_RADAR_LIVE_LONGITUDINAL_CAR, \ - kia_ev6_gt_line_longitudinal_tuning +from opendbc.car.hyundai.values import HyundaiFlags, Buttons, CarControllerParams, CAR, CANFD_ANGLE_LONGITUDINAL_CAR, \ + CANFD_RADAR_LIVE_LONGITUDINAL_CAR, kia_ev6_gt_line_longitudinal_tuning from opendbc.car.interfaces import CarControllerBase from opendbc.car.vehicle_model import VehicleModel from openpilot.common.params import Params @@ -80,10 +80,16 @@ EV9_DRIVER_OVERRIDE_RECOVERY_ANGLE_BP = [0, EV9_DRIVER_OVERRIDE_RECOVERY_FRAMES EV9_DRIVER_OVERRIDE_RECOVERY_ANGLE_V = [0.75, 2.0, 5.0] EV9_DRIVER_OVERRIDE_RECOVERY_GAIN_V = [0.08, 0.20, 0.45] EV9_DRIVER_OVERRIDE_RECOVERY_ALPHA = 0.02 +EV9_STOP_REQUEST_SPEED = 0.47 +EV9_STANDSTILL_DELAY_FRAMES = 178 +EV9_STOP_RELEASE_DELAY_FRAMES = 6 +BLINDSPOT_WARNING_FLASH_SAMPLES = 20 +BLINDSPOT_WARNING_FLASH_ON_SAMPLES = 16 +BLINDSPOT_WARNING_SOUND_SAMPLES = 36 def egmp_dynamic_longitudinal_tuning(CP) -> bool: - return CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 or \ + return CP.carFingerprint in (CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV9) or \ kia_ev6_gt_line_longitudinal_tuning(CP.carFingerprint, getattr(CP, "carVin", "")) @@ -119,6 +125,29 @@ class GenesisG90LongitudinalTuningState: long_control_state_last: LongCtrlState = LongCtrlState.off +@dataclass(frozen=True) +class EV9LongitudinalTuningState: + stop_request: bool = False + cruise_standstill: bool = False + stop_request_frames: int = 0 + release_frames: int = 0 + + +@dataclass(frozen=True) +class BlindspotWarningOutput: + mirror_lamp_active: bool = False + sound_active: bool = False + + +@dataclass +class BlindspotWarningState: + flash_phase: int = 0 + mirror_warning_active: bool = False + escalated_prev: bool = False + sound_remaining: int = 0 + sound_armed: bool = True + + def _jerk_limited_integrator(desired_accel: float, last_accel: float, jerk_upper: float, jerk_lower: float) -> float: step = (jerk_upper if desired_accel >= last_accel else jerk_lower) * DT_CTRL * 5.0 return float(np.clip(desired_accel, last_accel - step, last_accel + step)) @@ -131,14 +160,93 @@ def _calculate_ioniq_6_dynamic_lower_jerk(accel_error: float) -> float: return IONIQ_6_LONG_MIN_JERK +def should_track_stop_accel_directly(stopping: bool, v_ego: float, + accel_cmd: float, actual_accel: float) -> bool: + return bool(stopping and v_ego > EV6_GT_LINE_STOP_BRAKE_CAP_MAX_SPEED and accel_cmd < actual_accel) + + def should_use_ev6_gt_line_stop_direct_tracking(ev6_gt_line: bool, stopping: bool, v_ego: float, accel_cmd: float, actual_accel: float) -> bool: return bool(ev6_gt_line and stopping and v_ego > EV6_GT_LINE_STOP_BRAKE_CAP_MAX_SPEED and accel_cmd < actual_accel) +def update_ev9_longitudinal_tuning(state: EV9LongitudinalTuningState, enabled: bool, + stopping: bool, v_ego: float) -> EV9LongitudinalTuningState: + if not enabled: + return EV9LongitudinalTuningState() + + if stopping: + if not state.stop_request and v_ego > EV9_STOP_REQUEST_SPEED: + return EV9LongitudinalTuningState() + frames = state.stop_request_frames + 1 if state.stop_request else 0 + return EV9LongitudinalTuningState( + stop_request=True, + cruise_standstill=frames >= EV9_STANDSTILL_DELAY_FRAMES, + stop_request_frames=frames, + ) + + if state.stop_request: + release_frames = state.release_frames + 1 + if release_frames <= EV9_STOP_RELEASE_DELAY_FRAMES: + return EV9LongitudinalTuningState( + stop_request=True, + cruise_standstill=False, + stop_request_frames=state.stop_request_frames, + release_frames=release_frames, + ) + + return EV9LongitudinalTuningState() + + +def update_blindspot_warning(state: BlindspotWarningState, escalated: bool, + blinker: bool) -> BlindspotWarningOutput: + if not blinker: + state.flash_phase = 0 + state.mirror_warning_active = False + state.escalated_prev = False + state.sound_remaining = 0 + state.sound_armed = True + return BlindspotWarningOutput() + + rising = escalated and not state.escalated_prev + if rising: + state.flash_phase = 0 + state.mirror_warning_active = True + if state.sound_armed: + state.sound_remaining = BLINDSPOT_WARNING_SOUND_SAMPLES + state.sound_armed = False + elif escalated: + state.flash_phase = (state.flash_phase + 1) % BLINDSPOT_WARNING_FLASH_SAMPLES + state.mirror_warning_active = True + elif state.mirror_warning_active and state.flash_phase < BLINDSPOT_WARNING_FLASH_ON_SAMPLES - 1: + state.flash_phase += 1 + else: + state.flash_phase = 0 + state.mirror_warning_active = False + + state.escalated_prev = escalated + sound_active = state.sound_remaining > 0 + if state.sound_remaining > 0: + state.sound_remaining -= 1 + return BlindspotWarningOutput( + mirror_lamp_active=state.mirror_warning_active and state.flash_phase < BLINDSPOT_WARNING_FLASH_ON_SAMPLES, + sound_active=sound_active, + ) + + +def reset_egmp_longitudinal_tuning(state: Ioniq6LongitudinalTuningState) -> Ioniq6LongitudinalTuningState: + state.desired_accel = 0.0 + state.actual_accel = 0.0 + state.accel_last = 0.0 + state.jerk_upper = 0.0 + state.jerk_lower = 0.0 + state.launch_active = False + return state + + def update_ioniq_6_longitudinal_tuning(state: Ioniq6LongitudinalTuningState, accel_cmd: float, v_ego: float, a_ego: float, long_control_state: LongCtrlState, long_active: bool, - ev6_gt_line: bool = False) -> Ioniq6LongitudinalTuningState: + ev6_gt_line: bool = False, low_speed_stop_brake_cap: bool = False) -> Ioniq6LongitudinalTuningState: starting = long_control_state == LongCtrlState.starting stopping = long_control_state == LongCtrlState.stopping restart_from_stop = state.long_control_state_last in (LongCtrlState.stopping, LongCtrlState.starting) and \ @@ -180,7 +288,8 @@ def update_ioniq_6_longitudinal_tuning(state: Ioniq6LongitudinalTuningState, acc state.jerk_lower = min(dynamic_lower_jerk, lower_speed_limit) if state.stopping: - stop_brake_cap_max_speed = EV6_GT_LINE_STOP_BRAKE_CAP_MAX_SPEED if ev6_gt_line else IONIQ_6_STOP_BRAKE_CAP_MAX_SPEED + stop_brake_cap_max_speed = EV6_GT_LINE_STOP_BRAKE_CAP_MAX_SPEED if ev6_gt_line or low_speed_stop_brake_cap else \ + IONIQ_6_STOP_BRAKE_CAP_MAX_SPEED if v_ego <= stop_brake_cap_max_speed: stop_brake_cap = float(np.interp(v_ego, IONIQ_6_STOP_BRAKE_CAP_SPEED_BP, IONIQ_6_STOP_BRAKE_CAP_ACCEL_V)) state.desired_accel = min(0.0, max(accel_cmd, stop_brake_cap)) @@ -355,6 +464,10 @@ class CarController(CarControllerBase): self.ecu_disable_failed = False self._ecu_disable_checked = False self._params = Params() + if CP.carFingerprint == CAR.KIA_EV9: + self._ev9_long_tuning = EV9LongitudinalTuningState() + self._left_blindspot_warning = BlindspotWarningState() + self._right_blindspot_warning = BlindspotWarningState() self.long_active_ecu = self.CP.openpilotLongitudinalControl self._ioniq_6_lane_change_ui_side = None self._ioniq_6_lane_change_ui_frames = 0 @@ -549,6 +662,9 @@ class CarController(CarControllerBase): use_egmp_dynamic_long_tuning = egmp_dynamic_longitudinal_tuning(self.CP) and self.long_active_ecu and \ actuators.longControlState in (LongCtrlState.starting, LongCtrlState.pid, LongCtrlState.stopping) is_ev6_gt_line = kia_ev6_gt_line_longitudinal_tuning(self.CP.carFingerprint, getattr(self.CP, "carVin", "")) + is_ev9 = self.CP.carFingerprint == CAR.KIA_EV9 + if is_ev9 and (self._ev9_long_tuning.stop_request or not CC.enabled or CC.cruiseControl.override): + self._ioniq_6_long_tuning = reset_egmp_longitudinal_tuning(self._ioniq_6_long_tuning) if should_reset_ev6_gt_line_longitudinal_tuning(self.CP, actuators.longControlState): self._ioniq_6_long_tuning = reset_ev6_gt_line_longitudinal_tuning(self._ioniq_6_long_tuning, self.CP, actuators.longControlState) @@ -556,7 +672,8 @@ class CarController(CarControllerBase): self._ioniq_6_long_tuning = update_ioniq_6_longitudinal_tuning(self._ioniq_6_long_tuning, accel_cmd, CS.out.vEgo, CS.out.aEgo, actuators.longControlState, self.long_active_ecu, - ev6_gt_line=is_ev6_gt_line) + ev6_gt_line=is_ev6_gt_line, + low_speed_stop_brake_cap=is_ev9) use_egmp_smoothed_accel = use_egmp_dynamic_long_tuning and ( accel_cmd >= self._ioniq_6_long_tuning.actual_accel or self._ioniq_6_long_tuning.launch_active or @@ -565,6 +682,9 @@ class CarController(CarControllerBase): if should_use_ev6_gt_line_stop_direct_tracking(is_ev6_gt_line, self._ioniq_6_long_tuning.stopping, CS.out.vEgo, accel_cmd, self._ioniq_6_long_tuning.actual_accel): use_egmp_smoothed_accel = False + if is_ev9 and should_track_stop_accel_directly(self._ioniq_6_long_tuning.stopping, CS.out.vEgo, + accel_cmd, self._ioniq_6_long_tuning.actual_accel): + use_egmp_smoothed_accel = False if use_egmp_dynamic_long_tuning: if use_egmp_smoothed_accel: accel = self._ioniq_6_long_tuning.actual_accel @@ -706,6 +826,8 @@ class CarController(CarControllerBase): preserve_stock_lkas = bool(self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING) and not self.long_active_ecu angle_lkas_alt = bool(self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING and self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING_ALT) + ccnc_angle_long = self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR and \ + self.CP.flags & HyundaiFlags.CCNC and angle_lkas_alt and self.long_active_ecu steering_msg_active = apply_steer_req if angle_lkas_alt: # Angle LKAS_ALT cars fault if the angle-steering status drops inactive during torque limiting. @@ -717,23 +839,33 @@ class CarController(CarControllerBase): if angle_lkas_alt: steering_msg_active = bool(steering_msg_active and drive_gear) forward_stock_lkas = angle_lkas_alt and not (drive_gear and (CC.latActive or CC.enabled)) - if not forward_stock_lkas: + if not forward_stock_lkas and not ccnc_angle_long: can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.enabled, steering_msg_active, apply_torque, apply_angle, CS.stock_lfa_msg, CS.stock_lkas_msg if preserve_stock_lkas else None, lka_icon=lka_icon)) + direct_steering_active = ccnc_angle_long and drive_gear and CC.latActive and not CS.angle_steering_fault + if ccnc_angle_long and drive_gear: + can_sends.append(hyundaicanfd.create_angle_adas_cmd( + self.packer, self.CAN, + apply_angle if direct_steering_active else CS.angle_steering_angle, + direct_steering_active, apply_torque if direct_steering_active else 0.0, + )) + if ccnc_angle_long and not drive_gear: + can_sends.extend(hyundaicanfd.create_inactive_angle_steering_messages(self.packer, self.CAN, + CS.angle_steering_angle)) # prevent LFA from activating on LKA steering cars by sending "no lane lines detected" to ADAS ECU suppress_lfa = bool(lka_steering) if angle_lkas_alt: - suppress_lfa = bool(lka_steering and CC.latActive and drive_gear) + suppress_lfa = bool(lka_steering and drive_gear and (CC.latActive or (ccnc_angle_long and CC.enabled))) if self.frame % 5 == 0 and suppress_lfa: can_sends.append(hyundaicanfd.create_suppress_lfa(self.packer, self.CAN, CS.lfa_block_msg, self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING_ALT)) # LFA and HDA icons - if self.frame % 5 == 0 and (not lka_steering or lka_steering_long): + if self.frame % 5 == 0 and (not lka_steering or lka_steering_long) and not ccnc_angle_long: if ccnc_non_hda2: can_sends.extend(hyundaicanfd.create_ccnc(self.packer, self.CAN, self.long_active_ecu, CC.enabled, CC.hudControl, CC.leftBlinker, CC.rightBlinker, CS.msg_161, CS.msg_162, CS.msg_1b5, @@ -769,13 +901,41 @@ class CarController(CarControllerBase): if self.long_active_ecu: if lka_steering: - can_sends.extend(hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame)) - # Ioniq 5/6: front radar treats ADAS_DRV's 0x100 broadcast as its host heartbeat - # and stops publishing object tracks when it disappears. Spoof it periodically on - # PT bus so the radar keeps tracking. - if self.CP.carFingerprint in CANFD_RADAR_LIVE_LONGITUDINAL_CAR and self.frame % 4 == 0: - can_sends.append(hyundaicanfd.create_accelerator_brake_alt_spoof(0, self.frame // 4, CS.out.brakePressed, - CS.out.gasPressed, self.CP.carFingerprint)) + if ccnc_angle_long: + left_escalated = CS.left_blindspot_from_radar and CC.leftBlinker and not CC.rightBlinker + right_escalated = CS.right_blindspot_from_radar and CC.rightBlinker and not CC.leftBlinker + left_warning = BlindspotWarningOutput() + right_warning = BlindspotWarningOutput() + if self.frame % 5 == 0: + left_warning = update_blindspot_warning( + self._left_blindspot_warning, left_escalated, CC.leftBlinker, + ) + right_warning = update_blindspot_warning( + self._right_blindspot_warning, right_escalated, CC.rightBlinker, + ) + steering_available = CC.latActive or CC.enabled + steering_active = direct_steering_active and apply_steer_req and not CS.out.steeringPressed + adrv_messages = hyundaicanfd.create_ccnc_adrv_messages( + self.packer, self.CP, self.CAN, self.frame, CC.enabled, CS.out.cruiseState.available, CC.hudControl, + CS.out, CS.is_metric, steering_available, steering_active, + CS.left_blindspot_from_radar, CS.right_blindspot_from_radar, + drive_gear=drive_gear, + hba_icon=CS.hba_icon, + left_escalated=left_escalated, right_escalated=right_escalated, + left_warning_lamp=left_warning.mirror_lamp_active, + right_warning_lamp=right_warning.mirror_lamp_active, + left_sound_active=left_warning.sound_active, right_sound_active=right_warning.sound_active, + ) + else: + adrv_messages = hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame) + can_sends.extend(adrv_messages) + # The front radar treats ADAS_DRV's 0x100 broadcast as its host heartbeat + # and stops publishing object tracks when it disappears. + radar_heartbeat_step = 1 if ccnc_angle_long else 4 + if self.CP.carFingerprint in CANFD_RADAR_LIVE_LONGITUDINAL_CAR and self.frame % radar_heartbeat_step == 0: + can_sends.append(hyundaicanfd.create_accelerator_brake_alt_spoof(0, self.frame // radar_heartbeat_step, + CS.out.brakePressed, CS.out.gasPressed, + self.CP.carFingerprint)) elif not ccnc_non_hda2: can_sends.extend(hyundaicanfd.create_fca_warning_light(self.packer, self.CAN, self.frame)) if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and self.frame % 5 == 0: @@ -810,9 +970,27 @@ class CarController(CarControllerBase): if use_egmp_smoothed_accel: acc_kwargs["jerk_lower"] = self._ioniq_6_long_tuning.jerk_lower acc_kwargs["jerk_upper"] = self._ioniq_6_long_tuning.jerk_upper - can_sends.append(hyundaicanfd.create_acc_control(self.packer, self.CAN, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override, - set_speed_in_units, hud_control, cruise_info=CS.cruise_info if ccnc_non_hda2 else None, - **acc_kwargs)) + if ccnc_angle_long: + self._ev9_long_tuning = update_ev9_longitudinal_tuning( + self._ev9_long_tuning, CC.enabled and not CC.cruiseControl.override, + CC.actuators.longControlState == LongCtrlState.stopping, float(CS.out.vEgo), + ) + if self._ev9_long_tuning.stop_request or not CC.enabled or CC.cruiseControl.override: + self._ioniq_6_long_tuning = reset_egmp_longitudinal_tuning(self._ioniq_6_long_tuning) + accel = 0.0 + can_sends.append(hyundaicanfd.create_ccnc_acc_control( + self.packer, self.CAN, CC.enabled, accel, + self._ev9_long_tuning.stop_request, self._ev9_long_tuning.cruise_standstill, CC.cruiseControl.override, + set_speed_in_units, int(CS.out.cruiseState.available), lead_distance, lead_rel_speed, lead_visible, + float(CS.out.vEgo), + jerk_lower=acc_kwargs["jerk_lower"], + jerk_upper=acc_kwargs["jerk_upper"], + )) + else: + can_sends.append(hyundaicanfd.create_acc_control( + self.packer, self.CAN, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override, + set_speed_in_units, hud_control, cruise_info=CS.cruise_info if ccnc_non_hda2 else None, **acc_kwargs, + )) self.accel_last = accel else: # button presses diff --git a/opendbc_repo/opendbc/car/hyundai/carstate.py b/opendbc_repo/opendbc/car/hyundai/carstate.py index aa29e673e..e2a992746 100644 --- a/opendbc_repo/opendbc/car/hyundai/carstate.py +++ b/opendbc_repo/opendbc/car/hyundai/carstate.py @@ -8,6 +8,7 @@ from opendbc.car import Bus, create_button_events, structs from opendbc.car.common.conversions import Conversions as CV from opendbc.car.hyundai.hyundaicanfd import CanBus from opendbc.car.hyundai.values import HyundaiFlags, HyundaiStarPilotFlags, HyundaiStarPilotSafetyFlags, CAR, DBC, Buttons, CarControllerParams, \ + CANFD_ANGLE_LONGITUDINAL_CAR, CANFD_CORNER_RADAR_BSM_CAR, \ hyundai_cancel_button_enables_cruise, ALT_BUS_LDA_BUTTON_CARS, ALT_BUS_LDA_BUTTON_SWL_STAT_CARS from opendbc.car.interfaces import CarStateBase @@ -136,6 +137,11 @@ class CarState(CarStateBase): self.blindspots_front_corner_1_ts = 0 self.left_blindspot_from_radar = False self.right_blindspot_from_radar = False + if CP.carFingerprint == CAR.KIA_EV9: + self.hba_icon = 0 + self.main_cruise_on = False + self.angle_steering_angle = 0.0 + self.angle_steering_fault = False # On some cars, CLU15->CF_Clu_VehicleSpeed can oscillate faster than the dash updates. Sample at 5 Hz self.cluster_speed = 0 @@ -149,6 +155,12 @@ class CarState(CarStateBase): # Main button also can trigger an engagement on these cars return any(btn in ENABLE_BUTTONS for btn in self.cruise_buttons) or any(self.main_buttons) + def update_main_cruise(self, ret: structs.CarState) -> bool: + if any(be.type == ButtonType.mainCruise and be.pressed for be in ret.buttonEvents): + self.main_cruise_on = not self.main_cruise_on + + return bool(ret.cruiseState.available and self.main_cruise_on) + def create_cruise_button_events(self, cur_button: int, prev_button: int) -> list[structs.CarState.ButtonEvent]: if cur_button != prev_button and prev_button != Buttons.CANCEL and cur_button == Buttons.CANCEL: self.cancel_button_enable_in_progress = ( @@ -448,6 +460,10 @@ class CarState(CarStateBase): ret.steeringTorqueEps = cp.vl["MDPS"]["STEERING_OUT_TORQUE"] ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.params.STEER_THRESHOLD, 5) ret.steerFaultTemporary = cp.vl["MDPS"]["LKA_FAULT"] != 0 + if self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR: + self.angle_steering_angle = cp.vl["MDPS"]["STEERING_ANGLE"] + self.angle_steering_fault = cp.vl["MDPS"]["LKA_ANGLE_FAULT"] != 0 + ret.steerFaultTemporary = ret.steerFaultTemporary or self.angle_steering_fault ccnc_non_hda2 = self.CP.flags & HyundaiFlags.CCNC and not self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING if ccnc_non_hda2: @@ -463,11 +479,12 @@ class CarState(CarStateBase): cp.vl["BLINKERS"][right_blinker_sig]) self.left_blindspot_from_radar = False self.right_blindspot_from_radar = False - if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6: + corner_radar_bsm = self.CP.carFingerprint in CANFD_CORNER_RADAR_BSM_CAR + if corner_radar_bsm: self.left_blindspot_from_radar, self.right_blindspot_from_radar = decode_ioniq_6_blindspot_radar_state( cp.vl["BLINDSPOTS_FRONT_CORNER_2"]["SIDE_DETECT_STATE"]) if self.CP.enableBsm: - if self.CP.carFingerprint == CAR.HYUNDAI_IONIQ_6: + if corner_radar_bsm: ret.leftBlindspot = (bool(cp.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_LtIndSta"]) or self.left_blindspot_from_radar) ret.rightBlindspot = (bool(cp.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_RtIndSta"]) or @@ -537,6 +554,9 @@ class CarState(CarStateBase): self.stock_lfa_msg = copy.copy(cp.vl["LFA"]) if cp.ts_nanos["LFAHDA_CLUSTER"]["CHECKSUM"] > 0: self.stock_lfahda_cluster_msg = copy.copy(cp.vl["LFAHDA_CLUSTER"]) + if self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR and cp.ts_nanos["FR_CMR_01_10ms"]["FR_CMR_Crc1Val"] > 0: + hba_icon = int(cp.vl["FR_CMR_01_10ms"]["HBA_IndLmpReq"]) + self.hba_icon = hba_icon if hba_icon in (1, 2) else 0 if cp.ts_nanos["BLINKER_STALKS"]["CHECKSUM_MAYBE"] > 0: self.stock_blinker_stalks_ts = cp.ts_nanos["BLINKER_STALKS"]["CHECKSUM_MAYBE"] @@ -544,6 +564,8 @@ class CarState(CarStateBase): *create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}), *create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas}), *create_button_events(self.left_paddle, prev_left_paddle, {1: ButtonType.altButton2})] + if self.CP.openpilotLongitudinalControl and self.CP.carFingerprint == CAR.KIA_EV9: + ret.cruiseState.available = self.update_main_cruise(ret) ret.blockPcmEnable = not self.recent_button_interaction() @@ -592,6 +614,12 @@ class CarState(CarStateBase): ("LFAHDA_CLUSTER", 0), # optional: carries cluster icon state on some variants ("BLINKER_STALKS", 0), # optional: some trims publish live stalk/light state on ECAN during turn camera events ] + if CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR and CP.enableBsm: + # Keep the suppressed ADAS BSM output optional. + msgs.append(("BLINDSPOTS_REAR_CORNERS", 0)) + if CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR: + msgs.append(("BLINDSPOTS_FRONT_CORNER_2", 0)) + msgs.append(("FR_CMR_01_10ms", 0)) if CP.flags & HyundaiFlags.EV: msgs.append(("DRIVE_MODE_EV", 0)) # optional: not all CAN-FD EV variants publish drive mode msgs.append(("MANUAL_SPEED_LIMIT_ASSIST", 0)) # optional: used for non-adaptive cruise state and Ioniq 6 i-Pedal latch detection diff --git a/opendbc_repo/opendbc/car/hyundai/hyundaicanfd.py b/opendbc_repo/opendbc/car/hyundai/hyundaicanfd.py index 258ca73fe..182dc0302 100644 --- a/opendbc_repo/opendbc/car/hyundai/hyundaicanfd.py +++ b/opendbc_repo/opendbc/car/hyundai/hyundaicanfd.py @@ -3,7 +3,7 @@ import numpy as np from opendbc.car import CanBusBase, CanData from opendbc.car.common.conversions import Conversions as CV from opendbc.car.crc import CRC16_XMODEM -from opendbc.car.hyundai.values import HyundaiFlags +from opendbc.car.hyundai.values import HyundaiFlags, CAR def _set_value(msg: bytearray, sig, ival: int) -> None: @@ -92,6 +92,10 @@ def _create_angle_adas_cmd_msg(packer, CAN, apply_angle: float, lat_active: bool return packer.make_can_msg("ADAS_CMD_35_10ms", CAN.ECAN, values) +def create_angle_adas_cmd(packer, CAN, apply_angle: float, lat_active: bool, torque_reduction_gain: float): + return _create_angle_adas_cmd_msg(packer, CAN, apply_angle, lat_active, torque_reduction_gain) + + def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_torque, apply_angle, lfa_base_values=None, lkas_base_values=None, lka_icon=None): if lka_icon is None: @@ -204,6 +208,25 @@ def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_torque, return ret +def create_inactive_angle_steering_messages(packer, CAN, steering_angle: float): + lfa_values = { + "LKA_MODE": 2, + "LKA_ICON": 1, + "TORQUE_REQUEST": 0, + "LKA_ASSIST": 0, + "STEER_REQ": 0, + "STEER_MODE": 0, + "HAS_LANE_SAFETY": 0, + "NEW_SIGNAL_1": 0, + "NEW_SIGNAL_2": 0, + "DAMP_FACTOR": 100, + } + return [ + packer.make_can_msg("LFA", CAN.ECAN, lfa_values), + create_angle_adas_cmd(packer, CAN, steering_angle, False, 0.0), + ] + + def create_suppress_lfa(packer, CAN, lfa_block_msg, lka_steering_alt): suppress_msg = "CAM_0x362" if lka_steering_alt else "CAM_0x2a4" msg_bytes = 32 if lka_steering_alt else 24 @@ -397,6 +420,35 @@ def create_blindspot_status_messages(packer, CAN, rear_values, front_corner_valu ] +def create_ccnc_blindspot_status_messages(packer, CP, CAN, counter, left_blindspot=False, right_blindspot=False, + left_escalated=False, right_escalated=False, drive_gear=False, + left_warning_lamp=False, right_warning_lamp=False, + left_sound_active=False, right_sound_active=False): + left_state = 2 if left_blindspot and left_escalated else (1 if left_blindspot else 0) + right_state = 2 if right_blindspot and right_escalated else (1 if right_blindspot else 0) + left_osm_state = 2 if left_warning_lamp else 1 if left_state == 1 else 0 + right_osm_state = 2 if right_warning_lamp else 1 if right_state == 1 else 0 + desired_fields = { + "BCW_IndSta": 1, + "BCA_OnOffEquip2Sta": 2, + "BCA_Sta": int(drive_gear), + "BCW_LtIndSta": left_state, + "BCW_RtIndSta": right_state, + "BCW_LtSndWrngSta": int(left_sound_active), + "BCW_RtSndWrngSta": int(right_sound_active), + "OSMrrLamp_LtIndSta": left_osm_state, + "OSMrrLamp_RtIndSta": right_osm_state, + } + + return [ + _create_ccnc_adrv_message_with_signals( + packer, CP, CAN, 0x1BA, counter, "BLINDSPOTS_REAR_CORNERS", desired_fields, + ), + # No retained radar input reproduces the stock RCTA target decision across routes. + _create_ccnc_adrv_message(CP.carFingerprint, 0x1E5, CAN.ECAN, counter), + ] + + IONIQ_6_CLUSTER_BLINDSPOT_31A = { "right": ( bytes.fromhex("fa7c10f0f0ffff03898aff0b0a8678ff000000007e0055550000000000000000"), @@ -741,6 +793,30 @@ def create_adrv_messages(packer, CAN, frame): return ret +def create_ccnc_adrv_messages(packer, CP, CAN, frame, enabled, main_cruise_enabled, hud, out, is_metric, + steering_available, steering_active, left_blindspot, right_blindspot, + drive_gear=False, + hba_icon=0, + left_escalated=False, right_escalated=False, + left_warning_lamp=False, right_warning_lamp=False, + left_sound_active=False, right_sound_active=False): + ret = [ + _create_ccnc_adrv_message(CP.carFingerprint, address, CAN.ECAN, frame // period) + for address, period in _CCNC_ADRV_PERIODS[CP.carFingerprint].items() if frame % period == 0 + ] + if frame % 5 == 0: + ret.extend(create_ccnc_angle_long_status_messages( + packer, CP, CAN, frame // 5, enabled, main_cruise_enabled, hud, out, is_metric, + steering_available, steering_active, hba_icon, + )) + ret.extend(create_ccnc_blindspot_status_messages( + packer, CP, CAN, frame // 5, left_blindspot, right_blindspot, left_escalated, right_escalated, + drive_gear, + left_warning_lamp, right_warning_lamp, left_sound_active, right_sound_active, + )) + return ret + + def hkg_can_fd_checksum(address: int, sig, d: bytearray) -> int: crc = 0 for i in range(2, len(d)): @@ -767,10 +843,39 @@ def hkg_can_fd_checksum(address: int, sig, d: bytearray) -> int: # brake, and accelerator bits are updated for the radar heartbeat. _ACCEL_BRAKE_ALT_TEMPLATE = bytes.fromhex("000000020000fcff000000000020000055ff000068000000") _KIA_EV9_ACCEL_BRAKE_ALT_TEMPLATE = bytes.fromhex("00000000ff006f00e80400001201030055ffff0000000000") +# Neutral bodies verified across stock and successful suppression routes. Only +# rolling integrity fields and the decoded state above are changed at runtime. +_CCNC_ADRV_TEMPLATES = { + CAR.KIA_EV9: { + 0x160: bytes.fromhex("0000000100000000fffc0100a8001000"), + 0x1DA: bytes.fromhex("0000002200110000000000000000000000000000000000000000000000000000"), + 0x1EA: bytes.fromhex("000000080000000000000000000000ff000000000000000000000000000f0f00"), + 0x200: bytes.fromhex("00000014801a0000"), + 0x345: bytes.fromhex("0000001500560000"), + 0x161: bytes.fromhex("0000000000000000c0fff0c003000040000000000000000000ff000000000000"), + 0x162: bytes.fromhex("0000002700000000000000000000000000000000000000000000000000000000"), + 0x1BA: bytes.fromhex("00000000000000880200000000000000000100000000000f"), + 0x1E5: bytes.fromhex("00000000000000000000220300000080"), + 0x1E0: bytes.fromhex("00000002000000000000000000000000"), + 0x38C: bytes.fromhex("000000f71f000000000000000000000000000000000000000000000000000000"), + }, +} +_CCNC_ADRV_PERIODS = { + CAR.KIA_EV9: { + 0x160: 2, + 0x1DA: 100, + 0x1EA: 5, + 0x200: 5, + 0x345: 20, + 0x1E0: 5, + 0x38C: 20, + }, +} + def create_accelerator_brake_alt_spoof(bus: int, counter: int, brake_pressed: bool, accelerator_pressed: bool, car_fingerprint=None) -> CanData: - template = _KIA_EV9_ACCEL_BRAKE_ALT_TEMPLATE if str(car_fingerprint) == "KIA_EV9" else _ACCEL_BRAKE_ALT_TEMPLATE + template = _KIA_EV9_ACCEL_BRAKE_ALT_TEMPLATE if car_fingerprint == CAR.KIA_EV9 else _ACCEL_BRAKE_ALT_TEMPLATE d = bytearray(template) d[2] = counter & 0xFF # COUNTER (bit 16, 8-bit) d[4] = (d[4] & ~0x01) | (0x01 if brake_pressed else 0x00) # BRAKE_PRESSED (bit 32) @@ -779,3 +884,111 @@ def create_accelerator_brake_alt_spoof(bus: int, counter: int, brake_pressed: bo d[0] = crc & 0xFF d[1] = (crc >> 8) & 0xFF return CanData(0x100, bytes(d), bus) + + +def _create_ccnc_adrv_message(car_fingerprint, address: int, bus: int, counter: int) -> CanData: + d = bytearray(_CCNC_ADRV_TEMPLATES[car_fingerprint][address]) + d[2] = counter & 0xFF + crc = hkg_can_fd_checksum(address, None, d) + d[0] = crc & 0xFF + d[1] = (crc >> 8) & 0xFF + return CanData(address, bytes(d), bus) + + +def _set_ccnc_message_signals(packer, message_name: str, dat: bytearray, values: dict) -> None: + dbc_msg = packer.dbc.name_to_msg[message_name] + for name, value in values.items(): + sig = dbc_msg.sigs[name] + ival = int(np.floor((value - sig.offset) / sig.factor + 0.5)) + if ival < 0: + ival = (1 << sig.size) + ival + _set_value(dat, sig, ival) + + +def _create_ccnc_adrv_message_with_signals(packer, CP, CAN, address: int, counter: int, + message_name: str, values: dict) -> CanData: + msg = _create_ccnc_adrv_message(CP.carFingerprint, address, CAN.ECAN, counter) + dat = bytearray(msg.dat) + # Update decoded fields in the verified neutral payload. + _set_ccnc_message_signals(packer, message_name, dat, values) + crc = hkg_can_fd_checksum(address, None, dat) + dat[0] = crc & 0xFF + dat[1] = (crc >> 8) & 0xFF + return CanData(address, bytes(dat), CAN.ECAN) + + +def create_ccnc_acc_control(packer, CAN, enabled: bool, accel: float, + stop_request: bool, cruise_standstill: bool, gas_override: bool, set_speed: float, + main_mode_acc: int, lead_distance: float, lead_rel_speed: float, lead_visible: bool, + v_ego: float, jerk_lower: float = 0.7, jerk_upper: float = 0.7): + if not enabled or gas_override or stop_request: + accel = 0.0 + + lead_visible = bool(enabled and lead_visible) + desired_headway = min(max(round(1.625 * max(v_ego, 0.0), 1), 3.5), 204.6) if enabled else 204.6 + values = { + "ACCMode": 0 if not enabled else (2 if gas_override else 1), + "MainMode_ACC": int(bool(main_mode_acc)), + "StopReq": 1 if stop_request and enabled else 0, + "CRUISE_STANDSTILL": 1 if cruise_standstill and stop_request and enabled else 0, + "aReqValue": accel, + "aReqRaw": accel, + "VSetDis": set_speed, + "JerkLowerLimit": jerk_lower if enabled else 1.0, + "JerkUpperLimit": jerk_upper if enabled else 3.0, + "ACC_ObjDist": float(np.clip(lead_distance, 0.0, 204.7)) if lead_visible else 204.6, + "ACC_ObjRelSpd": float(np.clip(lead_rel_speed, -16.4, 34.7)) if lead_visible else 34.6, + "ObjValid": 0 if lead_visible else 1, + "OBJ_STATUS": 2 if enabled and lead_visible else 0, + "NEW_SIGNAL_3": 2 if lead_visible else 0, + "NEW_SIGNAL_15": desired_headway, + "SET_ME_2": 4, + "SET_ME_3": 3, + "SET_ME_TMP_64": 0x64, + # Stock CCNC LKA-long routes use raw 7. The DBC's physical range is stale. + "DISTANCE_SETTING": 7 if enabled else 0, + } + + return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values) + + +def create_ccnc_angle_long_status_messages(packer, CP, CAN, counter: int, enabled: bool = False, + main_cruise_enabled: bool = False, hud=None, out=None, + is_metric: bool = True, steering_available: bool = False, + steering_active: bool = False, hba_icon: int = 0) -> list[CanData]: + cruise_speed = round(out.vCruiseCluster * (1 if is_metric else CV.KPH_TO_MPH)) if out is not None else 0 + display_speed = (40 if is_metric else 25) if cruise_speed > (145 if is_metric else 90) else max(cruise_speed, 0) + main_standby = bool(main_cruise_enabled and not enabled) + values_161 = { + "FCA_ICON": 1, # orange: FCA unavailable + "FCA_ALT_ICON": 0, + "FCA_IMAGE": 0, + "ALERTS_1": 0, + "ALERTS_2": 0, + "ALERTS_3": 0, + "ALERTS_4": 0, + "ALERTS_5": 0, + "SOUNDS_1": 0, + "SOUNDS_2": 0, + "SOUNDS_3": 0, + "SOUNDS_4": 0, + "LFA_ICON": (2 if steering_active else 1) if steering_available else 0, + "HBA_ICON": hba_icon if hba_icon in (1, 2) else 0, + "HDA_ICON": 2 if enabled else 1 if main_standby else 0, + "TARGET": 3 if enabled else 0, + "SETSPEED": 3 if enabled else 1 if main_standby else 0, + "SETSPEED_HUD": 2 if enabled else 1 if main_standby else 0, + "SETSPEED_SPEED": display_speed if enabled or main_standby else 255, + "DISTANCE": hud.leadDistanceBars if enabled and hud is not None else 0, + "DISTANCE_SPACING": 3 if enabled or main_standby else 0, + "DISTANCE_CAR": 2 if enabled else 1 if main_standby else 0, + } + values_162 = {fault: 0 for fault in ( + "FAULT_FSS", "FAULT_FCA", "FAULT_LSS", "FAULT_SLA", "FAULT_HDA", "FAULT_DAS", "FAULT_LFA", "FAULT_DAW", + "FAULT_HBA", "FAULT_ESS", + )} + values_162["VIBRATE"] = 0 + return [ + _create_ccnc_adrv_message_with_signals(packer, CP, CAN, 0x161, counter, "CCNC_0x161", values_161), + _create_ccnc_adrv_message_with_signals(packer, CP, CAN, 0x162, counter, "CCNC_0x162", values_162), + ] diff --git a/opendbc_repo/opendbc/car/hyundai/interface.py b/opendbc_repo/opendbc/car/hyundai/interface.py index 78c16b210..656e7c0df 100644 --- a/opendbc_repo/opendbc/car/hyundai/interface.py +++ b/opendbc_repo/opendbc/car/hyundai/interface.py @@ -48,6 +48,11 @@ def apply_kia_ev6_gt_line_longitudinal_params(ret: structs.CarParams) -> None: ret.vEgoStarting = 0.5 +def apply_kia_ev9_longitudinal_params(ret: structs.CarParams) -> None: + ret.startAccel = 0.2 + ret.vEgoStarting = 0.5 + + def apply_ecu_disable_failure_fallback(CP: structs.CarParams, params) -> None: params.put_bool("EcuDisableFailed", True) CP.safetyConfigs[-1].safetyParam &= ~HyundaiSafetyFlags.LONG.value @@ -104,7 +109,7 @@ class CarInterface(CarInterfaceBase): # Most angle-steering LKA platforms still need stock longitudinal validation. ret.alphaLongitudinalAvailable = False - ret.enableBsm = 0x1ba in fingerprint[CAN.ECAN] + ret.enableBsm = 0x1ba in fingerprint[CAN.ECAN] or candidate == CAR.KIA_EV9 # Carnival HEV can fingerprint with too little E-CAN traffic to see 0xFA. if 0xFA in fingerprint[CAN.ECAN] or candidate == CAR.KIA_CARNIVAL_HEV_4TH_GEN: @@ -233,6 +238,8 @@ class CarInterface(CarInterfaceBase): if ret.openpilotLongitudinalControl: ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.LONG.value + if candidate in CANFD_ANGLE_LONGITUDINAL_CAR and ret.flags & HyundaiFlags.CCNC: + ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.CCNC.value if ret.flags & HyundaiFlags.HYBRID: ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.HYBRID_GAS.value elif ret.flags & HyundaiFlags.EV: @@ -261,6 +268,9 @@ class CarInterface(CarInterfaceBase): if candidate == CAR.HYUNDAI_IONIQ_6: ret.longitudinalActuatorDelay = 0.6 + if candidate == CAR.KIA_EV9 and ret.openpilotLongitudinalControl: + apply_kia_ev9_longitudinal_params(ret) + if candidate == CAR.KIA_NIRO_PHEV_2022: ret.stopAccel = -1.4 ret.stoppingDecelRate = 0.5 diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index 73ef7b2e2..33e358924 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -8,6 +8,9 @@ from opendbc.car import Bus, ButtonType, gen_empty_fingerprint, structs from opendbc.car.structs import CarControl, CarParams from opendbc.car.fw_versions import build_fw_dict, match_fw_to_car from opendbc.car.hyundai.carcontroller import CarController, Ioniq6LongitudinalTuningState, GenesisG90LongitudinalTuningState, \ + EV9LongitudinalTuningState, update_ev9_longitudinal_tuning, \ + BlindspotWarningState, update_blindspot_warning, \ + reset_egmp_longitudinal_tuning, \ update_ioniq_6_longitudinal_tuning, \ update_genesis_g90_longitudinal_tuning, egmp_dynamic_longitudinal_tuning, \ should_reset_ev6_gt_line_longitudinal_tuning, reset_ev6_gt_line_longitudinal_tuning, \ @@ -239,16 +242,21 @@ class TestHyundaiFingerprint: fingerprint[ev9_radar_config.bus][ev9_radar_config.start_addr] = ev9_radar_config.expected_length ev9_car_fw = [CarParams.CarFw(ecu=Ecu.adas, fwVersion=b"", address=0x730, brand="hyundai")] CP = CarInterface.get_params(CAR.KIA_EV9, fingerprint, ev9_car_fw, True, False, False, None) - assert not CP.alphaLongitudinalAvailable - assert not CP.openpilotLongitudinalControl + assert CP.alphaLongitudinalAvailable + assert CP.openpilotLongitudinalControl assert not CP.radarUnavailable assert CP.flags & HyundaiFlags.CANFD_LKA_STEERING_ALT - assert not (CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG) + assert CP.flags & HyundaiFlags.CCNC + assert egmp_dynamic_longitudinal_tuning(CP) + assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.LONG assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CANFD_ANGLE_STEERING - + assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CCNC CP = CarInterface.get_params(CAR.KIA_EV9, fingerprint, [], True, False, False, None) - assert not CP.openpilotLongitudinalControl + assert CP.openpilotLongitudinalControl + CP = CarInterface.get_params(CAR.KIA_EV9, fingerprint, ev9_car_fw, False, False, False, None) + assert not CP.openpilotLongitudinalControl + assert not (CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CCNC) for candidate in HYUNDAI_NON_SCC_CARS: CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, False, False, None) assert bool(CP.flags & HyundaiFlags.NON_SCC) @@ -258,6 +266,7 @@ class TestHyundaiFingerprint: CP = CarInterface.get_params(CAR.KIA_SPORTAGE_HEV_2026, gen_empty_fingerprint(), [], False, False, False, None) assert CP.steerControlType == CarParams.SteerControlType.angle assert CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CANFD_ANGLE_STEERING + assert not (CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.CCNC) fingerprint = gen_empty_fingerprint() cam_can = CanBus(None, fingerprint).CAM @@ -557,6 +566,20 @@ class TestHyundaiFingerprint: events = car_state.create_cruise_button_events(Buttons.CANCEL, Buttons.NONE) assert [(be.type, be.pressed) for be in events] == [(ButtonType.cancel, True)] + def test_ccnc_angle_long_main_cruise_toggle(self): + car_state = SimpleNamespace(main_cruise_on=False) + ret = SimpleNamespace( + cruiseState=SimpleNamespace(available=True), + buttonEvents=[structs.CarState.ButtonEvent(pressed=True, type=ButtonType.mainCruise)], + ) + assert CarState.update_main_cruise(car_state, ret) + + ret.buttonEvents = [structs.CarState.ButtonEvent(pressed=False, type=ButtonType.mainCruise)] + assert CarState.update_main_cruise(car_state, ret) + + ret.buttonEvents = [structs.CarState.ButtonEvent(pressed=True, type=ButtonType.mainCruise)] + assert not CarState.update_main_cruise(car_state, ret) + def test_palisade_2023_cancel_release_enables_from_standby(self): toggles = get_test_toggles() CP = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, gen_empty_fingerprint(), [], True, False, False, toggles) @@ -1228,6 +1251,52 @@ class TestHyundaiFingerprint: assert not should_use_ev6_gt_line_stop_direct_tracking(False, True, 1.8, -2.05, -1.29) assert not should_use_ev6_gt_line_stop_direct_tracking(True, True, 1.8, -1.0, -1.29) + def test_ev9_longitudinal_tuning_matches_stock_stop_hold_release_timing(self): + state = update_ev9_longitudinal_tuning(EV9LongitudinalTuningState(), True, True, 1.0) + assert not state.stop_request + + state = update_ev9_longitudinal_tuning(state, True, True, 0.4) + assert state.stop_request + assert not state.cruise_standstill + + for _ in range(178): + state = update_ev9_longitudinal_tuning(state, True, True, 0.0) + assert state.cruise_standstill + + for _ in range(6): + state = update_ev9_longitudinal_tuning(state, True, False, 0.0) + assert state.stop_request + assert not state.cruise_standstill + assert update_ev9_longitudinal_tuning(state, True, False, 0.0) == EV9LongitudinalTuningState() + + def test_ev9_blindspot_warning_matches_stock_envelope(self): + state = BlindspotWarningState() + outputs = [update_blindspot_warning(state, True, True) for _ in range(40)] + + assert [output.sound_active for output in outputs[:36]] == [True] * 36 + assert not any(output.sound_active for output in outputs[36:]) + assert [output.mirror_lamp_active for output in outputs[:20]] == [True] * 16 + [False] * 4 + assert [output.mirror_lamp_active for output in outputs[20:]] == [True] * 16 + [False] * 4 + + assert not update_blindspot_warning(state, False, True).sound_active + assert update_blindspot_warning(state, True, True).sound_active is False + update_blindspot_warning(state, False, False) + assert update_blindspot_warning(state, True, True).sound_active + + def test_ev9_longitudinal_tuning_resets_accel_before_stop_release(self): + accel_state = Ioniq6LongitudinalTuningState() + for _ in range(4): + accel_state = update_ioniq_6_longitudinal_tuning( + accel_state, accel_cmd=0.2, v_ego=0.0, a_ego=0.0, + long_control_state=LongCtrlState.starting, long_active=True, + low_speed_stop_brake_cap=True, + ) + assert accel_state.actual_accel == pytest.approx(0.75) + + accel_state = reset_egmp_longitudinal_tuning(accel_state) + assert accel_state.actual_accel == 0.0 + assert not accel_state.launch_active + def test_genesis_g90_longitudinal_tuning_softens_final_stop_hold(self): state = GenesisG90LongitudinalTuningState() @@ -2251,6 +2320,151 @@ class TestHyundaiFingerprint: assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_LtIndSta"] == 2 assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["OSMrrLamp_LtIndSta"] == 2 + def test_ev9_blindspot_status_uses_stock_warning_fields(self): + CP = CarParams.new_message() + CP.carFingerprint = CAR.KIA_EV9 + CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CANFD_LKA_STEERING | HyundaiFlags.CANFD_LKA_STEERING_ALT) + + packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) + can_bus = CanBus(CP) + parser = CANParser(DBC[CP.carFingerprint][Bus.pt], + [("BLINDSPOTS_REAR_CORNERS", 0), ("BLINDSPOTS_FRONT_CORNER_1", 0)], can_bus.ECAN) + + msgs = hyundaicanfd.create_ccnc_blindspot_status_messages( + packer, CP, can_bus, 7, left_blindspot=True, left_escalated=True, + drive_gear=True, left_warning_lamp=True, left_sound_active=True, + ) + parser.update([(1, msgs)]) + + assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_LtIndSta"] == 2 + assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["OSMrrLamp_LtIndSta"] == 2 + assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_LtSndWrngSta"] == 1 + assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_Sta"] == 0 + assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["FL_INDICATOR"] == 0 + assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["BCW_IndSta"] == 1 + assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["BCA_OnOffEquip2Sta"] == 2 + assert parser.vl["BLINDSPOTS_REAR_CORNERS"]["BCA_Sta"] == 1 + assert parser.vl["BLINDSPOTS_FRONT_CORNER_1"]["NEW_SIGNAL_7"] == 0 + + def test_ev9_ccnc_status_clears_faults_and_tracks_control_state(self): + CP = CarParams.new_message() + CP.carFingerprint = CAR.KIA_EV9 + CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CANFD_LKA_STEERING | HyundaiFlags.CANFD_LKA_STEERING_ALT) + + packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) + can_bus = CanBus(CP) + parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("CCNC_0x161", 0), ("CCNC_0x162", 0)], can_bus.ECAN) + + parser.update([(1, hyundaicanfd.create_ccnc_angle_long_status_messages(packer, CP, can_bus, 12, hba_icon=2))]) + + assert parser.vl["CCNC_0x161"]["FCA_ICON"] == 1 + assert parser.vl["CCNC_0x161"]["FCA_ALT_ICON"] == 0 + assert parser.vl["CCNC_0x161"]["FCA_IMAGE"] == 0 + assert parser.vl["CCNC_0x161"]["HBA_ICON"] == 2 + assert all(parser.vl["CCNC_0x161"][sound] == 0 for sound in ("SOUNDS_1", "SOUNDS_2", "SOUNDS_3", "SOUNDS_4")) + assert parser.vl["CCNC_0x162"]["VIBRATE"] == 0 + assert all(parser.vl["CCNC_0x162"][fault] == 0 for fault in ( + "FAULT_FSS", "FAULT_FCA", "FAULT_LSS", "FAULT_SLA", "FAULT_HDA", "FAULT_DAS", "FAULT_LFA", "FAULT_DAW", + "FAULT_HBA", "FAULT_ESS", + )) + + parser.update([(1, hyundaicanfd.create_ccnc_angle_long_status_messages( + packer, CP, can_bus, 13, main_cruise_enabled=True, steering_available=True, steering_active=False, + ))]) + assert parser.vl["CCNC_0x161"]["HDA_ICON"] == 1 + assert parser.vl["CCNC_0x161"]["LFA_ICON"] == 1 + + parser.update([(1, hyundaicanfd.create_ccnc_angle_long_status_messages( + packer, CP, can_bus, 14, enabled=True, main_cruise_enabled=True, + steering_available=True, steering_active=True, + ))]) + assert parser.vl["CCNC_0x161"]["HDA_ICON"] == 2 + assert parser.vl["CCNC_0x161"]["LFA_ICON"] == 2 + + parser.update([(1, hyundaicanfd.create_ccnc_angle_long_status_messages( + packer, CP, can_bus, 15, enabled=True, main_cruise_enabled=True, + steering_available=True, steering_active=False, + ))]) + assert parser.vl["CCNC_0x161"]["HDA_ICON"] == 2 + assert parser.vl["CCNC_0x161"]["LFA_ICON"] == 1 + + def test_ev9_ccnc_acc_control_uses_packer_counter(self): + CP = CarParams.new_message() + CP.carFingerprint = CAR.KIA_EV9 + CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.CCNC | HyundaiFlags.CANFD_ANGLE_STEERING | + HyundaiFlags.CANFD_LKA_STEERING | HyundaiFlags.CANFD_LKA_STEERING_ALT) + + packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) + can_bus = CanBus(CP) + parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("SCC_CONTROL", 0)], can_bus.ECAN) + + messages = [ + hyundaicanfd.create_ccnc_acc_control( + packer, can_bus, True, 0.2, False, False, False, 50.0, + 1, 27.5, -1.2, True, 10.0, + ) + for _ in range(2) + ] + parser.update([(1, [messages[0]])]) + parser.update([(2, [messages[1]])]) + + assert parser.can_valid + assert messages[0][1][2] == 0 + assert messages[1][1][2] == 1 + assert parser.vl["SCC_CONTROL"]["ACC_ObjDist"] == pytest.approx(27.5) + assert parser.vl["SCC_CONTROL"]["ACC_ObjRelSpd"] == pytest.approx(-1.2) + + @pytest.mark.parametrize(("steering_pressed", "steering_active"), ((False, True), (True, False))) + def test_ev9_ccnc_steering_icon_tracks_controller_authority(self, monkeypatch, steering_pressed, steering_active): + CP = CarParams.new_message() + CP.carFingerprint = CAR.KIA_EV9 + CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CCNC | + HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CANFD_LKA_STEERING | + HyundaiFlags.CANFD_LKA_STEERING_ALT) + CP.openpilotLongitudinalControl = True + + controller = CarController(DBC[CP.carFingerprint], CP) + controller.frame = 5 + captured = {} + + def capture_ccnc_adrv_messages(*args, **kwargs): + captured["steering_available"] = args[9] + captured["steering_active"] = args[10] + return [] + + monkeypatch.setattr(hyundaicanfd, "create_ccnc_adrv_messages", capture_ccnc_adrv_messages) + lfa_block_msg = {f"BYTE{i}": 0 for i in range(3, 32) if i != 7} + lfa_block_msg["COUNTER"] = 0 + cc = SimpleNamespace( + enabled=True, + latActive=True, + actuators=SimpleNamespace(longControlState=LongCtrlState.pid, accel=0.0), + leftBlinker=False, + rightBlinker=False, + hudControl=SimpleNamespace(), + ) + cs = SimpleNamespace( + angle_steering_fault=False, + angle_steering_angle=0.0, + hba_icon=0, + is_metric=True, + left_blindspot_from_radar=False, + right_blindspot_from_radar=False, + lfa_block_msg=lfa_block_msg, + out=SimpleNamespace( + brakePressed=False, + cruiseState=SimpleNamespace(available=True), + gasPressed=False, + gearShifter=structs.CarState.GearShifter.drive, + steeringPressed=steering_pressed, + ), + ) + + controller.create_canfd_msgs(0, True, 0.44, 0.0, 0.0, 0.0, False, cc.hudControl, cs, cc, + get_test_toggles(), lka_icon=2, lfa_icon=2) + + assert captured == {"steering_available": True, "steering_active": steering_active} + def test_ioniq_6_blindspot_radar_state_decode(self): assert decode_ioniq_6_blindspot_radar_state(0x02) == (False, False) assert decode_ioniq_6_blindspot_radar_state(0x0A) == (False, True) diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py index 3fb5c97e0..480980956 100644 --- a/opendbc_repo/opendbc/car/hyundai/values.py +++ b/opendbc_repo/opendbc/car/hyundai/values.py @@ -783,7 +783,7 @@ class CAR(Platforms): HyundaiCarDocs("Kia EV9 2025-26", car_parts=CarParts.common([CarHarness.hyundai_r])) ], CarSpecs(mass=2664, wheelbase=3.1, steerRatio=16), - flags=HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING, + flags=HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING | HyundaiFlags.CCNC, radar_dbc=HYUNDAI_MRR35_RADAR_DBC, ) KIA_CARNIVAL_4TH_GEN = HyundaiCanFDPlatformConfig( @@ -1172,10 +1172,15 @@ CANFD_CAR = CAR.with_flags(HyundaiFlags.CANFD) CANFD_RADAR_SCC_CAR = CAR.with_flags(HyundaiFlags.RADAR_SCC) # TODO: merge with UNSUPPORTED_LONGITUDINAL_CAR # CAN-FD cars with ADAS ECUs that work with the communication-control path. -CANFD_SECURITYACCESS_CAR = {CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_6, CAR.HYUNDAI_KONA_EV_2ND_GEN} +CANFD_SECURITYACCESS_CAR = { + CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_6, CAR.HYUNDAI_KONA_EV_2ND_GEN, CAR.KIA_EV9, +} CANFD_UNSUPPORTED_LONGITUDINAL_CAR = CAR.with_flags(HyundaiFlags.CANFD_NO_RADAR_DISABLE) - CANFD_SECURITYACCESS_CAR # TODO: merge with UNSUPPORTED_LONGITUDINAL_CAR -CANFD_ANGLE_LONGITUDINAL_CAR = set() -CANFD_RADAR_LIVE_LONGITUDINAL_CAR = {CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV6, CAR.GENESIS_GV60_EV_1ST_GEN} +CANFD_ANGLE_LONGITUDINAL_CAR = {CAR.KIA_EV9} +CANFD_CORNER_RADAR_BSM_CAR = {CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV9} +CANFD_RADAR_LIVE_LONGITUDINAL_CAR = { + CAR.HYUNDAI_IONIQ_5, CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV6, CAR.KIA_EV9, CAR.GENESIS_GV60_EV_1ST_GEN, +} RADAR_LIVE_LONGITUDINAL_CAR = CANFD_RADAR_LIVE_LONGITUDINAL_CAR | { CAR.HYUNDAI_IONIQ, CAR.HYUNDAI_KONA_EV_2022,