#!/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)