diff --git a/opendbc/safety/ignition.h b/opendbc/safety/ignition.h new file mode 100644 index 00000000000..f9be1070576 --- /dev/null +++ b/opendbc/safety/ignition.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +#include "opendbc/safety/can.h" + +static inline void ignition_can_hook(const CANPacket_t *msg, bool *ignition_can, uint32_t *ignition_can_cnt) { + if (msg->bus == 0U) { + int len = GET_LEN(msg); + + // GM exception + if ((msg->addr == 0x1F1U) && (len == 8)) { + // SystemPowerMode (2=Run, 3=Crank Request) + *ignition_can = (msg->data[0] & 0x2U) != 0U; + *ignition_can_cnt = 0U; + } + + // Rivian R1S/T GEN1 exception + if ((msg->addr == 0x152U) && (len == 8)) { + // 0x152 overlaps with Subaru pre-global which has this bit as the high beam + int counter = msg->data[1] & 0xFU; // max is only 14 + + static int prev_counter_rivian = -1; + if ((counter == ((prev_counter_rivian + 1) % 15)) && (prev_counter_rivian != -1)) { + // VDM_OutputSignals->VDM_EpasPowerMode + *ignition_can = ((msg->data[7] >> 4U) & 0x3U) == 1U; // VDM_EpasPowerMode_Drive_On=1 + *ignition_can_cnt = 0U; + } + prev_counter_rivian = counter; + } + + // Tesla Model 3/Y exception + if ((msg->addr == 0x221U) && (len == 8)) { + // 0x221 overlaps with Rivian which has random data on byte 0 + int counter = msg->data[6] >> 4; + + static int prev_counter_tesla = -1; + if ((counter == ((prev_counter_tesla + 1) % 16)) && (prev_counter_tesla != -1)) { + // VCFRONT_LVPowerState->VCFRONT_vehiclePowerState + int power_state = (msg->data[0] >> 5U) & 0x3U; + *ignition_can = power_state == 0x3; // VEHICLE_POWER_STATE_DRIVE=3 + *ignition_can_cnt = 0U; + } + prev_counter_tesla = counter; + } + + // Mazda exception + if ((msg->addr == 0x9EU) && (len == 8)) { + *ignition_can = (msg->data[0] >> 5) == 0x6U; + *ignition_can_cnt = 0U; + } + } + + // TODO: this is too loose, Teslas have 0x222 + // body v2 exception + // if (((msg->bus == 0U) || (msg->bus == 2U)) && (msg->addr == 0x222U)) { + // *ignition_can = true; + // *ignition_can_cnt = 0U; + // } +} diff --git a/opendbc/safety/tests/libsafety/libsafety_py.py b/opendbc/safety/tests/libsafety/libsafety_py.py index 7d88bdf301e..01eeb3b0f75 100644 --- a/opendbc/safety/tests/libsafety/libsafety_py.py +++ b/opendbc/safety/tests/libsafety/libsafety_py.py @@ -110,6 +110,10 @@ class CANPacket: void mutation_set_active_mutant(int id); int mutation_get_active_mutant(void); + +void ignition_can_hook_test(const CANPacket_t *msg); +bool get_ignition_can(void); +void reset_ignition_can(void); """) class LibSafety: diff --git a/opendbc/safety/tests/libsafety/safety.c b/opendbc/safety/tests/libsafety/safety.c index afb7054448d..469e2aebab0 100644 --- a/opendbc/safety/tests/libsafety/safety.c +++ b/opendbc/safety/tests/libsafety/safety.c @@ -11,6 +11,23 @@ uint32_t microsecond_timer_get(void) { #include "opendbc/safety/can.h" #include "opendbc/safety/safety.h" +#include "opendbc/safety/ignition.h" + +static bool test_ignition_can = false; +static uint32_t test_ignition_can_cnt = 0U; + +void ignition_can_hook_test(const CANPacket_t *msg) { + ignition_can_hook(msg, &test_ignition_can, &test_ignition_can_cnt); +} + +bool get_ignition_can(void) { + return test_ignition_can; +} + +void reset_ignition_can(void) { + test_ignition_can = false; + test_ignition_can_cnt = 0U; +} void safety_tick_current_safety_config() { safety_tick(¤t_safety_config); diff --git a/opendbc/safety/tests/test_ignition.py b/opendbc/safety/tests/test_ignition.py new file mode 100644 index 00000000000..6d2ae2de877 --- /dev/null +++ b/opendbc/safety/tests/test_ignition.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import unittest + +from opendbc.safety.tests.common import CANPackerSafety, make_msg +from opendbc.safety.tests.libsafety import libsafety_py + + +class TestIgnitionHook(unittest.TestCase): + TX_MSGS: list = [] + + def setUp(self): + self.safety = libsafety_py.libsafety + self.safety.reset_ignition_can() + self.gm_packer = CANPackerSafety("gm_global_a_powertrain_generated") + self.rivian_packer = CANPackerSafety("rivian_primary_actuator") + self.tesla_packer = CANPackerSafety("tesla_model3_party") + + def _hook(self, msg): + self.safety.ignition_can_hook_test(msg) + + def _ign(self): + return self.safety.get_ignition_can() + + # GM: SystemPowerMode 2=Run, 3=Crank Request + def test_gm_ignition_on(self): + self._hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) + self.assertTrue(self._ign()) + + def test_gm_ignition_off(self): + self._hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 2})) + self.assertTrue(self._ign()) + self._hook(self.gm_packer.make_can_msg_safety("BCMGeneralPlatformStatus", 0, {"SystemPowerMode": 0})) + self.assertFalse(self._ign()) + + # Rivian: VDM_EpasPowerMode_Drive_On=1 + def test_rivian_ignition_on(self): + for i in range(15): + self.safety.reset_ignition_can() + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": i, "VDM_EpasPowerMode": 1})) + self.assertFalse(self._ign()) + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": (i + 1) % 15, "VDM_EpasPowerMode": 1})) + self.assertTrue(self._ign()) + + def test_rivian_ignition_off(self): + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 0, "VDM_EpasPowerMode": 0})) + self._hook(self.rivian_packer.make_can_msg_safety("VDM_OutputSignals", 0, + {"VDM_OutputSigs_Counter": 1, "VDM_EpasPowerMode": 0})) + self.assertFalse(self._ign()) + + # Tesla: VEHICLE_POWER_STATE_DRIVE=3 + def test_tesla_ignition_on(self): + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 3})) + self.assertFalse(self._ign()) + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 3})) + self.assertTrue(self._ign()) + + def test_tesla_ignition_off(self): + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 0, "VCFRONT_vehiclePowerState": 2})) + self._hook(self.tesla_packer.make_can_msg_safety("VCFRONT_LVPowerState", 0, + {"VCFRONT_LVPowerStateCounter": 1, "VCFRONT_vehiclePowerState": 2})) + self.assertFalse(self._ign()) + + # Mazda: 0x9E byte 0 high 3 bits == 6 + def test_mazda_ignition_on(self): + self._hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) + self.assertTrue(self._ign()) + + def test_mazda_ignition_off(self): + self._hook(make_msg(0, 0x9E, dat=b"\xC0" + b"\x00" * 7)) + self.assertTrue(self._ign()) + self._hook(make_msg(0, 0x9E, dat=b"\x20" + b"\x00" * 7)) + self.assertFalse(self._ign()) + + def test_wrong_bus_ignored(self): + self._hook(make_msg(1, 0x1F1, dat=b"\x02" + b"\x00" * 7)) + self.assertFalse(self._ign()) + + def test_unknown_addr_ignored(self): + self._hook(make_msg(0, 0x123, dat=b"\xFF" * 8)) + self.assertFalse(self._ign()) + + +if __name__ == "__main__": + unittest.main()