carrot/selfdrive/car/gm/interface.py
FrogAi 1b9fc72d80 GM Stock Custom Longitudinal Control
Co-Authored-By: garrettpall <76917194+garrettpall@users.noreply.github.com>
2024-06-01 02:49:23 -07:00

407 lines
19 KiB
Python

#!/usr/bin/env python3
import os
from cereal import car, custom
from math import fabs, exp
from panda import Panda
from openpilot.common.basedir import BASEDIR
from openpilot.common.conversions import Conversions as CV
from openpilot.selfdrive.car import create_button_events, get_safety_config
from openpilot.selfdrive.car.gm.radar_interface import RADAR_HEADER_MSG
from openpilot.selfdrive.car.gm.values import CAR, CruiseButtons, CarControllerParams, EV_CAR, CAMERA_ACC_CAR, CanBus, GMFlags, CC_ONLY_CAR, SDGM_CAR, SLOW_ACC
from openpilot.selfdrive.car.interfaces import CarInterfaceBase, TorqueFromLateralAccelCallbackType, FRICTION_THRESHOLD, LatControlInputs, NanoFFModel
from openpilot.selfdrive.controls.lib.drive_helpers import get_friction
from openpilot.common.params import Params
params = Params()
ButtonType = car.CarState.ButtonEvent.Type
EventName = car.CarEvent.EventName
GearShifter = car.CarState.GearShifter
TransmissionType = car.CarParams.TransmissionType
NetworkLocation = car.CarParams.NetworkLocation
BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise,
CruiseButtons.MAIN: ButtonType.altButton3, CruiseButtons.CANCEL: ButtonType.cancel}
FrogPilotButtonType = custom.FrogPilotCarState.ButtonEvent.Type
PEDAL_MSG = 0x201
CAM_MSG = 0x320 # AEBCmd
# TODO: Is this always linked to camera presence?
ACCELERATOR_POS_MSG = 0xbe
NON_LINEAR_TORQUE_PARAMS = {
CAR.BOLT_EUV: [2.6531724862969748, 1.0, 0.1919764879840985, 0.009054123646805178],
CAR.BOLT_CC: [2.6531724862969748, 1.0, 0.1919764879840985, 0.009054123646805178],
CAR.ACADIA: [4.78003305, 1.0, 0.3122, 0.05591772],
CAR.SILVERADO: [3.29974374, 1.0, 0.25571356, 0.0465122]
}
NEURAL_PARAMS_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/neural_ff_weights.json')
class CarInterface(CarInterfaceBase):
@staticmethod
def get_pid_accel_limits(CP, current_speed, cruise_speed, frogpilot_variables):
if frogpilot_variables.sport_plus:
return CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX_PLUS
else:
return CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX
# Determined by iteratively plotting and minimizing error for f(angle, speed) = steer.
@staticmethod
def get_steer_feedforward_volt(desired_angle, v_ego):
desired_angle *= 0.02904609
sigmoid = desired_angle / (1 + fabs(desired_angle))
return 0.10006696 * sigmoid * (v_ego + 3.12485927)
def get_steer_feedforward_function(self):
if self.CP.carFingerprint in (CAR.VOLT, CAR.VOLT_CC):
return self.get_steer_feedforward_volt
else:
return CarInterfaceBase.get_steer_feedforward_default
def torque_from_lateral_accel_siglin(self, latcontrol_inputs: LatControlInputs, torque_params: car.CarParams.LateralTorqueTuning, lateral_accel_error: float,
lateral_accel_deadzone: float, friction_compensation: bool, gravity_adjusted: bool) -> float:
friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation)
def sig(val):
return 1 / (1 + exp(-val)) - 0.5
# The "lat_accel vs torque" relationship is assumed to be the sum of "sigmoid + linear" curves
# An important thing to consider is that the slope at 0 should be > 0 (ideally >1)
# This has big effect on the stability about 0 (noise when going straight)
# ToDo: To generalize to other GMs, explore tanh function as the nonlinear
non_linear_torque_params = NON_LINEAR_TORQUE_PARAMS.get(self.CP.carFingerprint)
assert non_linear_torque_params, "The params are not defined"
a, b, c, _ = non_linear_torque_params
steer_torque = (sig(latcontrol_inputs.lateral_acceleration * a) * b) + (latcontrol_inputs.lateral_acceleration * c)
return float(steer_torque) + friction
def torque_from_lateral_accel_neural(self, latcontrol_inputs: LatControlInputs, torque_params: car.CarParams.LateralTorqueTuning, lateral_accel_error: float,
lateral_accel_deadzone: float, friction_compensation: bool, gravity_adjusted: bool) -> float:
friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation)
inputs = list(latcontrol_inputs)
if gravity_adjusted:
inputs[0] += inputs[1]
return float(self.neural_ff_model.predict(inputs)) + friction
def torque_from_lateral_accel(self) -> TorqueFromLateralAccelCallbackType:
if self.CP.carFingerprint in (CAR.BOLT_EUV, CAR.BOLT_CC):
self.neural_ff_model = NanoFFModel(NEURAL_PARAMS_PATH, self.CP.carFingerprint)
return self.torque_from_lateral_accel_neural
elif self.CP.carFingerprint in NON_LINEAR_TORQUE_PARAMS:
return self.torque_from_lateral_accel_siglin
else:
return self.torque_from_lateral_accel_linear
@staticmethod
def _get_params(ret, params, candidate, fingerprint, car_fw, disable_openpilot_long, experimental_long, docs):
params.put_bool("HideDisableOpenpilotLongitudinal", candidate not in (SDGM_CAR | CAMERA_ACC_CAR))
cslcEnabled = params.get_bool("CSLCEnabled")
ret.carName = "gm"
ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.gm)]
ret.autoResumeSng = False
ret.enableBsm = 0x142 in fingerprint[CanBus.POWERTRAIN]
if PEDAL_MSG in fingerprint[0]:
ret.enableGasInterceptor = True
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_GAS_INTERCEPTOR
if candidate in EV_CAR:
ret.transmissionType = TransmissionType.direct
else:
ret.transmissionType = TransmissionType.automatic
ret.longitudinalTuning.deadzoneBP = [0.]
ret.longitudinalTuning.deadzoneV = [0.15]
ret.longitudinalTuning.kpBP = [5., 35.]
ret.longitudinalTuning.kiBP = [0.]
if candidate in CAMERA_ACC_CAR:
ret.experimentalLongitudinalAvailable = candidate not in CC_ONLY_CAR
ret.networkLocation = NetworkLocation.fwdCamera
ret.radarUnavailable = True # no radar
ret.pcmCruise = True
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_HW_CAM
ret.minEnableSpeed = 5 * CV.KPH_TO_MS
ret.minSteerSpeed = 10 * CV.KPH_TO_MS
# Tuning for experimental long
ret.longitudinalTuning.kpV = [2.0, 1.5]
ret.longitudinalTuning.kiV = [0.72]
ret.stoppingDecelRate = 2.0 # reach brake quickly after enabling
ret.vEgoStopping = 0.25
ret.vEgoStarting = 0.25
if candidate in SLOW_ACC and params.get_bool("GasRegenCmd"):
ret.longitudinalTuning.kpV = [1.5, 1.125]
ret.stopAccel = -0.25
if experimental_long and not cslcEnabled:
ret.pcmCruise = False
ret.openpilotLongitudinalControl = True
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_HW_CAM_LONG
if cslcEnabled:
# Used for CEM with CSLC
ret.openpilotLongitudinalControl = True
ret.longitudinalTuning.deadzoneBP = [0.]
ret.longitudinalTuning.deadzoneV = [0.9] # == 2 mph allowable delta
ret.stoppingDecelRate = 3.25 # == 8.33 mph/s (OFF + ON = 12 frames)
ret.longitudinalActuatorDelayLowerBound = 1.
ret.longitudinalActuatorDelayUpperBound = 2.
ret.longitudinalTuning.kpBP = [7.15, 7.2, 28.] # 7.15 m/s == 16 mph
ret.longitudinalTuning.kpV = [0., 4., 2.] # set lower end to 0 since we can't drive below that speed
ret.longitudinalTuning.kiBP = [0.]
ret.longitudinalTuning.kiV = [0.1]
elif candidate in SDGM_CAR:
ret.experimentalLongitudinalAvailable = False
ret.networkLocation = NetworkLocation.fwdCamera
ret.pcmCruise = True
ret.radarUnavailable = True
ret.minEnableSpeed = -1. # engage speed is decided by ASCM
ret.minSteerSpeed = 30 * CV.MPH_TO_MS
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_HW_SDGM
# Used for CEM with CSLC
ret.openpilotLongitudinalControl = cslcEnabled
ret.longitudinalTuning.deadzoneBP = [0.]
ret.longitudinalTuning.deadzoneV = [0.9] # == 2 mph allowable delta
ret.stoppingDecelRate = 7.45 # == 16.67 mph/s (OFF + ON = 30 frames)
ret.longitudinalActuatorDelayLowerBound = 1.
ret.longitudinalActuatorDelayUpperBound = 2.
ret.longitudinalTuning.kpBP = [7.15, 7.2, 28.] # 7.15 m/s == 16 mph
ret.longitudinalTuning.kpV = [0., 4., 2.] # set lower end to 0 since we can't drive below that speed
ret.longitudinalTuning.kiBP = [0.]
ret.longitudinalTuning.kiV = [0.1]
else: # ASCM, OBD-II harness
ret.openpilotLongitudinalControl = not disable_openpilot_long
ret.networkLocation = NetworkLocation.gateway
ret.radarUnavailable = RADAR_HEADER_MSG not in fingerprint[CanBus.OBSTACLE] and not docs
ret.pcmCruise = False # stock non-adaptive cruise control is kept off
# supports stop and go, but initial engage must (conservatively) be above 18mph
ret.minEnableSpeed = 18 * CV.MPH_TO_MS
ret.minSteerSpeed = 7 * CV.MPH_TO_MS
# Tuning
ret.longitudinalTuning.kpV = [2.4, 1.5]
ret.longitudinalTuning.kiV = [0.36]
if ret.enableGasInterceptor:
# Need to set ASCM long limits when using pedal interceptor, instead of camera ACC long limits
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_HW_ASCM_LONG
# Start with a baseline tuning for all GM vehicles. Override tuning as needed in each model section below.
ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]]
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.00]]
ret.lateralTuning.pid.kf = 0.00004 # full torque for 20 deg at 80mph means 0.00007818594
ret.steerActuatorDelay = 0.1 # Default delay, not measured yet
ret.steerLimitTimer = 0.4
ret.radarTimeStep = 0.0667 # GM radar runs at 15Hz instead of standard 20Hz
ret.longitudinalActuatorDelayUpperBound = 0.5 # large delay to initially start braking
if candidate in (CAR.VOLT, CAR.VOLT_CC):
ret.lateralTuning.pid.kpBP = [0., 40.]
ret.lateralTuning.pid.kpV = [0., 0.17]
ret.lateralTuning.pid.kiBP = [0.]
ret.lateralTuning.pid.kiV = [0.]
ret.lateralTuning.pid.kf = 1. # get_steer_feedforward_volt()
ret.steerActuatorDelay = 0.2
ret.minEnableSpeed = -1.
elif candidate == CAR.ACADIA:
ret.minEnableSpeed = -1. # engage speed is decided by pcm
ret.steerActuatorDelay = 0.2
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate == CAR.BUICK_LACROSSE:
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate == CAR.ESCALADE:
ret.minEnableSpeed = -1. # engage speed is decided by pcm
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate in (CAR.ESCALADE_ESV, CAR.ESCALADE_ESV_2019):
ret.minEnableSpeed = -1. # engage speed is decided by pcm
if candidate == CAR.ESCALADE_ESV:
ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[10., 41.0], [10., 41.0]]
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.13, 0.24], [0.01, 0.02]]
ret.lateralTuning.pid.kf = 0.000045
else:
ret.steerActuatorDelay = 0.2
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate in (CAR.BOLT_EUV, CAR.BOLT_CC):
ret.steerActuatorDelay = 0.2
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
if ret.enableGasInterceptor:
# ACC Bolts use pedal for full longitudinal control, not just sng
ret.flags |= GMFlags.PEDAL_LONG.value
elif candidate == CAR.SILVERADO:
# On the Bolt, the ECM and camera independently check that you are either above 5 kph or at a stop
# with foot on brake to allow engagement, but this platform only has that check in the camera.
# TODO: check if this is split by EV/ICE with more platforms in the future
if ret.openpilotLongitudinalControl:
ret.minEnableSpeed = -1.
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate in (CAR.EQUINOX, CAR.EQUINOX_CC):
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate in (CAR.TRAILBLAZER, CAR.TRAILBLAZER_CC):
ret.steerActuatorDelay = 0.2
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate in (CAR.SUBURBAN, CAR.SUBURBAN_CC):
ret.steerActuatorDelay = 0.075
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate == CAR.YUKON_CC:
ret.steerActuatorDelay = 0.2
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate == CAR.XT4:
ret.steerActuatorDelay = 0.2
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate == CAR.CT6_CC:
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate == CAR.MALIBU_CC:
ret.steerActuatorDelay = 0.2
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate == CAR.TRAX:
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
if ret.enableGasInterceptor:
ret.networkLocation = NetworkLocation.fwdCamera
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_HW_CAM
ret.minEnableSpeed = -1
ret.pcmCruise = False
ret.openpilotLongitudinalControl = not disable_openpilot_long
ret.stoppingControl = True
ret.autoResumeSng = True
if candidate in CC_ONLY_CAR:
ret.flags |= GMFlags.PEDAL_LONG.value
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_PEDAL_LONG
# Note: Low speed, stop and go not tested. Should be fairly smooth on highway
ret.longitudinalTuning.kpBP = [5., 35.]
ret.longitudinalTuning.kpV = [0.35, 0.5]
ret.longitudinalTuning.kiBP = [0., 35.0]
ret.longitudinalTuning.kiV = [0.1, 0.1]
ret.longitudinalTuning.kf = 0.15
ret.stoppingDecelRate = 0.8
else: # Pedal used for SNG, ACC for longitudinal control otherwise
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_HW_CAM_LONG
ret.startingState = True
ret.vEgoStopping = 0.25
ret.vEgoStarting = 0.25
elif candidate in CC_ONLY_CAR:
ret.flags |= GMFlags.CC_LONG.value
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_CC_LONG
ret.radarUnavailable = True
ret.experimentalLongitudinalAvailable = False
ret.minEnableSpeed = 24 * CV.MPH_TO_MS
ret.openpilotLongitudinalControl = not disable_openpilot_long
ret.pcmCruise = False
ret.longitudinalTuning.deadzoneBP = [0.]
ret.longitudinalTuning.deadzoneV = [0.56] # == 2 km/h/s, 1.25 mph/s
ret.stoppingDecelRate = 11.18 # == 25 mph/s (.04 rate)
ret.longitudinalActuatorDelayLowerBound = 1. # TODO: measure this
ret.longitudinalActuatorDelayUpperBound = 2.
ret.longitudinalTuning.kpBP = [10.7, 10.8, 28.] # 10.7 m/s == 24 mph
ret.longitudinalTuning.kpV = [0., 20., 20.] # set lower end to 0 since we can't drive below that speed
ret.longitudinalTuning.kiBP = [0.]
ret.longitudinalTuning.kiV = [0.1]
if candidate in CC_ONLY_CAR:
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_NO_ACC
# Exception for flashed cars, or cars whose camera was removed
if (ret.networkLocation == NetworkLocation.fwdCamera or candidate in CC_ONLY_CAR) and CAM_MSG not in fingerprint[CanBus.CAMERA] and not candidate in SDGM_CAR:
ret.flags |= GMFlags.NO_CAMERA.value
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_NO_CAMERA
if ACCELERATOR_POS_MSG not in fingerprint[CanBus.POWERTRAIN]:
ret.flags |= GMFlags.NO_ACCELERATOR_POS_MSG.value
if cslcEnabled:
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_CSLC
return ret
# returns a car.CarState
def _update(self, c, frogpilot_variables):
ret, fp_ret = self.CS.update(self.cp, self.cp_cam, self.cp_loopback, frogpilot_variables)
# Don't add event if transitioning from INIT, unless it's to an actual button
if self.CS.cruise_buttons != CruiseButtons.UNPRESS or self.CS.prev_cruise_buttons != CruiseButtons.INIT:
ret.buttonEvents = [
*create_button_events(self.CS.cruise_buttons, self.CS.prev_cruise_buttons, BUTTONS_DICT,
unpressed_btn=CruiseButtons.UNPRESS),
*create_button_events(self.CS.distance_button, self.CS.prev_distance_button,
{1: ButtonType.gapAdjustCruise}),
*create_button_events(self.CS.lkas_enabled, self.CS.lkas_previously_enabled, {1: FrogPilotButtonType.lkas}),
]
# The ECM allows enabling on falling edge of set, but only rising edge of resume
events = self.create_common_events(ret, extra_gears=[GearShifter.sport, GearShifter.low,
GearShifter.eco, GearShifter.manumatic],
pcm_enable=self.CP.pcmCruise, enable_buttons=(ButtonType.decelCruise,))
if not self.CP.pcmCruise:
if any(b.type == ButtonType.accelCruise and b.pressed for b in ret.buttonEvents):
events.add(EventName.buttonEnable)
# Enabling at a standstill with brake is allowed
# TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs
below_min_enable_speed = ret.vEgo < self.CP.minEnableSpeed or self.CS.moving_backward
if below_min_enable_speed and not (ret.standstill and ret.brake >= 20 and
(self.CP.networkLocation == NetworkLocation.fwdCamera and not self.CP.carFingerprint in SDGM_CAR)):
events.add(EventName.belowEngageSpeed)
if ret.cruiseState.standstill and not (self.CP.autoResumeSng or self.disable_resumeRequired):
events.add(EventName.resumeRequired)
self.resumeRequired_shown = True
# Disable the "resumeRequired" event after it's been shown once to not annoy the driver
if self.resumeRequired_shown and not ret.cruiseState.standstill:
self.disable_resumeRequired = True
if ret.vEgo < self.CP.minSteerSpeed and not self.disable_belowSteerSpeed:
events.add(EventName.belowSteerSpeed)
self.belowSteerSpeed_shown = True
# Disable the "belowSteerSpeed" event after it's been shown once to not annoy the driver
if self.belowSteerSpeed_shown and ret.vEgo >= self.CP.minSteerSpeed:
self.disable_belowSteerSpeed = True
if (self.CP.flags & GMFlags.CC_LONG.value) and ret.vEgo < self.CP.minEnableSpeed and ret.cruiseState.enabled:
events.add(EventName.speedTooLow)
if (self.CP.flags & GMFlags.PEDAL_LONG.value) and \
self.CP.transmissionType == TransmissionType.direct and \
not self.CS.single_pedal_mode and \
c.longActive:
events.add(EventName.pedalInterceptorNoBrake)
ret.events = events.to_msg()
return ret, fp_ret
def apply(self, c, now_nanos, frogpilot_variables):
return self.CC.update(c, self.CS, now_nanos, frogpilot_variables)