Skip to content

Commit

Permalink
Toyota: improve longitudinal control
Browse files Browse the repository at this point in the history
  • Loading branch information
Edison-CBS committed Jul 4, 2024
1 parent a52a979 commit fe97358
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 70 deletions.
63 changes: 41 additions & 22 deletions selfdrive/car/toyota/carcontroller.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from cereal import car
from openpilot.common.numpy_fast import clip
from openpilot.common.realtime import DT_CTRL
from openpilot.common.numpy_fast import clip, interp
from openpilot.selfdrive.car import apply_meas_steer_torque_limits, apply_std_steer_angle_limits, common_fault_avoidance, make_can_msg
from openpilot.selfdrive.car.interfaces import CarControllerBase
from openpilot.selfdrive.car.toyota import toyotacan
Expand All @@ -11,6 +10,7 @@

SteerControlType = car.CarParams.SteerControlType
VisualAlert = car.CarControl.HUDControl.VisualAlert
LongCtrlState = car.CarControl.Actuators.LongControlState

# LKA limits
# EPS faults if you apply torque while the steering rate is above 100 deg/s for too long
Expand All @@ -25,8 +25,12 @@
MAX_LTA_ANGLE = 94.9461 # deg
MAX_LTA_DRIVER_TORQUE_ALLOWANCE = 150 # slightly above steering pressed allows some resistance when changing lanes

# Time values for hysteresis
RESUME_HYSTERESIS_TIME = 1.5 # seconds
# PCM compensatory force calculation threshold
# a variation in accel command is more pronounced at higher speeds, let compensatory forces ramp to zero before
# applying when speed is high
COMPENSATORY_CALCULATION_THRESHOLD_V = [-0.3, -0.25, 0.] # m/s^2
COMPENSATORY_CALCULATION_THRESHOLD_BP = [0., 11., 23.] # m/s


class CarController(CarControllerBase):
def __init__(self, dbc_name, CP, VM):
Expand All @@ -36,9 +40,10 @@ def __init__(self, dbc_name, CP, VM):
self.last_steer = 0
self.last_angle = 0
self.alert_active = False
self.resume_off_frames = 0.
self.last_standstill = False
self.standstill_req = False
self.steer_rate_counter = 0
self.prohibit_neg_calculation = True
self.distance_button = 0

self.packer = CANPacker(dbc_name)
Expand All @@ -50,6 +55,7 @@ def update(self, CC, CS, now_nanos):
hud_control = CC.hudControl
pcm_cancel_cmd = CC.cruiseControl.cancel
lat_active = CC.latActive and abs(CS.out.steeringTorque) < MAX_USER_TORQUE
stopping = actuators.longControlState == LongCtrlState.stopping

# *** control msgs ***
can_sends = []
Expand Down Expand Up @@ -103,21 +109,32 @@ def update(self, CC, CS, now_nanos):
lta_active, self.frame // 2, torque_wind_down))

# *** gas and brake ***
pcm_accel_cmd = clip(actuators.accel, self.params.ACCEL_MIN, self.params.ACCEL_MAX)

# *** standstill logic ***
# mimic stock behaviour, set standstill_req to False only when openpilot wants to resume
if not CC.cruiseControl.resume:
self.resume_off_frames += 1 # frame counter for hysteresis
# add a 1.5 second hysteresis to when CC.cruiseControl.resume turns off in order to prevent
# vehicle's dash from blinking
if self.resume_off_frames >= RESUME_HYSTERESIS_TIME / DT_CTRL:
self.standstill_req = True
# prohibit negative compensatory calculations when first activating long after accelerator depression or engagement
if not CC.longActive:
self.prohibit_neg_calculation = True
comp_thresh = interp(CS.out.vEgo, COMPENSATORY_CALCULATION_THRESHOLD_BP, COMPENSATORY_CALCULATION_THRESHOLD_V)
# don't reset until a reasonable compensatory value is reached
if CS.pcm_neutral_force > comp_thresh * self.CP.mass:
self.prohibit_neg_calculation = False
# limit minimum to only positive until first positive is reached after engagement, don't calculate when long isn't active
if CC.longActive and not self.prohibit_neg_calculation:
accel_offset = CS.pcm_neutral_force / self.CP.mass
else:
accel_offset = 0.
# only calculate pcm_accel_cmd when long is active to prevent disengagement from accelerator depression
if CC.longActive:
pcm_accel_cmd = clip(actuators.accel + accel_offset, self.params.ACCEL_MIN, self.params.ACCEL_MAX)
else:
self.resume_off_frames = 0
self.standstill_req = False
# ignore standstill on NO_STOP_TIMER_CAR
self.standstill_req = self.standstill_req and self.CP.carFingerprint not in NO_STOP_TIMER_CAR
pcm_accel_cmd = 0.

