version: sunnypilot v2026.002.000 (staging) date: 2026-07-21T07:44:02 master commit: 2c334ede443d7391d27575af8c854a095ba702a8
543 lines
25 KiB
Python
Executable File
543 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import numpy as np
|
|
import random
|
|
import unittest
|
|
|
|
import opendbc.safety.tests.common as common
|
|
from opendbc.car.lateral import MAX_LATERAL_ACCEL, MAX_LATERAL_JERK
|
|
from opendbc.car.ford.values import FordSafetyFlags
|
|
from opendbc.car.structs import CarParams
|
|
from opendbc.safety.tests.libsafety import libsafety_py
|
|
from opendbc.safety.tests.common import CANPackerSafety
|
|
|
|
MSG_EngBrakeData = 0x165 # RX from PCM, for driver brake pedal and cruise state
|
|
MSG_EngVehicleSpThrottle = 0x204 # RX from PCM, for driver throttle input
|
|
MSG_BrakeSysFeatures = 0x415 # RX from ABS, for vehicle speed
|
|
MSG_EngVehicleSpThrottle2 = 0x202 # RX from PCM, for second vehicle speed
|
|
MSG_Yaw_Data_FD1 = 0x91 # RX from RCM, for yaw rate
|
|
MSG_Steering_Data_FD1 = 0x083 # TX by OP, various driver switches and LKAS/CC buttons
|
|
MSG_ACCDATA = 0x186 # TX by OP, ACC controls
|
|
MSG_ACCDATA_3 = 0x18A # TX by OP, ACC/TJA user interface
|
|
MSG_Lane_Assist_Data1 = 0x3CA # TX by OP, Lane Keep Assist
|
|
MSG_LateralMotionControl = 0x3D3 # TX by OP, Lateral Control message
|
|
MSG_LateralMotionControl2 = 0x3D6 # TX by OP, alternate Lateral Control message
|
|
MSG_IPMA_Data = 0x3D8 # TX by OP, IPMA and LKAS user interface
|
|
|
|
|
|
def checksum(msg):
|
|
addr, dat, bus = msg
|
|
ret = bytearray(dat)
|
|
|
|
if addr == MSG_Yaw_Data_FD1:
|
|
chksum = dat[0] + dat[1] # VehRol_W_Actl
|
|
chksum += dat[2] + dat[3] # VehYaw_W_Actl
|
|
chksum += dat[5] # VehRollYaw_No_Cnt
|
|
chksum += dat[6] >> 6 # VehRolWActl_D_Qf
|
|
chksum += (dat[6] >> 4) & 0x3 # VehYawWActl_D_Qf
|
|
chksum = 0xff - (chksum & 0xff)
|
|
ret[4] = chksum
|
|
|
|
elif addr == MSG_BrakeSysFeatures:
|
|
chksum = dat[0] + dat[1] # Veh_V_ActlBrk
|
|
chksum += (dat[2] >> 2) & 0xf # VehVActlBrk_No_Cnt
|
|
chksum += dat[2] >> 6 # VehVActlBrk_D_Qf
|
|
chksum = 0xff - (chksum & 0xff)
|
|
ret[3] = chksum
|
|
|
|
elif addr == MSG_EngVehicleSpThrottle2:
|
|
chksum = (dat[2] >> 3) & 0xf # VehVActlEng_No_Cnt
|
|
chksum += (dat[4] >> 5) & 0x3 # VehVActlEng_D_Qf
|
|
chksum += dat[6] + dat[7] # Veh_V_ActlEng
|
|
chksum = 0xff - (chksum & 0xff)
|
|
ret[1] = chksum
|
|
|
|
return addr, ret, bus
|
|
|
|
|
|
class Buttons:
|
|
CANCEL = 0
|
|
RESUME = 1
|
|
TJA_TOGGLE = 2
|
|
|
|
|
|
# Ford safety has four different configurations tested here:
|
|
# * CAN with openpilot longitudinal
|
|
# * CAN FD with stock longitudinal
|
|
# * CAN FD with openpilot longitudinal
|
|
|
|
class TestFordSafetyBase(common.CarSafetyTest):
|
|
STANDSTILL_THRESHOLD = 1
|
|
RELAY_MALFUNCTION_ADDRS = {0: (MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl,
|
|
MSG_LateralMotionControl2, MSG_IPMA_Data)}
|
|
|
|
FWD_BLACKLISTED_ADDRS = {2: [MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl,
|
|
MSG_LateralMotionControl2, MSG_IPMA_Data]}
|
|
|
|
STEER_MESSAGE = 0
|
|
|
|
# Curvature control limits
|
|
DEG_TO_CAN = 50000 # CAN units per rad/m
|
|
MAX_CURVATURE = 0.02 # rad/m, 1000 CAN units
|
|
MAX_CURVATURE_ERROR = 0.002 # rad/m, 100 CAN units
|
|
CURVATURE_ERROR_MIN_SPEED = 10.0 # m/s
|
|
LATERAL_FREQUENCY = 20 # Hz, for per-frame jerk limit
|
|
|
|
cnt_speed = 0
|
|
cnt_speed_2 = 0
|
|
cnt_yaw_rate = 0
|
|
cnt_lat_ctl = 0
|
|
|
|
packer: CANPackerSafety
|
|
safety: libsafety_py.LibSafety
|
|
|
|
def _get_max_curvature_can(self, speed):
|
|
fudged_speed = max(speed - 1.0, 1.0)
|
|
return int(MAX_LATERAL_ACCEL / (fudged_speed * fudged_speed) * self.DEG_TO_CAN) + 1
|
|
|
|
def _get_max_curvature_delta_can(self, speed):
|
|
fudged_speed = max(speed - 1.0, 1.0)
|
|
return int(MAX_LATERAL_JERK / (fudged_speed * fudged_speed) / self.LATERAL_FREQUENCY * self.DEG_TO_CAN) + 1
|
|
|
|
def _set_prev_desired_angle(self, t):
|
|
t = round(t * self.DEG_TO_CAN)
|
|
self.safety.set_desired_curvature_last(t)
|
|
|
|
def _reset_curvature_measurement(self, curvature, speed):
|
|
for _ in range(6):
|
|
self._rx(self._speed_msg(speed))
|
|
self._rx(self._speed_msg_2(speed))
|
|
self._rx(self._yaw_rate_msg(curvature, speed))
|
|
|
|
# Driver brake pedal
|
|
def _user_brake_msg(self, brake: bool):
|
|
# brake pedal and cruise state share same message, so we have to send
|
|
# the other signal too
|
|
enable = self.safety.get_controls_allowed()
|
|
values = {
|
|
"BpedDrvAppl_D_Actl": 2 if brake else 1,
|
|
"CcStat_D_Actl": 5 if enable else 0,
|
|
}
|
|
return self.packer.make_can_msg_safety("EngBrakeData", 0, values)
|
|
|
|
# ABS vehicle speed
|
|
def _speed_msg(self, speed: float, quality_flag=True):
|
|
values = {"Veh_V_ActlBrk": speed * 3.6, "VehVActlBrk_D_Qf": 3 if quality_flag else 0, "VehVActlBrk_No_Cnt": self.cnt_speed % 16}
|
|
self.__class__.cnt_speed += 1
|
|
return self.packer.make_can_msg_safety("BrakeSysFeatures", 0, values, fix_checksum=checksum)
|
|
|
|
# PCM vehicle speed
|
|
def _speed_msg_2(self, speed: float, quality_flag=True):
|
|
# Ford relies on speed for driver curvature limiting, so it checks two sources
|
|
values = {"Veh_V_ActlEng": speed * 3.6, "VehVActlEng_D_Qf": 3 if quality_flag else 0, "VehVActlEng_No_Cnt": self.cnt_speed_2 % 16}
|
|
self.__class__.cnt_speed_2 += 1
|
|
return self.packer.make_can_msg_safety("EngVehicleSpThrottle2", 0, values, fix_checksum=checksum)
|
|
|
|
# Standstill state
|
|
def _vehicle_moving_msg(self, speed: float):
|
|
values = {"VehStop_D_Stat": 1 if speed <= self.STANDSTILL_THRESHOLD else random.choice((0, 2, 3))}
|
|
return self.packer.make_can_msg_safety("DesiredTorqBrk", 0, values)
|
|
|
|
# Current curvature
|
|
def _yaw_rate_msg(self, curvature: float, speed: float, quality_flag=True):
|
|
values = {"VehYaw_W_Actl": curvature * speed, "VehYawWActl_D_Qf": 3 if quality_flag else 0,
|
|
"VehRollYaw_No_Cnt": self.cnt_yaw_rate % 256}
|
|
self.__class__.cnt_yaw_rate += 1
|
|
return self.packer.make_can_msg_safety("Yaw_Data_FD1", 0, values, fix_checksum=checksum)
|
|
|
|
# Drive throttle input
|
|
def _user_gas_msg(self, gas: float):
|
|
values = {"ApedPos_Pc_ActlArb": gas}
|
|
return self.packer.make_can_msg_safety("EngVehicleSpThrottle", 0, values)
|
|
|
|
# Cruise status
|
|
def _pcm_status_msg(self, enable: bool):
|
|
# brake pedal and cruise state share same message, so we have to send
|
|
# the other signal too
|
|
brake = self.safety.get_brake_pressed_prev()
|
|
values = {
|
|
"BpedDrvAppl_D_Actl": 2 if brake else 1,
|
|
"CcStat_D_Actl": 5 if enable else 0,
|
|
}
|
|
return self.packer.make_can_msg_safety("EngBrakeData", 0, values)
|
|
|
|
# LKAS command
|
|
def _lkas_command_msg(self, action: int):
|
|
values = {
|
|
"LkaActvStats_D2_Req": action,
|
|
}
|
|
return self.packer.make_can_msg_safety("Lane_Assist_Data1", 0, values)
|
|
|
|
# LCA command
|
|
def _lat_ctl_msg(self, enabled: bool, path_offset: float, path_angle: float, curvature: float, curvature_rate: float,
|
|
increment_timer: bool = True):
|
|
if increment_timer:
|
|
self.safety.set_timer(self.cnt_lat_ctl * int(1e6 / self.LATERAL_FREQUENCY))
|
|
self.__class__.cnt_lat_ctl += 1
|
|
if self.STEER_MESSAGE == MSG_LateralMotionControl:
|
|
values = {
|
|
"LatCtl_D_Rq": 1 if enabled else 0,
|
|
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
|
|
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
|
|
"LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2
|
|
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
|
|
}
|
|
return self.packer.make_can_msg_safety("LateralMotionControl", 0, values)
|
|
elif self.STEER_MESSAGE == MSG_LateralMotionControl2:
|
|
values = {
|
|
"LatCtl_D2_Rq": 1 if enabled else 0,
|
|
"LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
|
|
"LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
|
|
"LatCtlCrv_NoRate2_Actl": curvature_rate, # Curvature rate [-0.001024|0.001023] 1/meter^2
|
|
"LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
|
|
}
|
|
return self.packer.make_can_msg_safety("LateralMotionControl2", 0, values)
|
|
|
|
# Cruise control buttons
|
|
def _acc_button_msg(self, button: int, bus: int):
|
|
values = {
|
|
"CcAslButtnCnclPress": 1 if button == Buttons.CANCEL else 0,
|
|
"CcAsllButtnResPress": 1 if button == Buttons.RESUME else 0,
|
|
"TjaButtnOnOffPress": 1 if button == Buttons.TJA_TOGGLE else 0,
|
|
}
|
|
return self.packer.make_can_msg_safety("Steering_Data_FD1", bus, values)
|
|
|
|
def test_rx_hook_speed_mismatch(self):
|
|
for speed in np.arange(0, 40, 0.5):
|
|
for speed_delta in np.arange(-5, 5, 0.1):
|
|
speed_2 = round(max(speed + speed_delta, 0), 1)
|
|
self._rx(self._speed_msg(speed))
|
|
self._rx(self._speed_msg_2(speed_2))
|
|
self.safety.set_controls_allowed(True)
|
|
self._tx(self._lat_ctl_msg(True, 0, 0, 0, 0))
|
|
|
|
within_delta = abs(speed - speed_2) <= common.MAX_SPEED_DELTA
|
|
self.assertEqual(self.safety.get_controls_allowed(), within_delta)
|
|
|
|
def test_rx_hook(self):
|
|
# checksum, counter, and quality flag checks
|
|
for quality_flag in [True, False]:
|
|
for msg_type in ["speed", "speed_2", "yaw"]:
|
|
self.safety.set_controls_allowed(True)
|
|
# send multiple times to verify counter checks
|
|
for _ in range(10):
|
|
if msg_type == "speed":
|
|
msg = self._speed_msg(0, quality_flag=quality_flag)
|
|
elif msg_type == "speed_2":
|
|
msg = self._speed_msg_2(0, quality_flag=quality_flag)
|
|
elif msg_type == "yaw":
|
|
msg = self._yaw_rate_msg(0, 0, quality_flag=quality_flag)
|
|
|
|
self.assertEqual(quality_flag, self._rx(msg))
|
|
self.assertEqual(quality_flag, self.safety.get_controls_allowed())
|
|
|
|
# Mess with checksum to make it fail, checksum is not checked for 2nd speed
|
|
msg[0].data[3] = 0 # Speed checksum & half of yaw signal
|
|
should_rx = msg_type == "speed_2" and quality_flag
|
|
self.assertEqual(should_rx, self._rx(msg))
|
|
self.assertEqual(should_rx, self.safety.get_controls_allowed())
|
|
|
|
def test_angle_measurements(self):
|
|
"""Tests rx hook correctly parses the curvature measurement from the vehicle speed and yaw rate"""
|
|
for speed in np.arange(0.5, 40, 0.5):
|
|
for curvature in np.arange(0, self.MAX_CURVATURE * 2, 2e-3):
|
|
self._rx(self._speed_msg(speed))
|
|
for c in (curvature, -curvature, 0, 0, 0, 0):
|
|
self._rx(self._yaw_rate_msg(c, speed))
|
|
|
|
self.assertEqual(self.safety.get_curvature_meas_min(), round(-curvature * self.DEG_TO_CAN))
|
|
self.assertEqual(self.safety.get_curvature_meas_max(), round(curvature * self.DEG_TO_CAN))
|
|
|
|
self._rx(self._yaw_rate_msg(0, speed))
|
|
self.assertEqual(self.safety.get_curvature_meas_min(), round(-curvature * self.DEG_TO_CAN))
|
|
self.assertEqual(self.safety.get_curvature_meas_max(), 0)
|
|
|
|
self._rx(self._yaw_rate_msg(0, speed))
|
|
self.assertEqual(self.safety.get_curvature_meas_min(), 0)
|
|
self.assertEqual(self.safety.get_curvature_meas_max(), 0)
|
|
|
|
def test_max_lateral_acceleration(self):
|
|
# Ford CAN FD can achieve a higher max lateral acceleration than CAN so we limit curvature based on speed
|
|
max_curvature_can = round(self.MAX_CURVATURE * self.DEG_TO_CAN)
|
|
for speed in np.arange(0, 40, 0.5):
|
|
max_can = min(self._get_max_curvature_can(speed), max_curvature_can)
|
|
for offset in (-5, -1, 0, 1, 5):
|
|
curvature_can = max_can + offset
|
|
curvature = curvature_can / self.DEG_TO_CAN
|
|
|
|
for sign in (-1, 1):
|
|
signed_curvature = sign * curvature
|
|
self.safety.set_controls_allowed(True)
|
|
self._set_prev_desired_angle(signed_curvature)
|
|
self._reset_curvature_measurement(signed_curvature, speed)
|
|
|
|
should_tx = abs(curvature_can) <= max_can
|
|
self.assertEqual(should_tx, self._tx(self._lat_ctl_msg(True, 0, 0, signed_curvature, 0)))
|
|
|
|
def test_steer_allowed(self):
|
|
path_offsets = np.arange(-5.12, 5.11, 2.5).round()
|
|
path_angles = np.arange(-0.5, 0.5235, 0.25).round(1)
|
|
curvature_rates = np.arange(-0.001024, 0.00102375, 0.001).round(3)
|
|
curvatures = np.arange(-0.02, 0.02094, 0.01).round(2)
|
|
|
|
for speed in (self.CURVATURE_ERROR_MIN_SPEED - 1,
|
|
self.CURVATURE_ERROR_MIN_SPEED + 1):
|
|
max_curvature_can = self._get_max_curvature_can(speed)
|
|
for controls_allowed in (True, False):
|
|
for steer_control_enabled in (True, False):
|
|
for path_offset in path_offsets:
|
|
for path_angle in path_angles:
|
|
for curvature_rate in curvature_rates:
|
|
for curvature in curvatures:
|
|
self.safety.set_controls_allowed(controls_allowed)
|
|
self._set_prev_desired_angle(curvature)
|
|
self._reset_curvature_measurement(curvature, speed)
|
|
|
|
should_tx = path_offset == 0 and path_angle == 0 and curvature_rate == 0
|
|
# when request bit is 0, only allow curvature of 0 since the signal range
|
|
# is not large enough to enforce it tracking measured
|
|
should_tx = should_tx and (controls_allowed if steer_control_enabled else curvature == 0)
|
|
should_tx = should_tx and abs(round(curvature * self.DEG_TO_CAN)) <= max_curvature_can
|
|
|
|
with self.subTest(controls_allowed=controls_allowed, steer_control_enabled=steer_control_enabled,
|
|
path_offset=float(path_offset), path_angle=float(path_angle), curvature_rate=float(curvature_rate),
|
|
curvature=float(curvature)):
|
|
self.assertEqual(should_tx, self._tx(self._lat_ctl_msg(steer_control_enabled, path_offset, path_angle, curvature, curvature_rate)))
|
|
|
|
def test_curvature_rate_limits(self):
|
|
"""Curvature command must satisfy the ISO 11270 lateral jerk limit per frame."""
|
|
self.safety.set_controls_allowed(True)
|
|
max_encoded_can = int(self.MAX_CURVATURE * self.DEG_TO_CAN)
|
|
|
|
for speed in np.arange(0, 40, 0.5):
|
|
max_can = self._get_max_curvature_can(speed)
|
|
max_delta_can = self._get_max_curvature_delta_can(speed)
|
|
base_can = max_delta_can * 2
|
|
if base_can + max_delta_can + 5 > min(max_can, max_encoded_can):
|
|
continue
|
|
|
|
# (should_tx, desired_curvature_can) relative to base_can
|
|
cases = [
|
|
(True, max_delta_can), # at jerk limit
|
|
(False, max_delta_can + 5), # over jerk limit
|
|
]
|
|
|
|
for sign in (-1, 1):
|
|
self._reset_curvature_measurement(sign * base_can / self.DEG_TO_CAN, speed)
|
|
for should_tx, delta_can in cases:
|
|
self.safety.set_desired_curvature_last(sign * base_can)
|
|
self.assertEqual(should_tx, self._tx(self._lat_ctl_msg(True, 0, 0, sign * (base_can + delta_can) / self.DEG_TO_CAN, 0)))
|
|
|
|
def test_curvature_error_limits(self):
|
|
# above CURVATURE_ERROR_MIN_SPEED, command must be within max_curvature_error of measured, below the check is skipped
|
|
self.safety.set_controls_allowed(True)
|
|
max_error_can = round(self.MAX_CURVATURE_ERROR * self.DEG_TO_CAN)
|
|
|
|
for speed in (self.CURVATURE_ERROR_MIN_SPEED - 1, self.CURVATURE_ERROR_MIN_SPEED + 1):
|
|
limit_command = speed > self.CURVATURE_ERROR_MIN_SPEED
|
|
self._reset_curvature_measurement(0, speed)
|
|
for sign in (-1, 1):
|
|
self.safety.set_desired_curvature_last(sign * max_error_can)
|
|
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0, 0, sign * (max_error_can + 1) / self.DEG_TO_CAN, 0)))
|
|
self.safety.set_desired_curvature_last(sign * max_error_can)
|
|
self.assertEqual(not limit_command, self._tx(self._lat_ctl_msg(True, 0, 0, sign * (max_error_can + 2) / self.DEG_TO_CAN, 0)))
|
|
|
|
# newest speed sample gates the check
|
|
self._reset_curvature_measurement(0, self.CURVATURE_ERROR_MIN_SPEED - 1)
|
|
self._rx(self._speed_msg(self.CURVATURE_ERROR_MIN_SPEED + 1))
|
|
self.safety.set_desired_curvature_last(max_error_can)
|
|
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0, 0, (max_error_can + 2) / self.DEG_TO_CAN, 0)))
|
|
|
|
def test_curvature_violation(self):
|
|
# If violation occurs, curvature cmd is blocked until reset to 0
|
|
self.safety.set_controls_allowed(True)
|
|
speed = 25.
|
|
max_delta_can = self._get_max_curvature_delta_can(speed)
|
|
self._reset_curvature_measurement(0, speed)
|
|
|
|
self._tx(self._lat_ctl_msg(True, 0, 0, 0, 0))
|
|
over_curvature = (max_delta_can + 5) / self.DEG_TO_CAN
|
|
for _ in range(20):
|
|
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0, 0, over_curvature, 0)))
|
|
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0, 0, 0, 0)))
|
|
|
|
# prev tracks the commanded curvature on a passing tx (not reset to 0 every frame)
|
|
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0, 0, max_delta_can / self.DEG_TO_CAN, 0)))
|
|
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0, 0, 2 * max_delta_can / self.DEG_TO_CAN, 0)))
|
|
|
|
def test_rt_limits(self):
|
|
# send rate is limited over a rolling 250ms window split into two half-interval buckets
|
|
self.safety.set_controls_allowed(True)
|
|
self._reset_curvature_measurement(0, 0)
|
|
max_rt_msgs = int(self.LATERAL_FREQUENCY * common.RT_INTERVAL / 1e6 * 1.2 + 1)
|
|
half = common.RT_INTERVAL // 2
|
|
|
|
# too many messages within one window is blocked
|
|
self.safety.set_timer(0)
|
|
for i in range(max_rt_msgs * 2):
|
|
self.assertEqual(i <= max_rt_msgs, self._tx(self._lat_ctl_msg(True, 0, 0, 0, 0, increment_timer=False)))
|
|
|
|
# shift the overflow into the previous bucket
|
|
self.safety.set_timer(half)
|
|
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0, 0, 0, 0, increment_timer=False)))
|
|
|
|
# previous bucket still counts within the half interval
|
|
self.safety.set_timer(half + 2 * common.RT_INTERVAL // 5)
|
|
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0, 0, 0, 0, increment_timer=False)))
|
|
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0, 0, 0, 0, increment_timer=False)))
|
|
|
|
# both buckets clear after a full interval
|
|
self.safety.set_timer(half + common.RT_INTERVAL)
|
|
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0, 0, 0, 0, increment_timer=False)))
|
|
self.safety.set_timer(half + 2 * common.RT_INTERVAL)
|
|
for _ in range(max_rt_msgs):
|
|
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0, 0, 0, 0, increment_timer=False)))
|
|
|
|
def test_prevent_lkas_action(self):
|
|
self.safety.set_controls_allowed(1)
|
|
self.assertFalse(self._tx(self._lkas_command_msg(1)))
|
|
|
|
self.safety.set_controls_allowed(0)
|
|
self.assertFalse(self._tx(self._lkas_command_msg(1)))
|
|
|
|
def test_acc_buttons(self):
|
|
for allowed in (0, 1):
|
|
self.safety.set_controls_allowed(allowed)
|
|
for enabled in (True, False):
|
|
self._rx(self._pcm_status_msg(enabled))
|
|
self.assertTrue(self._tx(self._acc_button_msg(Buttons.TJA_TOGGLE, 2)))
|
|
|
|
for allowed in (0, 1):
|
|
self.safety.set_controls_allowed(allowed)
|
|
for bus in (0, 2):
|
|
self.assertEqual(allowed, self._tx(self._acc_button_msg(Buttons.RESUME, bus)))
|
|
|
|
for enabled in (True, False):
|
|
self._rx(self._pcm_status_msg(enabled))
|
|
for bus in (0, 2):
|
|
self.assertEqual(enabled, self._tx(self._acc_button_msg(Buttons.CANCEL, bus)))
|
|
|
|
def test_enable_control_allowed_from_acc_main_on(self):
|
|
for enable_mads in (True, False):
|
|
with self.subTest("enable_mads", mads_enabled=enable_mads):
|
|
for main_button_msg_valid in (True, False):
|
|
with self.subTest("main_button_msg_valid", state_valid=main_button_msg_valid):
|
|
self.safety.set_mads_params(enable_mads, False, False)
|
|
self._rx(self._pcm_status_msg(main_button_msg_valid))
|
|
self.assertEqual(enable_mads and main_button_msg_valid, self.safety.get_controls_allowed_lateral())
|
|
|
|
|
|
class TestFordCANFDStockSafety(TestFordSafetyBase):
|
|
STEER_MESSAGE = MSG_LateralMotionControl2
|
|
|
|
TX_MSGS = [
|
|
[MSG_Steering_Data_FD1, 0], [MSG_Steering_Data_FD1, 2], [MSG_ACCDATA_3, 0], [MSG_Lane_Assist_Data1, 0],
|
|
[MSG_LateralMotionControl2, 0], [MSG_IPMA_Data, 0],
|
|
]
|
|
RELAY_MALFUNCTION_ADDRS = {0: (MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl2,
|
|
MSG_IPMA_Data)}
|
|
|
|
FWD_BLACKLISTED_ADDRS = {2: [MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl2,
|
|
MSG_IPMA_Data]}
|
|
|
|
def setUp(self):
|
|
self.packer = CANPackerSafety("ford_lincoln_base_pt")
|
|
self.safety = libsafety_py.libsafety
|
|
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, FordSafetyFlags.CANFD)
|
|
self.safety.init_tests()
|
|
|
|
|
|
class TestFordLongitudinalSafetyBase(TestFordSafetyBase):
|
|
MAX_ACCEL = 2.0 # accel is used for brakes, but openpilot can set positive values
|
|
MIN_ACCEL = -3.5
|
|
INACTIVE_ACCEL = 0.0
|
|
|
|
MAX_GAS = 2.0
|
|
MIN_GAS = -0.5
|
|
INACTIVE_GAS = -5.0
|
|
|
|
# ACC command
|
|
def _acc_command_msg(self, gas: float, brake: float, brake_actuation: bool, cmbb_deny: bool = False):
|
|
values = {
|
|
"AccPrpl_A_Rq": gas, # [-5|5.23] m/s^2
|
|
"AccPrpl_A_Pred": gas, # [-5|5.23] m/s^2
|
|
"AccBrkTot_A_Rq": brake, # [-20|11.9449] m/s^2
|
|
"AccBrkPrchg_B_Rq": 1 if brake_actuation else 0, # Pre-charge brake request: 0=No, 1=Yes
|
|
"AccBrkDecel_B_Rq": 1 if brake_actuation else 0, # Deceleration request: 0=Inactive, 1=Active
|
|
"CmbbDeny_B_Actl": 1 if cmbb_deny else 0, # [0|1] deny AEB actuation
|
|
}
|
|
return self.packer.make_can_msg_safety("ACCDATA", 0, values)
|
|
|
|
def test_stock_aeb(self):
|
|
# Test that CmbbDeny_B_Actl is never 1, it prevents the ABS module from actuating AEB requests from ACCDATA_2
|
|
for controls_allowed in (True, False):
|
|
self.safety.set_controls_allowed(controls_allowed)
|
|
for cmbb_deny in (True, False):
|
|
should_tx = not cmbb_deny
|
|
self.assertEqual(should_tx, self._tx(self._acc_command_msg(self.INACTIVE_GAS, self.INACTIVE_ACCEL, controls_allowed, cmbb_deny)))
|
|
should_tx = controls_allowed and not cmbb_deny
|
|
self.assertEqual(should_tx, self._tx(self._acc_command_msg(self.MAX_GAS, self.MAX_ACCEL, controls_allowed, cmbb_deny)))
|
|
|
|
def test_gas_safety_check(self):
|
|
for controls_allowed in (True, False):
|
|
self.safety.set_controls_allowed(controls_allowed)
|
|
for gas in np.concatenate((np.arange(self.MIN_GAS - 2, self.MAX_GAS + 2, 0.05), [self.INACTIVE_GAS])):
|
|
gas = round(gas, 2) # floats might not hit exact boundary conditions without rounding
|
|
should_tx = (controls_allowed and self.MIN_GAS <= gas <= self.MAX_GAS) or gas == self.INACTIVE_GAS
|
|
self.assertEqual(should_tx, self._tx(self._acc_command_msg(gas, self.INACTIVE_ACCEL, controls_allowed)))
|
|
|
|
def test_brake_safety_check(self):
|
|
brake_values = self._boundary_values([self.MIN_ACCEL, self.MAX_ACCEL, self.INACTIVE_ACCEL],
|
|
self.MIN_ACCEL - 2, self.MAX_ACCEL + 2, 0.05)
|
|
for controls_allowed in (True, False):
|
|
self.safety.set_controls_allowed(controls_allowed)
|
|
for brake_actuation in (True, False):
|
|
for brake in brake_values:
|
|
should_tx = (controls_allowed and self.MIN_ACCEL <= brake <= self.MAX_ACCEL) or brake == self.INACTIVE_ACCEL
|
|
should_tx = should_tx and (controls_allowed or not brake_actuation)
|
|
self.assertEqual(should_tx, self._tx(self._acc_command_msg(self.INACTIVE_GAS, brake, brake_actuation)))
|
|
|
|
|
|
class TestFordLongitudinalSafety(TestFordLongitudinalSafetyBase):
|
|
STEER_MESSAGE = MSG_LateralMotionControl
|
|
|
|
TX_MSGS = [
|
|
[MSG_Steering_Data_FD1, 0], [MSG_Steering_Data_FD1, 2], [MSG_ACCDATA, 0], [MSG_ACCDATA_3, 0], [MSG_Lane_Assist_Data1, 0],
|
|
[MSG_LateralMotionControl, 0], [MSG_IPMA_Data, 0],
|
|
]
|
|
RELAY_MALFUNCTION_ADDRS = {0: (MSG_ACCDATA, MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl,
|
|
MSG_IPMA_Data)}
|
|
|
|
FWD_BLACKLISTED_ADDRS = {2: [MSG_ACCDATA, MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl,
|
|
MSG_IPMA_Data]}
|
|
|
|
def setUp(self):
|
|
self.packer = CANPackerSafety("ford_lincoln_base_pt")
|
|
self.safety = libsafety_py.libsafety
|
|
# Make sure we enforce long safety even without long flag for CAN
|
|
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, 0)
|
|
self.safety.init_tests()
|
|
|
|
|
|
class TestFordCANFDLongitudinalSafety(TestFordLongitudinalSafetyBase):
|
|
STEER_MESSAGE = MSG_LateralMotionControl2
|
|
|
|
TX_MSGS = [
|
|
[MSG_Steering_Data_FD1, 0], [MSG_Steering_Data_FD1, 2], [MSG_ACCDATA, 0], [MSG_ACCDATA_3, 0], [MSG_Lane_Assist_Data1, 0],
|
|
[MSG_LateralMotionControl2, 0], [MSG_IPMA_Data, 0],
|
|
]
|
|
RELAY_MALFUNCTION_ADDRS = {0: (MSG_ACCDATA, MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl2,
|
|
MSG_IPMA_Data)}
|
|
|
|
FWD_BLACKLISTED_ADDRS = {2: [MSG_ACCDATA, MSG_ACCDATA_3, MSG_Lane_Assist_Data1, MSG_LateralMotionControl2,
|
|
MSG_IPMA_Data]}
|
|
|
|
def setUp(self):
|
|
self.packer = CANPackerSafety("ford_lincoln_base_pt")
|
|
self.safety = libsafety_py.libsafety
|
|
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, FordSafetyFlags.LONG_CONTROL | FordSafetyFlags.CANFD)
|
|
self.safety.init_tests()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|