Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions opendbc/car/hyundai/carstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ def __init__(self, CP):
self.is_metric = False
self.buttons_counter = 0

# With openpilot longitudinal, stock SCC no longer publishes its main/set state.
# Mirror the steering-wheel state so button behavior remains stock-like.
self.main_enabled = False
self.main_enable_used = False
self.cruise_speed_set = False
self.cruise_available = False
self.pause_resume_is_resume = False

self.cruise_info = {}

# On some cars, CLU15->CF_Clu_VehicleSpeed can oscillate faster than the dash updates. Sample at 5 Hz
Expand All @@ -63,6 +71,51 @@ def __init__(self, CP):

self.params = CarControllerParams(CP)

@property
def has_pause_resume_button(self) -> bool:
return bool(self.CP.flags & HyundaiFlags.PAUSE_RESUME)

def update_cruise_button_state(self, ret: structs.CarState) -> None:
if self.CP.pcmCruise:
return

for event in ret.buttonEvents:
if event.type == ButtonType.mainCruise and event.pressed:
self.main_enabled = not self.main_enabled

if self.has_pause_resume_button and event.type == ButtonType.cancel:
# Latch the meaning for the whole press. Otherwise the release could be
# mistaken for resume after the press has already disengaged controls.
if event.pressed:
self.pause_resume_is_resume = not self.out.cruiseState.enabled
if self.pause_resume_is_resume:
event.type = ButtonType.accelCruise
if not event.pressed:
self.pause_resume_is_resume = False

ret.cruiseState.available = ret.cruiseState.available and self.main_enabled
self.cruise_available = ret.cruiseState.available

def update_button_enable(self, button_events: list[structs.CarState.ButtonEvent]) -> bool:
if self.CP.pcmCruise:
return False

for event in button_events:
if event.type == ButtonType.mainCruise and event.pressed:
if self.has_pause_resume_button and self.cruise_available and not self.main_enable_used:
self.main_enable_used = True
self.cruise_speed_set = True
return True
elif event.type == ButtonType.decelCruise and not event.pressed:
if self.cruise_available:
self.cruise_speed_set = True
return True
elif event.type == ButtonType.accelCruise and not event.pressed:
if self.cruise_available and self.cruise_speed_set:
return True

return False

def recent_button_interaction(self) -> bool:
# On some newer model years, the CANCEL button acts as a pause/resume button based on the PCM state
# To avoid re-engaging when openpilot cancels, check user engagement intention via buttons
Expand Down Expand Up @@ -191,6 +244,8 @@ def update(self, can_parsers) -> structs.CarState:
*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})]

self.update_cruise_button_state(ret)

ret.blockPcmEnable = not self.recent_button_interaction()

# low speed steer alert hysteresis logic (only for cars with steer cut off above 10 m/s)
Expand Down Expand Up @@ -289,6 +344,8 @@ def update_canfd(self, can_parsers) -> structs.CarState:
*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})]

self.update_cruise_button_state(ret)

ret.blockPcmEnable = not self.recent_button_interaction()

return ret
Expand Down
3 changes: 3 additions & 0 deletions opendbc/car/hyundai/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo
# see https://github.com/commaai/opendbc/pull/1137/
ret.dashcamOnly = True

if ret.flags & HyundaiFlags.PAUSE_RESUME:
ret.safetyConfigs[-1].safetyParam |= HyundaiSafetyFlags.PAUSE_RESUME.value

# Common longitudinal control setup

ret.radarUnavailable = RADAR_START_ADDR not in fingerprint[1] or Bus.radar not in DBC[ret.carFingerprint]
Expand Down
85 changes: 84 additions & 1 deletion opendbc/car/hyundai/tests/test_hyundai.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import unittest