# on entering standstill, send standstill request
if CS.out.standstill and not self.last_standstill and (self.CP.carFingerprint not in NO_STOP_TIMER_CAR):
self.standstill_req = True
if CS.pcm_acc_status != 8:
# pcm entered standstill or it's disabled
self.standstill_req = False

self.last_standstill = CS.out.standstill

# handle UI messages
fcw_alert = hud_control.visualAlert == VisualAlert.fcw
Expand All @@ -126,6 +143,8 @@ def update(self, CC, CS, now_nanos):
# we can spam can to cancel the system even if we are using lat only control
if (self.frame % 3 == 0 and self.CP.openpilotLongitudinalControl) or pcm_cancel_cmd:
lead = hud_control.leadVisible or CS.out.vEgo < 12. # at low speed we always assume the lead is present so ACC can be engaged
# when stopping, send -2.5 raw acceleration immediately to prevent vehicle from creeping, else send actuators.accel
accel_raw = -2.5 if stopping else actuators.accel

# Press distance button until we are at the correct bar length. Only change while enabled to avoid skipping startup popup
if self.frame % 6 == 0 and self.CP.openpilotLongitudinalControl:
Expand All @@ -139,11 +158,11 @@ def update(self, CC, CS, now_nanos):
if pcm_cancel_cmd and self.CP.carFingerprint in UNSUPPORTED_DSU_CAR:
can_sends.append(toyotacan.create_acc_cancel_command(self.packer))
elif self.CP.openpilotLongitudinalControl:
can_sends.append(toyotacan.create_accel_command(self.packer, pcm_accel_cmd, pcm_cancel_cmd, self.standstill_req, lead, CS.acc_type, fcw_alert,
self.distance_button))
can_sends.append(toyotacan.create_accel_command(self.packer, pcm_accel_cmd, accel_raw, pcm_cancel_cmd, self.standstill_req, lead,
CS.acc_type, fcw_alert, self.distance_button))
self.accel = pcm_accel_cmd
else:
can_sends.append(toyotacan.create_accel_command(self.packer, 0, pcm_cancel_cmd, False, lead, CS.acc_type, False, self.distance_button))
can_sends.append(toyotacan.create_accel_command(self.packer, 0, 0, pcm_cancel_cmd, False, lead, CS.acc_type, False, self.distance_button))

# *** hud ui ***
if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V:
Expand Down
20 changes: 5 additions & 15 deletions selfdrive/car/toyota/carstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,7 @@ def update(self, cp, cp_cam):
cp_acc = cp_cam if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR) else cp

if self.CP.carFingerprint in TSS2_CAR and not self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
if not (self.CP.flags & ToyotaFlags.SMART_DSU.value):
self.acc_type = cp_acc.vl["ACC_CONTROL"]["ACC_TYPE"]
self.acc_type = cp_acc.vl["ACC_CONTROL"]["ACC_TYPE"]
ret.stockFcw = bool(cp_acc.vl["PCS_HUD"]["FCW"])

# some TSS2 cars have low speed lockout permanently set, so ignore on those cars
Expand All @@ -149,7 +148,8 @@ def update(self, cp, cp_cam):
# ignore standstill state in certain vehicles, since pcm allows to restart with just an acceleration request
ret.cruiseState.standstill = self.pcm_acc_status == 7
ret.cruiseState.enabled = bool(cp.vl["PCM_CRUISE"]["CRUISE_ACTIVE"])
ret.cruiseState.nonAdaptive = self.pcm_acc_status in (1, 2, 3, 4, 5, 6)
ret.cruiseState.nonAdaptive = cp.vl["PCM_CRUISE"]["CRUISE_STATE"] in (1, 2, 3, 4, 5, 6)
self.pcm_neutral_force = cp.vl["PCM_CRUISE"]["NEUTRAL_FORCE"]

ret.genericToggle = bool(cp.vl["LIGHT_STALK"]["AUTO_HIGH_BEAM"])
ret.espDisabled = cp.vl["ESP_CONTROL"]["TC_DISABLED"] != 0
Expand All @@ -167,13 +167,11 @@ def update(self, cp, cp_cam):
if self.CP.carFingerprint not in UNSUPPORTED_DSU_CAR:
self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"]

