import os
import abc
import math
import unittest
import importlib
import numpy as np
from collections . abc import Callable
from opendbc . can . packer import CANPacker # pylint: disable=import-error
from opendbc . safety import ALTERNATIVE_EXPERIENCE
from opendbc . safety . tests . libsafety import libsafety_py
MAX_WRONG_COUNTERS = 5
MAX_SAMPLE_VALS = 6
VEHICLE_SPEED_FACTOR = 1000
MessageFunction = Callable [ [ float ] , libsafety_py . CANPacket ]
def sign_of ( a ) :
return 1 if a > 0 else - 1
def away_round ( x ) :
# non-banker's/away from zero rounding, C++ CANParser uses this style
return math . floor ( x + 0.5 ) if x > = 0 else math . ceil ( x - 0.5 )
def round_speed ( v ) :
return round ( v * VEHICLE_SPEED_FACTOR ) / VEHICLE_SPEED_FACTOR
def make_msg ( bus , addr , length = 8 , dat = None ) :
if dat is None :
dat = b ' \x00 ' * length
return libsafety_py . make_CANPacket ( addr , bus , dat )
class CANPackerPanda ( CANPacker ) :
def make_can_msg_panda ( self , name_or_addr , bus , values , fix_checksum = None ) :
msg = self . make_can_msg ( name_or_addr , bus , values )
if fix_checksum is not None :
msg = fix_checksum ( msg )
addr , dat , bus = msg
return libsafety_py . make_CANPacket ( addr , bus , dat )
def add_regen_tests ( cls ) :
""" Dynamically adds regen tests for all user brake tests. """
# only rx/user brake tests, not brake command
found_tests = [ func for func in dir ( cls ) if func . startswith ( " test_ " ) and " user_brake " in func ]
assert len ( found_tests ) > = 3 , " Failed to detect known brake tests "
for test in found_tests :
def _make_regen_test ( brake_func ) :
def _regen_test ( self ) :
# only for safety modes with a regen message
if self . _user_regen_msg ( 0 ) is None :
raise unittest . SkipTest ( " Safety mode implements no _user_regen_msg " )
getattr ( self , brake_func ) ( self . _user_regen_msg , self . safety . get_regen_braking_prev )
return _regen_test
setattr ( cls , test . replace ( " brake " , " regen " ) , _make_regen_test ( test ) )
return cls
class PandaSafetyTestBase ( unittest . TestCase ) :
safety : libsafety_py . Panda
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " PandaSafetyTestBase " :
cls . safety = None
raise unittest . SkipTest
def _reset_safety_hooks ( self ) :
self . safety . set_safety_hooks ( self . safety . get_current_safety_mode ( ) ,
self . safety . get_current_safety_param ( ) )
def _rx ( self , msg ) :
return self . safety . safety_rx_hook ( msg )
def _tx ( self , msg ) :
return self . safety . safety_tx_hook ( msg )
def _generic_limit_safety_check ( self , msg_function : MessageFunction , min_allowed_value : float , max_allowed_value : float ,
min_possible_value : float , max_possible_value : float , test_delta : float = 1 , inactive_value : float = 0 ,
msg_allowed = True , additional_setup : Callable [ [ float ] , None ] | None = None ) :
"""
Enforces that a signal within a message is only allowed to be sent within a specific range , min_allowed_value - > max_allowed_value .
Tests the range of min_possible_value - > max_possible_value with a delta of test_delta .
Message is also only allowed to be sent when controls_allowed is true , unless the value is equal to inactive_value .
Message is never allowed if msg_allowed is false , for example when stock longitudinal is enabled and you are sending acceleration requests .
additional_setup is used for extra setup before each _tx , ex : for setting the previous torque for rate limits
"""
# Ensure that we at least test the allowed_value range
self . assertGreater ( max_possible_value , max_allowed_value )
self . assertLessEqual ( min_possible_value , min_allowed_value )
for controls_allowed in [ False , True ] :
# enforce we don't skip over 0 or inactive
for v in np . concatenate ( ( np . arange ( min_possible_value , max_possible_value , test_delta ) , np . array ( [ 0 , inactive_value ] ) ) ) :
v = round ( v , 2 ) # floats might not hit exact boundary conditions without rounding
self . safety . set_controls_allowed ( controls_allowed )
if additional_setup is not None :
additional_setup ( v )
should_tx = controls_allowed and min_allowed_value < = v < = max_allowed_value
should_tx = ( should_tx or v == inactive_value ) and msg_allowed
self . assertEqual ( self . _tx ( msg_function ( v ) ) , should_tx , ( controls_allowed , should_tx , v ) )
def _common_measurement_test ( self , msg_func : Callable , min_value : float , max_value : float , factor : float ,
meas_min_func : Callable [ [ ] , int ] , meas_max_func : Callable [ [ ] , int ] ) :
""" Tests accurate measurement parsing, and that the struct is reset on safety mode init """
for val in np . arange ( min_value , max_value , 0.5 ) :
for i in range ( MAX_SAMPLE_VALS ) :
self . assertTrue ( self . _rx ( msg_func ( val + i * 0.1 ) ) )
# assert close by one decimal place
self . assertAlmostEqual ( meas_min_func ( ) / factor , val , delta = 0.1 )
self . assertAlmostEqual ( meas_max_func ( ) / factor - 0.5 , val , delta = 0.1 )
# ensure sample_t is reset on safety init
self . _reset_safety_hooks ( )
self . assertEqual ( meas_min_func ( ) , 0 )
self . assertEqual ( meas_max_func ( ) , 0 )
class LongitudinalAccelSafetyTest ( PandaSafetyTestBase , abc . ABC ) :
LONGITUDINAL = True
MAX_ACCEL : float = 2.0
MIN_ACCEL : float = - 3.5
INACTIVE_ACCEL : float = 0.0
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " LongitudinalAccelSafetyTest " :
cls . safety = None
raise unittest . SkipTest
@abc . abstractmethod
def _accel_msg ( self , accel : float ) :
pass
def test_accel_limits_correct ( self ) :
self . assertGreater ( self . MAX_ACCEL , 0 )
self . assertLess ( self . MIN_ACCEL , 0 )
def test_accel_actuation_limits ( self ) :
limits = ( ( self . MIN_ACCEL , self . MAX_ACCEL , ALTERNATIVE_EXPERIENCE . DEFAULT ) ,
( self . MIN_ACCEL , self . MAX_ACCEL , ALTERNATIVE_EXPERIENCE . RAISE_LONGITUDINAL_LIMITS_TO_ISO_MAX ) )
for min_accel , max_accel , alternative_experience in limits :
# enforce we don't skip over 0 or inactive accel
for accel in np . concatenate ( ( np . arange ( min_accel - 1 , max_accel + 1 , 0.05 ) , [ 0 , self . INACTIVE_ACCEL ] ) ) :
accel = round ( accel , 2 ) # floats might not hit exact boundary conditions without rounding
for controls_allowed in [ True , False ] :
self . safety . set_controls_allowed ( controls_allowed )
self . safety . set_alternative_experience ( alternative_experience )
if self . LONGITUDINAL :
should_tx = controls_allowed and min_accel < = accel < = max_accel
should_tx = should_tx or accel == self . INACTIVE_ACCEL
else :
should_tx = False
self . assertEqual ( should_tx , self . _tx ( self . _accel_msg ( accel ) ) )
class LongitudinalGasBrakeSafetyTest ( PandaSafetyTestBase , abc . ABC ) :
MIN_BRAKE : int = 0
MAX_BRAKE : int | None = None
MAX_POSSIBLE_BRAKE : int | None = None
MIN_GAS : int = 0
MAX_GAS : int | None = None
INACTIVE_GAS = 0
MIN_POSSIBLE_GAS : int = 0.
MAX_POSSIBLE_GAS : int | None = None
def test_gas_brake_limits_correct ( self ) :
self . assertIsNotNone ( self . MAX_POSSIBLE_BRAKE )
self . assertIsNotNone ( self . MAX_POSSIBLE_GAS )
self . assertGreater ( self . MAX_BRAKE , self . MIN_BRAKE )
self . assertGreater ( self . MAX_GAS , self . MIN_GAS )
@abc . abstractmethod
def _send_gas_msg ( self , gas : int ) :
pass
@abc . abstractmethod
def _send_brake_msg ( self , brake : int ) :
pass
def test_brake_safety_check ( self ) :
self . _generic_limit_safety_check ( self . _send_brake_msg , self . MIN_BRAKE , self . MAX_BRAKE , 0 , self . MAX_POSSIBLE_BRAKE , 1 )
def test_gas_safety_check ( self ) :
self . _generic_limit_safety_check ( self . _send_gas_msg , self . MIN_GAS , self . MAX_GAS , self . MIN_POSSIBLE_GAS , self . MAX_POSSIBLE_GAS , 1 , self . INACTIVE_GAS )
class TorqueSteeringSafetyTestBase ( PandaSafetyTestBase , abc . ABC ) :
MAX_RATE_UP = 0
MAX_RATE_DOWN = 0
MAX_TORQUE_LOOKUP : tuple [ list [ float ] , list [ int ] ] = ( [ 0 ] , [ 0 ] )
DYNAMIC_MAX_TORQUE = False
MAX_RT_DELTA = 0
RT_INTERVAL = 250000
NO_STEER_REQ_BIT = False
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " TorqueSteeringSafetyTestBase " :
cls . safety = None
raise unittest . SkipTest
@property
def MAX_TORQUE ( self ) :
return max ( self . MAX_TORQUE_LOOKUP [ 1 ] )
@property
def _torque_speed_range ( self ) :
if not self . DYNAMIC_MAX_TORQUE :
return [ 0 ]
else :
# test with more precision inside breakpoint range
min_speed = min ( self . MAX_TORQUE_LOOKUP [ 0 ] )
max_speed = max ( self . MAX_TORQUE_LOOKUP [ 0 ] )
return np . concatenate ( [ np . arange ( 0 , min_speed , 5 ) , np . arange ( min_speed , max_speed , 0.5 ) , np . arange ( max_speed , 40 , 5 ) ] )
def _get_max_torque ( self , speed ) :
# matches safety fudge
torque = int ( np . interp ( speed - 1 , self . MAX_TORQUE_LOOKUP [ 0 ] , self . MAX_TORQUE_LOOKUP [ 1 ] ) + 1 )
return min ( torque , self . MAX_TORQUE )
@abc . abstractmethod
def _torque_cmd_msg ( self , torque , steer_req = 1 ) :
pass
@abc . abstractmethod
def _speed_msg ( self , speed ) :
pass
def _reset_speed_measurement ( self , speed ) :
for _ in range ( MAX_SAMPLE_VALS ) :
self . _rx ( self . _speed_msg ( speed ) )
def _set_prev_torque ( self , t ) :
self . safety . set_desired_torque_last ( t )
self . safety . set_rt_torque_last ( t )
def test_steer_safety_check ( self ) :
for speed in self . _torque_speed_range :
self . _reset_speed_measurement ( speed )
max_torque = self . _get_max_torque ( speed )
for enabled in [ 0 , 1 ] :
for t in range ( int ( - max_torque * 1.5 ) , int ( max_torque * 1.5 ) ) :
self . safety . set_controls_allowed ( enabled )
self . _set_prev_torque ( t )
if abs ( t ) > max_torque or ( not enabled and abs ( t ) > 0 ) :
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( t ) ) )
else :
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( t ) ) )
def test_non_realtime_limit_up ( self ) :
self . safety . set_controls_allowed ( True )
self . _set_prev_torque ( 0 )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( self . MAX_RATE_UP ) ) )
self . _set_prev_torque ( 0 )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( - self . MAX_RATE_UP ) ) )
self . _set_prev_torque ( 0 )
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( self . MAX_RATE_UP + 1 ) ) )
self . safety . set_controls_allowed ( True )
self . _set_prev_torque ( 0 )
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( - self . MAX_RATE_UP - 1 ) ) )
def test_steer_req_bit ( self ) :
""" Asserts all torque safety modes check the steering request bit """
if self . NO_STEER_REQ_BIT :
raise unittest . SkipTest ( " No steering request bit " )
self . safety . set_controls_allowed ( True )
self . _set_prev_torque ( self . MAX_TORQUE )
# Send torque successfully, then only drop the request bit and ensure it stays blocked
for _ in range ( 10 ) :
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , 1 ) ) )
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , 0 ) ) )
for _ in range ( 10 ) :
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , 1 ) ) )
class SteerRequestCutSafetyTest ( TorqueSteeringSafetyTestBase , abc . ABC ) :
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " SteerRequestCutSafetyTest " :
cls . safety = None
raise unittest . SkipTest
# Safety around steering request bit mismatch tolerance
MIN_VALID_STEERING_FRAMES : int
MAX_INVALID_STEERING_FRAMES : int
STEER_STEP : int = 1
@property
def MIN_VALID_STEERING_RT_INTERVAL ( self ) :
# a ~10% buffer
return int ( ( self . MIN_VALID_STEERING_FRAMES + 1 ) * self . STEER_STEP * 10000 * 0.9 )
def test_steer_req_bit_frames ( self ) :
"""
Certain safety modes implement some tolerance on their steer request bits matching the
requested torque to avoid a steering fault or lockout and maintain torque . This tests :
- We can ' t cut torque for more than one frame
- We can ' t cut torque until at least the minimum number of matching steer_req messages
- We can always recover from violations if steer_req = 1
"""
for min_valid_steer_frames in range ( self . MIN_VALID_STEERING_FRAMES * 2 ) :
# Reset match count and rt timer to allow cut (valid_steer_req_count, ts_steer_req_mismatch_last)
self . safety . init_tests ( )
self . safety . set_timer ( self . MIN_VALID_STEERING_RT_INTERVAL )
# Allow torque cut
self . safety . set_controls_allowed ( True )
self . _set_prev_torque ( self . MAX_TORQUE )
for _ in range ( min_valid_steer_frames ) :
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 1 ) ) )
# should tx if we've sent enough valid frames, and we're not cutting torque for too many frames consecutively
should_tx = min_valid_steer_frames > = self . MIN_VALID_STEERING_FRAMES
for idx in range ( self . MAX_INVALID_STEERING_FRAMES * 2 ) :
tx = self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 0 ) )
self . assertEqual ( should_tx and idx < self . MAX_INVALID_STEERING_FRAMES , tx )
# Keep blocking after one steer_req mismatch
for _ in range ( 100 ) :
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 0 ) ) )
# Make sure we can recover
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( 0 , steer_req = 1 ) ) )
self . _set_prev_torque ( self . MAX_TORQUE )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 1 ) ) )
def test_steer_req_bit_multi_invalid ( self ) :
"""
For safety modes allowing multiple consecutive invalid frames , this ensures that once a valid frame
is sent after an invalid frame ( even without sending the max number of allowed invalid frames ) ,
all counters are reset .
"""
for max_invalid_steer_frames in range ( 1 , self . MAX_INVALID_STEERING_FRAMES * 2 ) :
self . safety . init_tests ( )
self . safety . set_timer ( self . MIN_VALID_STEERING_RT_INTERVAL )
# Allow torque cut
self . safety . set_controls_allowed ( True )
self . _set_prev_torque ( self . MAX_TORQUE )
for _ in range ( self . MIN_VALID_STEERING_FRAMES ) :
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 1 ) ) )
# Send partial amount of allowed invalid frames
for idx in range ( max_invalid_steer_frames ) :
should_tx = idx < self . MAX_INVALID_STEERING_FRAMES
self . assertEqual ( should_tx , self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 0 ) ) )
# Send one valid frame, and subsequent invalid should now be blocked
self . _set_prev_torque ( self . MAX_TORQUE )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 1 ) ) )
for _ in range ( self . MIN_VALID_STEERING_FRAMES + 1 ) :
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 0 ) ) )
def test_steer_req_bit_realtime ( self ) :
"""
Realtime safety for cutting steer request bit . This tests :
- That we allow messages with mismatching steer request bit if time from last is > = MIN_VALID_STEERING_RT_INTERVAL
- That frame mismatch safety does not interfere with this test
"""
for rt_us in np . arange ( self . MIN_VALID_STEERING_RT_INTERVAL - 50000 , self . MIN_VALID_STEERING_RT_INTERVAL + 50000 , 10000 ) :
# Reset match count and rt timer (valid_steer_req_count, ts_steer_req_mismatch_last)
self . safety . init_tests ( )
# Make sure valid_steer_req_count doesn't affect this test
self . safety . set_controls_allowed ( True )
self . _set_prev_torque ( self . MAX_TORQUE )
for _ in range ( self . MIN_VALID_STEERING_FRAMES ) :
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 1 ) ) )
# Normally, sending MIN_VALID_STEERING_FRAMES valid frames should always allow
self . safety . set_timer ( max ( rt_us , 0 ) )
should_tx = rt_us > = self . MIN_VALID_STEERING_RT_INTERVAL
for _ in range ( self . MAX_INVALID_STEERING_FRAMES ) :
self . assertEqual ( should_tx , self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 0 ) ) )
# Keep blocking after one steer_req mismatch
for _ in range ( 100 ) :
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 0 ) ) )
# Make sure we can recover
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( 0 , steer_req = 1 ) ) )
self . _set_prev_torque ( self . MAX_TORQUE )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( self . MAX_TORQUE , steer_req = 1 ) ) )
class DriverTorqueSteeringSafetyTest ( TorqueSteeringSafetyTestBase , abc . ABC ) :
DRIVER_TORQUE_ALLOWANCE = 0
DRIVER_TORQUE_FACTOR = 0
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " DriverTorqueSteeringSafetyTest " :
cls . safety = None
raise unittest . SkipTest
@abc . abstractmethod
def _torque_driver_msg ( self , torque ) :
pass
def _reset_torque_driver_measurement ( self , torque ) :
for _ in range ( MAX_SAMPLE_VALS ) :
self . _rx ( self . _torque_driver_msg ( torque ) )
def test_non_realtime_limit_up ( self ) :
self . _reset_torque_driver_measurement ( 0 )
super ( ) . test_non_realtime_limit_up ( )
def test_against_torque_driver ( self ) :
# Tests down limits and driver torque blending
self . safety . set_controls_allowed ( True )
for speed in self . _torque_speed_range :
self . _reset_speed_measurement ( speed )
max_torque = self . _get_max_torque ( speed )
# Cannot stay at MAX_TORQUE if above DRIVER_TORQUE_ALLOWANCE
for sign in [ - 1 , 1 ] :
for driver_torque in np . arange ( 0 , self . DRIVER_TORQUE_ALLOWANCE * 2 , 1 ) :
self . _reset_torque_driver_measurement ( - driver_torque * sign )
self . _set_prev_torque ( max_torque * sign )
should_tx = abs ( driver_torque ) < = self . DRIVER_TORQUE_ALLOWANCE
self . assertEqual ( should_tx , self . _tx ( self . _torque_cmd_msg ( max_torque * sign ) ) )
# arbitrary high driver torque to ensure max steer torque is allowed
max_driver_torque = int ( max_torque / self . DRIVER_TORQUE_FACTOR + self . DRIVER_TORQUE_ALLOWANCE + 1 )
# spot check some individual cases
for sign in [ - 1 , 1 ] :
# Ensure we wind down factor units for every unit above allowance
driver_torque = ( self . DRIVER_TORQUE_ALLOWANCE + 10 ) * sign
torque_desired = ( max_torque - 10 * self . DRIVER_TORQUE_FACTOR ) * sign
delta = 1 * sign
self . _set_prev_torque ( torque_desired )
self . _reset_torque_driver_measurement ( - driver_torque )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( torque_desired ) ) )
self . _set_prev_torque ( torque_desired + delta )
self . _reset_torque_driver_measurement ( - driver_torque )
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( torque_desired + delta ) ) )
# If we're well past the allowance, minimum wind down is MAX_RATE_DOWN
self . _set_prev_torque ( max_torque * sign )
self . _reset_torque_driver_measurement ( - max_driver_torque * sign )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( ( max_torque - self . MAX_RATE_DOWN ) * sign ) ) )
self . _set_prev_torque ( max_torque * sign )
self . _reset_torque_driver_measurement ( - max_driver_torque * sign )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( 0 ) ) )
self . _set_prev_torque ( max_torque * sign )
self . _reset_torque_driver_measurement ( - max_driver_torque * sign )
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( ( max_torque - self . MAX_RATE_DOWN + 1 ) * sign ) ) )
def test_realtime_limits ( self ) :
self . safety . set_controls_allowed ( True )
for sign in [ - 1 , 1 ] :
self . safety . init_tests ( )
self . _set_prev_torque ( 0 )
self . _reset_torque_driver_measurement ( 0 )
for t in np . arange ( 0 , self . MAX_RT_DELTA , 1 ) :
t * = sign
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( t ) ) )
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( sign * ( self . MAX_RT_DELTA + 1 ) ) ) )
self . _set_prev_torque ( 0 )
for t in np . arange ( 0 , self . MAX_RT_DELTA , 1 ) :
t * = sign
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( t ) ) )
# Increase timer to update rt_torque_last
self . safety . set_timer ( self . RT_INTERVAL + 1 )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( sign * ( self . MAX_RT_DELTA - 1 ) ) ) )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( sign * ( self . MAX_RT_DELTA + 1 ) ) ) )
def test_reset_driver_torque_measurements ( self ) :
# Tests that the driver torque measurement sample_t is reset on safety mode init
for t in np . linspace ( - self . MAX_TORQUE , self . MAX_TORQUE , MAX_SAMPLE_VALS ) :
self . assertTrue ( self . _rx ( self . _torque_driver_msg ( t ) ) )
self . assertNotEqual ( self . safety . get_torque_driver_min ( ) , 0 )
self . assertNotEqual ( self . safety . get_torque_driver_max ( ) , 0 )
self . _reset_safety_hooks ( )
self . assertEqual ( self . safety . get_torque_driver_min ( ) , 0 )
self . assertEqual ( self . safety . get_torque_driver_max ( ) , 0 )
class MotorTorqueSteeringSafetyTest ( TorqueSteeringSafetyTestBase , abc . ABC ) :
MAX_TORQUE_ERROR = 0
TORQUE_MEAS_TOLERANCE = 0
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " MotorTorqueSteeringSafetyTest " :
cls . safety = None
raise unittest . SkipTest
@abc . abstractmethod
def _torque_meas_msg ( self , torque ) :
pass
def _set_prev_torque ( self , t ) :
super ( ) . _set_prev_torque ( t )
self . safety . set_torque_meas ( t , t )
def test_torque_absolute_limits ( self ) :
for speed in self . _torque_speed_range :
self . _reset_speed_measurement ( speed )
max_torque = self . _get_max_torque ( speed )
for controls_allowed in [ True , False ] :
for torque in np . arange ( - max_torque - 1000 , max_torque + 1000 , self . MAX_RATE_UP ) :
self . safety . set_controls_allowed ( controls_allowed )
self . safety . set_rt_torque_last ( torque )
self . safety . set_torque_meas ( torque , torque )
self . safety . set_desired_torque_last ( torque - self . MAX_RATE_UP )
if controls_allowed :
send = ( - max_torque < = torque < = max_torque )
else :
send = torque == 0
self . assertEqual ( send , self . _tx ( self . _torque_cmd_msg ( torque ) ) )
def test_non_realtime_limit_down ( self ) :
self . safety . set_controls_allowed ( True )
for speed in self . _torque_speed_range :
self . _reset_speed_measurement ( speed )
max_torque = self . _get_max_torque ( speed )
torque_meas = max_torque - self . MAX_TORQUE_ERROR - 50
self . safety . set_rt_torque_last ( max_torque )
self . safety . set_torque_meas ( torque_meas , torque_meas )
self . safety . set_desired_torque_last ( max_torque )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( max_torque - self . MAX_RATE_DOWN ) ) )
self . safety . set_rt_torque_last ( max_torque )
self . safety . set_torque_meas ( torque_meas , torque_meas )
self . safety . set_desired_torque_last ( max_torque )
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( max_torque - self . MAX_RATE_DOWN + 1 ) ) )
def test_exceed_torque_sensor ( self ) :
self . safety . set_controls_allowed ( True )
for sign in [ - 1 , 1 ] :
self . _set_prev_torque ( 0 )
for t in np . arange ( 0 , self . MAX_TORQUE_ERROR + 2 , 2 ) : # step needs to be smaller than MAX_TORQUE_ERROR
t * = sign
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( t ) ) )
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( sign * ( self . MAX_TORQUE_ERROR + 2 ) ) ) )
def test_realtime_limit_up ( self ) :
self . safety . set_controls_allowed ( True )
for sign in [ - 1 , 1 ] :
self . safety . init_tests ( )
self . _set_prev_torque ( 0 )
for t in np . arange ( 0 , self . MAX_RT_DELTA + 1 , 1 ) :
t * = sign
self . safety . set_torque_meas ( t , t )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( t ) ) )
self . assertFalse ( self . _tx ( self . _torque_cmd_msg ( sign * ( self . MAX_RT_DELTA + 1 ) ) ) )
self . _set_prev_torque ( 0 )
for t in np . arange ( 0 , self . MAX_RT_DELTA + 1 , 1 ) :
t * = sign
self . safety . set_torque_meas ( t , t )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( t ) ) )
# Increase timer to update rt_torque_last
self . safety . set_timer ( self . RT_INTERVAL + 1 )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( sign * self . MAX_RT_DELTA ) ) )
self . assertTrue ( self . _tx ( self . _torque_cmd_msg ( sign * ( self . MAX_RT_DELTA + 1 ) ) ) )
def test_torque_measurements ( self ) :
trq = 50
for t in [ trq , - trq , 0 , 0 , 0 , 0 ] :
self . _rx ( self . _torque_meas_msg ( t ) )
max_range = range ( trq , trq + self . TORQUE_MEAS_TOLERANCE + 1 )
min_range = range ( - ( trq + self . TORQUE_MEAS_TOLERANCE ) , - trq + 1 )
self . assertTrue ( self . safety . get_torque_meas_min ( ) in min_range )
self . assertTrue ( self . safety . get_torque_meas_max ( ) in max_range )
max_range = range ( self . TORQUE_MEAS_TOLERANCE + 1 )
min_range = range ( - ( trq + self . TORQUE_MEAS_TOLERANCE ) , - trq + 1 )
self . _rx ( self . _torque_meas_msg ( 0 ) )
self . assertTrue ( self . safety . get_torque_meas_min ( ) in min_range )
self . assertTrue ( self . safety . get_torque_meas_max ( ) in max_range )
max_range = range ( self . TORQUE_MEAS_TOLERANCE + 1 )
min_range = range ( - self . TORQUE_MEAS_TOLERANCE , 0 + 1 )
self . _rx ( self . _torque_meas_msg ( 0 ) )
self . assertTrue ( self . safety . get_torque_meas_min ( ) in min_range )
self . assertTrue ( self . safety . get_torque_meas_max ( ) in max_range )
def test_reset_torque_measurements ( self ) :
# Tests that the torque measurement sample_t is reset on safety mode init
for t in np . linspace ( - self . MAX_TORQUE , self . MAX_TORQUE , MAX_SAMPLE_VALS ) :
self . assertTrue ( self . _rx ( self . _torque_meas_msg ( t ) ) )
self . assertNotEqual ( self . safety . get_torque_meas_min ( ) , 0 )
self . assertNotEqual ( self . safety . get_torque_meas_max ( ) , 0 )
self . _reset_safety_hooks ( )
self . assertEqual ( self . safety . get_torque_meas_min ( ) , 0 )
self . assertEqual ( self . safety . get_torque_meas_max ( ) , 0 )
class VehicleSpeedSafetyTest ( PandaSafetyTestBase ) :
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " VehicleSpeedSafetyTest " :
cls . safety = None
raise unittest . SkipTest
@abc . abstractmethod
def _speed_msg ( self , speed ) :
pass
def test_vehicle_speed_measurements ( self ) :
# TODO: lower tolerance on these tests
self . _common_measurement_test ( self . _speed_msg , 0 , 80 , 1 , self . safety . get_vehicle_speed_min , self . safety . get_vehicle_speed_max )
class AngleSteeringSafetyTest ( VehicleSpeedSafetyTest ) :
STEER_ANGLE_MAX : float = 300
DEG_TO_CAN : float
ANGLE_RATE_BP : list [ float ]
ANGLE_RATE_UP : list [ float ] # windup limit
ANGLE_RATE_DOWN : list [ float ] # unwind limit
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " AngleSteeringSafetyTest " :
cls . safety = None
raise unittest . SkipTest
@abc . abstractmethod
def _angle_cmd_msg ( self , angle : float , enabled : bool ) :
pass
@abc . abstractmethod
def _angle_meas_msg ( self , angle : float ) :
pass
def _set_prev_desired_angle ( self , t ) :
t = round ( t * self . DEG_TO_CAN )
self . safety . set_desired_angle_last ( t )
def _reset_angle_measurement ( self , angle ) :
for _ in range ( MAX_SAMPLE_VALS ) :
self . _rx ( self . _angle_meas_msg ( angle ) )
def _reset_speed_measurement ( self , speed ) :
for _ in range ( MAX_SAMPLE_VALS ) :
self . _rx ( self . _speed_msg ( speed ) )
def test_steering_angle_measurements ( self ) :
self . _common_measurement_test ( self . _angle_meas_msg , - self . STEER_ANGLE_MAX , self . STEER_ANGLE_MAX , self . DEG_TO_CAN ,
self . safety . get_angle_meas_min , self . safety . get_angle_meas_max )
def test_angle_cmd_when_enabled ( self ) :
# when controls are allowed, angle cmd rate limit is enforced
speeds = [ 0. , 1. , 5. , 10. , 15. , 50. ]
angles = np . concatenate ( ( np . arange ( - self . STEER_ANGLE_MAX * 2 , self . STEER_ANGLE_MAX * 2 , 5 ) , [ 0 ] ) )
for a in angles :
for s in speeds :
max_delta_up = np . interp ( s , self . ANGLE_RATE_BP , self . ANGLE_RATE_UP )
max_delta_down = np . interp ( s , self . ANGLE_RATE_BP , self . ANGLE_RATE_DOWN )
# first test against false positives
self . _reset_angle_measurement ( a )
self . _reset_speed_measurement ( s )
self . _set_prev_desired_angle ( a )
self . safety . set_controls_allowed ( 1 )
# Stay within limits
# Up
self . assertTrue ( self . _tx ( self . _angle_cmd_msg ( a + sign_of ( a ) * max_delta_up , True ) ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
# Don't change
self . assertTrue ( self . _tx ( self . _angle_cmd_msg ( a , True ) ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
# Down
self . assertTrue ( self . _tx ( self . _angle_cmd_msg ( a - sign_of ( a ) * max_delta_down , True ) ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
# Inject too high rates
# Up
self . assertFalse ( self . _tx ( self . _angle_cmd_msg ( a + sign_of ( a ) * ( max_delta_up + 1.1 ) , True ) ) )
# Don't change
self . safety . set_controls_allowed ( 1 )
self . _set_prev_desired_angle ( a )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
self . assertTrue ( self . _tx ( self . _angle_cmd_msg ( a , True ) ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
# Down
self . assertFalse ( self . _tx ( self . _angle_cmd_msg ( a - sign_of ( a ) * ( max_delta_down + 1.1 ) , True ) ) )
# Check desired steer should be the same as steer angle when controls are off
self . safety . set_controls_allowed ( 0 )
should_tx = abs ( a ) < = abs ( self . STEER_ANGLE_MAX )
self . assertEqual ( should_tx , self . _tx ( self . _angle_cmd_msg ( a , False ) ) )
def test_angle_cmd_when_disabled ( self ) :
# Tests that only angles close to the meas are allowed while
# steer actuation bit is 0, regardless of controls allowed.
for controls_allowed in ( True , False ) :
self . safety . set_controls_allowed ( controls_allowed )
for steer_control_enabled in ( True , False ) :
for angle_meas in np . arange ( - 90 , 91 , 10 ) :
self . _reset_angle_measurement ( angle_meas )
for angle_cmd in np . arange ( - 90 , 91 , 10 ) :
self . _set_prev_desired_angle ( angle_cmd )
# controls_allowed is checked if actuation bit is 1, else the angle must be close to meas (inactive)
should_tx = controls_allowed if steer_control_enabled else angle_cmd == angle_meas
self . assertEqual ( should_tx , self . _tx ( self . _angle_cmd_msg ( angle_cmd , steer_control_enabled ) ) )
class PandaSafetyTest ( PandaSafetyTestBase ) :
TX_MSGS : list [ list [ int ] ] | None = None
SCANNED_ADDRS = [ * range ( 0x800 ) , # Entire 11-bit CAN address space
* range ( 0x18DA00F1 , 0x18DB00F1 , 0x100 ) , # 29-bit UDS physical addressing
* range ( 0x18DB00F1 , 0x18DC00F1 , 0x100 ) , # 29-bit UDS functional addressing
* range ( 0x3300 , 0x3400 ) ] # Honda
FWD_BLACKLISTED_ADDRS : dict [ int , list [ int ] ] = { } # {bus: [addr]}
FWD_BUS_LOOKUP : dict [ int , int ] = { 0 : 2 , 2 : 0 }
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " PandaSafetyTest " or cls . __name__ . endswith ( ' Base ' ) :
cls . safety = None
raise unittest . SkipTest
# ***** standard tests for all safety modes *****
def test_tx_msg_in_scanned_range ( self ) :
# the relay malfunction, fwd hook, and spam can tests don't exhaustively
# scan the entire 29-bit address space, only some known important ranges
# make sure SCANNED_ADDRS stays up to date with car port TX_MSGS; new
# model ports should expand the range if needed
for msg in self . TX_MSGS :
self . assertTrue ( msg [ 0 ] in self . SCANNED_ADDRS , f " { msg [ 0 ] =: #x } " )
def test_fwd_hook ( self ) :
# some safety modes don't forward anything, while others blacklist msgs
for bus in range ( 3 ) :
for addr in self . SCANNED_ADDRS :
# assume len 8
fwd_bus = self . FWD_BUS_LOOKUP . get ( bus , - 1 )
if bus in self . FWD_BLACKLISTED_ADDRS and addr in self . FWD_BLACKLISTED_ADDRS [ bus ] :
fwd_bus = - 1
self . assertEqual ( fwd_bus , self . safety . safety_fwd_hook ( bus , addr ) , f " { addr =: #x } from { bus =} to { fwd_bus =} " )
def test_spam_can_buses ( self ) :
for bus in range ( 4 ) :
for addr in self . SCANNED_ADDRS :
if [ addr , bus ] not in self . TX_MSGS :
self . assertFalse ( self . _tx ( make_msg ( bus , addr , 8 ) ) , f " allowed TX { addr =} { bus =} " )
def test_default_controls_not_allowed ( self ) :
self . assertFalse ( self . safety . get_controls_allowed ( ) )
def test_manually_enable_controls_allowed ( self ) :
self . safety . set_controls_allowed ( 1 )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
self . safety . set_controls_allowed ( 0 )
self . assertFalse ( self . safety . get_controls_allowed ( ) )
def test_tx_hook_on_wrong_safety_mode ( self ) :
files = os . listdir ( os . path . dirname ( os . path . realpath ( __file__ ) ) )
test_files = [ f for f in files if f . startswith ( " test_ " ) and f . endswith ( " .py " ) ]
current_test = self . __class__ . __name__
all_tx = [ ]
for tf in test_files :
test = importlib . import_module ( " opendbc.safety.tests. " + tf [ : - 3 ] )
for attr in dir ( test ) :
if attr . startswith ( " Test " ) and attr != current_test :
tc = getattr ( test , attr )
tx = tc . TX_MSGS
if tx is not None and not attr . endswith ( ' Base ' ) :
# No point in comparing different Tesla safety modes
if ' Tesla ' in attr and ' Tesla ' in current_test :
continue
# No point in comparing to ALLOUTPUT which allows all messages
if attr . startswith ( ' TestAllOutput ' ) :
continue
if attr . startswith ( ' TestToyota ' ) and current_test . startswith ( ' TestToyota ' ) :
continue
if attr . startswith ( ' TestSubaruGen ' ) and current_test . startswith ( ' TestSubaruGen ' ) :
continue
if attr . startswith ( ' TestSubaruPreglobal ' ) and current_test . startswith ( ' TestSubaruPreglobal ' ) :
continue
if { attr , current_test } . issubset ( { ' TestVolkswagenPqSafety ' , ' TestVolkswagenPqStockSafety ' , ' TestVolkswagenPqLongSafety ' } ) :
continue
if { attr , current_test } . issubset ( { ' TestGmCameraSafety ' , ' TestGmCameraLongitudinalSafety ' , ' TestGmAscmSafety ' ,
' TestGmCameraEVSafety ' , ' TestGmCameraLongitudinalEVSafety ' , ' TestGmAscmEVSafety ' } ) :
continue
if attr . startswith ( ' TestFord ' ) and current_test . startswith ( ' TestFord ' ) :
continue
if attr . startswith ( ' TestHyundaiCanfd ' ) and current_test . startswith ( ' TestHyundaiCanfd ' ) :
continue
if { attr , current_test } . issubset ( { ' TestHyundaiLongitudinalSafety ' , ' TestHyundaiLongitudinalSafetyCameraSCC ' , ' TestHyundaiSafetyFCEVLong ' } ) :
continue
if { attr , current_test } . issubset ( { ' TestVolkswagenMqbSafety ' , ' TestVolkswagenMqbStockSafety ' , ' TestVolkswagenMqbLongSafety ' } ) :
continue
# overlapping TX addrs, but they're not actuating messages for either car
if attr == ' TestHyundaiCanfdLKASteeringLongEV ' and current_test . startswith ( ' TestToyota ' ) :
tx = list ( filter ( lambda m : m [ 0 ] not in [ 0x160 , ] , tx ) )
# Volkswagen MQB longitudinal actuating message overlaps with the Subaru lateral actuating message
if attr == ' TestVolkswagenMqbLongSafety ' and current_test . startswith ( ' TestSubaru ' ) :
tx = list ( filter ( lambda m : m [ 0 ] not in [ 0x122 , ] , tx ) )
# Volkswagen MQB and Honda Nidec ACC HUD messages overlap
if attr == ' TestVolkswagenMqbLongSafety ' and current_test . startswith ( ' TestHondaNidec ' ) :
tx = list ( filter ( lambda m : m [ 0 ] not in [ 0x30c , ] , tx ) )
# Volkswagen MQB and Honda Bosch Radarless ACC HUD messages overlap
if attr == ' TestVolkswagenMqbLongSafety ' and current_test . startswith ( ' TestHondaBoschRadarless ' ) :
tx = list ( filter ( lambda m : m [ 0 ] not in [ 0x30c , ] , tx ) )
# TODO: Temporary, should be fixed in panda firmware, safety_honda.h
if attr . startswith ( ' TestHonda ' ) :
# exceptions for common msgs across different hondas
tx = list ( filter ( lambda m : m [ 0 ] not in [ 0x1FA , 0x30C , 0x33D , 0x33DB ] , tx ) )
if attr . startswith ( ' TestHyundaiLongitudinal ' ) :
# exceptions for common msgs across different Hyundai CAN platforms
tx = list ( filter ( lambda m : m [ 0 ] not in [ 0x420 , 0x50A , 0x389 , 0x4A2 ] , tx ) )
all_tx . append ( [ [ m [ 0 ] , m [ 1 ] , attr ] for m in tx ] )
# make sure we got all the msgs
self . assertTrue ( len ( all_tx ) > = len ( test_files ) - 1 )
for tx_msgs in all_tx :
for addr , bus , test_name in tx_msgs :
msg = make_msg ( bus , addr )
self . safety . set_controls_allowed ( 1 )
# TODO: this should be blocked
if current_test in [ " TestNissanSafety " , " TestNissanSafetyAltEpsBus " , " TestNissanLeafSafety " ] and [ addr , bus ] in self . TX_MSGS :
continue
self . assertFalse ( self . _tx ( msg ) , f " transmit of { addr =: #x } { bus =} from { test_name } during { current_test } was allowed " )
@add_regen_tests
class PandaCarSafetyTest ( PandaSafetyTest ) :
STANDSTILL_THRESHOLD : float = 0.0
GAS_PRESSED_THRESHOLD = 0
RELAY_MALFUNCTION_ADDRS : dict [ int , tuple [ int , . . . ] ] | None = None
@classmethod
def setUpClass ( cls ) :
if cls . __name__ == " PandaCarSafetyTest " or cls . __name__ . endswith ( ' Base ' ) :
cls . safety = None
raise unittest . SkipTest
@abc . abstractmethod
def _user_brake_msg ( self , brake ) :
pass
def _user_regen_msg ( self , regen ) :
pass
@abc . abstractmethod
def _speed_msg ( self , speed ) :
pass
# Safety modes can override if vehicle_moving is driven by a different message
def _vehicle_moving_msg ( self , speed : float ) :
return self . _speed_msg ( speed )
@abc . abstractmethod
def _user_gas_msg ( self , gas ) :
pass
@abc . abstractmethod
def _pcm_status_msg ( self , enable ) :
pass
# ***** standard tests for all car-specific safety modes *****
def test_relay_malfunction ( self ) :
# each car has an addr that is used to detect relay malfunction
# if that addr is seen on specified bus, triggers the relay malfunction
# protection logic: both tx_hook and fwd_hook are expected to return failure
self . assertFalse ( self . safety . get_relay_malfunction ( ) )
for bus in range ( 3 ) :
for addr in self . SCANNED_ADDRS :
self . safety . set_relay_malfunction ( False )
self . _rx ( make_msg ( bus , addr , 8 ) )
should_relay_malfunction = addr in self . RELAY_MALFUNCTION_ADDRS . get ( bus , ( ) )
self . assertEqual ( should_relay_malfunction , self . safety . get_relay_malfunction ( ) , ( bus , hex ( addr ) ) )
# test relay malfunction protection logic
self . safety . set_relay_malfunction ( True )
for bus in range ( 3 ) :
for addr in self . SCANNED_ADDRS :
self . assertFalse ( self . _tx ( make_msg ( bus , addr , 8 ) ) )
self . assertEqual ( - 1 , self . safety . safety_fwd_hook ( bus , addr ) )
def test_prev_gas ( self ) :
self . assertFalse ( self . safety . get_gas_pressed_prev ( ) )
for pressed in [ self . GAS_PRESSED_THRESHOLD + 1 , 0 ] :
self . _rx ( self . _user_gas_msg ( pressed ) )
self . assertEqual ( bool ( pressed ) , self . safety . get_gas_pressed_prev ( ) )
def test_allow_engage_with_gas_pressed ( self ) :
self . _rx ( self . _user_gas_msg ( 1 ) )
self . safety . set_controls_allowed ( True )
self . _rx ( self . _user_gas_msg ( 1 ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
self . _rx ( self . _user_gas_msg ( 1 ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
def test_no_disengage_on_gas ( self ) :
self . _rx ( self . _user_gas_msg ( 0 ) )
self . safety . set_controls_allowed ( True )
self . _rx ( self . _user_gas_msg ( self . GAS_PRESSED_THRESHOLD + 1 ) )
# Test we allow lateral, but not longitudinal
self . assertTrue ( self . safety . get_controls_allowed ( ) )
self . assertFalse ( self . safety . get_longitudinal_allowed ( ) )
# Make sure we can re-gain longitudinal actuation
self . _rx ( self . _user_gas_msg ( 0 ) )
self . assertTrue ( self . safety . get_longitudinal_allowed ( ) )
def test_prev_user_brake ( self , _user_brake_msg = None , get_brake_pressed_prev = None ) :
if _user_brake_msg is None :
_user_brake_msg = self . _user_brake_msg
get_brake_pressed_prev = self . safety . get_brake_pressed_prev
self . assertFalse ( get_brake_pressed_prev ( ) )
for pressed in [ True , False ] :
self . _rx ( _user_brake_msg ( not pressed ) )
self . assertEqual ( not pressed , get_brake_pressed_prev ( ) )
self . _rx ( _user_brake_msg ( pressed ) )
self . assertEqual ( pressed , get_brake_pressed_prev ( ) )
def test_enable_control_allowed_from_cruise ( self ) :
self . _rx ( self . _pcm_status_msg ( False ) )
self . assertFalse ( self . safety . get_controls_allowed ( ) )
self . _rx ( self . _pcm_status_msg ( True ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
def test_disable_control_allowed_from_cruise ( self ) :
self . safety . set_controls_allowed ( 1 )
self . _rx ( self . _pcm_status_msg ( False ) )
self . assertFalse ( self . safety . get_controls_allowed ( ) )
def test_cruise_engaged_prev ( self ) :
for engaged in [ True , False ] :
self . _rx ( self . _pcm_status_msg ( engaged ) )
self . assertEqual ( engaged , self . safety . get_cruise_engaged_prev ( ) )
self . _rx ( self . _pcm_status_msg ( not engaged ) )
self . assertEqual ( not engaged , self . safety . get_cruise_engaged_prev ( ) )
def test_allow_user_brake_at_zero_speed ( self , _user_brake_msg = None , get_brake_pressed_prev = None ) :
if _user_brake_msg is None :
_user_brake_msg = self . _user_brake_msg
# Brake was already pressed
self . _rx ( self . _vehicle_moving_msg ( 0 ) )
self . _rx ( _user_brake_msg ( 1 ) )
self . safety . set_controls_allowed ( 1 )
self . _rx ( _user_brake_msg ( 1 ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
self . assertTrue ( self . safety . get_longitudinal_allowed ( ) )
self . _rx ( _user_brake_msg ( 0 ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
self . assertTrue ( self . safety . get_longitudinal_allowed ( ) )
# rising edge of brake should disengage
self . _rx ( _user_brake_msg ( 1 ) )
self . assertFalse ( self . safety . get_controls_allowed ( ) )
self . assertFalse ( self . safety . get_longitudinal_allowed ( ) )
self . _rx ( _user_brake_msg ( 0 ) ) # reset no brakes
def test_not_allow_user_brake_when_moving ( self , _user_brake_msg = None , get_brake_pressed_prev = None ) :
if _user_brake_msg is None :
_user_brake_msg = self . _user_brake_msg
# Brake was already pressed
self . _rx ( _user_brake_msg ( 1 ) )
self . safety . set_controls_allowed ( 1 )
self . _rx ( self . _vehicle_moving_msg ( self . STANDSTILL_THRESHOLD ) )
self . _rx ( _user_brake_msg ( 1 ) )
self . assertTrue ( self . safety . get_controls_allowed ( ) )
self . assertTrue ( self . safety . get_longitudinal_allowed ( ) )
self . _rx ( self . _vehicle_moving_msg ( self . STANDSTILL_THRESHOLD + 1 ) )
self . _rx ( _user_brake_msg ( 1 ) )
self . assertFalse ( self . safety . get_controls_allowed ( ) )
self . assertFalse ( self . safety . get_longitudinal_allowed ( ) )
self . _rx ( self . _vehicle_moving_msg ( 0 ) )
def test_vehicle_moving ( self ) :
self . assertFalse ( self . safety . get_vehicle_moving ( ) )
# not moving
self . _rx ( self . _vehicle_moving_msg ( 0 ) )
self . assertFalse ( self . safety . get_vehicle_moving ( ) )
# speed is at threshold
self . _rx ( self . _vehicle_moving_msg ( self . STANDSTILL_THRESHOLD ) )
self . assertFalse ( self . safety . get_vehicle_moving ( ) )
# past threshold
self . _rx ( self . _vehicle_moving_msg ( self . STANDSTILL_THRESHOLD + 1 ) )
self . assertTrue ( self . safety . get_vehicle_moving ( ) )
def test_safety_tick ( self ) :
self . safety . set_timer ( int ( 2e6 ) )
self . safety . set_controls_allowed ( True )
self . safety . safety_tick_current_safety_config ( )
self . assertFalse ( self . safety . get_controls_allowed ( ) )
self . assertFalse ( self . safety . safety_config_valid ( ) )