From 011cd410a0beec83c1e69b17c05cd70aa9886886 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jul 2026 15:11:27 -0700 Subject: [PATCH 1/7] try MADS --- opendbc/safety/__init__.py | 1 + opendbc/safety/declarations.h | 4 +++ opendbc/safety/lateral.h | 26 +++++++------- opendbc/safety/modes/hyundai.h | 17 +++++++++ opendbc/safety/safety.h | 5 +++ .../safety/tests/libsafety/libsafety_py.py | 2 ++ opendbc/safety/tests/libsafety/safety.c | 8 +++++ opendbc/safety/tests/test_hyundai.py | 36 ++++++++++++++++++- 8 files changed, 85 insertions(+), 14 deletions(-) diff --git a/opendbc/safety/__init__.py b/opendbc/safety/__init__.py index 5d4c2464d4c..53426586c42 100644 --- a/opendbc/safety/__init__.py +++ b/opendbc/safety/__init__.py @@ -8,3 +8,4 @@ class ALTERNATIVE_EXPERIENCE: DISABLE_STOCK_AEB = 2 RAISE_LONGITUDINAL_LIMITS_TO_ISO_MAX = 8 ALLOW_AEB = 16 + ENABLE_MADS = 1024 diff --git a/opendbc/safety/declarations.h b/opendbc/safety/declarations.h index d2b72a99b7f..061cfdc881d 100644 --- a/opendbc/safety/declarations.h +++ b/opendbc/safety/declarations.h @@ -67,6 +67,9 @@ extern const int MAX_WRONG_COUNTERS; #define VEHICLE_SPEED_FACTOR 1000.0 #define MAX_RT_INTERVAL 250000U +// MADS keeps lateral authorization separate from longitudinal controls_allowed. +#define ALT_EXP_ENABLE_MADS 1024 + // Conversions #define KPH_TO_MS (1.0 / 3.6) @@ -322,6 +325,7 @@ extern CurvatureSteeringState curvature_state; #define ALT_EXP_ALLOW_AEB 16 extern int alternative_experience; +extern bool controls_allowed_lateral; // time since safety mode has been changed extern uint32_t safety_mode_cnt; diff --git a/opendbc/safety/lateral.h b/opendbc/safety/lateral.h index 38b1d3f41eb..5f760286800 100644 --- a/opendbc/safety/lateral.h +++ b/opendbc/safety/lateral.h @@ -61,7 +61,7 @@ bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueStee bool violation = false; uint32_t ts = microsecond_timer_get(); - if (controls_allowed) { + if (controls_allowed || controls_allowed_lateral) { // Some safety models support variable torque limit based on vehicle speed int max_torque = limits.max_torque; if (limits.dynamic_max_torque) { @@ -96,7 +96,7 @@ bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueStee } // no torque if controls is not allowed - if (!controls_allowed && (desired_torque != 0)) { + if (!(controls_allowed || controls_allowed_lateral) && (desired_torque != 0)) { violation = true; } @@ -138,7 +138,7 @@ bool steer_torque_cmd_checks(int desired_torque, int steer_req, const TorqueStee } // reset to 0 if either controls is not allowed or there's a violation - if (violation || !controls_allowed) { + if (violation || !(controls_allowed || controls_allowed_lateral)) { valid_steer_req_count = 0; invalid_steer_req_count = 0; desired_torque_last = 0; @@ -198,7 +198,7 @@ static bool rt_curvature_rate_limit_check(CurvatureSteeringLimits limits) { bool steer_angle_cmd_checks(int desired_angle, bool steer_control_enabled, const AngleSteeringLimits limits) { bool violation = false; - if (controls_allowed && steer_control_enabled) { + if ((controls_allowed || controls_allowed_lateral) && steer_control_enabled) { // convert floating point angle rate limits to integers in the scale of the desired angle on CAN, // add 1 to not false trigger the violation. also fudge the speed by 1 m/s so rate limits are // always slightly above openpilot's in case we read an updated speed in between angle commands @@ -225,12 +225,12 @@ bool steer_angle_cmd_checks(int desired_angle, bool steer_control_enabled, const } // No angle control allowed when controls are not allowed - if (!controls_allowed) { + if (!(controls_allowed || controls_allowed_lateral)) { violation |= steer_control_enabled; } // reset to current angle if either controls is not allowed or there's a violation - if (violation || !controls_allowed) { + if (violation || !(controls_allowed || controls_allowed_lateral)) { desired_angle_last = SAFETY_CLAMP(angle_meas.values[0], -limits.max_angle, limits.max_angle); } @@ -249,7 +249,7 @@ bool steer_curvature_cmd_checks(int desired_curvature, int steer_power, bool ste speed_mismatch_check((float)vehicle_speed_2.values[0] / VEHICLE_SPEED_FACTOR); - if (controls_allowed && steer_control_enabled) { + if ((controls_allowed || controls_allowed_lateral) && steer_control_enabled) { // *** absolute curvature cap *** violation |= safety_max_limit_check(desired_curvature, limits.max_curvature, -limits.max_curvature); @@ -289,15 +289,15 @@ bool steer_curvature_cmd_checks(int desired_curvature, int steer_power, bool ste if (limits.max_steer_power != 0) { violation |= safety_max_limit_check(steer_power, limits.max_steer_power, 0); violation |= (steer_power != 0) && !steer_control_enabled; - violation |= !controls_allowed && (steer_power != 0) && (steer_power >= curvature_state.steer_power_last); + violation |= !(controls_allowed || controls_allowed_lateral) && (steer_power != 0) && (steer_power >= curvature_state.steer_power_last); curvature_state.steer_power_last = steer_power; } else { // No curvature control allowed when controls are not allowed - violation |= !controls_allowed && steer_control_enabled; + violation |= !(controls_allowed || controls_allowed_lateral) && steer_control_enabled; } // reset to zero or measured curvature depending on EPS expectation - if (violation || !controls_allowed) { + if (violation || !(controls_allowed || controls_allowed_lateral)) { curvature_state.desired_last = limits.inactive_curvature_is_zero ? 0 : curvature_state.meas.values[0]; } @@ -330,7 +330,7 @@ bool steer_angle_cmd_checks_vm(int desired_angle, bool steer_control_enabled, co bool violation = false; - if (controls_allowed && steer_control_enabled) { + if ((controls_allowed || controls_allowed_lateral) && steer_control_enabled) { // *** ISO lateral jerk limit *** // calculate maximum angle rate per second const float max_curvature_rate_sec = MAX_LATERAL_JERK / (fudged_speed * fudged_speed); @@ -366,12 +366,12 @@ bool steer_angle_cmd_checks_vm(int desired_angle, bool steer_control_enabled, co } // No angle control allowed when controls are not allowed - if (!controls_allowed) { + if (!(controls_allowed || controls_allowed_lateral)) { violation |= steer_control_enabled; } // reset to current angle if either controls is not allowed or there's a violation - if (violation || !controls_allowed) { + if (violation || !(controls_allowed || controls_allowed_lateral)) { desired_angle_last = SAFETY_CLAMP(angle_meas.values[0], -limits.max_angle, limits.max_angle); } diff --git a/opendbc/safety/modes/hyundai.h b/opendbc/safety/modes/hyundai.h index b867f1850a0..5cbf405b0a1 100644 --- a/opendbc/safety/modes/hyundai.h +++ b/opendbc/safety/modes/hyundai.h @@ -49,6 +49,9 @@ const LongitudinalLimits HYUNDAI_LONG_LIMITS = { #define HYUNDAI_SCC12_ADDR_CHECK(scc_bus) \ {.msg = {{0x421, (scc_bus), 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ +#define HYUNDAI_SCC11_ADDR_CHECK(scc_bus) \ + {.msg = {{0x420, (scc_bus), 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + #define HYUNDAI_FCEV_GAS_ADDR_CHECK \ {.msg = {{0x91, 0, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ @@ -128,6 +131,14 @@ static uint32_t hyundai_compute_checksum(const CANPacket_t *msg) { static void hyundai_rx_hook(const CANPacket_t *msg) { + // MainMode_ACC is the physical ACC main switch. With MADS it is the + // independent authorization source for lateral control. + if (msg->addr == 0x420U) { + if (((msg->bus == 0U) && !hyundai_camera_scc) || ((msg->bus == 2U) && hyundai_camera_scc)) { + acc_main_on = GET_BIT(msg, 0U); + } + } + // SCC12 is on bus 2 for camera-based SCC cars, bus 0 on all others if (msg->addr == 0x421U) { if (((msg->bus == 0U) && !hyundai_camera_scc) || ((msg->bus == 2U) && hyundai_camera_scc)) { @@ -174,6 +185,8 @@ static void hyundai_rx_hook(const CANPacket_t *msg) { brake_pressed = ((msg->data[5] >> 5U) & 0x3U) == 0x2U; } } + + controls_allowed_lateral = ((alternative_experience & ALT_EXP_ENABLE_MADS) != 0) && acc_main_on; } static bool hyundai_tx_hook(const CANPacket_t *msg) { @@ -294,6 +307,7 @@ static safety_config hyundai_init(uint16_t param) { static RxCheck hyundai_cam_scc_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(false) HYUNDAI_SCC12_ADDR_CHECK(2) + HYUNDAI_SCC11_ADDR_CHECK(2) }; ret = BUILD_SAFETY_CFG(hyundai_cam_scc_rx_checks, HYUNDAI_CAMERA_SCC_TX_MSGS); @@ -301,11 +315,13 @@ static safety_config hyundai_init(uint16_t param) { static RxCheck hyundai_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(false) HYUNDAI_SCC12_ADDR_CHECK(0) + HYUNDAI_SCC11_ADDR_CHECK(0) }; static RxCheck hyundai_fcev_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(false) HYUNDAI_SCC12_ADDR_CHECK(0) + HYUNDAI_SCC11_ADDR_CHECK(0) HYUNDAI_FCEV_GAS_ADDR_CHECK }; @@ -324,6 +340,7 @@ static safety_config hyundai_legacy_init(uint16_t param) { static RxCheck hyundai_legacy_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(true) HYUNDAI_SCC12_ADDR_CHECK(0) + HYUNDAI_SCC11_ADDR_CHECK(0) }; hyundai_common_init(param); diff --git a/opendbc/safety/safety.h b/opendbc/safety/safety.h index 77f6c345f52..93f993448b0 100644 --- a/opendbc/safety/safety.h +++ b/opendbc/safety/safety.h @@ -43,6 +43,7 @@ const int MAX_WRONG_COUNTERS = 5; // This can be set by the safety hooks bool controls_allowed = false; +bool controls_allowed_lateral = false; bool relay_malfunction = false; bool gas_pressed = false; bool gas_pressed_prev = false; @@ -103,6 +104,7 @@ static bool is_msg_valid(RxCheck addr_list[], int index) { if (!addr_list[index].status.valid_checksum || !addr_list[index].status.valid_quality_flag || (addr_list[index].status.wrong_counters >= MAX_WRONG_COUNTERS)) { valid = false; controls_allowed = false; + controls_allowed_lateral = false; } } return valid; @@ -330,6 +332,7 @@ void safety_tick(const safety_config *cfg) { cfg->rx_checks[i].status.lagging = lagging; if (lagging) { controls_allowed = false; + controls_allowed_lateral = false; } // enforce minimum frequency for safety-relevant messages @@ -337,6 +340,7 @@ void safety_tick(const safety_config *cfg) { if (lagging || frequency_invalid || !is_msg_valid(cfg->rx_checks, i)) { rx_checks_invalid = true; controls_allowed = false; + controls_allowed_lateral = false; } } } @@ -463,6 +467,7 @@ int set_safety_hooks(uint16_t mode, uint16_t param) { reset_sample(&curvature_state.meas); controls_allowed = false; + controls_allowed_lateral = false; relay_malfunction_reset(); safety_rx_checks_invalid = false; diff --git a/opendbc/safety/tests/libsafety/libsafety_py.py b/opendbc/safety/tests/libsafety/libsafety_py.py index 23c9dccc260..1509d49c91c 100644 --- a/opendbc/safety/tests/libsafety/libsafety_py.py +++ b/opendbc/safety/tests/libsafety/libsafety_py.py @@ -63,6 +63,8 @@ class CANPacket: void set_controls_allowed(bool c); bool get_controls_allowed(void); +void set_controls_allowed_lateral(bool c); +bool get_controls_allowed_lateral(void); bool get_longitudinal_allowed(void); void set_alternative_experience(int mode); int get_alternative_experience(void); diff --git a/opendbc/safety/tests/libsafety/safety.c b/opendbc/safety/tests/libsafety/safety.c index 64f981c8981..4a836b8e708 100644 --- a/opendbc/safety/tests/libsafety/safety.c +++ b/opendbc/safety/tests/libsafety/safety.c @@ -38,6 +38,10 @@ void set_controls_allowed(bool c){ controls_allowed = c; } +void set_controls_allowed_lateral(bool c){ + controls_allowed_lateral = c; +} + void set_alternative_experience(int mode){ alternative_experience = mode; } @@ -54,6 +58,10 @@ bool get_controls_allowed(void){ return controls_allowed; } +bool get_controls_allowed_lateral(void){ + return controls_allowed_lateral; +} + bool get_ignition_can(void){ return ignition_can; } diff --git a/opendbc/safety/tests/test_hyundai.py b/opendbc/safety/tests/test_hyundai.py index e53a448f37b..9689cc85367 100755 --- a/opendbc/safety/tests/test_hyundai.py +++ b/opendbc/safety/tests/test_hyundai.py @@ -8,6 +8,7 @@ import opendbc.safety.tests.common as common from opendbc.safety.tests.common import CANPackerSafety from opendbc.safety.tests.hyundai_common import HyundaiButtonBase, HyundaiLongitudinalBase +from opendbc.safety import ALTERNATIVE_EXPERIENCE # 4 bit checkusm used in some hyundai messages @@ -103,6 +104,9 @@ def _pcm_status_msg(self, enable): self.__class__.cnt_cruise += 1 return self.packer.make_can_msg_safety("SCC12", self.SCC_BUS, values, fix_checksum=checksum) + def _main_status_msg(self, enable): + return self.packer.make_can_msg_safety("SCC11", self.SCC_BUS, {"MainMode_ACC": enable}) + def _torque_driver_msg(self, torque): values = {"CR_Mdps_StrColTq": torque} return self.packer.make_can_msg_safety("MDPS12", 0, values) @@ -111,7 +115,6 @@ def _torque_cmd_msg(self, torque, steer_req=1): values = {"CR_Lkas_StrToqReq": torque, "CF_Lkas_ActToi": steer_req} return self.packer.make_can_msg_safety("LKAS11", 0, values) - class TestHyundaiSafetyAltLimits(TestHyundaiSafety): MAX_RATE_UP = 2 MAX_RATE_DOWN = 3 @@ -124,6 +127,37 @@ def setUp(self): self.safety.init_tests() +class TestHyundaiMadsSafety(common.SafetyTestBase): + def setUp(self): + self.packer = CANPackerSafety("hyundai_can_generated") + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0) + self.safety.init_tests() + + def _main_status_msg(self, enable): + return self.packer.make_can_msg_safety("SCC11", 0, {"MainMode_ACC": enable}) + + def _torque_cmd_msg(self, torque, steer_req=1): + values = {"CR_Lkas_StrToqReq": torque, "CF_Lkas_ActToi": steer_req} + return self.packer.make_can_msg_safety("LKAS11", 0, values) + + def test_main_switch_controls_lateral_independently(self): + self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ENABLE_MADS) + self._rx(self._main_status_msg(True)) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertTrue(self.safety.get_controls_allowed_lateral()) + self.assertTrue(self._tx(self._torque_cmd_msg(1))) + + self._rx(self._main_status_msg(False)) + self.assertFalse(self.safety.get_controls_allowed_lateral()) + self.assertFalse(self._tx(self._torque_cmd_msg(1))) + self.assertTrue(self._tx(self._torque_cmd_msg(0, steer_req=0))) + + def test_main_switch_does_not_enable_mads_without_alternative_experience(self): + self._rx(self._main_status_msg(True)) + self.assertFalse(self.safety.get_controls_allowed_lateral()) + + class TestHyundaiSafetyAltLimits2(TestHyundaiSafety): MAX_RATE_UP = 2 MAX_RATE_DOWN = 3 From faacf978d26a117b228b091c77e344d9c05ea0ed Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jul 2026 15:19:22 -0700 Subject: [PATCH 2/7] scope MADS safety checks --- opendbc/safety/modes/hyundai.h | 10 +++++++--- opendbc/safety/tests/test_hyundai.py | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/opendbc/safety/modes/hyundai.h b/opendbc/safety/modes/hyundai.h index 5cbf405b0a1..9081b054b54 100644 --- a/opendbc/safety/modes/hyundai.h +++ b/opendbc/safety/modes/hyundai.h @@ -307,7 +307,6 @@ static safety_config hyundai_init(uint16_t param) { static RxCheck hyundai_cam_scc_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(false) HYUNDAI_SCC12_ADDR_CHECK(2) - HYUNDAI_SCC11_ADDR_CHECK(2) }; ret = BUILD_SAFETY_CFG(hyundai_cam_scc_rx_checks, HYUNDAI_CAMERA_SCC_TX_MSGS); @@ -315,19 +314,25 @@ static safety_config hyundai_init(uint16_t param) { static RxCheck hyundai_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(false) HYUNDAI_SCC12_ADDR_CHECK(0) + }; + + static RxCheck hyundai_mads_rx_checks[] = { + HYUNDAI_COMMON_RX_CHECKS(false) + HYUNDAI_SCC12_ADDR_CHECK(0) HYUNDAI_SCC11_ADDR_CHECK(0) }; static RxCheck hyundai_fcev_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(false) HYUNDAI_SCC12_ADDR_CHECK(0) - HYUNDAI_SCC11_ADDR_CHECK(0) HYUNDAI_FCEV_GAS_ADDR_CHECK }; SET_TX_MSGS(HYUNDAI_TX_MSGS, ret); if (hyundai_fcev_gas_signal) { SET_RX_CHECKS(hyundai_fcev_rx_checks, ret); + } else if ((alternative_experience & ALT_EXP_ENABLE_MADS) != 0) { + SET_RX_CHECKS(hyundai_mads_rx_checks, ret); } else { SET_RX_CHECKS(hyundai_rx_checks, ret); } @@ -340,7 +345,6 @@ static safety_config hyundai_legacy_init(uint16_t param) { static RxCheck hyundai_legacy_rx_checks[] = { HYUNDAI_COMMON_RX_CHECKS(true) HYUNDAI_SCC12_ADDR_CHECK(0) - HYUNDAI_SCC11_ADDR_CHECK(0) }; hyundai_common_init(param); diff --git a/opendbc/safety/tests/test_hyundai.py b/opendbc/safety/tests/test_hyundai.py index 9689cc85367..836f154f178 100755 --- a/opendbc/safety/tests/test_hyundai.py +++ b/opendbc/safety/tests/test_hyundai.py @@ -133,6 +133,8 @@ def setUp(self): self.safety = libsafety_py.libsafety self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0) self.safety.init_tests() + self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ENABLE_MADS) + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0) def _main_status_msg(self, enable): return self.packer.make_can_msg_safety("SCC11", 0, {"MainMode_ACC": enable}) @@ -142,7 +144,6 @@ def _torque_cmd_msg(self, torque, steer_req=1): return self.packer.make_can_msg_safety("LKAS11", 0, values) def test_main_switch_controls_lateral_independently(self): - self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ENABLE_MADS) self._rx(self._main_status_msg(True)) self.assertFalse(self.safety.get_controls_allowed()) self.assertTrue(self.safety.get_controls_allowed_lateral()) @@ -154,6 +155,7 @@ def test_main_switch_controls_lateral_independently(self): self.assertTrue(self._tx(self._torque_cmd_msg(0, steer_req=0))) def test_main_switch_does_not_enable_mads_without_alternative_experience(self): + self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.DEFAULT) self._rx(self._main_status_msg(True)) self.assertFalse(self.safety.get_controls_allowed_lateral()) From 4522657c26c842fcf762f400935ec058bc0d7930 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jul 2026 15:29:09 -0700 Subject: [PATCH 3/7] harden MADS safety authorization --- opendbc/safety/modes/hyundai.h | 7 ++++-- opendbc/safety/safety.h | 1 + .../safety/tests/libsafety/libsafety_py.py | 1 + opendbc/safety/tests/libsafety/safety.c | 4 +++ opendbc/safety/tests/test_hyundai.py | 25 ++++++++++++++++++- 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/opendbc/safety/modes/hyundai.h b/opendbc/safety/modes/hyundai.h index 9081b054b54..3fce208a0c8 100644 --- a/opendbc/safety/modes/hyundai.h +++ b/opendbc/safety/modes/hyundai.h @@ -50,7 +50,7 @@ const LongitudinalLimits HYUNDAI_LONG_LIMITS = { {.msg = {{0x421, (scc_bus), 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ #define HYUNDAI_SCC11_ADDR_CHECK(scc_bus) \ - {.msg = {{0x420, (scc_bus), 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ + {.msg = {{0x420, (scc_bus), 8, 50U, .ignore_checksum = true, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ #define HYUNDAI_FCEV_GAS_ADDR_CHECK \ {.msg = {{0x91, 0, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \ @@ -72,6 +72,8 @@ static uint8_t hyundai_get_counter(const CANPacket_t *msg) { cnt = (msg->data[1] >> 5) & 0x7U; } else if (msg->addr == 0x421U) { cnt = msg->data[7] & 0xFU; + } else if (msg->addr == 0x420U) { + cnt = (msg->data[0] >> 4) & 0xFU; } else if (msg->addr == 0x4F1U) { cnt = (msg->data[3] >> 4) & 0xFU; } else { @@ -186,7 +188,8 @@ static void hyundai_rx_hook(const CANPacket_t *msg) { } } - controls_allowed_lateral = ((alternative_experience & ALT_EXP_ENABLE_MADS) != 0) && acc_main_on; + const bool mads_requested = ((alternative_experience & ALT_EXP_ENABLE_MADS) != 0) && acc_main_on; + controls_allowed_lateral = mads_requested && heartbeat_engaged && !safety_rx_checks_invalid; } static bool hyundai_tx_hook(const CANPacket_t *msg) { diff --git a/opendbc/safety/safety.h b/opendbc/safety/safety.h index 93f993448b0..69c1b146e0d 100644 --- a/opendbc/safety/safety.h +++ b/opendbc/safety/safety.h @@ -105,6 +105,7 @@ static bool is_msg_valid(RxCheck addr_list[], int index) { valid = false; controls_allowed = false; controls_allowed_lateral = false; + safety_rx_checks_invalid = true; } } return valid; diff --git a/opendbc/safety/tests/libsafety/libsafety_py.py b/opendbc/safety/tests/libsafety/libsafety_py.py index 1509d49c91c..44468fdd52a 100644 --- a/opendbc/safety/tests/libsafety/libsafety_py.py +++ b/opendbc/safety/tests/libsafety/libsafety_py.py @@ -65,6 +65,7 @@ class CANPacket: bool get_controls_allowed(void); void set_controls_allowed_lateral(bool c); bool get_controls_allowed_lateral(void); +void set_heartbeat_engaged(bool engaged); bool get_longitudinal_allowed(void); void set_alternative_experience(int mode); int get_alternative_experience(void); diff --git a/opendbc/safety/tests/libsafety/safety.c b/opendbc/safety/tests/libsafety/safety.c index 4a836b8e708..7683be8abd4 100644 --- a/opendbc/safety/tests/libsafety/safety.c +++ b/opendbc/safety/tests/libsafety/safety.c @@ -62,6 +62,10 @@ bool get_controls_allowed_lateral(void){ return controls_allowed_lateral; } +void set_heartbeat_engaged(bool engaged){ + heartbeat_engaged = engaged; +} + bool get_ignition_can(void){ return ignition_can; } diff --git a/opendbc/safety/tests/test_hyundai.py b/opendbc/safety/tests/test_hyundai.py index 836f154f178..5397a639fd3 100755 --- a/opendbc/safety/tests/test_hyundai.py +++ b/opendbc/safety/tests/test_hyundai.py @@ -135,9 +135,13 @@ def setUp(self): self.safety.init_tests() self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ENABLE_MADS) self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0) + self.safety.set_heartbeat_engaged(True) + self.cnt_main = 0 def _main_status_msg(self, enable): - return self.packer.make_can_msg_safety("SCC11", 0, {"MainMode_ACC": enable}) + values = {"MainMode_ACC": enable, "AliveCounterACC": self.cnt_main % 16} + self.cnt_main += 1 + return self.packer.make_can_msg_safety("SCC11", 0, values) def _torque_cmd_msg(self, torque, steer_req=1): values = {"CR_Lkas_StrToqReq": torque, "CF_Lkas_ActToi": steer_req} @@ -159,6 +163,25 @@ def test_main_switch_does_not_enable_mads_without_alternative_experience(self): self._rx(self._main_status_msg(True)) self.assertFalse(self.safety.get_controls_allowed_lateral()) + def test_rx_lag_cannot_immediately_reauthorize_lateral(self): + self._rx(self._main_status_msg(True)) + self.assertTrue(self.safety.get_controls_allowed_lateral()) + + self.safety.set_timer(2_000_000) + self.safety.safety_tick_current_safety_config() + self.assertFalse(self.safety.get_controls_allowed_lateral()) + + self._rx(self._main_status_msg(True)) + self.assertFalse(self.safety.get_controls_allowed_lateral()) + + def test_heartbeat_disengaged_blocks_reauthorization(self): + self._rx(self._main_status_msg(True)) + self.assertTrue(self.safety.get_controls_allowed_lateral()) + + self.safety.set_heartbeat_engaged(False) + self._rx(self._main_status_msg(True)) + self.assertFalse(self.safety.get_controls_allowed_lateral()) + class TestHyundaiSafetyAltLimits2(TestHyundaiSafety): MAX_RATE_UP = 2 From 6a830e6cc97eb8ea1029f7c1d977a2815d280c31 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jul 2026 15:51:23 -0700 Subject: [PATCH 4/7] require valid RX set before MADS authorization --- opendbc/safety/safety.h | 4 ++- opendbc/safety/tests/test_hyundai.py | 42 +++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/opendbc/safety/safety.h b/opendbc/safety/safety.h index 69c1b146e0d..97598b0ef90 100644 --- a/opendbc/safety/safety.h +++ b/opendbc/safety/safety.h @@ -470,7 +470,9 @@ int set_safety_hooks(uint16_t mode, uint16_t param) { controls_allowed = false; controls_allowed_lateral = false; relay_malfunction_reset(); - safety_rx_checks_invalid = false; + // Lateral-only authorization must remain fail-closed until safety_tick has + // verified that the complete configured RX set is present and valid. + safety_rx_checks_invalid = true; current_safety_config.rx_checks = NULL; current_safety_config.rx_checks_len = 0; diff --git a/opendbc/safety/tests/test_hyundai.py b/opendbc/safety/tests/test_hyundai.py index 5397a639fd3..aa6bac4e2aa 100755 --- a/opendbc/safety/tests/test_hyundai.py +++ b/opendbc/safety/tests/test_hyundai.py @@ -7,7 +7,7 @@ from opendbc.safety.tests.libsafety import libsafety_py import opendbc.safety.tests.common as common from opendbc.safety.tests.common import CANPackerSafety -from opendbc.safety.tests.hyundai_common import HyundaiButtonBase, HyundaiLongitudinalBase +from opendbc.safety.tests.hyundai_common import Buttons, HyundaiButtonBase, HyundaiLongitudinalBase from opendbc.safety import ALTERNATIVE_EXPERIENCE @@ -137,6 +137,11 @@ def setUp(self): self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0) self.safety.set_heartbeat_engaged(True) self.cnt_main = 0 + self.cnt_gas = 0 + self.cnt_speed = 0 + self.cnt_brake = 0 + self.cnt_cruise = 0 + self.cnt_button = 0 def _main_status_msg(self, enable): values = {"MainMode_ACC": enable, "AliveCounterACC": self.cnt_main % 16} @@ -147,7 +152,28 @@ def _torque_cmd_msg(self, torque, steer_req=1): values = {"CR_Lkas_StrToqReq": torque, "CF_Lkas_ActToi": steer_req} return self.packer.make_can_msg_safety("LKAS11", 0, values) + def _prime_rx_checks(self): + gas = self.packer.make_can_msg_safety("EMS16", 0, {"AliveCounter": self.cnt_gas % 4}, fix_checksum=checksum) + speed = self.packer.make_can_msg_safety("WHL_SPD11", 0, {"WHL_SPD_AliveCounter_LSB": self.cnt_speed & 0x3, + "WHL_SPD_AliveCounter_MSB": self.cnt_speed >> 2}, fix_checksum=checksum) + brake = self.packer.make_can_msg_safety("TCS13", 0, {"AliveCounterTCS": self.cnt_brake % 8}, fix_checksum=checksum) + torque = self.packer.make_can_msg_safety("MDPS12", 0, {}) + button = self.packer.make_can_msg_safety("CLU11", 0, {"CF_Clu_AliveCnt1": self.cnt_button % 16}) + cruise = self.packer.make_can_msg_safety("SCC12", 0, {"CR_VSM_Alive": self.cnt_cruise % 16}, fix_checksum=checksum) + for msg in (gas, speed, brake, torque, button, cruise, self._main_status_msg(True)): + self._rx(msg) + self.safety.safety_tick_current_safety_config() + + def test_cold_start_requires_complete_rx_checks(self): + self._rx(self._main_status_msg(True)) + self.assertFalse(self.safety.get_controls_allowed_lateral()) + + self._prime_rx_checks() + self._rx(self._main_status_msg(True)) + self.assertTrue(self.safety.get_controls_allowed_lateral()) + def test_main_switch_controls_lateral_independently(self): + self._prime_rx_checks() self._rx(self._main_status_msg(True)) self.assertFalse(self.safety.get_controls_allowed()) self.assertTrue(self.safety.get_controls_allowed_lateral()) @@ -158,12 +184,25 @@ def test_main_switch_controls_lateral_independently(self): self.assertFalse(self._tx(self._torque_cmd_msg(1))) self.assertTrue(self._tx(self._torque_cmd_msg(0, steer_req=0))) + def test_lateral_only_does_not_authorize_longitudinal(self): + self._prime_rx_checks() + self._rx(self._main_status_msg(True)) + self.assertTrue(self.safety.get_controls_allowed_lateral()) + self.assertFalse(self.safety.get_controls_allowed()) + + resume = self.packer.make_can_msg_safety("CLU11", 0, {"CF_Clu_CruiseSwState": Buttons.RESUME}) + accel = self.packer.make_can_msg_safety("SCC12", 0, {"ACCMode": 1, "aReqRaw": 1.0, "aReqValue": 1.0}) + self.assertFalse(self._tx(resume)) + self.assertFalse(self._tx(accel)) + def test_main_switch_does_not_enable_mads_without_alternative_experience(self): + self._prime_rx_checks() self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.DEFAULT) self._rx(self._main_status_msg(True)) self.assertFalse(self.safety.get_controls_allowed_lateral()) def test_rx_lag_cannot_immediately_reauthorize_lateral(self): + self._prime_rx_checks() self._rx(self._main_status_msg(True)) self.assertTrue(self.safety.get_controls_allowed_lateral()) @@ -175,6 +214,7 @@ def test_rx_lag_cannot_immediately_reauthorize_lateral(self): self.assertFalse(self.safety.get_controls_allowed_lateral()) def test_heartbeat_disengaged_blocks_reauthorization(self): + self._prime_rx_checks() self._rx(self._main_status_msg(True)) self.assertTrue(self.safety.get_controls_allowed_lateral()) From 60e9d2e92e3b6f7b716834893181fedd7427115c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jul 2026 16:42:53 -0700 Subject: [PATCH 5/7] expire stale MADS main authorization --- opendbc/safety/modes/hyundai.h | 9 +++++++++ opendbc/safety/tests/test_hyundai.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/opendbc/safety/modes/hyundai.h b/opendbc/safety/modes/hyundai.h index 3fce208a0c8..b43a96dacc8 100644 --- a/opendbc/safety/modes/hyundai.h +++ b/opendbc/safety/modes/hyundai.h @@ -60,6 +60,7 @@ static const CanMsg HYUNDAI_TX_MSGS[] = { }; static bool hyundai_legacy = false; +static uint32_t hyundai_mads_main_ts = 0U; static uint8_t hyundai_get_counter(const CANPacket_t *msg) { @@ -138,6 +139,7 @@ static void hyundai_rx_hook(const CANPacket_t *msg) { if (msg->addr == 0x420U) { if (((msg->bus == 0U) && !hyundai_camera_scc) || ((msg->bus == 2U) && hyundai_camera_scc)) { acc_main_on = GET_BIT(msg, 0U); + hyundai_mads_main_ts = microsecond_timer_get(); } } @@ -193,12 +195,18 @@ static void hyundai_rx_hook(const CANPacket_t *msg) { } static bool hyundai_tx_hook(const CANPacket_t *msg) { + const uint32_t HYUNDAI_MADS_MAIN_TIMEOUT = 100000U; const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS = HYUNDAI_LIMITS(384, 3, 7); const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS_ALT = HYUNDAI_LIMITS(270, 2, 3); const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS_ALT_2 = HYUNDAI_LIMITS(170, 2, 3); bool tx = true; + if (((alternative_experience & ALT_EXP_ENABLE_MADS) != 0) && controls_allowed_lateral && + (safety_get_ts_elapsed(microsecond_timer_get(), hyundai_mads_main_ts) > HYUNDAI_MADS_MAIN_TIMEOUT)) { + controls_allowed_lateral = false; + } + // FCA11: Block any potential actuation if (msg->addr == 0x38DU) { int CR_VSM_DecCmd = msg->data[1]; @@ -282,6 +290,7 @@ static safety_config hyundai_init(uint16_t param) { hyundai_common_init(param); hyundai_legacy = false; + hyundai_mads_main_ts = 0U; safety_config ret; if (hyundai_longitudinal) { diff --git a/opendbc/safety/tests/test_hyundai.py b/opendbc/safety/tests/test_hyundai.py index aa6bac4e2aa..ef3e0614e6f 100755 --- a/opendbc/safety/tests/test_hyundai.py +++ b/opendbc/safety/tests/test_hyundai.py @@ -195,6 +195,15 @@ def test_lateral_only_does_not_authorize_longitudinal(self): self.assertFalse(self._tx(resume)) self.assertFalse(self._tx(accel)) + def test_stale_main_status_blocks_lateral(self): + self._prime_rx_checks() + self._rx(self._main_status_msg(True)) + self.assertTrue(self._tx(self._torque_cmd_msg(1))) + + self.safety.set_timer(100_001) + self.assertFalse(self._tx(self._torque_cmd_msg(1))) + self.assertFalse(self.safety.get_controls_allowed_lateral()) + def test_main_switch_does_not_enable_mads_without_alternative_experience(self): self._prime_rx_checks() self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.DEFAULT) From 626088eb14a3985ddaffa06e4d131c9e98f1bb85 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jul 2026 16:47:38 -0700 Subject: [PATCH 6/7] keep MADS health authorization fresh --- opendbc/safety/modes/hyundai.h | 5 +++-- opendbc/safety/tests/test_hyundai.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/opendbc/safety/modes/hyundai.h b/opendbc/safety/modes/hyundai.h index b43a96dacc8..19ad9b6030e 100644 --- a/opendbc/safety/modes/hyundai.h +++ b/opendbc/safety/modes/hyundai.h @@ -61,6 +61,7 @@ static const CanMsg HYUNDAI_TX_MSGS[] = { static bool hyundai_legacy = false; static uint32_t hyundai_mads_main_ts = 0U; +static const uint32_t HYUNDAI_MADS_MAIN_TIMEOUT = 100000U; static uint8_t hyundai_get_counter(const CANPacket_t *msg) { @@ -191,11 +192,11 @@ static void hyundai_rx_hook(const CANPacket_t *msg) { } const bool mads_requested = ((alternative_experience & ALT_EXP_ENABLE_MADS) != 0) && acc_main_on; - controls_allowed_lateral = mads_requested && heartbeat_engaged && !safety_rx_checks_invalid; + const bool mads_main_fresh = safety_get_ts_elapsed(microsecond_timer_get(), hyundai_mads_main_ts) <= HYUNDAI_MADS_MAIN_TIMEOUT; + controls_allowed_lateral = mads_requested && mads_main_fresh && heartbeat_engaged && !safety_rx_checks_invalid; } static bool hyundai_tx_hook(const CANPacket_t *msg) { - const uint32_t HYUNDAI_MADS_MAIN_TIMEOUT = 100000U; const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS = HYUNDAI_LIMITS(384, 3, 7); const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS_ALT = HYUNDAI_LIMITS(270, 2, 3); const TorqueSteeringLimits HYUNDAI_STEERING_LIMITS_ALT_2 = HYUNDAI_LIMITS(170, 2, 3); diff --git a/opendbc/safety/tests/test_hyundai.py b/opendbc/safety/tests/test_hyundai.py index ef3e0614e6f..b043fdccfb9 100755 --- a/opendbc/safety/tests/test_hyundai.py +++ b/opendbc/safety/tests/test_hyundai.py @@ -204,6 +204,12 @@ def test_stale_main_status_blocks_lateral(self): self.assertFalse(self._tx(self._torque_cmd_msg(1))) self.assertFalse(self.safety.get_controls_allowed_lateral()) + # Other valid RX traffic must not make stale Main authorization appear + # active in panda health or reauthorize steering. + self._rx(self.packer.make_can_msg_safety("MDPS12", 0, {})) + self.assertFalse(self.safety.get_controls_allowed_lateral()) + self.assertFalse(self._tx(self._torque_cmd_msg(1))) + def test_main_switch_does_not_enable_mads_without_alternative_experience(self): self._prime_rx_checks() self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.DEFAULT) From cfe48276d84a7a12e8208a633a66c3a7bbcfa593 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jul 2026 17:03:35 -0700 Subject: [PATCH 7/7] support MADS with Hyundai longitudinal control --- opendbc/safety/modes/hyundai.h | 16 ++++++- opendbc/safety/modes/hyundai_common.h | 3 +- opendbc/safety/tests/test_hyundai.py | 69 +++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/opendbc/safety/modes/hyundai.h b/opendbc/safety/modes/hyundai.h index 19ad9b6030e..68441869d39 100644 --- a/opendbc/safety/modes/hyundai.h +++ b/opendbc/safety/modes/hyundai.h @@ -61,6 +61,7 @@ static const CanMsg HYUNDAI_TX_MSGS[] = { static bool hyundai_legacy = false; static uint32_t hyundai_mads_main_ts = 0U; +static bool hyundai_mads_main_button_prev = false; static const uint32_t HYUNDAI_MADS_MAIN_TIMEOUT = 100000U; static uint8_t hyundai_get_counter(const CANPacket_t *msg) { @@ -164,6 +165,15 @@ static void hyundai_rx_hook(const CANPacket_t *msg) { if (msg->addr == 0x4F1U) { int cruise_button = msg->data[0] & 0x7U; bool main_button = GET_BIT(msg, 3U); + if (hyundai_longitudinal && ((alternative_experience & ALT_EXP_ENABLE_MADS) != 0)) { + if (main_button && !hyundai_mads_main_button_prev) { + acc_main_on = !acc_main_on; + if (!acc_main_on) { + controls_allowed = false; + } + } + hyundai_mads_main_button_prev = main_button; + } hyundai_common_cruise_buttons_check(cruise_button, main_button); } @@ -192,7 +202,8 @@ static void hyundai_rx_hook(const CANPacket_t *msg) { } const bool mads_requested = ((alternative_experience & ALT_EXP_ENABLE_MADS) != 0) && acc_main_on; - const bool mads_main_fresh = safety_get_ts_elapsed(microsecond_timer_get(), hyundai_mads_main_ts) <= HYUNDAI_MADS_MAIN_TIMEOUT; + const bool mads_main_fresh = hyundai_longitudinal || + (safety_get_ts_elapsed(microsecond_timer_get(), hyundai_mads_main_ts) <= HYUNDAI_MADS_MAIN_TIMEOUT); controls_allowed_lateral = mads_requested && mads_main_fresh && heartbeat_engaged && !safety_rx_checks_invalid; } @@ -203,7 +214,7 @@ static bool hyundai_tx_hook(const CANPacket_t *msg) { bool tx = true; - if (((alternative_experience & ALT_EXP_ENABLE_MADS) != 0) && controls_allowed_lateral && + if (!hyundai_longitudinal && ((alternative_experience & ALT_EXP_ENABLE_MADS) != 0) && controls_allowed_lateral && (safety_get_ts_elapsed(microsecond_timer_get(), hyundai_mads_main_ts) > HYUNDAI_MADS_MAIN_TIMEOUT)) { controls_allowed_lateral = false; } @@ -292,6 +303,7 @@ static safety_config hyundai_init(uint16_t param) { hyundai_common_init(param); hyundai_legacy = false; hyundai_mads_main_ts = 0U; + hyundai_mads_main_button_prev = false; safety_config ret; if (hyundai_longitudinal) { diff --git a/opendbc/safety/modes/hyundai_common.h b/opendbc/safety/modes/hyundai_common.h index 6797ae74c44..fe477e951d8 100644 --- a/opendbc/safety/modes/hyundai_common.h +++ b/opendbc/safety/modes/hyundai_common.h @@ -99,7 +99,8 @@ void hyundai_common_cruise_buttons_check(const int cruise_button, const bool mai // 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 mads_longitudinal_allowed = ((alternative_experience & ALT_EXP_ENABLE_MADS) == 0) || acc_main_on; + if ((set || res) && mads_longitudinal_allowed) { controls_allowed = true; } diff --git a/opendbc/safety/tests/test_hyundai.py b/opendbc/safety/tests/test_hyundai.py index b043fdccfb9..1931e3e5ea7 100755 --- a/opendbc/safety/tests/test_hyundai.py +++ b/opendbc/safety/tests/test_hyundai.py @@ -238,6 +238,75 @@ def test_heartbeat_disengaged_blocks_reauthorization(self): self.assertFalse(self.safety.get_controls_allowed_lateral()) +class TestHyundaiLongitudinalMadsSafety(common.SafetyTestBase): + def setUp(self): + self.packer = CANPackerSafety("hyundai_can_generated") + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, HyundaiSafetyFlags.LONG) + self.safety.init_tests() + self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ENABLE_MADS) + self.safety.set_safety_hooks(CarParams.SafetyModel.hyundai, HyundaiSafetyFlags.LONG) + self.safety.set_heartbeat_engaged(True) + self.cnt_button = 0 + + def _button_msg(self, button=Buttons.NONE, main_button=0): + values = {"CF_Clu_CruiseSwState": button, "CF_Clu_CruiseSwMain": main_button, + "CF_Clu_AliveCnt1": self.cnt_button % 16} + self.cnt_button += 1 + return self.packer.make_can_msg_safety("CLU11", 0, values) + + def _torque_cmd_msg(self, torque, steer_req=1): + values = {"CR_Lkas_StrToqReq": torque, "CF_Lkas_ActToi": steer_req} + return self.packer.make_can_msg_safety("LKAS11", 0, values) + + def _accel_msg(self, accel): + return self.packer.make_can_msg_safety("SCC12", 0, {"aReqRaw": accel, "aReqValue": accel}) + + def _prime_rx_checks(self): + messages = ( + self.packer.make_can_msg_safety("EMS16", 0, {"AliveCounter": 0}, fix_checksum=checksum), + self.packer.make_can_msg_safety("WHL_SPD11", 0, {}, fix_checksum=checksum), + self.packer.make_can_msg_safety("TCS13", 0, {"AliveCounterTCS": 0}, fix_checksum=checksum), + self.packer.make_can_msg_safety("MDPS12", 0, {}), + self._button_msg(), + ) + for msg in messages: + self._rx(msg) + self.safety.safety_tick_current_safety_config() + + def test_main_latches_lateral_and_gates_longitudinal(self): + self._prime_rx_checks() + self._rx(self._button_msg(main_button=1)) + self.assertTrue(self.safety.get_controls_allowed_lateral()) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertTrue(self._tx(self._torque_cmd_msg(1))) + self.assertFalse(self._tx(self._accel_msg(0.1))) + + self._rx(self._button_msg(button=Buttons.SET)) + self._rx(self._button_msg()) + self.assertTrue(self.safety.get_controls_allowed()) + self.assertTrue(self.safety.get_controls_allowed_lateral()) + self.assertTrue(self._tx(self._accel_msg(0.1))) + + self._rx(self._button_msg(button=Buttons.CANCEL)) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertTrue(self.safety.get_controls_allowed_lateral()) + self.assertFalse(self._tx(self._accel_msg(0.1))) + + self._rx(self._button_msg()) + self._rx(self._button_msg(main_button=1)) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertFalse(self.safety.get_controls_allowed_lateral()) + self.assertFalse(self._tx(self._torque_cmd_msg(1))) + + def test_set_cannot_enable_longitudinal_with_main_off(self): + self._prime_rx_checks() + self._rx(self._button_msg(button=Buttons.SET)) + self._rx(self._button_msg()) + self.assertFalse(self.safety.get_controls_allowed()) + self.assertFalse(self.safety.get_controls_allowed_lateral()) + + class TestHyundaiSafetyAltLimits2(TestHyundaiSafety): MAX_RATE_UP = 2 MAX_RATE_DOWN = 3