from opendbc.car import gen_empty_fingerprint
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.structs import CarParams
from opendbc.car.fw_versions import build_fw_dict
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.carstate import CarState
from opendbc.car.hyundai.hyundaicanfd import CanBus
from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR
from opendbc.car.hyundai.values import CAR, DATE_FW_ECUS, FW_QUERY_CONFIG, CANFD_FUZZY_WHITELIST, \
Expand All @@ -13,6 +14,7 @@
from opendbc.testing import fuzzy_test

Ecu = CarParams.Ecu
ButtonType = structs.CarState.ButtonEvent.Type

# Some platforms have date codes in a different format we don't yet parse (or are missing).
# For now, assert list of expected missing date cars
Expand Down Expand Up @@ -256,3 +258,84 @@ def test_fuzzy_excluded_platforms(self):
platforms_with_shared_codes.add(platform)

assert platforms_with_shared_codes == excluded_platforms


class TestHyundaiButtonLogic(unittest.TestCase):
@staticmethod
def make_state(candidate):
CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], True, False, False)
return CP, CarState(CP)

@staticmethod
def update_buttons(CS, events, cruise_enabled=False):
CS.out.cruiseState.enabled = cruise_enabled
ret = structs.CarState()
ret.cruiseState.available = True
ret.buttonEvents = events
CS.update_cruise_button_state(ret)
return ret, CS.update_button_enable(ret.buttonEvents)

@staticmethod
def event(button_type, pressed):
return structs.CarState.ButtonEvent(type=button_type, pressed=pressed)

def test_main_gates_cruise_without_clearing_set_speed(self):
_, CS = self.make_state(CAR.HYUNDAI_SONATA)

ret, enable = self.update_buttons(CS, [self.event(ButtonType.decelCruise, False)])
self.assertFalse(ret.cruiseState.available)
self.assertFalse(enable)

ret, enable = self.update_buttons(CS, [self.event(ButtonType.mainCruise, True)])
self.assertTrue(ret.cruiseState.available)
self.assertFalse(enable)
_, enable = self.update_buttons(CS, [self.event(ButtonType.accelCruise, False)])
self.assertFalse(enable)

_, enable = self.update_buttons(CS, [self.event(ButtonType.decelCruise, False)])
self.assertTrue(enable)
self.update_buttons(CS, [self.event(ButtonType.mainCruise, True)])
self.update_buttons(CS, [self.event(ButtonType.mainCruise, False)])
ret, enable = self.update_buttons(CS, [self.event(ButtonType.mainCruise, True)])
self.assertTrue(ret.cruiseState.available)
self.assertFalse(enable)
_, enable = self.update_buttons(CS, [self.event(ButtonType.accelCruise, False)])
self.assertTrue(enable)

def test_pause_resume_latches_press_meaning(self):
_, CS = self.make_state(CAR.HYUNDAI_KONA_EV_2022)

_, enable = self.update_buttons(CS, [self.event(ButtonType.mainCruise, True)])
self.assertTrue(enable)

ret, enable = self.update_buttons(CS, [self.event(ButtonType.cancel, True)], cruise_enabled=True)
self.assertEqual(ret.buttonEvents[0].type, ButtonType.cancel)
self.assertFalse(enable)
ret, enable = self.update_buttons(CS, [self.event(ButtonType.cancel, False)], cruise_enabled=False)
self.assertEqual(ret.buttonEvents[0].type, ButtonType.cancel)
self.assertFalse(enable)

ret, enable = self.update_buttons(CS, [self.event(ButtonType.cancel, True)], cruise_enabled=False)
self.assertEqual(ret.buttonEvents[0].type, ButtonType.accelCruise)
self.assertFalse(enable)
ret, enable = self.update_buttons(CS, [self.event(ButtonType.cancel, False)], cruise_enabled=False)
self.assertEqual(ret.buttonEvents[0].type, ButtonType.accelCruise)
self.assertTrue(enable)

