mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-14 00:13:42 +08:00
22a8575199
Keep official openpilot ancestry and dependency licenses. Store resources directly in Git, remove LFS setup requirements, and leave the downloadable big model outside the source tree.
530 lines
26 KiB
Python
Executable File
530 lines
26 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
|
|
|
|
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
|
|
|
|
|
|
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):
|
|
DBC = "ford_lincoln_base_pt"
|
|
SAFETY_MODEL = CarParams.SafetyModel.ford
|
|
|
|
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
|
|
|
|
lat_ctl_frame = 0
|
|
|
|
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 _get_max_curvature_delta_relaxed_can(self, speed):
|
|
# flipped fudge, this is the least movement toward the error bounds safety requires
|
|
fudged_speed = speed + 1.0
|
|
return int(MAX_LATERAL_JERK / (fudged_speed * fudged_speed) / self.LATERAL_FREQUENCY * self.DEG_TO_CAN) - 1
|
|
|
|
def _get_max_curvature_relaxed_can(self, speed):
|
|
# flipped fudge, safety never requires commanding more curvature than openpilot can send
|
|
fudged_speed = speed + 1.0
|
|
max_curvature_accel_can = int(MAX_LATERAL_ACCEL / (fudged_speed * fudged_speed) * self.DEG_TO_CAN) - 1
|
|
return min(max_curvature_accel_can, round(self.MAX_CURVATURE * self.DEG_TO_CAN))
|
|
|
|
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}
|
|
return self.packer.make_can_msg_safety("BrakeSysFeatures", 0, values)
|
|
|
|
# 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}
|
|
return self.packer.make_can_msg_safety("EngVehicleSpThrottle2", 0, values)
|
|
|
|
# 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}
|
|
return self.packer.make_can_msg_safety("Yaw_Data_FD1", 0, values)
|
|
|
|
# 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)
|
|
|
|
def test_enable_control_allowed_from_queue_assist(self):
|
|
msg = self.packer.make_can_msg_safety("EngBrakeData", 0, {"BpedDrvAppl_D_Actl": 1, "CcStat_D_Actl": 4})
|
|
self.assertTrue(self._rx(msg))
|
|
self.assertTrue(self.safety.get_controls_allowed())
|
|
|
|
# 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.lat_ctl_frame * int(1e6 / self.LATERAL_FREQUENCY))
|
|
self.__class__.lat_ctl_frame += 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):
|
|
"""
|
|
When the curvature error is exceeded, commanded curvature must start moving towards meas respecting rate limits.
|
|
Since safety allows higher rate limits to avoid false positives, we need to allow a lower rate to move towards meas.
|
|
"""
|
|
self.safety.set_controls_allowed(True)
|
|
# safety fudges the speed (1 m/s) and rate limits (1 CAN unit) to avoid false positives
|
|
small_curvature = 1 / self.DEG_TO_CAN # significant small amount of curvature to cross boundary
|
|
|
|
for speed in np.arange(0, 40, 0.5):
|
|
curvature_accel_limit = self._get_max_curvature_can(speed) / self.DEG_TO_CAN
|
|
limit_command = speed > self.CURVATURE_ERROR_MIN_SPEED
|
|
# ensure our limits match the safety's rounded limits
|
|
# lateral jerk is symmetric, so the wind up and wind down limits are the same
|
|
max_delta = self._get_max_curvature_delta_can(speed) / self.DEG_TO_CAN
|
|
max_delta_relaxed = self._get_max_curvature_delta_relaxed_can(speed) / self.DEG_TO_CAN
|
|
|
|
up_cases = (self.MAX_CURVATURE_ERROR * 2, [
|
|
(not limit_command, 0, 0),
|
|
(not limit_command, 0, max_delta_relaxed - small_curvature),
|
|
(True, 0, max_delta_relaxed),
|
|
(True, 0, max_delta),
|
|
(False, 0, max_delta + small_curvature),
|
|
# stay at boundary limit
|
|
(True, self.MAX_CURVATURE_ERROR - small_curvature, self.MAX_CURVATURE_ERROR - small_curvature),
|
|
# 1 unit below boundary limit
|
|
(not limit_command, self.MAX_CURVATURE_ERROR - small_curvature * 2, self.MAX_CURVATURE_ERROR - small_curvature * 2),
|
|
# shouldn't allow command to move outside the boundary limit if last was inside
|
|
(not limit_command, self.MAX_CURVATURE_ERROR - small_curvature, self.MAX_CURVATURE_ERROR - small_curvature * 2),
|
|
])
|
|
|
|
down_cases = (self.MAX_CURVATURE - self.MAX_CURVATURE_ERROR * 2, [
|
|
(not limit_command, self.MAX_CURVATURE, self.MAX_CURVATURE),
|
|
(not limit_command, self.MAX_CURVATURE, self.MAX_CURVATURE - max_delta_relaxed + small_curvature),
|
|
(True, self.MAX_CURVATURE, self.MAX_CURVATURE - max_delta_relaxed),
|
|
(True, self.MAX_CURVATURE, self.MAX_CURVATURE - max_delta),
|
|
(False, self.MAX_CURVATURE, self.MAX_CURVATURE - max_delta - small_curvature),
|
|
])
|
|
|
|
# the driver can hold a curvature openpilot may not command, safety must never require moving past
|
|
# the most it can send: the lower of the lateral acceleration limit and what the EPS accepts
|
|
max_curvature_relaxed = self._get_max_curvature_relaxed_can(speed) / self.DEG_TO_CAN
|
|
# safety fudges the speed down for the accel check and up for the cap, so the last command can sit above the cap
|
|
max_curvature_allowed_can = min(self._get_max_curvature_can(speed), round(self.MAX_CURVATURE * self.DEG_TO_CAN))
|
|
winds_down_within_jerk = (max_curvature_allowed_can - self._get_max_curvature_relaxed_can(speed) <=
|
|
self._get_max_curvature_delta_can(speed))
|
|
relaxed_cases = (self.MAX_CURVATURE * 2, [
|
|
(True, max_curvature_relaxed, max_curvature_relaxed),
|
|
(not limit_command, max_curvature_relaxed, max_curvature_relaxed - small_curvature),
|
|
# no longer requiring the command to wind towards meas doesn't stop rate limiting it winding away
|
|
(winds_down_within_jerk, max_curvature_allowed_can / self.DEG_TO_CAN, max_curvature_relaxed),
|
|
])
|
|
|
|
for sign in (-1, 1):
|
|
for angle_meas, cases in (up_cases, down_cases, relaxed_cases):
|
|
self._reset_curvature_measurement(sign * angle_meas, speed)
|
|
for should_tx, initial_curvature, desired_curvature in cases:
|
|
|
|
# at low speeds one frame of jerk exceeds the curvature signal, so the should_tx=False cases will rightly not fail.
|
|
# assert we never drop a case at a speed where the curvature error is enforced
|
|
if abs(desired_curvature) > self.MAX_CURVATURE:
|
|
self.assertLess(speed, self.CURVATURE_ERROR_MIN_SPEED)
|
|
continue
|
|
|
|
# can not send if the curvature is above the max lateral acceleration
|
|
should_tx = should_tx and abs(desired_curvature) <= curvature_accel_limit
|
|
|
|
self._set_prev_desired_angle(sign * initial_curvature)
|
|
self.assertEqual(should_tx, self._tx(self._lat_ctl_msg(True, 0, 0, sign * desired_curvature, 0)))
|
|
|
|
# the newest speed sample gates the check, not the whole sample window
|
|
max_error = self.MAX_CURVATURE_ERROR + small_curvature * 2
|
|
for sign in (-1, 1):
|
|
self._reset_curvature_measurement(0, self.CURVATURE_ERROR_MIN_SPEED - 1)
|
|
self._rx(self._speed_msg(self.CURVATURE_ERROR_MIN_SPEED + 1))
|
|
self._set_prev_desired_angle(sign * self.MAX_CURVATURE_ERROR)
|
|
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0, 0, sign * max_error, 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)))
|
|
|
|
|
|
class TestFordCANFDStockSafety(TestFordSafetyBase):
|
|
SAFETY_PARAM = FordSafetyFlags.CANFD
|
|
|
|
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]}
|
|
|
|
|
|
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):
|
|
# Make sure we enforce long safety even without long flag for CAN
|
|
SAFETY_PARAM = 0
|
|
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]}
|
|
|
|
|
|
class TestFordCANFDLongitudinalSafety(TestFordLongitudinalSafetyBase):
|
|
SAFETY_PARAM = FordSafetyFlags.LONG_CONTROL | FordSafetyFlags.CANFD
|
|
|
|
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]}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|