if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR) or (self.CP.flags & ToyotaFlags.SMART_DSU and not self.CP.flags & ToyotaFlags.RADAR_CAN_FILTER):
if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR):
# distance button is wired to the ACC module (camera or radar)
self.prev_distance_button = self.distance_button
if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR):
self.distance_button = cp_acc.vl["ACC_CONTROL"]["DISTANCE"]
else:
self.distance_button = cp.vl["SDSU"]["FD_BUTTON"]

return ret

Expand Down Expand Up @@ -208,24 +206,16 @@ def get_can_parser(CP):
messages.append(("BSM", 1))

if CP.carFingerprint in RADAR_ACC_CAR and not CP.flags & ToyotaFlags.DISABLE_RADAR.value:
if not CP.flags & ToyotaFlags.SMART_DSU.value:
messages += [
("ACC_CONTROL", 33),
]
messages += [
("PCS_HUD", 1),
("ACC_CONTROL", 33),
]

if CP.carFingerprint not in (TSS2_CAR - RADAR_ACC_CAR) and not CP.enableDsu and not CP.flags & ToyotaFlags.DISABLE_RADAR.value:
messages += [
("PRE_COLLISION", 33),
]

if CP.flags & ToyotaFlags.SMART_DSU and not CP.flags & ToyotaFlags.RADAR_CAN_FILTER:
messages += [
("SDSU", 100),
]

return CANParser(DBC[CP.carFingerprint]["pt"], messages, 0)

@staticmethod
Expand Down
48 changes: 17 additions & 31 deletions selfdrive/car/toyota/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,9 @@ def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs):

stop_and_go = candidate in TSS2_CAR

# Detect smartDSU, which intercepts ACC_CMD from the DSU (or radar) allowing openpilot to send it
# 0x2AA is sent by a similar device which intercepts the radar instead of DSU on NO_DSU_CARs
if 0x2FF in fingerprint[0] or (0x2AA in fingerprint[0] and candidate in NO_DSU_CAR):
ret.flags |= ToyotaFlags.SMART_DSU.value

if 0x2AA in fingerprint[0] and candidate in NO_DSU_CAR:
ret.flags |= ToyotaFlags.RADAR_CAN_FILTER.value

# In TSS2 cars, the camera does long control
found_ecus = [fw.ecu for fw in car_fw]
ret.enableDsu = len(found_ecus) > 0 and Ecu.dsu not in found_ecus and candidate not in (NO_DSU_CAR | UNSUPPORTED_DSU_CAR) \
and not (ret.flags & ToyotaFlags.SMART_DSU)
ret.enableDsu = len(found_ecus) > 0 and Ecu.dsu not in found_ecus and candidate not in (NO_DSU_CAR | UNSUPPORTED_DSU_CAR)

if candidate == CAR.TOYOTA_PRIUS:
stop_and_go = True
Expand Down Expand Up @@ -98,7 +89,7 @@ def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs):
# TODO: these models can do stop and go, but unclear if it requires sDSU or unplugging DSU.
# For now, don't list stop and go functionality in the docs
if ret.flags & ToyotaFlags.SNG_WITHOUT_DSU:
stop_and_go = stop_and_go or bool(ret.flags & ToyotaFlags.SMART_DSU.value) or (ret.enableDsu and not docs)
stop_and_go = stop_and_go or (ret.enableDsu and not docs)

ret.centerToFront = ret.wheelbase * 0.44

Expand All @@ -110,28 +101,20 @@ def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs):
# TODO: make an adas dbc file for dsu-less models
ret.radarUnavailable = DBC[candidate]['radar'] is None or candidate in (NO_DSU_CAR - TSS2_CAR)

# if the smartDSU is detected, openpilot can send ACC_CONTROL and the smartDSU will block it from the DSU or radar.
# since we don't yet parse radar on TSS2/TSS-P radar-based ACC cars, gate longitudinal behind experimental toggle
use_sdsu = bool(ret.flags & ToyotaFlags.SMART_DSU)
if candidate in (RADAR_ACC_CAR | NO_DSU_CAR):
ret.experimentalLongitudinalAvailable = use_sdsu or candidate in RADAR_ACC_CAR
ret.experimentalLongitudinalAvailable = candidate in RADAR_ACC_CAR

if not use_sdsu:
# Disabling radar is only supported on TSS2 radar-ACC cars
if experimental_long and candidate in RADAR_ACC_CAR:
ret.flags |= ToyotaFlags.DISABLE_RADAR.value
else:
use_sdsu = use_sdsu and experimental_long
# Disabling radar is only supported on TSS2 radar-ACC cars
if experimental_long and candidate in RADAR_ACC_CAR:
ret.flags |= ToyotaFlags.DISABLE_RADAR.value

