import numpy as np import math from opendbc.can.packer import CANPacker from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, AngleSteeringLimits, DT_CTRL, rate_limit from opendbc.car.interfaces import CarControllerBase, ISO_LATERAL_ACCEL from opendbc.car.tesla.teslacan import TeslaCAN from opendbc.car.tesla.values import CarControllerParams from opendbc.car.vehicle_model import VehicleModel # limit angle rate to both prevent a fault and for low speed comfort (~12 mph rate down to 0 mph) MAX_ANGLE_RATE = 5 # deg/20ms frame, EPS faults at 12 at a standstill # Add extra tolerance for average banked road since safety doesn't have the roll AVERAGE_ROAD_ROLL = 0.06 # ~3.4 degrees, 6% superelevation. higher actual roll lowers lateral acceleration MAX_LATERAL_ACCEL = ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL) # ~3.6 m/s^2 MAX_LATERAL_JERK = 3.0 + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL) # ~3.6 m/s^3 def get_max_angle_delta(v_ego_raw: float, VM: VehicleModel): max_curvature_rate_sec = MAX_LATERAL_JERK / (max(v_ego_raw, 1) ** 2) # (1/m)/s max_angle_rate_sec = math.degrees(VM.get_steer_from_curvature(max_curvature_rate_sec, v_ego_raw, 0)) # deg/s return max_angle_rate_sec * (DT_CTRL * CarControllerParams.STEER_STEP) def get_max_angle(v_ego_raw: float, VM: VehicleModel): max_curvature = MAX_LATERAL_ACCEL / (max(v_ego_raw, 1) ** 2) # 1/m return math.degrees(VM.get_steer_from_curvature(max_curvature, v_ego_raw, 0)) # deg def apply_tesla_steer_angle_limits(apply_angle: float, apply_angle_last: float, v_ego_raw: float, steering_angle: float, lat_active: bool, limits: AngleSteeringLimits, VM: VehicleModel) -> float: # *** max lateral jerk limit *** max_angle_delta = get_max_angle_delta(v_ego_raw, VM) # prevent fault max_angle_delta = min(max_angle_delta, MAX_ANGLE_RATE) new_apply_angle = rate_limit(apply_angle, apply_angle_last, -max_angle_delta, max_angle_delta) # *** max lateral accel limit *** max_angle = get_max_angle(v_ego_raw, VM) new_apply_angle = np.clip(new_apply_angle, -max_angle, max_angle) # angle is current angle when inactive if not lat_active: new_apply_angle = steering_angle # prevent fault return float(np.clip(new_apply_angle, -limits.STEER_ANGLE_MAX, limits.STEER_ANGLE_MAX)) def get_safety_CP(): # We use the TESLA_MODEL_Y platform for lateral limiting to match safety # A Model 3 at 40 m/s using the Model Y limits sees a <0.3% difference in max angle (from curvature factor) from opendbc.car.tesla.interface import CarInterface return CarInterface.get_non_essential_params("TESLA_MODEL_Y") class CarController(CarControllerBase): def __init__(self, dbc_names, CP): super().__init__(dbc_names, CP) self.apply_angle_last = 0 self.packer = CANPacker(dbc_names[Bus.party]) self.tesla_can = TeslaCAN(self.packer) # Vehicle model used for lateral limiting self.VM = VehicleModel(get_safety_CP()) def update(self, CC, CS, now_nanos): actuators = CC.actuators can_sends = [] # Tesla EPS enforces disabling steering on heavy lateral override force. # When enabling in a tight curve, we wait until user reduces steering force to start steering. # Canceling is done on rising edge and is handled generically with CC.cruiseControl.cancel lat_active = CC.latActive and CS.hands_on_level < 3 if self.frame % 2 == 0: # Angular rate limit based on speed self.apply_angle_last = apply_tesla_steer_angle_limits(actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgoRaw, CS.out.steeringAngleDeg, lat_active, CarControllerParams.ANGLE_LIMITS, self.VM) can_sends.append(self.tesla_can.create_steering_control(self.apply_angle_last, lat_active)) if self.frame % 10 == 0: can_sends.append(self.tesla_can.create_steering_allowed()) # Longitudinal control if self.CP.openpilotLongitudinalControl: if self.frame % 4 == 0: state = 13 if CC.cruiseControl.cancel else 4 # 4=ACC_ON, 13=ACC_CANCEL_GENERIC_SILENT accel = float(np.clip(actuators.accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX)) cntr = (self.frame // 4) % 8 can_sends.append(self.tesla_can.create_longitudinal_command(state, accel, cntr, CS.out.vEgo, CC.longActive)) else: # Increment counter so cancel is prioritized even without openpilot longitudinal if CC.cruiseControl.cancel: cntr = (CS.das_control["DAS_controlCounter"] + 1) % 8 can_sends.append(self.tesla_can.create_longitudinal_command(13, 0, cntr, CS.out.vEgo, False)) # TODO: HUD control new_actuators = actuators.as_builder() new_actuators.steeringAngleDeg = self.apply_angle_last self.frame += 1 return new_actuators, can_sends