def test_pause_resume_platform_flags(self):
expected = {
CAR.HYUNDAI_KONA_EV_2022,
CAR.HYUNDAI_SANTA_FE_2022,
CAR.HYUNDAI_SANTA_FE_HEV_2022,
CAR.HYUNDAI_SANTA_FE_PHEV_2022,
CAR.HYUNDAI_IONIQ_5,
CAR.HYUNDAI_SANTA_CRUZ_1ST_GEN,
CAR.KIA_EV6,
CAR.GENESIS_GV70_1ST_GEN,
}
self.assertEqual(cars_with(HyundaiFlags.PAUSE_RESUME), expected)

for candidate in expected:
CP, CS = self.make_state(candidate)
self.assertTrue(CS.has_pause_resume_button)
self.assertTrue(CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.PAUSE_RESUME)
19 changes: 12 additions & 7 deletions opendbc/car/hyundai/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class HyundaiSafetyFlags(IntFlag):
CANFD_LKA_STEER_MSG_ALT = 128
FCEV_GAS = 256
ALT_LIMITS_2 = 512
PAUSE_RESUME = 1024


# Hyundai/Kia/Genesis SCC (Smart Cruise Control) and steering architecture:
Expand Down Expand Up @@ -147,6 +148,9 @@ class HyundaiFlags(IntFlag):

ALT_LIMITS_2 = 2 ** 26

# The button encoded as CANCEL is labeled pause/resume and toggles cruise.
PAUSE_RESUME = 2 ** 27