# openpilot longitudinal enabled by default:
# - non-(TSS2 radar ACC cars) w/ smartDSU installed
# - cars w/ DSU disconnected
# - TSS2 cars with camera sending ACC_CONTROL where we can block it
# openpilot longitudinal behind experimental long toggle:
# - TSS2 radar ACC cars w/ smartDSU installed
# - TSS2 radar ACC cars w/o smartDSU installed (disables radar)
# - TSS-P DSU-less cars w/ CAN filter installed (no radar parser yet)
ret.openpilotLongitudinalControl = use_sdsu or ret.enableDsu or candidate in (TSS2_CAR - RADAR_ACC_CAR) or bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value)
# - TSS2 radar ACC cars (disables radar)
ret.openpilotLongitudinalControl = ret.enableDsu or candidate in (TSS2_CAR - RADAR_ACC_CAR) or bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value)
ret.autoResumeSng = ret.openpilotLongitudinalControl and candidate in NO_STOP_TIMER_CAR

if not ret.openpilotLongitudinalControl:
Expand All @@ -141,22 +124,25 @@ def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs):
# to a negative value, so it won't matter.
ret.minEnableSpeed = -1. if stop_and_go else MIN_ACC_SPEED

# on stock Toyota this is -2.5
ret.stopAccel = -2.5

tune = ret.longitudinalTuning
ret.stoppingDecelRate = 0.3 # reach stopping target smoothly
if candidate in TSS2_CAR:
tune.kpV = [0.0]
tune.kiV = [0.5]
ret.vEgoStopping = 0.25
ret.vEgoStarting = 0.25
ret.stoppingDecelRate = 0.3 # reach stopping target smoothly
else:
tune.kiBP = [0., 5., 35.]
tune.kiV = [3.6, 2.4, 1.5]
tune.kpV = [0.0]
tune.kiV = [1.2] # appears to produce minimal oscillation on TSS-P

return ret

@staticmethod
def init(CP, logcan, sendcan):
# disable radar if alpha longitudinal toggled on radar-ACC car without CAN filter/smartDSU
# disable radar if alpha longitudinal toggled on radar-ACC car
if CP.flags & ToyotaFlags.DISABLE_RADAR.value:
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX, uds.MESSAGE_TYPE.NORMAL])
disable_ecu(logcan, sendcan, bus=0, addr=0x750, sub_addr=0xf, com_cont_req=communication_control)
Expand All @@ -165,7 +151,7 @@ def init(CP, logcan, sendcan):
def _update(self, c):
ret = self.CS.update(self.cp, self.cp_cam)

if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR) or (self.CP.flags & ToyotaFlags.SMART_DSU and not self.CP.flags & ToyotaFlags.RADAR_CAN_FILTER):
if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR):
ret.buttonEvents = create_button_events(self.CS.distance_button, self.CS.prev_distance_button, {1: ButtonType.gapAdjustCruise})

# events
Expand All @@ -177,7 +163,7 @@ def _update(self, c):
events.add(EventName.vehicleSensorsInvalid)

if self.CP.openpilotLongitudinalControl:
if ret.cruiseState.standstill and not ret.brakePressed and not self.CC.standstill_req:
if ret.cruiseState.standstill and not ret.brakePressed:
events.add(EventName.resumeRequired)
if self.CS.low_speed_lockout:
events.add(EventName.lowSpeedLockout)
Expand Down
5 changes: 3 additions & 2 deletions selfdrive/car/toyota/toyotacan.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ def create_lta_steer_command(packer, steer_control_type, steer_angle, steer_req,
return packer.make_can_msg("STEERING_LTA", 0, values)


def create_accel_command(packer, accel, pcm_cancel, standstill_req, lead, acc_type, fcw_alert, distance):
def create_accel_command(packer, accel, accel_raw, pcm_cancel, standstill_req, lead, acc_type, fcw_alert, distance):
# TODO: find the exact canceling bit that does not create a chime
values = {
"ACCEL_CMD": accel,
"ACCEL_CMD": accel, # compensated accel command
"ACC_TYPE": acc_type,
"DISTANCE": distance,
"MINI_CAR": lead,
Expand All @@ -45,6 +45,7 @@ def create_accel_command(packer, accel, pcm_cancel, standstill_req, lead, acc_ty
"CANCEL_REQ": pcm_cancel,
"ALLOW_LONG_PRESS": 1,
"ACC_CUT_IN": fcw_alert, # only shown when ACC enabled
"ACCEL_CMD_ALT": accel_raw, # raw accel command, pcm uses this to calculate a compensatory force
}
return packer.make_can_msg("ACC_CONTROL", 0, values)

Expand Down

0 comments on commit fe97358

Please sign in to comment.