@dataclass
class HyundaiCarDocs(CarDocs):
Expand Down Expand Up @@ -273,7 +277,7 @@ class CAR(Platforms):
HYUNDAI_KONA_EV_2022 = HyundaiPlatformConfig(
[HyundaiCarDocs("Hyundai Kona Electric 2022-23", car_parts=CarParts.common([CarHarness.hyundai_o]))],
CarSpecs(mass=1743, wheelbase=2.6, steerRatio=13.42, tireStiffnessFactor=0.385),
flags=HyundaiFlags.CAMERA_SCC | HyundaiFlags.EV | HyundaiFlags.ALT_LIMITS,
flags=HyundaiFlags.CAMERA_SCC | HyundaiFlags.EV | HyundaiFlags.ALT_LIMITS | HyundaiFlags.PAUSE_RESUME,
)
HYUNDAI_KONA_EV_2ND_GEN = HyundaiCanFDPlatformConfig(
[HyundaiCarDocs("Hyundai Kona Electric (with HDA II, Korea only) 2023", video="https://www.youtube.com/watch?v=U2fOCmcQ8hw",
Expand Down Expand Up @@ -301,17 +305,17 @@ class CAR(Platforms):
[HyundaiCarDocs("Hyundai Santa Fe 2021-23", "All", video="https://youtu.be/VnHzSTygTS4",
car_parts=CarParts.common([CarHarness.hyundai_l]))],
HYUNDAI_SANTA_FE.specs,
flags=HyundaiFlags.CHECKSUM_CRC8,
flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.PAUSE_RESUME,
)
HYUNDAI_SANTA_FE_HEV_2022 = HyundaiPlatformConfig(
[HyundaiCarDocs("Hyundai Santa Fe Hybrid 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_l]))],
HYUNDAI_SANTA_FE.specs,
flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID,
flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID | HyundaiFlags.PAUSE_RESUME,
)
HYUNDAI_SANTA_FE_PHEV_2022 = HyundaiPlatformConfig(
[HyundaiCarDocs("Hyundai Santa Fe Plug-in Hybrid 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_l]))],
HYUNDAI_SANTA_FE.specs,
flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID,
flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID | HyundaiFlags.PAUSE_RESUME,
)
HYUNDAI_SONATA = HyundaiPlatformConfig(
[HyundaiCarDocs("Hyundai Sonata 2020-23", "All", video="https://www.youtube.com/watch?v=ix63r9kE3Fw",
Expand Down Expand Up @@ -361,7 +365,7 @@ class CAR(Platforms):
HyundaiCarDocs("Hyundai Ioniq 5 (with HDA II) 2022-24", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_q])),
],
CarSpecs(mass=1948, wheelbase=2.97, steerRatio=14.26, tireStiffnessFactor=0.65),
flags=HyundaiFlags.EV,
flags=HyundaiFlags.EV | HyundaiFlags.PAUSE_RESUME,
)
HYUNDAI_IONIQ_6 = HyundaiCanFDPlatformConfig(
[
Expand All @@ -384,6 +388,7 @@ class CAR(Platforms):
[HyundaiCarDocs("Hyundai Santa Cruz 2022-24", car_parts=CarParts.common([CarHarness.hyundai_n]))],
# weight from Limited trim - the only supported trim, steering ratio according to Hyundai News https://www.hyundainews.com/assets/documents/original/48035-2022SantaCruzProductGuideSpecsv2081521.pdf
CarSpecs(mass=1870, wheelbase=3, steerRatio=14.2),
flags=HyundaiFlags.PAUSE_RESUME,
)
HYUNDAI_CUSTIN_1ST_GEN = HyundaiPlatformConfig(
[HyundaiCarDocs("Hyundai Custin 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_k]))],
Expand Down Expand Up @@ -543,7 +548,7 @@ class CAR(Platforms):
HyundaiCarDocs("Kia EV6 (with HDA II) 2022-24", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_p]))
],
CarSpecs(mass=2055, wheelbase=2.9, steerRatio=16, tireStiffnessFactor=0.65),
flags=HyundaiFlags.EV,
flags=HyundaiFlags.EV | HyundaiFlags.PAUSE_RESUME,
)
KIA_CARNIVAL_4TH_GEN = HyundaiCanFDPlatformConfig(
[
Expand Down Expand Up @@ -585,7 +590,7 @@ class CAR(Platforms):
HyundaiCarDocs("Genesis GV70 (3.5T Trim, without HDA II) 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_m])),
],
CarSpecs(mass=1950, wheelbase=2.87, steerRatio=14.6),
flags=HyundaiFlags.CANFD_RADAR_SCC,
flags=HyundaiFlags.CANFD_RADAR_SCC | HyundaiFlags.PAUSE_RESUME,
)
GENESIS_GV70_ELECTRIFIED_1ST_GEN = HyundaiCanFDPlatformConfig(
[
Expand Down
46 changes: 40 additions & 6 deletions opendbc/safety/modes/hyundai_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ bool hyundai_fcev_gas_signal = false;
extern bool hyundai_alt_limits_2;
bool hyundai_alt_limits_2 = false;

extern bool hyundai_pause_resume;
bool hyundai_pause_resume = false;

static bool hyundai_main_button_prev;
static bool hyundai_main_enabled;
static bool hyundai_main_enable_used;
static bool hyundai_cruise_speed_set;

static uint8_t hyundai_last_button_interaction; // button messages since the user pressed an enable button

void hyundai_common_init(uint16_t param) {
Expand All @@ -52,6 +60,7 @@ void hyundai_common_init(uint16_t param) {
const uint16_t HYUNDAI_PARAM_ALT_LIMITS = 64; // TODO: shift this down with the rest of the common flags
const uint16_t HYUNDAI_PARAM_FCEV_GAS = 256;
const uint16_t HYUNDAI_PARAM_ALT_LIMITS_2 = 512;
const uint16_t HYUNDAI_PARAM_PAUSE_RESUME = 1024;

hyundai_ev_gas_signal = GET_FLAG(param, HYUNDAI_PARAM_EV_GAS);
hyundai_hybrid_gas_signal = !hyundai_ev_gas_signal && GET_FLAG(param, HYUNDAI_PARAM_HYBRID_GAS);
Expand All @@ -60,8 +69,13 @@ void hyundai_common_init(uint16_t param) {
hyundai_alt_limits = GET_FLAG(param, HYUNDAI_PARAM_ALT_LIMITS);
hyundai_fcev_gas_signal = GET_FLAG(param, HYUNDAI_PARAM_FCEV_GAS);
hyundai_alt_limits_2 = GET_FLAG(param, HYUNDAI_PARAM_ALT_LIMITS_2);
hyundai_pause_resume = GET_FLAG(param, HYUNDAI_PARAM_PAUSE_RESUME);

hyundai_last_button_interaction = HYUNDAI_PREV_BUTTON_SAMPLES;
hyundai_main_button_prev = false;
hyundai_main_enabled = false;
hyundai_main_enable_used = false;
hyundai_cruise_speed_set = false;

#ifdef ALLOW_DEBUG
const uint16_t HYUNDAI_PARAM_LONGITUDINAL = 4;
Expand Down Expand Up @@ -96,19 +110,39 @@ void hyundai_common_cruise_buttons_check(const int cruise_button, const bool mai
}

if (hyundai_longitudinal) {
// enter controls on falling edge of resume or set
bool set = (cruise_button != HYUNDAI_BTN_SET) && (cruise_button_prev == HYUNDAI_BTN_SET);
bool res = (cruise_button != HYUNDAI_BTN_RESUME) && (cruise_button_prev == HYUNDAI_BTN_RESUME);
if (set || res) {
const bool main_rising = main_button && !hyundai_main_button_prev;
const bool set_falling = (cruise_button != HYUNDAI_BTN_SET) && (cruise_button_prev == HYUNDAI_BTN_SET);
const bool resume_falling = (cruise_button != HYUNDAI_BTN_RESUME) && (cruise_button_prev == HYUNDAI_BTN_RESUME);
const bool pause_resume_rising = (cruise_button == HYUNDAI_BTN_CANCEL) && (cruise_button_prev != HYUNDAI_BTN_CANCEL);

if (main_rising) {
hyundai_main_enabled = !hyundai_main_enabled;
if (!hyundai_main_enabled) {
controls_allowed = false;
} else if (hyundai_pause_resume && !hyundai_main_enable_used) {
controls_allowed = true;
hyundai_cruise_speed_set = true;
hyundai_main_enable_used = true;
}
}

if (set_falling && hyundai_main_enabled) {
hyundai_cruise_speed_set = true;
controls_allowed = true;
}

if (resume_falling && !hyundai_pause_resume && hyundai_main_enabled && hyundai_cruise_speed_set) {
controls_allowed = true;
}

// exit controls on cancel press
if (cruise_button == HYUNDAI_BTN_CANCEL) {
if (pause_resume_rising && hyundai_pause_resume && hyundai_main_enabled && hyundai_cruise_speed_set) {
controls_allowed = !controls_allowed;
} else if ((cruise_button == HYUNDAI_BTN_CANCEL) && !hyundai_pause_resume) {
controls_allowed = false;
}

cruise_button_prev = cruise_button;
hyundai_main_button_prev = main_button;
}
}

Expand Down
3 changes: 2 additions & 1 deletion opendbc/safety/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,8 @@ def test_tx_hook_on_wrong_safety_mode(self):
continue
if attr.startswith('TestHyundaiCanfd') and current_test.startswith('TestHyundaiCanfd'):
continue
if {attr, current_test}.issubset({'TestHyundaiLongitudinalSafety', 'TestHyundaiLongitudinalSafetyCameraSCC', 'TestHyundaiSafetyFCEVLong'}):
if {attr, current_test}.issubset({'TestHyundaiLongitudinalSafety', 'TestHyundaiLongitudinalSafetyCameraSCC',
'TestHyundaiLongitudinalSafetyPauseResume', 'TestHyundaiSafetyFCEVLong'}):
continue
volkswagen_shared = ('TestVolkswagenMqb', 'TestVolkswagenMlb', 'TestVolkswagenMeb')
if attr.startswith(volkswagen_shared) and current_test.startswith(volkswagen_shared):
Expand Down
Loading
Loading